методичка белая страница 22
Массивы языков Си и Си++ обладают очень интересным свойством, отличающим их от массивов в других алгоритмических языках. При объявлении массивов, как и в других алгоритмических языках, им выделяются участки оперативной памяти. Но как только память для массива выделена, имя массива воспринимается как константный (т. е. неизменный) указатель того типа, к которому отнесены элементы массива. Однако для этого указателя две операции выполняются особенным образом:
- Операция sizeof, примененная к имени массива, определяет размер выделенного участка памяти для всего массива, а не участка, выделенного для указателя. Например, значение выражения sizeof(x) для последнего примера равно 12.
- Операция получения адреса, примененная к имени массива, определяет адрес начального (с нулевым индексом) элемента массива.
Во всех остальных случаях значением имени любого массива является адрес первого (нулевого) элемента массива, и это значение невозможно изменить. Таким образом, для любого массива соблюдается соотношение:
Имя_массива == &имя_массива == имя_массива [0]
Если применить это правило к рассмотренному ранее массиву х, то получим:
x == &x == &x[0]
Да, но что такое x[0]? Ведь массив х объявлен как двумерный! Дело в том, что в соответствии с синтаксисом языка Си, в языке существуют только одномерные массивы. Однако, элементами одномерного массива, в свою очередь, могут быть массивы. Поэтому двумерный массив определяется как массив массивов. Таким образом, х[0] – это нулевой элемент массива х (нулевая строка матрицы х), который в свою очередь является одномерным массивом элементов типа int, x[1] – первый элемент (первая строка матрицы) и т. д. Аналогично строятся трехмерные и n – мерные массивы. Но, если x[0] – это нулевая строка матрицы х, то как обозначится ее нулевой элемент? Очевидно, как x[0][0]. Поэтому:
x == &x == x[0] = &x[0] == &x[0][0].
Итак, запомним, что имя массива – это константный указатель на тип данных входящих в него элементов, значение которого – адрес нулевого элемента. Например, int a[10] означает, что а – константный указатель на тип int и a == &a[0], float y[5][8] – означает, что у - константный указатель на тип float и y == y[0] = &y[0][0]. Но в таком случае, что может означать выражение х + 1 и y[0] +1? Очевидно, a + 1 означает увеличение адреса на одно поле типа int (вспомним операции над указателями), т. е. на два байта, что предполагает перемещение к элементу x[1]. Выражение y[0] +1 означает увеличение адреса на одно поле типа float, т. е. на 4 байта, что предполагает перемещение к элементу y[0][1]. Таким образом, справедливо:
a[1] == *(a + 1)
y[0][1] == *(y[0] +1) .
Или в общем виде:
a[i] == *(a + i)
y[0][j] == *(y[0] +j) .
Следует отметить, что хотя y == y[0], выражение *( y + j) не воспринимается транслятором как перемещение к j –ому элементу нулевой строки. Дело в том, что в этом выражении слева от знака ‘+’ должен размещаться объект, который может восприниматься как одномерный массив (y – это двумерный массив, а y[0] – одномерный массив).
А теперь вернемся к указателям и рассмотрим одновременное участие указателя и массива. Пусть имеются такие объявления:
int z[10];
int *pz;
pz = &z[0];
В таком случае:
pz + 1 - это адрес первого элемента;
pz + 2 - это адрес второго элемента;
pz + i - это адрес i -го элемента;
кроме того:
pz == z
pz[i] == *(pz + i) == z[i]
&z[i] == z + i
Ниже приведен пример программы, демонстрирующий использование указателей при обращении к элементам массива и использование имен массивов как указателей при обращении к тем же элементам.
#include<stdio.h>
#include<conio.h>
void main ()
{ int z[2][3]={1,2,3,4,5,6};
int a[3]={1,2,3};
int *p;
int i,j;
clrscr();
printf("***array z***\n");
for (i=0;i<2;i++)
{ for (j=0;j<3;j++)
printf("%d ",z[i][j]);
printf("\n");
}
printf("************\n");
p = z[1];
printf("z[0][1] = %d\n",z[0][1]);
printf("*(z[0]+1) = %d\n",*(z[0]+1));
printf("*(p + 1)= %d\n",*(p + 1));
printf("z[1][1] = %d\n",z[1][1]);
p=a;
printf("***array a***\n");
for (i=0;i<3;i++)
printf("%d ",*(p + i));
getch();
}
При выполнении этой программы на экране получим:
|
В этой программе объявляются и инициируются два массива целых чисел: двумерный массив и одномерный массив а. На экране видно, что вначале выданы элементы массива z. Затем р настраивается на первую строку массива z (оператор p=z[1];). Далее мы видим, что выражение z[0][1] эквивалентно выражению *(z[0] +1) (и то и другое “достают” из массива для выдачи элемент z[0][1], равный 2); выражение *(p + 1) эквивалентно выражению z[1][1] (и то и другое “достают” из массива для выдачи элемент z[1][1], равный 5). Далее р настраивается на массив а и значения элементов массива а выдаются в цикле по i с помощью выражения *(p + i).
Любознательный читатель спросит: “А зачем нам нужны такие хитрости? Почему нельзя всегда “честно” пользоваться обращением к элементам массивов переменными с индексами (z[i][j], a[i])?”. Ответ такой: “Да, можно поступить и так, но тогда мы не сможем использовать рассмотренные возможности для разработки эффективных программ на Си и вряд ли сможем считать себя программистами на Си ”. А теперь вернемся к вопросу, который возник при решении задачи 2 настоящего раздела. Как реализовать динамический массив, т. е. массив с заранее неизвестным числом элементов?