О динамических массивах в языке Си: что это такое, виды, как с ними работать

Что такое динамический массив

Определение

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

Они занимают область в памяти. Компилятор резервирует соответствующий объем памяти для каждого типа и количества элементов каждого массива.

Чтобы зарезервировать память при помощи компилятора для статического массива, необходимо указать ему сколько в нем будет элементов целых чисел. Для этого используется объявление int a [] ;

Осторожно! Если преподаватель обнаружит плагиат в работе, не избежать крупных проблем (вплоть до отчисления). Если нет возможности написать самому, закажите тут.

Пример

Пример объявления для 12 элементов:

int a [12] ;

Для динамического массива количество элементов не указывают.

На этапе написания программы размер динамических массивов неизвестен. Ввод количества элементов осуществляется во время работы программы.

Размером при объявлении динамического массива является числовая переменная, а не константа.

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

В динамических массивах также не указывают длину строк и их количество. Границы массивов полностью контролируются разработчиком.

Указатели для динамических массивов

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

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

Действия с указателями

  1. Объявление.

Указатели:

  • int *pI;
  • char *pC;
  • float *pF;

Чтобы объявить указатель, объявляют тип переменной и ставят перед именем символ *. Указанная переменная должна быть одного типа с указателем, который на ее указывает.

  1. Присвоение адреса.

Указатели:

  • pI = &i;
  • int i, *pI;

Адрес присваивается указателю с помощью оператора &. Знак &, стоящий перед переменной, будет указывать на ее адрес.

  1. Получение значения по этому адресу.

Указатели:

  • f = *pF;
  • float f, *pF;

Для переменной определенного типа, на которую определен указатель, например, pF, указывается число соответствующего типа. Символ * перед pF означает информацию ячейки, указанной pF.

  1. Сдвижение.

Указатели:

  • int *pI;
  • pI ++;
  • pI -= 4;
  • pI += 4;

Подобные операции позволяют сдвигать значение указателя на то или иное целое число или количество байтов.

  1. Обнуление.

Указатели:

  • char *pC;
  • pC = NULL;

Указатель указывает на недействительный адрес, который равен нулю, соответственно, указатель тоже недействительный. Запись по нему не может быть осуществлена.

  1. Выведение на экран.

Указатели:

  • int i, *pI;
  • printf("Адр.i =%p", pI);
  • pI = &i;

Для вывода указателей на экран применяют формат %p.

Ошибки, возникающие при неверном использовании динамических массивов

  1. Запись в чужую область памяти.

Причина: Выделение памяти произошло неудачно, при этом массив используется.

Вывод: необходимо всегда осуществлять проверку указателя на NULL. Если значение указателя будет равно нулю при проверке после выделения памяти, то использование такого массива будет приводить к зависанию компьютера.

  1. Повторное удаление указателя.

Причина: Массив уже удален и теперь удаляется снова.

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

  1. Выход за границы массива

Причина: В массив записан элемент с отрицательным индексом или индексом, выходящим за границу массива.

  1. Утечка памяти

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

Виды динамических массивов

Классификация динамических массивов:

  • одномерные: int * A;
  • двумерные: int ** A;

Одномерный динамический массив представляет собой множество элементов одного типа данных.

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

Переменная А сама является указателем и объявляется как указатель на указатель на нужный тип. Данный указатель нужно инициализировать, то есть положить ему адрес массива адресов. В памяти программы должен существовать массив адресов указателей на int и сам массив указателей на int. В этом массиве указателей должны быть разложены адреса, возможно лежащие отдельно друг от друга в памяти.

Пример двумерного массива:

  1. #include <iostream> // здесь указывают заголовок файла, содержащего функции, переменные, классы, для организации операций ввода-вывода
  2. using namespace std; // объявление пространства имен для ограничения видимости переменных, функций и т.д.
  3. int main() {
  4. setlocale(0, ""); “// указывают тип значения, возвращаемого функцией, и задают локаль для программы
  5. int **dinamic_array2 = new int* [5];
  6. for (int i = 0; i < 5; i++) {
  7. dinamic_array2[i] = new int [i + 1];
  8. } // Создание двумерного массива
  9. for (int i = 0; i < 5; i++) {
  10. cout << "Введите числа" << "(" << i + 1 << ")" << ":";
  11. for (int j = 0; j < i + 1; j++) {
  12. cin >> dinamic_array2[i][j];
  13. }
  14. } // заполнение массива
  15. for (int j = 0; j < i + 1; j++) {
  16. sum += dinamic_array2[i][j];
  17. }
  18. cout << "Сумма " << i + 1 << " массива равна " << sum << endl;
  19. } // проведение различных операций по вводу-выводу
  20. for (int i = 0; i < 5; i++) {
  21. delete [] dinamic_array2[i]; // удаление массива
  22. }
  23. system("pause");
  24. return 0;
  25. }

Строки 5-8 — создание динамического массива.

Строки 9-14 — заполнение.

Строки 15-19 — подсчет, сортировка и вывод на экран суммы всех массивов.

Строки 20-22 — удаление массива.

Стандартные функции динамического выделения памяти

В языке программирования Си имеются стандартные функции управления памятью:

  1. Malloc (от англ. memory allocation, выделение памяти). Функция malloc возвращает указатель на n байт неинициализированной памяти или NULL, если запрос на память нельзя выполнить: void *malloc(size_t n). Значение, которое возвращает malloc, — это адрес начала выделенной области памяти. При этом гарантируется соответствующее выравнивание этого адреса на границу памяти, обеспечивающую хранение в области любого объекта.
  2. Calloc (от англ. clear allocation, чистое выделение памяти). Функция calloc возвращает указатель на участок памяти, достаточный для размещения п объектов заданного размера size или NULL, если запрос на память невыполним. Память инициализируется нулями; void *calloc(size_t n, size_t size).
  3. Realloc (от англ. reallocation, перераспределение памяти). Используется для изменения величины выделенной памяти, на которую указывает ptr, на новую величину, задаваемую параметром newsize. Эта функция может перемещать блок памяти на новое место, в этом случае функция возвращает указатель на новое место в памяти.
  4. Free (англ. free, освободить). Функция free(p) освобождает участок памяти, на который указывает указатель р, первоначально полученный вызовом функции malloc или calloc. Порядок освобождения выделенных участков памяти не регламентируется. Однако если указатель не был получен с помощью malloc или calloc, то его освобождение является грубой ошибкой. Обращение по указателю после его освобождения — также ошибка. Правильным было бы записать все, что нужно, перед освобождением:

for (p = head; p != NULL; p = q) {

q = p->next;

free(p);

}

Выделение памяти под двумерный массив

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

Например, есть указатель А: int ** A ;

Сначала создают массив, состоящий из указателей, которые будут отвечать количеству строк:

A = (int **) malloc (N * sizeof(int *)). Здесь важно указать тип указателя, а не int. Потому что в этом массиве, на который указывает A, будут храниться указатели, а не int.

Далее инициализируют уже сами элементы этого массива указателей путем запуска цикла:

for (int i = 0 ; i < N ; ++i).

Для каждого A[i] проводим инициализацию по отдельности. По сути этого разыменование А со смещением i:

A[i] = (int *) malloc (M * sizeof (int)). Указатели A[i] уже указывают на массив int. Если бы был другой тип, то указывали бы на него.

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

int ** A ;

A = (int **) malloc (N * sizeof(int *)).

for (int i = 0 ; i < N ; ++i).

free (A[i]) ;

free (A) ;

Нестандартные функции динамического выделения памяти

Когда требуемый размер массива становится известен, то используют оператор new из языка Си ++, который является расширенной версией языка Си. В скобках указывают размер массива:

pI =new int [N] ;

При отрицательном или нулевом N использовать оператор new нельзя.

При динамическом распределении памяти основными операциями являются new и delete.

Операция new принимает в качестве аргумента тип динамически размещаемого объекта и возвращает на него указатель.

Например, оператор Node *newPtr = new NodeA0); используется для выделения области памяти размером sizeof(Node) байтов, а также его применяют для сохранения указателя на данной области памяти в указателе newPtr.

Если планируется использовать память повторно, после того, как динамический массив в какой-то момент работы программы перестанет быть нужным, то ее освобождают с помощью delete[],

Пример

delete [] a

При этом не указывают размерность массива. Операция delete освобождает область памяти, выделенную при помощи оператора new. Таким образом, она возвращает системе эту область памяти для дальнейшего распределения. Чтобы освободить память, которая была динамически выделена предыдущим оператором, применяют оператор delete newPtr;

Иногда выделение памяти под динамические массивы осуществляют с помощью двух языков Си и Си++ одновременно: операции new и функции malloc.

Пример

int n = 10;

int *a = new int[n];

double *b = (double *)malloc(n * sizeof (double));

Во второй строке описан указатель на целую величину. Ему присвоен адрес начала непрерывной области динамической памяти, выделенной с помощью операции new. В зависимости от того, сколько присутствует n величин типа int, столько выделяется памяти для их хранения. Величина n может являться переменной.

В третьей строке применяется функция malloc, которая в данном случае выделяет память под n элементов типа double. Для того, чтобы освободить память, выделенную с помощью функции malloc, применяют функцию free. Память не обнуляется при ее выделении. Динамический массив нельзя инициировать.

Перераспределение памяти

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

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

Функция realloc осуществляет перераспределение памяти. Она меняет размер динамически выделяемой области памяти, адресуемой указателем ptr, на новый размер size.

Realloc может возвращать адрес новой области памяти:

realloc(void *ptr, size_t size);

Если ptr равен NULL, то realloc ведет себя подобно функции malloc.

Поведение функции realloc будет неопределенным, если:

  • значение ptr не было возвращено функциями calloc, malloc или realloc;
  • пространство памяти было освобождено функцией free, и ptr указывают на него.

Size указывают в абсолютном значении.

Важные моменты в перераспределении памяти с помощью функции realloc:

  1. Если существующее пространство меньше по размеру, чем size, то в конце будет выделяться новое неинициализированное непрерывное пространство, при этом предыдущие данные пространства будут сохранены.
  2. Значение, равное NULL, будет возвращено, если realloc не может выделить запрашиваемое пространство, а в указанном ptr пространстве содержимое остается нетронутым.
  3. Realloc будет действовать, как функция free, если size=0, а ptr не равен NULL.
  4. Если первым параметром функции realloc использовать NULL, то можно получить эффект, который достигается с помощью функции malloc с тем же значением size.

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

Например, при создании связанного списка linked list и применения realloc, чтобы выделить для цепочки пространство памяти, может оказаться, что оно будет перемещаться. В таком случае, указатели будут адресованы не в место их расположения в настоящий момент, а в то место, где были размещены последовательные звенья цепочки.

Пример

Пример использования функции realloc:

p2 = realloc(p1, new_size);

if (p2 1= NULL) p1 = p2;

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

Насколько полезной была для вас статья?

У этой статьи пока нет оценок.

Заметили ошибку?

Выделите текст и нажмите одновременно клавиши «Ctrl» и «Enter»