Как проверить выделена ли память c

Обновлено: 05.07.2024

Можем ли мы проверить, выделен ли указатель, переданный функции, в памяти C?

Я написал свою собственную функцию на C, которая принимает указатель символа - buf [указатель на буфер] и размер - buf_siz [размер буфера]. Фактически перед вызовом этой функции пользователь должен создать буфер и выделить для него память buf_siz.

Поскольку есть вероятность, что пользователь может забыть выделить память и просто передать указатель на мою функцию, я хочу это проверить. Итак, есть ли способ проверить свою функцию, чтобы увидеть, действительно ли переданный указатель выделен с объемом памяти buf_siz .. ??

EDIT1: Кажется, нет стандартной библиотеки для проверки .. но есть ли какой-нибудь грязный хак для проверки .. ??

EDIT2: Я знаю, что моя функция будет использоваться хорошим программистом на C . Но я хочу знать, можем ли мы проверить или нет . если сможем я хотел бы это услышать ..

Вывод. Таким образом, невозможно проверить, выделен ли конкретный указатель в памяти или нет внутри функции

Вы не можете проверить, кроме некоторых хаков, связанных с реализацией.

У указателей нет никакой информации, кроме того, где они указывают. Лучшее, что вы можете сделать, это сказать: «Я знаю, как эта конкретная версия компилятора выделяет память, поэтому я разыменую память, переместу указатель на 4 байта назад, проверю размер, убедится, что он совпадает . » и так далее. Вы не можете сделать это стандартным способом, поскольку распределение памяти определяется реализацией. Не говоря уже о том, что они могли вообще не выделять его динамически.

Вам просто нужно предположить, что ваш клиент знает, как программировать на C. Единственное решение, которое я могу придумать, - это выделить память самостоятельно и вернуть ее, но это вряд ли небольшое изменение. (Это более крупное изменение дизайна.)

Приведенный ниже код - это то, что я однажды использовал, чтобы проверить, пытается ли какой-либо указатель получить доступ к незаконной памяти. Механизм состоит в том, чтобы вызвать SIGSEGV. Сигнал SEGV ранее был перенаправлен частной функции, которая использует longjmp для возврата к программе. Это своего рода взлом, но он работает.

Код можно улучшить (используйте «sigaction» вместо «signal» и т. Д.), Но это просто для общего представления. Также он переносится на другие версии Unix, для Windows я не уверен. Обратите внимание, что сигнал SIGSEGV не следует использовать где-либо еще в вашей программе.

Для решения для конкретной платформы вас может заинтересовать функция Win32 IsBadReadPtr (и другим нравится). Эта функция сможет (почти) предсказать, возникнет ли ошибка сегментации при чтении из определенного фрагмента памяти.

Однако это не защищает вас в общем случае, потому что операционная система ничего не знает о диспетчере кучи времени выполнения C, и если вызывающий объект передает буфер, размер которого не такой большой, как вы ожидаете, тогда остальная часть блока кучи по-прежнему будет доступна для чтения с точки зрения ОС.

Я всегда инициализирую указатели нулевым значением. Поэтому, когда я выделяю память, она изменится. Когда я проверяю, выделена ли память, я делаю pointer != NULL . Когда я освобождаю память, я также устанавливаю указатель на ноль. Я не могу придумать способ узнать, достаточно ли выделенной памяти.

Это не решает вашу проблему, но вы должны верить, что если кто-то пишет программы на C, то он достаточно квалифицирован, чтобы делать это правильно.

Однажды я применил грязный хакер на своем 64-битном Solaris. В 64-битном режиме куча начинается с 0x1 0000 0000. Сравнивая указатель, я мог определить, был ли это указатель в сегменте данных или кода p < (void*)0x100000000 , указатель в куче p > (void*)0x100000000 или указатель в область отображения памяти (intptr_t)p < 0 (mmap возвращает адреса из верхней части адресуемой области). Это позволило в моей программе хранить выделенные и отображенные в памяти указатели на одной карте, а мой модуль карты освобождал правильные указатели.

Но такой трюк крайне непереносим, ​​и если ваш код опирается на что-то подобное, пора переосмыслить архитектуру вашего кода. Вы, наверное, что-то делаете не так.

Нет, вообще нет возможности это сделать.

Кроме того, если ваш интерфейс просто «передает указатель на буфер, в который я буду помещать материал», то вызывающий может выбрать не для выделения памяти вообще и вместо этого использовать буфер фиксированного размера, который выделяется статически. или автоматическая переменная, или что-то в этом роде. Или, возможно, это указатель на часть более крупного объекта в куче.

Если ваш интерфейс специально говорит «передать указатель на выделенную память (потому что я собираюсь освободить его)», то вы должны ожидать, что вызывающий объект сделает это. Невозможность сделать это - это не то, что вы можете надежно обнаружить.

Один из способов взлома, который вы можете попробовать, - это проверить, указывает ли ваш указатель на выделенную стеком память. В целом это вам не поможет, поскольку выделенный буфер может быть слишком маленьким или указатель указывает на какой-то раздел глобальной памяти (.bss, .const, . ).

Чтобы выполнить этот взлом, вы сначала сохраните адрес первой переменной в main (). Позже вы можете сравнить этот адрес с адресом локальной переменной в вашей конкретной программе. Все адреса между обоими адресами находятся в стеке.

Я знаю, что это старый вопрос, но почти все возможно в C. Здесь уже есть несколько хакерских решений, но действительный способ определить, правильно ли выделена память, - это использовать оракул вместо malloc , calloc , realloc и free . Таким же образом фреймворки тестирования (такие как cmocka) могут обнаруживать проблемы с памятью (сбои сегментов, невыполнение памяти и т. Д.). Вы можете вести список адресов памяти, выделяемых по мере их выделения, и просто проверять этот список, когда пользователь хочет использовать вашу функцию. Я реализовал нечто очень похожее для своего собственного тестового фреймворка. Пример кода:

У вас будут аналогичные функции для calloc , realloc и free , каждая оболочка имеет префикс __wrap_ . Настоящий malloc доступен при использовании __real_malloc (аналогично другим функциям, которые вы переносите). Всякий раз, когда вы хотите проверить, действительно ли выделена память, просто перебирайте связанный список memory_ref и ищите адрес памяти. Если вы найдете его, и он достаточно большой, вы точно знаете, что адрес памяти не приведет к сбою вашей программы; в противном случае вернуть ошибку. В заголовочный файл, который использует ваша программа, вы должны добавить следующие строки:

Мои потребности были довольно простыми, поэтому я реализовал очень простую реализацию, но вы можете представить, как это можно было бы расширить, чтобы иметь лучшую систему отслеживания (например, создать struct , который отслеживает местоположение в памяти в дополнение к размеру) . Затем вы просто компилируете код с помощью

Недостатком является то, что пользователь должен компилировать свой исходный код с указанными выше директивами; однако это далеко не худшее, что я видел. Есть некоторые накладные расходы на выделение и освобождение памяти, но всегда есть некоторые накладные расходы при добавлении безопасности.

Я не знаю, как это сделать из вызова библиотеки, но в Linux вы можете посмотреть /proc/<pid>/numa_maps . Он покажет все разделы памяти, а в третьем столбце будет указано «куча» или «стек». Вы можете посмотреть на значение необработанного указателя, чтобы увидеть, где оно совпадает.

Таким образом, указатели, которые находятся выше 0x01167000, но ниже 0x7f39904d2000, находятся в куче.

Вы не можете проверить что-либо, доступное в стандарте C. Даже если бы ваш конкретный компилятор предоставлял функцию для этого, это все равно было бы плохой идеей. Вот пример того, почему:

Как все говорили, стандартного способа сделать это не существует.

Пока еще никто не упомянул «Написание надежного кода» Стива Магуайр. Несмотря на то, что в некоторых кругах, в книге есть главы, на тему управления памятью и обсуждает, как с осторожностью и полным контролем над всем распределением памяти в программе вы можете делать то, что вы спрашиваете, и определять, является ли указанный вам указатель действительным указателем на динамически выделяемую память. Однако, если вы планируете использовать сторонние библиотеки, вы обнаружите, что некоторые из них позволяют изменять процедуры выделения памяти на свои собственные, что значительно усложняет такой анализ.

Как правило, пользователи библиотеки несут ответственность за проверку и проверку ввода. Вы можете увидеть ASSERT или что-то еще в коде библиотеки, и они используются только для отладки. это стандартный способ написания C / C ++. в то время как очень многие программисты любят очень тщательно проверять и проверять свой код библиотеки. действительно "ПЛОХИЕ" привычки. Как указано в IOP / IOD, интерфейсы lib должны быть контрактами и четко указывать, что будет делать lib, а что нет, и что должен делать пользователь lib, а что не должно быть необходимым.

Есть простой способ сделать это. Каждый раз, когда вы создаете указатель, напишите вокруг него оболочку. Например, если ваш программист использует вашу библиотеку для создания структуры.

Убедитесь, что он выделяет память, используя вашу функцию, например

Если эта struct_var содержит память, которая выделяется динамически, например,

Если определение struct_type было

Затем в вашей функции init_struct_type () сделайте это,

Таким образом, если он не назначит строку temp-> значению, оно останется NULL. Вы можете проверить функции, которые используют эту структуру, если строка имеет значение NULL или нет.

Еще одна вещь: если программист настолько плох, что не может использовать ваши функции, а, скорее, напрямую обращается к нераспределенной памяти, он не заслуживает использования вашей библиотеки. Просто убедитесь, что в вашей документации все указано.

Ну, я не знаю, если это кто-то уже не поместил здесь, или это будет возможно в вашей программе. Я боролся с подобной вещью в моем университетском проекте.

Я решил это довольно просто - в части инициализации main (), после того как я объявил LIST *ptr , я просто поставил это ptr=NULL . Нравится -

Поэтому, когда выделение не удается или ваш указатель вообще не выделяется, он будет NULL. Итак, вы можете просто проверить это с помощью if.

Я не знаю, как написана ваша программа, но вы наверняка понимаете, о чем я пытаюсь указать. Если можно таким образом проверить свое распределение, а затем передать свои аргументы функции, у вас может быть простое решение.

Конечно, вы должны быть осторожны, чтобы ваши функции с распределением и созданием структуры были выполнены хорошо, но там, где в C, вам не нужно быть осторожным.

Нет, не можешь. Вы заметите, что никакие функции в стандартной библиотеке или где-либо еще не делают этого. Это потому, что нет стандартного способа сказать. Вызывающий код просто должен взять на себя ответственность за правильное управление памятью.

Неинициализированный указатель и есть неинициализированный. Он может указывать на что угодно или просто быть недопустимым адресом (то есть адресом, который не сопоставлен с физической или виртуальной памятью).

Практическое решение - иметь подпись действительности в указанных объектах. Создайте оболочку malloc (), которая выделяет запрошенный размер блока плюс размер структуры подписи, создает структуру подписи в начале блока, но возвращает указатель на место после подписи. Затем вы можете создать функцию проверки, которая принимает указатель, использует отрицательное смещение для получения структуры достоверности и проверяет ее. Вам, конечно, понадобится соответствующая оболочка free (), чтобы сделать блок недействительным, перезаписав подпись действительности, и чтобы выполнить освобождение от истинного начала выделенного блока.

В качестве структуры действительности вы можете использовать размер блока и его дополнение. Таким образом, у вас не только есть способ проверки блока (XOR двух значений и сравнение с нулем), но и информация о размере блока.

Трекер указателя, отслеживает и проверяет действительность указателя

Создать память int * ptr = malloc (sizeof (int) * 10);

Добавить адрес указателя в трекер Ptr (& ptr);

Проверить указатели на сбой PtrCheck ();

И освободите все трекеры в конце вашего кода

В компьютерах почти никогда не бывает «никогда». Кроссплатформенность намного превосходит ожидания. Спустя 25 лет я работал над сотнями проектов, каждый из которых предполагал кроссплатформенность, но это так и не было реализовано.

Очевидно, что переменная в стеке будет указывать на область в стеке, которая является почти линейной. Кросс-платформенные сборщики мусора работают, отмечая верх или (низ) стека, вызывая небольшую функцию, чтобы проверить, растет ли стек вверх или вниз, а затем проверять указатель стека, чтобы узнать, насколько велик стек. Это ваш диапазон. Я не знаю машины, которая не реализует стек таким образом (растет или опускается).

Вы просто проверяете, находится ли адрес нашего объекта или указателя между верхом и низом стека. Вот как вы узнаете, является ли это переменной стека.

Слишком просто. Эй, это правильный c ++? Нет. Правильно важно? За 25 лет я увидел гораздо больше оценок правильных. Что ж, скажем так: если вы взламываете, вы не занимаетесь настоящим программированием, вы, вероятно, просто переопределяете то, что уже было сделано.

Есть указатель на объект, и он указывает на выделенную память через new. Можно спокойно вызывать для него delete? А если он не указывает на созданный в динамической памяти объект, то delete ошибку выполнения не вызовет? Как проверить, что объект указывает на выделенный участок который можно удалить? Просто если if(object != null) или (object) и потом аж вызывать delete? Или delete и сам может определить, есть ли что-то для удаления или нет?


30.6k 18 18 золотых знаков 75 75 серебряных знаков 98 98 бронзовых знаков 513 3 3 золотых знака 8 8 серебряных знаков 18 18 бронзовых знаков

Если указатель указывает на объект, выделенный при помощи new , и для него ещё ни разу не был вызван delete , то вызывать delete можно. Также можно вызывать delete по нулевому указателю (сколько угодно раз).

В любом другом случае — нельзя.

Ответственность за соблюдение этого условия лежит на программисте, ни компилятор, ни runtime-библиотека за вас этого не проверят.

Хуже того, несоблюдение этого условия есть undefined behaviour: программа считается необратимо сломанной, и может вылететь, а может (что гораздо хуже) не вылететь, а работать как будто бы всё в порядке.

Обычно помогает простой принцип: если вы сами выделили объект, вам его и удалять. Если к вам он пришёл со стороны, как правило ответственность за удаление лежит на том, кто его создавал.

Но возможны и исключения, когда вместе с объектом вам передаётся и обязанность его удалить. Смотрите документацию функций, которые вы вызываете.

С другой стороны, в современном C++ работа с «сырыми» указателями происходит довольно редко. Обычно между частями программы передаётся «умный» указатель (например, shared_ptr ), при этом проблема снимается, поскольку объект по указателю автоматически удаляется, когда нужно.


202k 25 25 золотых знаков 273 273 серебряных знака 501 501 бронзовый знак

Стандартный вариант использования new/delete включает в себя манипуляции с установкой указателя при его инициализации в nullptr и после вызова delete в nullptr. Таким образом, если указатель указывает в nullptr, это является сигналом, что он не указывает на участок памяти. В противном случае указывает. Но это, скорее, вопрос стиля.

Определить по указателю, указывает ли он на начало выделенной памяти через new или куда-то еще - невозможно (теоретически возможно, зная реализацию new и malloc, но это костыли и танцы с бубном, которые нельзя делать никогда).

Хорошим сегодня стилем является использование vector< char > в качестве переменной для хранения буффера. Стандарт C++ гарантирует, что будет выделен один блок памяти. При этом работа через vector освобождает программиста от заботы об освобождении памяти. Кроме того, освобождает от проблем с exception как при выделении памяти, так и в случае, если exception будет выброшен позже, при сворачивании стэка выделенная под вектор память корректно освободится, что не произойдет, если она выделена через new. Издержки производительности не должны волновать до этапа оптимизации, и должны быть очень незначительными.

Кроме того, важный момент про использование сторонних библиотек: поскольку варианты реализации alloc/free и new/delete могут разниться от компилятора к компилятору, удалять можно лишь то, что вы сами создали. Никакая память, выделенная в уже собранной библиотеке не должна освобождаться в ее клиенте и наоборот. UPD: в большей степени это относится к ОС семейства Windows.


Сегодня я хочу немного приоткрыть свет над тем, как бороться с утечкой памяти в Си или С++.

На Хабре уже существует две статьи, а именно: Боремся с утечками памяти (C++ CRT) и Утечки памяти в С++: Visual Leak Detector. Однако я считаю, что они недостаточно раскрыты, или данные способы могут не дать нужного вам результата, поэтому я хотел бы по возможности разобрать всем доступные способы, дабы облегчить вам жизнь.

Windows — разработка
Начнем с Windows, а именно разработка под Visual Studio, так как большинство начинающих программистов пишут именно под этой IDE.


Для понимания, что происходит, прикладываю реальный пример:

А также есть Student.h и Student.c в котором объявлены структуры и функции.

Есть задача: продемонстрировать отсутствие утечек памяти. Первое, что приходит в голову — это CRT. Тут все достаточно просто.

А перед return 0 нужно прописать это: _CrtDumpMemoryLeaks(); .

В итоге, в режиме Debug, студия будет выводить это:

Супер! Теперь вы знаете, что у вас утечка памяти. Теперь нужно устранить это, поэтому необходимо просто узнать, где мы забываем очистить память. И вот тут возникает проблема: а где, собственно, выделялась эта память?

После того, как я повторил все шаги, я выяснил, что память теряется где-то здесь:

Но как так — то? Я же все освобождаю? Или нет?

И тут мне сильно не хватало Valgrind, с его трассировкой вызовов.

В итоге, после 15 минут прогугливания, я нашел аналог Valgrind — Visual Leak Detector. Это сторонняя библиотека, обертка над CRT, которая обещала показывать трассировку! Это то, что мне необходимо.

Чтобы её установить, необходимо перейти в репозиторий и в assets найти vld-2.5.1-setup.exe

Правда, последнее обновление было со времен Visual Studio 2015, но оно работает и с Visual Studio 2019. Установка стандартная, просто следуйте инструкциям.

Преимущество этой утилиты заключается в том, что можно не запускать в режиме debug (F5), ибо все выводится в консоль. В самом начале будет выводиться это:

И вот, что будет выдавать при утечке памяти:

Вот, я вижу трассировку! Так, а где строки кода? А где названия функций?


Ладно, обещание сдержали, однако это не тот результат, который я хотел.

Остается один вариант, который я нашел в гугле: моментальный снимок памяти. Он делается просто: в режиме debug, когда доходите до return 0, необходимо в средстве диагностики перейти во вкладку "Использование памяти" и нажать на "Сделать снимок". Возможно, у вас будет отключена эта функция, как на первом скриншоте. Тогда необходимо включить, и перезапустить дебаг.



После того, как вы сделали снимок, у вас появится под кучей размер. Я думаю, это сколько всего было выделено памяти в ходе работы программы. Нажимаем на этот размер. У нас появится окошко, в котором будут содержаться объекты, которые хранятся в этой куче. Чтобы посмотреть подробную информацию, необходимо выбрать объект и нажать на кнопку "Экземпляры представления объекта Foo".



Да! Это победа! Полная трассировка с местоположением вызовов! Это то, что было необходимо изначально.


Linux — разработка
Теперь, посмотрим, что творится в Linux.

В Linux существует утилита valgrind. Чтобы установить valgrind, необходимо в консоли прописать sudo apt install valgrind (Для Debian-семейства).

Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:

Скомпилировав программу с помощью CLang, мы получаем .out файл, который мы подкидываем valgrind'у.

С помощью команды valgrind ./a.out . Как работает valgrind, думаю, есть смысл описать в отдельной статье, а сейчас, как выполнится программа, valgrind выведет это:

Таким образом, valgrind пока показывает, сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо прописать --leak-check=full , и тогда, valgrind, помимо выше описанного, выведет это:

Конечно, тут не указана строка, однако уже указана функция, что не может не радовать.

Есть альтернативы valgrind’у, такие как strace или Dr.Memory, но я ими не пользовался, да и они применяется в основном там, где valgrind бессилен.

Выводы

Я рад, что мне довелось столкнуться с проблемой поиска утечки памяти в Visual Studio, так как я узнал много новых инструментов, когда и как ими пользоваться и начал разбирать, как работают эти инструменты.

В предыдущей главе уже обсуждалось, что локальные переменные кладутся на стек и существую до тех пор, пока мы не вышли из функции. С одной стороны, это позволяет автоматически очищать память, с другой стороны, существует необходимость в переменных, время жизни которых мы можем контролировать самостоятельно. Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен. Для этого используется выделение памяти на куче. Недостатков у такого подхода два: во-первых, память необходимо вручную очищать, во-вторых, выдеение памяти – достаточно дорогостоящая операция.

Для выделения памяти на куче в си используется функция malloc (memory allocation) из библиотеки stdlib.h

Функция выделяет size байтов памяти и возвращает указатель на неё. Если память выделить не удалось, то функция возвращает NULL. Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу. Например, создадим указатель, после этого выделим память размером в 100 байт.

После того, как мы поработали с памятью, необходимо освободить память функцией free.
Используя указатель, можно работать с выделенной памятью как с массивом. Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем его квадратами чисел по порядку. После этого выводим и удаляем массив.

Здесь (int *) – приведение типов. Пишем такой же тип, как и у указателя.
size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива.
После этого работаем с указателем точно также, как и с массивом. В конце не забываем удалять выделенную память.

Теперь представим на рисунке, что у нас происходило. Пусть мы ввели число 5.

Функция malloc выделила память на куче по определённому адресу, после чего вернула его. Теперь указатель p хранит этот адрес и может им пользоваться для работы. В принципе, он может пользоваться и любым другим адресом.
Когда функция malloc "выделяет память", то она резервирует место на куче и возвращает адрес этого участка. У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё. Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим. Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша. При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.

Это очень похоже на съём номера в отеле. Мы получаем дубликат ключа от номера, живём в нём, а потом сдаём комнату обратно. Но дубликат ключа у нас остаётся. Всегда можно зайти в этот номер, но в нём уже кто-то может жить. Так что наша обязанность – удалить дубликат.

Иногда думают, что происходит "создание" или "удаление" памяти. На самом деле происходит только перераспределение ресурсов.

Освобождение памяти с помощью free

Т еперь рассмотри, как происходит освобождение памяти. Переменная указатель хранит адрес области памяти, начиная с которого она может им пользоваться. Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?

  • 1. Можно создать карту, в которой будет храниться размер выделенного участка. Каждый раз при освобождении памяти компьютер будет обращаться к этим данным и получать нужную информацию.
  • 2. Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти функция free "подсматривает", сколько памяти необходимо удалить.

Работа с двумерными и многомерными массивами

Д ля динамического создания двумерного массива сначала необходимо создать массив указателей, после чего каждому из элементов этого массива присвоить адрес нового массива.
Для удаления массива необходимо повторить операцию в обратном порядке - удалить сначала подмассивы, а потом и сам массив указателей.

  • 1. Создавать массивы "неправильной формы", то есть массив строк, каждая из которых имеет свой размер.
  • 2. Работать по отдельности с каждой строкой массива: освобождать память или изменять размер строки.

Создадим "треугольный" массив и заполним его значениями

Чтобы создать трёхмерный массив, по аналогии, необходимо сначала определить указатель на указатель на указатель, после чего выделить память под массив указателей на указатель, после чего проинициализировать каждый из массивов и т.д.

calloc

Ф ункция calloc выделяет n объектов размером m и заполняет их нулями. Обычно она используется для выделения памяти под массивы. Синтаксис

realloc

Е щё одна важная функция – realloc (re-allocation). Она позволяет изменить размер ранее выделенной памяти и получает в качестве аргументов старый указатель и новый размер памяти в байтах:

Функция realloc может как использовать ранее выделенный участок памяти, так и новый. При этом не важно, меньше или больше новый размер – менеджер памяти сам решает, где выделять память.
Пример – пользователь вводит слова. Для начала выделяем под слова массив размером 10. Если пользователь ввёл больше слов, то изменяем его размер, чтобы хватило места. Когда пользователь вводит слово end, прекращаем ввод и выводим на печать все слова.

Хочу обратить внимание, что мы при выделении памяти пишем sizeof(char*), потому что размер указателя на char не равен одному байту, как размер переменной типа char.

Ошибки при выделении памяти

1. Бывает ситуация, при которой память не может быть выделена. В этом случае функция malloc (и calloc) возвращает NULL. Поэтому, перед выделением памяти необходимо обнулить указатель, а после выделения проверить, не равен ли он NULL. Так же ведёт себя и realloc. Когда мы используем функцию free проверять на NULL нет необходимости, так как согласно документации free(NULL) не производит никаких действий. Применительно к последнему примеру:

Хотелось бы добавить, что ошибки выделения памяти могут случиться, и просто выходить из приложения и выкидывать ошибку плохо. Решение зависит от ситуации. Например, если не хватает памяти, то можно подождать некоторое время и после этого опять попытаться выделить память, или использовать для временного хранения файл и переместить туда часть объектов. Или выполнить очистку, сократив используемую память и удалив ненужные объекты.

2. Изменение указателя, который хранит адрес выделенной области памяти. Как уже упоминалось выше, в выделенной области хранятся данные об объекте - его размер. При удалении free получает эту информацию. Однако, если мы изменили указатель, то удаление приведёт к ошибке, например

Таким образом, если указатель хранит адрес, то его не нужно изменять. Для работы лучше создать дополнительную переменную указатель, с которой работать дальше.

3. Использование освобождённой области. Почему это работает в си, описано выше. Эта ошибка выливается в другую – так называемые висячие указатели (dangling pointers или wild pointers). Вы удаляете объект, но при этом забываете изменить значение указателя на NULL. В итоге, он хранит адрес области памяти, которой уже нельзя воспользоваться, при этом проверить, валидная эта область или нет, у нас нет возможности.

Эта программа отработает и выведет мусор, или не мусор, или не выведет. Поведение не определено.

Если же мы напишем

то программа выкинет исключение. Это определённо лучше, чем неопределённое поведение. Если вы освобождаете память и используете указатель в дальнейшем, то обязательно обнулите его.

4. Освобождение освобождённой памяти. Пример

Здесь дважды вызывается free для переменной a. При этом, переменная a продолжает хранить адрес, который может далее быть передан кому-нибудь для использования. Решение здесь такое же как и раньше - обнулить указатель явно после удаления:

5. Одновременная работа с двумя указателями на одну область памяти. Пусть, например, у нас два указателя p1 и p2. Если под первый указатель была выделена память, то второй указатель может запросто скомпрометировать эту область:

Рассмотрим код ещё раз.

Теперь оба указателя хранят один адрес.

А вот здесь происходит непредвиденное. Мы решили выделить под p2 новый участок памяти. realloc гарантирует сохранение контента, но вот сам указатель p1 может перестать быть валидным. Есть разные ситуации. Во-первых, вызов malloc мог выделить много памяти, часть которой не используется. После вызова ничего не поменяется и p1 продолжит оставаться валидным. Если же потребовалось перемещение объекта, то p1 может указывать на невалидный адрес (именно это с большой вероятностью и произойдёт в нашем случае). Тогда p1 выведет мусор (или же произойдёт ошибка, если p1 полезет в недоступную память), в то время как p2 выведет старое содержимое p1. В этом случае поведение не определено.

Два указателя на одну область памяти это вообще-то не ошибка. Бывают ситуации, когда без них не обойтись. Но это очередное минное поле для программиста.

Различные аргументы realloc и malloc.

При вызове функции malloc, realloc и calloc с нулевым размером поведение не определено. Это значит, что может быть возвращён как NULL, так и реальный адрес. Им можно пользоваться, но к нему нельзя применять операцию разадресации.
Вызов realloc(NULL, size_t) эквиваленте вызову malloc(size_t).
Однако, вызов realloc(NULL, 0) не эквивалентен вызову malloc(0) :) Понимайте это, как хотите.

Примеры

1. Простое скользящее среднее равно среднему арифметическому функции за период n. Пусть у нас имеется ряд измерений значения функции. Часто эти измерения из-за погрешности "плавают" или на них присутствуют высокочастотные колебания. Мы хотим сгладить ряд, для того, чтобы избавиться от этих помех, или для того, чтобы выявить общий тренд. Самый простой способ: взять n элементов ряда и получить их среднее арифметическое. n в данном случае - это период простого скользящего среднего. Так как мы берём n элементов для нахождения среднего, то в результирующем массиве будет на n чисел меньше.

Это простой пример. Большая его часть связана со считыванием данных, вычисление среднего всего в девяти строчках.

2. Сортировка двумерного массива. Самый простой способ сортировки - перевести двумерный массив MxN в одномерный размером M*N, после чего отсортировать одномерный массив, а затем заполнить двумерный массив отсортированными данными. Чтобы не тратить место под новый массив, мы поступим по-другому: если проходить по всем элементам массива k от 0 до M*N, то индексы текущего элемента можно найти следующим образом:
j = k / N;
i = k - j*M;
Заполним массив случайными числами и отсортируем

3. Бином Ньютона. Создадим треугольную матрицу и заполним биномиальными коэффициентами

Если Вы желаете изучать этот материал с преподавателем, советую обратиться к репетитору по информатике

email

Всё ещё не понятно? – пиши вопросы на ящик

Читайте также: