[recovery mode] О динамической памяти, или когда компилятор молчит

image ++recoveryModePublicationCount; // счетчик увеличивается уже n-й раз Дорогой читатель, если ты только начинаешь изучать С (не С++) или хочешь немного систематизировать знания (из уровня прописных истин) тогда эта статья возможно лишит тебя счастья прострелить себе ногу пару раз и будет полезной.

Правила наименований По этому поводу написано много статей и книг, но я еще раз повторюсь, давайте переменным и функциям адекватные имена, которые отображают их сущность, а не набор букв, как а, b, c, i. Лучше array, iterator, pointer, index и т.д. и читабельность и анализ кода на ошибки вырастет в разы просто от того, что вы понимаете, что написали вы, или кто-то другой.

Размерность массивов Известно, что вся динамическая память в С выделяется с помощью трех функций из stdlib — malloc, calloc, realloc. Основной проблемой тут становится факт, что все выделяемые размеры памяти считаются в байтах, а не в количествах элементов.И если начинать изучать С с более высокоуровневых языков, то обычно очень часто про это можно забыть, и потом не понятно, почему при вызове realloc массива, стираются данные в m-го поля в n-й структуре.

Типичный пример того, как делать нельзя:

int *array = malloc (10); И тут понимаешь, что при записи в лучшем случае в 3-ю переменную (array[3] = 1;) ты получаешь SIGSEGV, и не понимаешь, что произошло.

Всегда выделение должно происходить по следующей схеме:

Type *array = malloc (sizeof (Type) * arraySize); Т.е. правильно

int *array = malloc (sizeof (int) * 10); Пример выше выделяет память на 10 элементов типа int в куче.Realloc Тут же возникает аналогичная проблема в realloc«e, где размер точно так же не умножается на sizeof (type).Почти правильная сигнатура:

realloc (somePtr, sizeof (type) * newArrayCount); Еще проблема realloc«a — в том, что многие забывают, что realloc может полностью перенести данный ему указатель в совсем другое место. Отсюда, необходимость принимать значение указателя, которое возвращает realloc:

newPtr = realloc (somePtr, sizeof (type) * newArrayCount); При небольших изменениях размера newPtr и somePtr обычно совпадают, но true debug начинается тогда, когда они отличаются, и опять при попытке записи получаем SIGSEGV.

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

#define arraySize (className, size) (sizeof (className) * size) таким образом создание массива выглядит в форме:

int *first = malloc (arraySize (int, 10)); , а перераспределение как:

int *second = realloc (first, arraySize (int, 20)); При этом, если память переносится т.е. second!= first, то все элементы из first копируются в second (), а first перестает существовать и стает невалидным указателем (но очень часто не nil, так что зануляйте указатели после free, для повторного использования).

Я для себя сделал еще два простых макро для аллокации одного обьекта и массива обьектов:

#define allocator (className) malloc (sizeof (className)) #define arrayAllocator (className, size) malloc (sizeof (className) * (size)) выглядит довольно неплохо, например:

someStruct *temp = allocator (someStruct); или

someStruct *array = arrayAllocator (someStruct, 42); Null pointers Если долго программировать на С++, и вернутся в С, то можно открыть много нового для себя. Например то, что в C нету обьектного nullPtr, который выдает ошибку компиляции. В стандарте C определено, что null pointer — это указатель, который не указывает ни на какой обьект в программе, и определяется как макро

#define NULL ((void*)0)  — кастованой к типу указателей численной константы 0. Это в лучшем случае выдает warning о несоответствии типов. Всегда читайте warnings компилятора, если их нет — хорошо, если есть возможно один из них представляет корень зла.Из синтаксического сахара, обычно использую пару штук для указателей: typedef void* pointer; #define nil ((pointer) 0) По моему pointer выглядит более концептуально чем void*.

Проверка на NULL Обычно, когда работаешь в прикладной сфере программирования, с exception, не задумываешься об ошибках вообще. Когда проектировали unix, решили, что-бы не убивать программу целиком, при недостатке памяти возвращаем нулевой указатель, как просьбу немного почистить память. То есть malloc, calloc, realloc, возможно могут просто не выделить память (ну кончилась озу, что поделать), тогда они возвращают NULL (а я далее буду писать nil, т.к. мне он более мил)

int *array = arrayAllocator (int, 10); if (a!= nil) { array[9] = 9; doSome (array); } else { someCleanup (); } если же попробовать разыменовать невалидный указатель (nil так же является невалидным) то программа получает SIGSEGV, или еще что-нибудь более интересное типа general protection fault. И тогда пограмма падает полностью. Если обрабатывать nil, то можно успеть задампить критичные даные например.

Приведение типов Очень часто в примерах можно увидеть строки

int *array = (int*)malloc (sizeof (int) * 10); Так вот, там приведение типов не нужно, т.к. malloc возвращает сырой указатель (void*), который автоматически является прообразом всех указателей. Любой указатель в С, например, int * — это тот же void* только с маленькой отметкой компилятора, о том, что его можно разыменовать в данный тип. Пусть даже это будет SomeStruct* — это все равно 4-х или 8-ми байтный void*. Проблемы с приведением типов (кастингом) возникают только тогда, когда указатель надо разыменовать и он разыменоввуется некорректно. Поэтому никогда не приводите типы разных структур друг к другу, если нет уверенности, как структуры распологаются в памяти. Если же она есть — использутеся direct cast, типа: SomeStruct * arrayOfStructs = (SomeStruct*)arrayOfInts; И тогда компилятор не выдаст даже warning, т.к. считает что вы понимаете, что вы делаете.

Memory management level advanced (custom allocators) Крайне часто программисты становятся ленивыми, и забывают чистить за собой память, для этого придумали кучу интсрументов, как reference counting, RAII, но в C этого нету, по этому используются кастомные аллокаторы памяти, с усиленым контролем. Например ARP Pool или RPool, а на системном уровне обычно это ASLR — песочницы которые работают немного по другому принципу, но все же их можно отнести к custom allocators. В openBSD, или OSX например.

metaAlloc Еще одно расширение RPool (описанным в ссылке выше) — metaAlloc — позволяет вести записи во время выделения памяти, чтобы понимать, где конкретно остались не освобожденные ресурсы.

Например

enablePool (RPool);

Int *array = metaAlloc (arraySize (int, 10), «Array for storing some indexes»); double *somePointer = metaAlloc (arraySize (double, 1), «Some double pointer»);

printerOfRAutoPool (RPool); deleter (RPool, RAutoPool); Выделенная память, если не освобождена то показывается в конце работы программы с подсказкой:

0 — 0×100104b00 [s: 8] (tuid: 149303) — Some double pointer 1 — 0×100105170 [s: 40] (tuid: 149303) — Int Array for storing some indexes И сразу понятно, где конкретно не очищается память. В release моде, для того чтобы не тратить дополнительных ресурсов (память и время выполнения), просто отключается флаг, и metaAlloc превращается в обычный malloc.

Выводы Всегда необходимо следить за размерностью выделяемой памяти, и помнить, что она в байтах. Сохранять результирующий указатель, который возращает realloc. Проверять указатели на NULL. Если необходимо отпарсить сложный открытый код на ошибки динамической памяти — использовать кастомные аллокаторы или чекеры.

P.S. Не претендую на гениальность, но возможно статья будет кому-нибудь полезной. Если у кого возникнут вопросы или идеи по использованию RPool, всегда буду рад помочь, чем только смогу.

© Habrahabr.ru