Указатели. Статические массивы. Динамическое распределение памяти. Динамические массивы.
1.1. Указатели.
Указатель это переменная, которая содержит адрес другой переменной. Для объявления указателя нужно перед именем переменной поставить символ ‘*’. Например,
char *pch; // указатель на символьную переменную
int *pn1, *pn2; // два указателя на переменные типа int
double *pd, d; // pd – указатель на переменную типа double, d – переменная типа double
Один указатель часто объявляется в следующем стиле:
int* pn;
При объявлении указателя желательно сразу же его проинициализировать. Это значительно упрощает отладку программы. Если начальное значение указателя неизвестно, то ему нужно присвоить значение NULL, которое будет явно указывать, что указатель пока не используется. Например,
int* pn = NULL;
Для получения адреса переменной используется оператор ‘&’, который так и называется – оператор получения адреса. В следующем примере указателю присваивается адрес переменной.
int n;
int* pn = &n; // pn указывает на переменную n
Для получения значения переменной, на которую указывает указатель, используется оператор ‘*’, который называется – обращение по адресу. Например,
int n = 2;
int* pn = &n; // pn указывает на переменную n
int m = *pn; // m = 2
1.2. Массивы.
Массив это конечная последовательность однотипных данных. Каждый член этой последовательности называется элементом массива. Доступ к элементам массива производится по их номеру в последовательности, который в этом случае называется индексом. Количество элементов в массиве называется размерностью массива. Причем важно заметить, что если размерность массива равна n, то индекс этого массива изменяется от 0 до n-1. Например, в языке программирования С массив из десяти целых чисел объявляется следующим образом:
int a[10];
Имя массива является константным указателем на его первый элемент, то есть указателем, значение которого нельзя изменить. При определении массива его можно проинициализировать. Например,
int a[3] = {0, 1, 2};
Доступ к элементам массива выполняется при помощи оператора индексирования [ ], результатом выполнения которого является значение элемента массива с заданным индексом. Например,
int n;
int a[3] = {10, 20, 30};
n = a[0]; // n = 10
a[1] = 40;
В языке программирования С можно определять и многомерные массивы. В этом случае граница каждого измерения массива указывается в квадратных скобках. Например,
int a[2] [2] = {{1, 2}, {3, 4}};
int n = a[0] [0]; // n = 1
В заключении этого параграфа приведем простую программу, которая вводит, а затем выводит на консоль элементы массива из трех элементов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdio.h> int main() { int a[3]; int i; printf("Input three integers: "); for (i = 0; i < 3; ++i) scanf("%d", &a[i]); printf("The array:\n"); for (i = 0; i < 3; ++i) printf("a[%d] = %d ", i, a[i]); printf("\n"); return 1; } |
1.3. Арифметические действия с указателями.
Над указателями одного типа можно выполнять арифметическую операцию вычитания ‘-‘. В результате выполнения этой операции получаем целое число, которое указывает, на сколько один адрес памяти смещен от другого в единицах равных длине типа данных, на который указывают указатели. Например,
int a[5];
int n, m;
n = &a[5] – &a[3]; // n = 2
m = &a[3] – &a[5]; // m = -2
К указателям можно применять операторы ++ и — . В этом случае значение указателя соответственно увеличится или уменьшится на длину типа данных, на который указывает указатель. Например,
int* a;
++a; // a = &a[1]
К указателю можно прибавить или вычесть целое число. В этом случае значения указателя соответственно увеличится или уменьшится на это число, умноженное на длину типа данных, на который указывает указатель. Например,
int a[5];
int *p = a+2; // p = &a[2]
1.4. Динамическое распределение памяти.
Под динамической памятью понимают память, которая выделяется программе во время её работы. Динамическое распределение памяти включает выделение программе памяти по её запросу и последующее освобождение программой этой памяти. Для выделения памяти в языке С используются следующие функции:
void* malloc( size_t size );
void* calloc( size_t nitems, size_t size);
void* realloc( void* ptr, size_t size);
Прототипы этих функций находятся в заголовочном файле stdlib.h. Параметры этих функций содержат тип size_t, который является переопределением типа unsigned int. Тип void* является родовым указателем, то есть указателем на «пустую» память. Впоследствии этот указатель приводится к указателю на нужный тип. Опишем кратко работу этих функций.
Функция malloc выделяет программе память размером в size байт.
Функция calloc выделяет программе память размером в (nitems * size) байт, при этом выделенная память обнуляется.
Функция realloc перераспределяет память, на которую указывает ptr, до размера в size байт. То есть программе распределяется новый блок памяти в size байт. При этом size байт информации из старого блока копируются в новый блок. После этого старый блок памяти освобождается.
Все эти функции в случае успеха возвращают указатель на выделенную память, в случае неудачи – NULL.
Для освобождения памяти используется функция
void free(void* ptr);
прототип которой также находятся в заголовочном файле stdlib.h. Параметр ptr этой функции является указателем на освобождаемую память.
Важное замечание. Динамическую память нужно освобождать после её использования. Иначе остаются неиспользованные блоки памяти, которые называются «мусором». Также заметим, что динамическое выделение памяти используется только в том случае, если программе заранее неизвестно, какой размер памяти нужно отвести под данные. В следующем примере показано, как динамически выделить память под целое число.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <stdio.h> #include <stdlib.h> int main() { char c; int* pn = NULL; printf("Press 'y' to input integer: "); scanf("%c", &c); if (c == 'y') { // выделение память под целое число pn = (int*)malloc(sizeof(int)); // если ошибка, то выходим из программы if (!pn) { printf("Error: there is no memory.\n"); return 0; } printf("Input integer: "); scanf("%d", pn); printf("You input integer: %d\n", *pn); // освобождаем память free(pn); } else printf("You don't want to input integer.\n"); return 1; } |
1.5. Динамические массивы.
Для того чтобы динамически создать одномерный массив нужно, во-первых, объявить в программе указатель на тип, соответствующий типу элементов этого массива, а, во-вторых, выделить память под массив, используя одну из функций malloc или calloc. Ниже приведена программа, которая динамически создает одномерный целочисленный массив.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <stdio.h> #include <stdlib.h> int main() { int *a, n; // указатель на массив, размерность массива printf("Input a size of an array: "); scanf("%d", &n); // выделяем память под массив a = (int*)malloc(n * sizeof(int)); // если ошибка, то выходим из программы if (!a) { printf("Error: there is no memory.\n"); return 0; } // вводим элементы массива printf("Input elements of the array: "); for (int i = 0; i < n; ++i) scanf("%d", &a[i]); // можно ввести элементы массива и так printf("Input elements of the array: "); for (i = 0; i < n; ++i) scanf("%d", a + i); // что-то делаем с массивом printf("You input the array: "); for (i = 0; i < n; ++i) printf("%d ", a[i]); printf("\n"); // освобождаем выделенную память free(a); return 1; } |
Память под многомерные динамические массивы выделяется по строкам, начиная с первого индекса. Это делается для того, чтобы обеспечить применение операции индексирования [ ] столько раз какова размерность многомерного массива. В этом случае тип динамического массива объявляется как указатель, который содержит оператор обращения по адресу ‘*’ столько раз, какова размерность массива. Например, указатели на двумерный и трехмерный целочисленные массивы могут быть объявлены следующим образом:
int **a; // указатель на двумерный массив
int ***b; // указатель на трехмерный массив
Приведем примеры динамического создания и уничтожения двумерной матрицы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#include <stdio.h> #include <stdlib.h> int main() { int **a, n, m; // указатель на массив, размерности массива int i, j; // индексы элементов матрицы printf("Input two sizes of an array: "); scanf("%d%d", &n, &m); // выделяем память для указателей на строки a = (int**)malloc(n * sizeof(int*)); // если ошибка, то выходим из программы if (!a) { printf("Error: there is no memory."); return 0; } // выделяем память для строк for (i = 0; i < n; ++i) { a[i] = (int*)malloc(m * sizeof(int)); // если ошибка, то освобождаем память и выходим из программы if (!a[i]) { for (j = 0; j < i; ++j) free(a[j]); free(a); printf("Error: there is no memory.\n"); return 0; } } // вводим элементы массива printf("Input elements of the matrix:\n"); for (i = 0; i < n; ++i) for (j = 0; j < m; ++j) scanf("%d", &a[i][j]); // что-то делаем с матрицей printf("You input the matrix:\n"); for (i = 0; i < n; ++i) { for (j = 0; j < m; ++j) printf("%d ", a[i][j]); printf("\n"); } // освобождаем захваченную память for (i = 0; i < n; ++i) free(a[i]); free(a); return 1; } |
А почему в последнем примере в конце память
освобождается именно таким образом, а не как в начале, при ошибке, через j и до m?
не совсем понял вопрос.