Работа с двумерными массивами в языке С

 

Прежде всего, надо подчеркнуть, что двумерных массивов в языке С не бывает. Бывают только массивы массивов. Для одномерных массивов существует две реализации – случай, когда при написании программы (до ее компиляции) вы знаете размер массива (в этом случае используется обычный одномерный массив), и случай, когда размер массива станет известен только при выполнении программы (в этом случае используется указатель с последующим отведением памяти). При наличии двух размерностей, соответственно, получается четыре варианта реализации двумерных массивов, зависящих от того, знаете ли вы каждую из размерностей массива. Рассмотрим варианты, зависящие от того, что знаем ли мы размерности массива до компиляции программы.

 

1.     Известны обе размерности массива

В этом случае массив реализуется, как стандартный массив массивов:

int m1[10][20];

здесь задан массив из 10 строк и 20 столбцов.

В реальности в этом случае в памяти хранятся только данные массива. Т.е. отводится 10*20*sizeof(int) , байт памяти. Данные хранятся по строкам, т.е. элементы массива хранятся в следующем порядке:

m1[0][0], m1[0][1],…,m1[0][19],m1[1][0],m1[1][1],…,m1[1][19]…

Важно понимать, как компилятор считает адрес элемента массива m1[i][j]. Согласно вышеприведенному порядку элементов массива в памяти, получается, что данный элемент имеет порядковый номер i*20+j, т.е. для вычисления адреса данного элемента первая размерность не используется. Т.о. если мы захотим передавать данный массив в функцию, то мы должны описать функцию примерно следующим образом:

void Fun(int m[][20]);

здесь пустые первые скобки указывают на то, что их значение никак компилятором не используется, т.е. в реальности в функцию передается указатель (на массив из 20 переменных типа int).  Из последнего следует странный вывод: после определения m1 верно, что sizeof(m1) равно 10*20*sizeof(int), но если вы определите функцию void Fun(int m1[10][20]){}, то внутри нее sizeof(m1) равно размеру одного указателя (!), хотя описание переменной не отличается от описания переменной при ее исходном определении.

Возможно корректное использование в программе имени массива без квадратных скобок. Разберемся с этим. При этом, будем исходить из того, что для массива M верно, что элемент M[i] имеет адрес M+i  и M является указателем на элемент M[0].

m1[i] является массивом из 20 переменных типа int , т.е. указателем на элемент m1[i][0], т.е. указателем на начало i-ой строки. Важно, что m1[i] является константой, а не переменной, т.е. нет ячейки памяти, в которой m1[i] хранится.

m1 является, по определению, адресом m1[0] , т.е. &(m1[0]) , но m1[0] является константой, а у нее адреса быть не может, поэтому компилятор здесь игнорирует & и получается, что m1 имеет то же самое значение, что и m1[0]  (но другой тип!). Т.о. m1 указывает на начало данных массива.

Аналогично, m1+i является, по определению, адресом m1[i] , т.е. &(m1[i]) , но m1[i] является константой, поэтому компилятор здесь игнорирует & и получается, что m1+i имеет то же самое значение, что и m1[i] (тип другой!). Т.о.  m1+i  указывает на начало i-ой строки данных массива.

2.     Известна вторая размерность массива, первая – неизвестна

В этом случае вместо массива массивов приходится использовать указатель на массив:

int (*m2)[20];

Здесь надо напомнить, что символ * в описании переменной читается, как указатель, квадратная скобка читается, как массив. Чтение происходит слева направо, но квадратные скобки имеют бОльший приоритет. В нашем случае круглые скобки вокруг имени переменной меняют порядок чтения. Чтение всегда начинается с имени переменной: m2 – это есть… и заканчивается основным типом (у нас это – int). Т.о. получается: m2 это есть указатель на массив из 20 переменных типа int.

Здесь m2 – реальная переменная, под которую отводится память. Т.е. у нее уже коректно запрашивать адрес: &m2.

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

void Fun(int (*m)[20]);

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

Единственное отличие от случая 1 – теперь надо отводить память под массив строк:

m2=(int(*)[20])malloc(n1*sizeof(int)*20);

здесь n1 – количество строк в массиве.

 

3.     Известна первая размерность массива, вторая – неизвестна

В этом случае приходится использовать массив указателей:

int *m3[10];

Чтение описания происходит по вышеописанным правилам: m3 это есть массив из 10 указателей на переменную типа int.

Здесь под, собственно, m3  память не отводится (это же массив), но отводится память под элементы массива – указатели на целую переменную.  

Адресация (т.е. способ вычисления адреса элемента массива компилятором) в данном случае существенно отличается от случая 1. При использовании выражения m3[i][j] компилятор сначала берет адрес начала массива m3, потом отсчитывает от него i  указателей и берет из памяти значение полученного указателя, потом отсчитывает от  найденного адреса еще  j  целых переменных и только после этого получает адрес искомой переменной.

Отводить/очищать  память теперь придется для каждой строки массива:

for(i=0;i<10;i++)m3[i]=(int*)malloc(n2*sizeof(int));

for(i=0;i<10;i++){free(m3[i]);m3[i]=NULL;}

 

здесь n2 – количество элементов в строке массива.

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

m2[0]=(int*)malloc(10*n2*sizeof(int));

 for(i=1;i<10;i++)m2[i]=m2[i-1]+n2;

free(m2[0]); m2[0]=NULL;

 

При этом, естественно, самое главное – не забывать при очистке памяти, каким образом память была отведена.

 

4.     Обе размерности массива неизвестны

В этом случае приходится использовать указатель на указатель:

int **m4;

Это наиболее обший способ задания двумерного массива. Чтение описания происходит по вышеописанным правилам: m4 это есть указатель на указатель на переменную типа int.

Здесь под, собственно, m4  память уже отводится (т.е. от m4 корректно брать адрес). Изначально память не отводится более ни подо что.

Адресация (т.е. способ вычисления адреса элемента массива компилятором) в данном случае ничем не отличается от случая 3.

Отводить/очищать  память теперь придется сначала под массив указателей, а потом для каждой строки массива:

m4=(int**)malloc(n1*sizeof(int*));

for(i=0;i<10;i++)m4[i]=(int*)malloc(n2*sizeof(int));

for(i=0;i<10;i++){free(m4[i]);m4[i]=NULL;}

free(m4);m4=NULL;

 

здесь n1 – количество строк массива, n2 – количество элементов в строке массива.

Как и ранее, нежелательно вызывать много раз функцию malloc, поэтому желательно отводить память сразу  подо все используемые данные (массив указателей и, собственно, данные), а потом `нарезать’ полученный массив на куски. Т.о. желательно отведение/очистку памяти производить следующим способом:

m2=(int**)malloc(n1*sizeof(int*)+n1*n2*sizeof(int));

m2[0]=(int*)(m2+n1);

 for(i=1;i<10;i++)m2[i]=m2[i-1]+n2;

free(m2); m2=NULL;

 

При этом, как и ранее, самое главное – не забывать при очистке памяти, каким образом память была отведена.