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

Теги: Си память, malloc, calloc, realloc, free, Ошибки выделения памяти, Висячие указатели, Динамические массивы, Многомерные динамические массивы.

malloc

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

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

void * malloc(size_t size);

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	int *p = NULL;
	p = (int*) malloc(100);

	free(p);
}

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	const int maxNumber = 100;
	int *p = NULL;
	unsigned i, size;

	do {
		printf("Enter number from 0 to %d: ", maxNumber);
		scanf("%d", &size);
		if (size < maxNumber) {
			break;
		}
	} while (1);

	p = (int*) malloc(size * sizeof(int));

	for (i = 0; i < size; i++) {
		p[i] = i*i;
	}

	for (i = 0; i < size; i++) {
		printf("%d ", p[i]);
	}

	_getch();
	free(p);
}

Разбираем код

p = (int*) malloc(size * sizeof(int));

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

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

Выделение памяти.

Выделение памяти.

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

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

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

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

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

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

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

Функция free освобождает память, но при этом не изменяет значение указателя, о чём нужно помнить.

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

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

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define COL_NUM 10
#define ROW_NUM 10

void main() {
	float **p = NULL;
	unsigned i;

	p = (float**) malloc(ROW_NUM * sizeof(float*));
	for (i = 0; i < ROW_NUM; i++) {
		p[i] = (float*) malloc(COL_NUM * sizeof(float));
	}

	//Здесь какой-то важный код
	for (i = 0; i < ROW_NUM; i++) {
		free(p[i]);
	}
	free(p);
}

Сначала мы создаём массив указателей, а после этого каждому элементу этого массива присваиваем адрес вновь созданного массива. Это значит, что можно

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

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int **A;
	int i, j;
	A = (int**) malloc(SIZE * sizeof(int*));

	for (i = 0; i < SIZE; i++) {
		A[i] = (int*) malloc((i + 1) * sizeof(int));
	}

	for (i = 0; i < SIZE; i++) {
		for (j = i; j > 0; j--) {
			A[i][j] = i * j;
		}
	}

	for (i = 0; i < SIZE; i++) {
		for (j = i; j > 0; j--) {
			printf("%d ", A[i][j]);
		}
		printf("\n");
	}

	for (i = SIZE-1; i > 0; i--) {
		free(A[i]);
	}
	free(A);
	
	_getch();
}

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

calloc

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

void* calloc(size_t num, size_t size);

realloc

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

void* realloc(void* ptr, size_t size)

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TERM_WORD "end"
#define SIZE_INCREMENT 10

void main() {
	//Массив указателей на слова
	char **words;
	//Строка, которая используется для считывания введённого пользователем слова
	char buffer[128];
	//Счётчик слов
	unsigned wordCounter = 0;
	//Длина введённого слова
	unsigned length;
	//Размер массива слов. Для уменьшения издержек на выделение памяти 
	//каждый раз будем увеличивать массив слов не на одно значение, а на
	//SIZE_INCREMENT слов
	unsigned size = SIZE_INCREMENT;
	int i;

	//Выделяем память под массив из size указателей
	words = (char**) malloc(size*sizeof(char*));
	do {
		printf("%d: ", wordCounter);
		scanf("%127s", buffer);
		//Функция strcmp возвращает 0, если две строки равны
		if (strcmp(TERM_WORD, buffer) == 0) {
			break;
		}
		//Определяем длину слова
		length = strlen(buffer);
		//В том случае, если введено слов больше, чем длина массива, то
		//увеличиваем массив слов
		if (wordCounter >= size) {
			size += SIZE_INCREMENT;
			words = (char**) realloc(words, size*sizeof(char*));
		}
		//Выделяем память непосредственно под слово
		//на 1 байт больше, так как необходимо хранить терминальный символ
		words[wordCounter] = (char*) malloc(length + 1);
		//Копируем слово из буффера по адресу, который 
		//хранится в массиве указателей на слова
		strcpy(words[wordCounter], buffer);
		wordCounter++;
	} while(1);

	for (i = 0; i < wordCounter; i++) {
		printf("%s\n", words[i]);
	}
	_getch();

	for (i = 0; i < wordCounter; i++) {
		free(words[i]);
	}
	free(words);
}

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

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

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TERM_WORD "end"
#define SIZE_INCREMENT 10

void main() {
	char **words;
	char buffer[128];
	unsigned wordCounter = 0;
	unsigned length;
	unsigned size = SIZE_INCREMENT;
	int i;

	if (!(words = (char**) malloc(size*sizeof(char*)))) {
		printf("Error: can't allocate memory");
		_getch();
		exit(1);
	}

	do {
		printf("%d: ", wordCounter);
		scanf("%127s", buffer);
		
		if (strcmp(TERM_WORD, buffer) == 0) {
			break;
		}

		length = strlen(buffer);

		if (wordCounter >= size) {
			size += SIZE_INCREMENT;
			if (!(words = (char**) realloc(words, size*sizeof(char*)))) {
				printf("Error: can't reallocate memory");
				_getch();
				exit(2);
			}
		}

		if (!(words[wordCounter] = (char*)malloc(length + 1))) {
			printf("Error: can't allocate memory");
			_getch();
			exit(3);
		}

		strcpy(words[wordCounter], buffer);
		wordCounter++;
	} while(1);

	for (i = 0; i < wordCounter; i++) {
		printf("%s\n", words[i]);
	}
	_getch();

	for (i = 0; i < wordCounter; i++) {
		free(words[i]);
	}
	free(words);
}

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

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

void main() {
	int *p = NULL;
	if (!(p = (int*) malloc(100 * sizeof(int)))) {
		printf("Error");
		exit(1);
	}
	//Изменили указатель
	p++;
	//Теперь free не может найти метаданные об объекте
	free(p);
	//На некоторых компиляторах ошибки не будет
	_getch();
}

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

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int *p = NULL;
	int i;

	p = (int*) malloc(SIZE * sizeof(int));
	for (i = 0; i < SIZE; i++) {
		p[i] = i;
	}
	free(p);
	for (i = 0; i < SIZE; i++) {
		printf("%i ", p[i]);
	}
	_getch();
}

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

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

free(p);
p = NULL;

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

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

#include <conio.h>
#include <stdio.h>

void main() {
	int *a, *b;
	a = (int*) malloc(sizeof(int));
	free(a);
	b = (int*) malloc(sizeof(int));
	free(a);
	free(b);
	_getch();
}

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

#include <conio.h>
#include <stdio.h>

void main() {
	int *a, *b;
	a = (int*) malloc(sizeof(int));
	free(a);
	a = NULL;
	b = (int*) malloc(sizeof(int));
	free(a);//вызов free(NULL) ничего не делает
	free(b);
	b = NULL;
	_getch();
}

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

void main() {
	int *p1 = NULL;
	int *p2 = NULL;
	size_t i;

	p1 = malloc(sizeof(int) * SIZE);
	p2 = p1;

	for (i = 0; i < SIZE; i++) {
		p1[i] = i;
	}
	p2 = realloc(p1, SIZE * 5000 * sizeof(int));
	for (i = 0; i < SIZE; i++) {
		printf("%d ", p1[i]);
	}
	printf("\n");
	for (i = 0; i < SIZE; i++) {
		printf("%d ", p2[i]);
	}
	_getch();
}

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

	int *p1 = NULL;
	int *p2 = NULL;
	size_t i;

	p1 = malloc(sizeof(int) * SIZE);
	p2 = p1;

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

	p2 = realloc(p1, SIZE * 5000 * sizeof(int));

А вот здесь происходит непредвиденное. Мы решили выделить под 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
чисел меньше.

Скользящее среднее.

Скользящее среднее.

Пусть есть ряд
1, 4, 4, 6, 7, 8, 9, 11, 12, 11, 15
Тогда если период среднего будет 3, то мы получим ряд
(1+4+4)/3, (4+4+6)/3, (4+6+7)/3, (6+7+8)/3, (7+8+9)/3, (8+9+11)/3, (9+11+12)/3, (11+12+11)/3, (12+11+15)/3
Видно, что сумма находится в «окне», которое скользит по ряду. Вместо того, чтобы каждый раз в цикле находить сумму, можно найти её для
первого периода, а затем вычитать из суммы крайнее левое значение предыдущего периода и прибавлять крайнее правое значение следующего.
Будем запрашивать у пользователя числа и период, а затем создадим новый массив и заполним его средними значениями.

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_INCREMENT 20

void main() {
	//Считанные числа
	float *numbers = NULL;
	//Найденные значения
	float *mean = NULL;
	float readNext;
	//Максимальный размер массива чисел
	unsigned maxSize = MAX_INCREMENT;
	//Количество введённых чисел
	unsigned curSize = 0;
	//Строка для считывания действия
	char next[2];
	//Шаг
	unsigned delta;
	//float переменная для хранения шага
	float realDelta;
	unsigned i, j;
	//Сумма чисел
	float sum;

	numbers = (float*) malloc(maxSize * sizeof(float));
	do {
		//Пока пользователь вводит строку, которая начинается с y или Y,
		//то продолжаем считывать числа
		printf("next? [y/n]: ");
		scanf("%1s", next);
		if (next[0] == 'y' || next[0] == 'Y') {
			printf("%d. ", curSize);
			scanf("%f", &readNext);
			if (curSize >= maxSize) {
				maxSize += MAX_INCREMENT;
				numbers = (float*) realloc(numbers, maxSize * sizeof(float));
			}
			numbers[curSize] = readNext;
			curSize++;
		} else {
			break;
		}
	} while(1);

	//Считываем период, он должен быть меньше, чем
	//количество элементов в массиве. Если оно равно,
	//то результатом станет среднее арифметическое всех введённых чисел
	do {
		printf("enter delta (>=%d): ", curSize);
		scanf("%d", &delta);
		if (delta <= curSize) {
			break;
		}
	} while(1);
	realDelta = (float) delta;

	//Находим среднее для первого периода
	mean = (float*) malloc(curSize * sizeof(float));
	sum = 0;
	for (i = 0; i < delta; i++) {
		sum += numbers[i];
	}

	//Среднее для всех остальных
	mean[0] = sum / delta;
	for (i = delta, j = 1; i < curSize; i++, j++) {
		sum = sum - numbers[j-1] + numbers[i];		
		mean[j] = sum / realDelta;
	}

	//Выводим. Чисел в массиве mean меньше на delta
	curSize = curSize - delta + 1;
	for (i = 0; i < curSize; i++) {
		printf("%.3f ", mean[i]);
	}

	free(numbers);
	free(mean);
	_getch();
}

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

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

j = k / N;
i = k - j*M;

Заполним массив случайными числами и отсортируем

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MAX_SIZE_X 20
#define MAX_SIZE_Y 20

void main() {
	int **mrx = NULL;
	int tmp;
	unsigned i, j, ip, jp, k, sizeX, sizeY, flag;

	printf("cols: ");
	scanf("%d", &sizeY);
	printf("rows: ");
	scanf("%d", &sizeX);

	//Если введённый размер больше MAX_SIZE_?, то присваиваем
	//значение MAX_SIZE_?
	sizeX = sizeX <= MAX_SIZE_X? sizeX: MAX_SIZE_X;
	sizeY = sizeY <= MAX_SIZE_Y? sizeY: MAX_SIZE_Y;

	//Задаём начальное значение для генератора псевдослучайных чисел
	srand(time(NULL));
	//Выделяем память под массив указателей
	mrx = (int**) malloc(sizeX * sizeof(int*));
	for (i = 0; i < sizeX; i++) {
		//Выделяем память под строку и сразу же заполняем элементы
		//случайными значениями
		mrx[i] = (int*) malloc(sizeY * sizeof(int));
		for (j = 0; j < sizeY; j++) {
			mrx[i][j] = rand();
		}
	}

	//Выводим массив
	for (i = 0; i < sizeX; i++) {
		for (j = 0; j < sizeY; j++) {
			printf("%6d ", mrx[i][j]);
		}
		printf("\n");
	}

	//Сортируем пузырьком, обходя все sizeX*sizeY элементы
	do {
		flag = 0;
		for (k = 1; k < sizeX * sizeY; k++) {
			//Вычисляем индексы текущего элемента
			j = k / sizeX;
			i = k - j*sizeX;
			//Вычисляем индексы предыдущего элемента
			jp = (k-1) / sizeX;
			ip = (k-1) - jp*sizeX;
			if (mrx[i][j] > mrx[ip][jp]) {
                tmp = mrx[i][j];
                mrx[i][j] = mrx[ip][jp];
                mrx[ip][jp] = tmp;
				flag = 1;
            }
		}
	} while(flag);

	printf("-----------------------\n");
	for (i = 0; i < sizeX; i++) {
		for (j = 0; j < sizeY; j++) {
			printf("%6d ", mrx[i][j]);
		}
		free(mrx[i]);
		printf("\n");
	}
	free(mrx);

	_getch();
}

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

#include <conio.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_BINOM_HEIGHT 20

void main() {
	int** binom = NULL;
	unsigned height;
	unsigned i, j;

	printf("Enter height: ");
	scanf("%d", &height);
	height = height <= MAX_BINOM_HEIGHT? height: MAX_BINOM_HEIGHT;

	binom = (int**) malloc(height * sizeof(int*));
	for (i = 0; i < height; i++) {
		binom[i] = (int*) malloc((i + 1) * sizeof(int));
	}

	binom[0][0] = 1;
	for (i = 1; i < height; i++) {
		binom[i][0] = binom[i][i] = 1;
		for (j = i - 1; j > 0; j--) {
			binom[i][j] = binom[i-1][j-1] + binom[i-1][j];
		}
	}

	for (i = 0; i < height; i++) {
		for (j = 0; j <= i; j++) {
			printf("%4d ", binom[i][j]);
		}
		free(binom[i]);
		printf("\n");
	}
	free(binom);

	_getch();
}

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

Q&A

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

Структура программы на си

malloc

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

Примечание. В статье под функцией malloc часто будет подразумеваться, что речь идёт не только именно об этой функции, но и о calloc, realloc, _aligned_malloc, _recalloc, strdup и так далее. Не хочется загромождать текст статьи, постоянно повторяя названия всех этих функций. Общее у них то, что они могут вернуть нулевой указатель.

malloc

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

К сожалению, многие программисты небрежно относятся к проверке указателей, а иногда сознательно не проверяют, удалось ли выделить память или нет. Их логика следующая:

Если функция malloc не смогла выделить память, то вряд ли моя программа продолжит функционировать должным образом. Скорее всего, памяти будет не хватать и для других операций, поэтому можно вообще не заморачиваться об ошибках выделения памяти. Первое же обращение к памяти по нулевому указателю приведёт к генерации Structured Exception в Windows, или процесс получит сигнал SIGSEGV, если речь идёт о Unix-подобных системах. В результате программа упадёт, что меня устраивает. Раз нет памяти, то и нечего мучаться. Как вариант, можно перехватить структурное исключение/сигнал и обрабатывать разыменовывания нулевого указателя более централизовано. Это удобнее, чем писать тысячи проверок.

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

Кстати, существует ещё одно оправдание разработчиков, почему они не проверяют, что вернула функция malloc. Функция malloc только резервирует память, но вовсе нет гарантии, что хватит физической памяти, когда мы начнём использовать выделенный буфер памяти. Поэтому, раз всё равно гарантии нет, то и проверять не надо. Например, именно так Carsten Haitzler, являющийся одним из разработчиков библиотеки EFL Core, объяснял, почему я насчитал более 500 мест в коде библиотеки, где отсутствуют проверки. Вот его комментарий к статье:

OK so this is a general acceptance that at least on Linux which was always our primary focus and for a long time was our only target, returns from malloc/calloc/realloc can’t be trusted especially for small amounts. Linux overcommits memory by default. That means you get new memory but the kernel has not actually assigned real physical memory pages to it yet. Only virtual space. Not until you touch it. If the kernel cannot service this request your program crashes anyway trying to access memory in what looks like a valid pointer. So all in all the value of checking returns of allocs that are small at least on Linux is low. Sometimes we do it… sometimes not. But the returns cannot be trusted in general UNLESS its for very large amounts of memory and your alloc is never going to be serviced — e.g. your alloc cannot fit in virtual address space at all (happens sometimes on 32bit). Yes overcommit can be tuned but it comes at a cost that most people never want to pay or no one even knows they can tune. Secondly, fi an alloc fails for a small chunk of memory — e.g. a linked list node… realistically if NULL is returned… crashing is about as good as anything you can do. Your memory is so low that you can crash, call abort() like glib does with g_malloc because if you can’t allocate 20-40 bytes… your system is going to fall over anyway as you have no working memory left anyway. I’m not talking about tiny embedded systems here, but large machines with virtual memory and a few megabytes of memory etc. which has been our target. I can see why PVS-Studio doesn’t like this. Strictly it is actually correct, but in reality code spent on handling this stuff is kind of a waste of code given the reality of the situation. I’ll get more into that later.

Приведённые рассуждения программистов являются неправильными, и ниже я подробно объясню почему. Но прежде надо ответить на вопрос: «а причём здесь Chromium?».

Chromium

Chromium здесь при том, что в используемых в нём библиотеках имеется не менее 70 ошибок, связанных с отсутствием проверки после вызова таких функций, как malloc, calloc, realloc. Да, в самом Chromium эти функции почти нигде не используются. В Chromium применяются только контейнеры или operator new. Однако, раз ошибки есть в используемых библиотеках, то значит, можно сказать, что они есть и в Chromium. Конечно, какие-то части библиотек могут не использоваться при работе Chromium, но определять это сложно и ненужно. Всё равно надо править все ошибки.

Я не буду приводить в статье множество фрагментов кода с ошибками, так как они однотипны. Приведу для примера только одну ошибку, обнаруженную в библиотеке Yasm:

static SubStr *
SubStr_new_u(unsigned char *s, unsigned int l)
{
    SubStr *r = malloc(sizeof(SubStr));
    r->str = (char*)s;
    r->len = l;
    return r;
}

Предупреждение PVS-Studio: V522 CWE-690 There might be dereferencing of a potential null pointer ‘r’. Check lines: 52, 51. substr.h 52

В коде нет никакой защиты от нулевого указателя. Другие подобные ошибки из Chromium и используемых библиотек я собрал вместе в файл и выложил их здесь: chromium_malloc.txt. В файле упоминаются 72 ошибки, но на самом деле их может быть больше. Как я писал в вводной статье, я просматривал отчёт только поверхностно.

Согласно Common Weakness Enumeration обнаруженные ошибки PVS-Studio классифицирует как:

  1. CWE-690: Unchecked Return Value to NULL Pointer Dereference.
  2. CWE-628: Function Call with Incorrectly Specified Arguments.
  3. CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer

Как видите, даже в таком высококачественном проекте как Chromium, можно заметить массу дефектов, связанных с отсутствием проверок. Теперь я перехожу к самому интересному и расскажу, почему проверки обязательно нужны.

Почему обязательно нужна проверка

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

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

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

В разных операционных системах для этих целей резервируется разное количество памяти. При этом в некоторых ОС это значение можно настраивать. Поэтому нет смысла называть какое-то конкретное число зарезервированных байт памяти. Но чтобы как-то сориентировать читателя, скажу, что в Linux системах типовым значением является 64 Кбайт.

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

Заваривайте кофе, мы начинаем!

Разыменовывание нулевого указателя — это неопределённое поведение

С точки зрения языка C и C++ разыменовывание нулевого указателя приводит к неопределенному поведению. Неопределённое поведение — это что угодно. Не думайте, что вы знаете, как будет вести себя программа, если произойдёт разыменовывание nullptr. Современные компиляторы занимаются серьезными оптимизациями, в результате чего бывает невозможно предсказать, как проявит себя та или иная ошибка в коде.

Неопределённое поведение программы — это очень плохо. Вы не должны допускать его в своём коде.

Не думайте, что сможете совладать с разыменовыванием нулевого указателя, используя обработчики структурных исключений (SEH в Windows) или сигналы (в UNIX-like системах). Раз разыменовывание нулевого указателя было, то работа программы уже нарушена, и может произойти что угодно. Давайте рассмотрим абстрактный пример, почему нельзя полагаться на SEH-обработчики и т.п.

size_t *ptr = (size_t *)malloc(sizeof(size_t) * N * 2);
for (size_t i = 0; i != N; ++i)
{
  ptr[i] = i;
  ptr[N * 2 - i - 1] = i;
}

Этот код заполняет массив от краёв к центру. К центру значения элементов увеличиваются. Это придуманный за 1 минуту пример, поэтому не гадайте, зачем такой массив кому-то нужен. Я и сам не знаю. Мне было важно, чтобы в соседних строках программы происходила запись в начало массива и куда-то в его конец. Такое иногда бывает нужно и в практических задачах, и мы рассмотрим реальный код, когда доберёмся до 4-ой причины.

Ещё раз внимательно посмотрим на эти две строки:

ptr[i] = i;
ptr[N * 2 - i - 1] = i;

С точки зрения программиста, в начале цикла произойдёт запись в элемент ptr[0], и возникнет структурное исключение/сигнал. Оно будет обработано, и всё будет хорошо.

Однако компилятор в каких-то целях оптимизации может переставить присваивания местами. Он имеет на это полное право. С точки зрения компилятора, если указатель разыменовывается, то он не может быть равен nullptr. Если указатель нулевой, то это неопределённое поведение, и компилятор не обязан думать о последствиях оптимизации.

Так вот, компилятор может решить, что в целях оптимизации выгоднее выполнить присваивания так:

ptr[N * 2 - i - 1] = i;
ptr[i] = i;

В результате в начале произойдет запись по адресу ((size_t *)nullptr)[N * 2 — 0 — 1]. Если значение N достаточно велико, то страница защиты в начале памяти будет «перепрыгнута» и значение переменной i может быть записано в какую-то ячейку, доступную для записи. В общем, произойдёт порча каких-то данных.

И только после этого будет выполнено присваивание по адресу ((size_t *)nullptr)[0]. Операционная система заметит попытку записи в контролируемую ею область и сгенерирует сигнал/исключение.

Программа может обработать это структурное исключение/сигнал. Но уже поздно. Где-то в памяти есть испорченные данные. Причем непонятно, какие данные испорчены и к каким последствиям это может привести!

Виноват ли компилятор, что поменял операции присваивания местами? Нет. Программист допустил разыменовывание нулевого указателя и тем самым ввёл программу в состояние неопределённого поведения. В данном конкретном случае неопределённое поведение программы будет заключаться в том, что где-то в памяти испорчены данные.

Вывод

Исходите из аксиомы: любое разыменовывание нулевого указателя — это неопределённое поведение программы. Не бывает «безобидного» неопределённого поведения. Любое неопределённое поведение недопустимо.

Не допускайте разыменовывания указателей, которые вернула функция malloc и её аналоги, без их предварительной проверки. Не полагайтесь на какие-то другие способы перехвата разыменовывания нулевого указателя. Следует использовать только старый добрый оператор if.

Разыменовывание нулевого указателя — это уязвимость

То, что некоторые разработчики вообще не считают за ошибку, другие воспринимают как уязвимость. Именно так обстоит дело с разыменовыванием нулевого указателя.

Одним нормально, если программа из-за разыменовывания нулевого указателя упадёт или если ошибка будет обработана каким-то общим способом с помощью перехвата сигнала/структурного исключения.

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

Не буду голословным. Есть, такая программа как Ytnef, предназначенная для декодирования TNEF потоков, например, созданных в Outlook. Так вот, разработчики приложения считают отсутствие проверки после вызова calloc уязвимостью CVE-2017-6298.

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

vl->data = calloc(vl->size, sizeof(WORD));
temp_word = SwapWord((BYTE*)d, sizeof(WORD));
memcpy(vl->data, &temp_word, vl->size);

Выводы

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

Однако, если вы разрабатываете какую-то библиотеку, то отсутствие проверок недопустимо! Вашей библиотекой могут пользоваться не только ленивые программисты, создающие безответственные приложения, типа игры Тетрис. Надо заботиться и о нормальных программистах, и нормальных программах.

Поэтому я идеологически не согласен, например, с Carsten Haitzler, что в библиотеке EFL Core нет проверок (подробности в статье). Это не позволяет построить на основе таких библиотек надёжные приложения.

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

Где гарантии, что будет разыменовывание именно нулевого указателя?

Те, кто ленится писать проверки, почему-то думают, что разыменование затрагивает именно нулевые указатели. Да, часто именно так и бывает. Но может ли поручиться программист за код всего приложения? Уверен, что нет.

Сейчас я на практических примерах покажу, что я имею в виду. Возьмём, например, код из библиотеки LLVM-subzero, которая используется в Chromium. Если честно, я теряюсь в догадках, какая связь между проектом Chromium и LLVM, но она есть.

void StringMapImpl::init(unsigned InitSize) {
  assert((InitSize & (InitSize-1)) == 0 &&
         "Init Size must be a power of 2 or zero!");
  NumBuckets = InitSize ? InitSize : 16;
  NumItems = 0;
  NumTombstones = 0;
  
  TheTable = (StringMapEntryBase **)
             calloc(NumBuckets+1,
                    sizeof(StringMapEntryBase **) + 
                    sizeof(unsigned));

  // Allocate one extra bucket, set it to look filled
  // so the iterators stop at end.
  TheTable[NumBuckets] = (StringMapEntryBase*)2;
}

Предупреждение PVS-Studio: V522 CWE-690 There might be dereferencing of a potential null pointer ‘TheTable’. Check lines: 65, 59. stringmap.cpp 65

Сразу после выделения буфера памяти происходит запись в ячейку TheTable[NumBuckets]. Если значение переменной NumBuckets достаточно большое, то мы испортим какие-то данные с непредсказуемыми последствиями. После такой порчи вообще нет смысла рассуждать, как будет работать программа. Могут последовать самые неожиданнейшие последствия.

Аналогичные опасные присваивания я вижу ещё в двух местах этого проекта:

  • V522 CWE-690 There might be dereferencing of a potential null pointer ‘Buckets’. Check lines: 219, 217. foldingset.cpp 219
  • V769 CWE-119 The ‘NewTableArray’ pointer in the ‘NewTableArray + NewSize’ expression could be nullptr. In such case, resulting value will be senseless and it should not be used. Check lines: 218, 216. stringmap.cpp 218

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

Продолжу заочную дискуссию с Carsten Haitzler. Он утверждает, что они понимают, что делают, когда не проверяют результат вызова функции malloc. Нет, не понимают. Давайте взглянем, например, на вот такой фрагмент кода из библиотеки EFL:

static void
st_collections_group_parts_part_description_filter_data(void)
{
  ....
   filter->data_count++;
   array = realloc(filter->data,
     sizeof(Edje_Part_Description_Spec_Filter_Data) *
     filter->data_count);
   array[filter->data_count - 1].name = name;
   array[filter->data_count - 1].value = value;
   filter->data = array;
}

Предупреждение PVS-Studio: V522 There might be dereferencing of a potential null pointer ‘array’. edje_cc_handlers.c 14249

Примечание. Я использую старые исходники EFL Core Libraries, которые остались у меня со времён написания статьи про эту библиотеку. Поэтому код или номера строк могут уже не соответствовать тому, что есть сейчас. Однако это не важно для повествования.

Перед нами типовая ситуация: в буфере не хватает свободного места для хранения данных, и его следует увеличить. Для увеличения размера буфера используется функция realloc, которая может вернуть NULL.

Если это произойдёт, то вовсе не обязательно возникнет структурное исключение/сигнал из-за разыменовывания нулевого указателя. Взглянем вот на эти строчки:

array[filter->data_count - 1].name = name;
array[filter->data_count - 1].value = value;

Если значение переменной filter->data_count достаточно большое, то значения будут записаны по какому-то непонятному адресу.

В памяти будут испорчены какие-то данные, а программа продолжит своё выполнение. Последствия вновь непредсказуемые, но ничего хорошего точно не получится.

Я внимательно не стал изучать старый отчёт, касающийся EFL Core Libraries, но это точно не единственная подобная ошибка. Я заметил как минимум ещё два похожих места, где после realloc данные дописываются по какому-то индексу.

Вывод

Я вновь задаю вопрос: «где гарантии, что будет разыменовывание именно нулевого указателя?». Нет такой гарантий. Невозможно, разрабатывая или модифицируя код, помнить про только что рассмотренный нюанс. Запросто можно что-то испортить в памяти, при этом программа продолжит выполняться как ни в чём не бывало.

Единственный способ написать надёжный и правильный код — это всегда проверять результат, который вернула функция malloc. Проверь и живи спокойно.

Где гарантии, что memset заполняет память в прямом порядке?

Найдётся кто-то, кто скажет что-то подобное:

Я отлично понимаю про realloc и всё остальное, что написано в статье. Но я профессионал и, выделяя память, сразу заполняю её нулями с помощью memset. Там, где действительно необходимо, я использую проверки. Но лишние проверки после каждого malloc я писать не буду.

Вообще заполнять память сразу после выделения буфера достаточно странная идея. Странная потому, что есть функция calloc. Тем не менее, так поступают очень часто. Далеко за примером ходить не надо, вот код из библиотеки WebRTC, используемой в Chromium:

int Resampler::Reset(int inFreq, int outFreq, size_t num_channels) {
  ....
  state1_ = malloc(8 * sizeof(int32_t));
  memset(state1_, 0, 8 * sizeof(int32_t));
  ....
}

Выделяется память, затем буфер заполняется нулями. Очень частая практика, хотя, на самом деле, две строчки можно сократить до одной, используя calloc. Но всё это не важно.

Главное, что даже подобный код не безопасен! Функция memset не обязана начать заполнять память с начала и тем самым вызывать разыменовывание нулевого указателя.

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

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

Соглашусь, что подобная реализация memset действительно экзотика, и я даже задавал вопрос на StackOverflow на эту тему. В ответе говорится:

The Linux kernel’s memset for the SuperH architecture has this property: link.

К сожалению, это код на незнакомой мне разновидности ассемблера, поэтому я не берусь рассуждать о нём. Зато ещё есть вот такая интересная реализация на языке Си. Приведу начало это функции:

void *memset(void *dest, int c, size_t n)
{
  unsigned char *s = dest;
  size_t k;
  if (!n) return dest;
  s[0] = c;
  s[n-1] = c;
  ....
}

Обратите внимание на:

s[0] = c;
s[n-1] = c;

Здесь мы возвращаемся к причине N1 «Разыменовывание нулевого указателя — это неопределённое поведение». Нет гарантии, что компилятор в целях оптимизации не поменяет присваивания местами. Если компилятор это сделает, и аргумент n будет иметь большое значение, то вначале будет испорчен какой-то байт памяти. И только потом произойдёт разыменовывание нулевого указателя.

Опять не убедительно? Хорошо, а как вам вот такая реализация:

void *memset(void *dest, int c, size_t n)
{
  size_t k;
  if (!n) return dest;
  s[0] = s[n-1] = c;
  if (n <= 2) return dest;
  ....
}

Вывод

Нельзя доверять даже функции memset. Да, это во многом искусственная и надуманная проблема. Я просто хотел показать, как много существует нюансов, если не проверять значение указателя. Просто невозможно всё это учесть. Поэтому не надо выпендриваться, а следует аккуратно проверять каждый указатель, который вернула функция malloc и аналогичные ей. И вот именно тогда вы станете профессионалом.

Заключение

Всегда сразу проверяйте указатель, который вернула функция malloc или аналогичная ей.

Как видите, анализатор PVS-Studio совсем не зря предупреждает о том, что нет проверки указателя после вызова malloc. Невозможно написать надёжный код, не делая проверки. Особенно это важно и актуально для разработчиков библиотек.

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

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Why it is important to check what the malloc function returned.

What are the possible errors that can occur during memory allocation using malloc except out of memory? What are the best strategies to handle those errors?

For an out of memory exception is it necessary to free the pointer even if memory allocation fails?

Nathaniel Ford's user avatar

asked Jun 19, 2012 at 17:31

Aman Deep Gautam's user avatar

Aman Deep GautamAman Deep Gautam

8,11121 gold badges74 silver badges130 bronze badges

2

In C there are no exceptions (not you can use in the language anyway), so the only way malloc can signal failure is returning a null pointer. So you have to check the return value. If it is 0, allocation failed (for whatever reason) and no memory allocated — nothing to free; otherwise allocation for the requested amount(*) succeeded and you will have to free the memory when no longer needed.

(*) beware of overflows: malloc takes a size_t parameter, which is most likely an unsigned number. If you request size * sizeof(int) bytes with an unsigned size and the multiplication overflows (possibly an error in obtaining the value of size), the result is a small number. malloc() will allocate this small number of bytes for you returning with non-null and you index into the returned array based on the actual (large) value of size, possibly resulting in segmentation fault or its equivalent

answered Jun 19, 2012 at 17:34

Attila's user avatar

2

I realize this looks like a product plug, but you can read about various kinds of memory allocation errors in our writeup on CheckPointer, our tool for finding memory management errors, including such allocation mistakes.

answered Jun 26, 2012 at 8:01

Ira Baxter's user avatar

Ira BaxterIra Baxter

93.7k22 gold badges172 silver badges341 bronze badges

Out of memory is the only detectable error … other errors such as freeing memory that has already been freed can lead to crashes.

One strategy for out of memory checking in C is to use wrappers for malloc and realloc (you could possibly call them xmalloc and xrealloc) that check for out of memory and if so take an error action … printing a message and exiting, or possibly trying to free memory pools and then retrying the allocation. This puts all the testing in one place, produces consistent failure messages, and guarantees that all allocation attempts are checked for failure. Possible downsides are discussed in the comments below.

Historically, this strategy was rare in C code (consistent with a general low quality throughout the code written in this ancient language), but nowadays some mature library frameworks incorporate this sort of thing (although the implementations leave something to desire; again, see comments below). Another approach, which is highly advisable, is to abandon C and move to a more modern language … possibly C++, in which any failure of new results in a bad_alloc exception.

As for your question … if malloc fails, it returns NULL; there is no pointer to free. (free(NULL) is a no-op). If realloc fails, then the original allocation remains unchanged. You can find these things out by reading the manual pages or specifications such as http://pubs.opengroup.org/onlinepubs/7908799/xsh/realloc.html

answered Jun 19, 2012 at 17:55

Jim Balter's user avatar

Jim BalterJim Balter

16.2k3 gold badges43 silver badges66 bronze badges

8

I want to allocate memory using malloc and check that it succeeded. something like:

if (!(new_list=(vlist)malloc(sizeof (var_list))))
  return -1;

how do I check success?

asked Apr 9, 2011 at 19:50

SIMEL's user avatar

1

malloc returns a null pointer on failure. So, if what you received isn’t null, then it points to a valid block of memory.

Since NULL evaluates to false in an if statement, you can check it in a very straightforward manner:

value = malloc(...);
if(value)
{
    // value isn't null
}
else
{
    // value is null
}

answered Apr 9, 2011 at 19:51

Etienne de Martel's user avatar

Etienne de MartelEtienne de Martel

34.8k8 gold badges91 silver badges112 bronze badges

7

new_list=(vlist)malloc(sizeof (var_list)
if (new_list != NULL) {
  /* succeeded */
} else {
  /* failed */
}

answered Apr 9, 2011 at 19:53

jdehaan's user avatar

jdehaanjdehaan

19.7k6 gold badges58 silver badges97 bronze badges

Man page :

If successful, calloc(), malloc(), realloc(), reallocf(), and valloc() functions return a pointer to allocated memory. If there is an error, they return a NULL pointer and set errno to ENOMEM.

NathanOliver's user avatar

NathanOliver

172k28 gold badges289 silver badges404 bronze badges

answered Apr 9, 2011 at 19:52

Spyros's user avatar

SpyrosSpyros

46.9k25 gold badges86 silver badges129 bronze badges

The code you have already tests for error, although I normally write the assignment and check as two separate lines:

new_list = malloc(sizeof *new_list);
if (!new_list)
    /* error handling here */;

(Note two small changes — you shouldn’t cast the return value, and we take the size from the variable rather than its type to reduce the chance of a mismatch).

If malloc() fails, it returns a null pointer, which is the only pointer value that is false.

The error handling you have is simply return -1; — how you handle that in the calling function is up to you, really.

answered Oct 20, 2016 at 12:03

Toby Speight's user avatar

Toby SpeightToby Speight

27.7k48 gold badges66 silver badges103 bronze badges

Материал из Seo Wiki — Поисковая Оптимизация и Программирование

Перейти к: навигация, поиск

malloc (от англ. memory allocation, выделение памяти) и calloc (от англ. clear allocation, чистое выделение (памяти)) — функции выделения динамической памяти, входящие в стандартную библиотеку языка Си.

Содержание

  • 1 Описание функций
  • 2 Назначение
  • 3 Параметры функций
    • 3.1 malloc
    • 3.2 calloc
  • 4 Возвращаемое значение
  • 5 Характерные ошибки при использовании
  • 6 Примеры использования
    • 6.1 malloc
    • 6.2 calloc
  • 7 См. также
  • 8 Источники

Описание функций

#include <stdlib.h>
 
void *malloc (size_t size);
void *calloc (size_t num, size_t size);

Назначение

malloc принимает в качестве аргумента размер выделяемой области в байтах; возвращает нетипизированный указатель (void*) на область памяти заявленного размера или NULL в случае, если выделить память невозможно. Содержимое выделяемой области памяти не определено.

calloc принимает в качестве аргумента количество элементов и размер каждого элемента в байтах; возвращает нетипизированный указатель (void*) на область памяти заявленного размера или NULL в случае, если выделить память невозможно. Значения элементов устанавливаются в ноль. malloc работает быстрее, чем calloc, в связи с отсутствием функции обнуления выделяемой памяти.

Параметры функций

malloc

  • size — размер распределяемой области памяти

calloc

  • num — количество распределяемых элементов
  • size — размер каждого элемента

Возвращаемое значение

Функции возвращают нетипизированный (void*) указатель на область памяти в случае успеха, либо NULL в противном случае.

Характерные ошибки при использовании

  • Память остаётся «занятой», даже если ни один указатель в программе на неё не ссылается (для освобождения памяти используется функция free()). Накопление «потерянных» участков памяти приводит к постепенной деградации системы. Ошибки, связанные с неосвобождением занятых участков памяти, называются утечками памяти (англ. memory leaks).
  • Если объём обрабатываемых данных больше, чем объём выделенной памяти, возможно повреждение других областей динамической памяти. Такие ошибки называются ошибками переполнения буфера (англ. buffer overflow).
  • Если указатель на выделенную область памяти после освобождения продолжает использоваться, то при обращении к «уже не существующему» блоку динамической памяти может произойти исключение (англ. exception), сбой программы, повреждение других данных или не произойти ничего (в зависимости от типа операционной системы и используемого аппаратного обеспечения).
  • Если для одной области памяти free() вызывается более чем один раз, то это может повредить данные самой библиотеки, содержащей malloc/free, и привести к непредсказуемому поведению в произвольные моменты времени.
  • Неудачная организация программы, в которой выделяется и освобождается множество небольших объёмов памяти — возможна фрагментация свободной памяти («пунктир»), при которой свободной памяти в сумме остаётся много, но выделить большой кусок невозможно.

Точное поведение функций описано в стандарте ANSI C, на него же ссылается определение функции в стандарте POSIX.

Примеры использования

malloc

dynamic_array = malloc(number_of_elements * sizeof(float));
if(!dynamic_array)
{
   /* обработка ошибки выделения памяти */
}
 
/* … работа с элементами массива … */
 
free(dynamic_array);
dynamic_array = NULL;

calloc

dynamic_array = calloc(number_of_elements, sizeof(float));
if(!dynamic_array)
{
   /* обработка ошибки выделения памяти */
}
 
/* … работа с элементами массива … */
 
free(dynamic_array);
dynamic_array = NULL;

См. также

  • Динамическая память
  • Си
  • NULL (Си)
  • New (C++)
  • stdlib
  • POSIX
  • ANSI C
  • free()
  • alloca
  • Менеджер памяти
  • Куча
  • soap malloc
  • soap destroy

Источники

  • malloc (англ.). — Описание функции malloc в стандарте POSIX.
  • calloc (англ.). — Описание функции calloc в стандарте POSIX.en:Malloc

fr:Malloc
it:Malloc
ja:Malloc
pt:Malloc
sr:Malloc

Понравилась статья? Поделить с друзьями:
  • Обработка ошибок компилятором
  • Обработка ошибок и исключений php
  • Обработка ошибок ввода python
  • Обработка ошибок ввода java
  • Обработка ошибок вба