Создание нового проекта консольного типа, страница 6

a+1 = 0012FF74 a[0]+1 = 0012FF6C

Результат приводит к заключению, что единица в выражении a+1 «весит» 12 байт, так как в строке три элемента по четыре байта (не забывайте о том, что подсчеты надо производить в шестнадцатиричной системе исчисления), а единица в выражении a[0]+1 сдвигает нас от адреса первого элемента на четыре байта (один элемент типа int). Следующие две строки вывода — это единицы, что означает истинность равенств. Равенства: a==&a[0] и a[0]==&a[0][0] проходят без замечаний. Если же испытать другие два равенства: a==a[0] и a==&a[0][0], то они вызовут со стороны компилятора сообщения об ошибке. Интересно, что компилятор Borland C++ 4.5 в этом случае выводит лишь предупреждающее замечание (warning) о возможных проблемах при переносе кода. Результаты численного эксперимента могут быть описаны такой схемой:

a    Ы &a[0] ~   &a[0][0]

*a     Ы a[0]  Ы  &a[0][0]

**a  Ы *a[0] Ы  a[0][0]

Каждая следующая строчка в этой таблице получена из предыдущей путем применения операции разадресации (*). Здесь использованы следующие обозначения: Ы означает полную эквивалентность, в то время как ~ означает только численное равенство. Так и хочется для восстановления симметрии в правый верхний угол вместо &a[0][0] записать &&a[0][0], но такой объект не существует в языке С, хотя мы и говорим, что a имеет тип адрес адреса. Существует еще один объект (&a) из той же «обоймы», который не нашел своего места в таблице. Имеют место такие соотношения:

&a ~ a Ы &a[0] ~ &a[0][0]

Попробуйте самостоятельно прокомментировать эту цепочку равенств. Теперь посмотрим, как компилятор интерпретирует выражение a[i][j]. Сначала вычисляется a+i, что является адресом i+1 строки. Здесь по правилам адресной арифметики i — это число байт в i строках. Затем производится разадресация *(a+i) и добывается (еще только) адрес первого элемента i+1 строки (отсчет от единицы) . После этого вычисляется адрес элемента, сдвинутого на j единиц (уже других единиц): *(a+i)+j. Осталось разадресовать это выражение, чтобы достать элемент a[i][j]. Следовательно, a[i][j]==*(*(a+i)+j). Если задан массив int a[2][3];, то доступ к элементу a[1][1] может быть осуществлен с помощью выражения: *(*(a+1)+1). Согласно правилам адресной арифметики и в предположении, что sizeof(int)==4, единицы в этом выражении будут «весить» 12 и 4 байта. Предлагаем убедиться в справедливости сказанного, добавив в программу следующий вызов:

printf ("\n a[1][1]=%d *(*(a+1)+1)=%d\n\n a=%p  a+1=%p *(a+1)+1=%p\n",

    a[1][1],*(*(a+1)+1),a,a+1,*(a+1)+1);

Вопросы и упражнения

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

#define P puts("\n\n")

int a[]={0,1,2,3};

void main()

{

int i, *p;

for (P, p=&a[0];  p<=&a[3];  p++)

  printf ("  *p=%d",*p);

for (P, p=&a[0],i=1;  i<4;  i++)

  printf (" p[%d]=%d",i,p[i]);

for (P, p=a,i=0;  p+i<=a+3;  p++,i++)

  printf (" *(%p+%d)=%d",p,i,*(p+i));

for (P, p=a+3;  p>=a;  p--)

  printf ("  *p=%d",*p);

for (P, p=a+3;  p>=a;  p--)

  printf ("  a[%p-a]=%d",p,a[p-a]);

for (P, i=0;  i<4;  i++)

  printf ("  %d[a]=%d",i,i[a]);

P;

}

int a[]={0,1,2,3};

int *p[]={a,a+1,a+2,a+3},   **pp=p;

void main()

{

printf ("\n a[0]=%d   a[1]=    %d",a[0],a[1]);

printf ("\n **p= %d   **(p+1)= %d",**p,**(p+1));

printf ("\n **pp=%d   **(pp+1)=%d",**pp,**(pp+1));

printf ("\n a=  %p    a+1=    %p",a,a+1);

printf ("\n *p= %p    *(p+1)= %p",*p,*(p+1));

printf ("\n *pp=%p    *(pp+1)=%p",*pp,*(pp+1));

printf ("\n p=  %p    p+1=    %p",p,p+1);

printf ("\n pp= %p    pp+1=   %p",p,pp+1);

pp++;

printf ("\n pp= %p    *pp =   %p  **pp=%d   pp-p=%u", pp,*pp,**pp,pp-p);

printf ("\n *pp-a=%u",*pp-a);

printf ("\n p[1]-a=%u",p[1]-a);

printf ("\n **pp-*a=%d",**pp-*a);

(*pp)++;

printf ("\n pp=%p  *pp=%p  **pp=%d   pp-p=%u", pp,*pp,**pp,pp-p);

printf ("\n *pp-a=%u",*pp-a);

printf ("\n **pp-*a=%d",**pp-*a);

++**pp;

printf ("\n pp=%p  *pp=%p  **pp=%d   pp-p=%u", pp,*pp,**pp,pp-p);

puts("\n\n");

}

char *s[]={"Text","array","contains","five","elements"};

char **sp[]={s+4,s+3,s+2,s+1,s};

char ***spp=sp;

void main()

{

printf ("\n%s ",**++spp);

printf ("\n%s ",*--*++spp);

printf ("\n%s ",*++*--spp);

printf ("\n%s ",*(spp[1]+1));

printf ("\n%s ",*spp[3]);

printf ("\n%s ",*spp[-1]+3);

puts("\n\n");

}

Динамические многомерные массивы

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