int * pt; int (*ра) [3]; int ar1[2] [3]; int ar2[3] [2];
int **р2; // указатель на указатель
Взгляните на показанный далее код:
pt = &ar1 [0][0]; // оба - указатели на int
pt = &ar1 [0]; // оба - указатели на int
pt = arl; // недопустимо
pa = arl; // оба - указатели на int[3]

// недопустимо // оба - указатели на int // оба - указатели на int // недопустимо
Обратите внимание, что во всех недопустимых случаях присваивания вовлечены два указателя, которые не указывают на один и тот же тип. Например, pt указывает на одиночное значение int, но arl — на массив из трех значений int. Аналогично, ра указывает на массив из двух значений int, следовательно, он совместим с arl, но не с ar2, который указывает на массив из двух значений int.
Два последних примера немного запутаны. Переменная р2 представляет собой указатель на указатель на тип int, в то время как ar2 — это указатель на массив из двух значений int (или, выражаясь короче, указатель на массив int [2]). Таким образом, р2 и ar2 — разные типы, и вы не можете присвоить ar2 указателю р2. Но *р2 имеет тип указателя на int, что обеспечивает его совместимость с ar2 [0]. Вспомните, что ar2 [0] является указателем на свой первый элемент, ar2 [0] [0], что делает ar2 [0] также и типом указателя на int.
В целом, многократные операции разыменования сложны. Например, рассмотрим следующий фрагмент кода:
int х = 20; const int у = 23; int * p1 = &х; const int * р2 = &у; const int ** рр2;
p1 = р2; // небезопасно — присваивание константного значения неконстантному
р2 = p1; // допустимо -- присваивание константного значения константному рр2 = &р1; // небезопасно -- присваивание вложенных типов указателей
Как вы видели ранее, присваивание указателя const указателю, отличному от const, не является безопасным, т.к. новый указатель мог бы применяться для изменения данных типа const. Хотя код и скомпилируется, возможно, с выдачей предупреждения, результат его выполнения не определен. Но присваивание указателя не const указателю const допустимо при условии, что вы имеете дело только с одним уровнем косвенности:
р2 = p1; // допустимо -- присваивание неконстантного значения константному
Тем не менее, такие присваивания перестают быть безопасными, когда вы переходите к двум уровням косвенности. Например, вы могли бы написать такой код:
const int **рр2; int *р1;
const int n = 13;
рр2 = &р1; // разрешено, но квалификатор const игнорируется
*рр2 = &n; // допустимо, оба const, но p1 устанавливается указывающим на п
*р1 =10; // допустимо, но производится попытка изменить константу п
Что происходит? Как упоминалось ранее, в стандарте говорится, что результат изменения константных данных с использованием указателя, отличного от const, не определен. Например, компиляция короткой программы с этим кодом с помощью gcc в среде Terminal (интерфейс OS X для доступа к лежащей в основе системе Unix) приводит к тому, что n получает значение 13, но применение компилятора clang в той же среде обеспечивает для n значение 10. При этом оба компилятора предупреждают о несовместимых типах указателей. Разумеется, предупреждения можно игнорировать, но лучше не доверять результатам выполнения этой программы.
Массивы и указатели 403
Квалификатор const в С и C++
В С и C++ квалификатор const используется похожим, но не идентичным образом. Одно из отличий состоит в том, что C++ позволяет применять целочисленное значение const для объявления размера массива, в то время как язык С является более ограничивающим. Второе отличие заключается в том, что язык C++ обладает более строгими правилами присваивания указателей:
const int у; const int * р2 = &у; int * p1;
p1 = р2; // ошибка в C++, возможное предупреждение в С
В C++ не разрешено присваивать указатель const указателю, не являющемуся const. В С это присваивание возможно, но попытка использования p1 для изменения у ведет к неопределенному поведению.
Функции и многомерные массивы
Если вы намерены создавать функции, которые обрабатывают двумерные массивы, то должны достаточно хорошо понимать указатели, чтобы делать подходящие объявления для аргументов функций. В самом теле функции обычно можно обойтись записью в виде массива.
Давайте напищем функцию для взаимодействия с двумерными массивами. Одна из возможностей предусматривает использование цикла for для применения функции обработки одномерных массивов к каждой строке двумерного массива. Другими ело вами, можно предпринять примерно такие действия:
int junk [3] [4] = { {2, 4,5, 8 }, {3,5,6,91, {12,10,8,6) );
int i, j;
int total = 0;
Читать дальше