Использование двумерного массива с использованием одного указателя

Есть тонны кода, подобного этому:

#include  int main(void) { int a[2][2] = {{0, 1}, {2, -1}}; int *p = &a[0][0]; while (*p != -1) { printf("%d\n", *p); p++; } return 0; } 

Но на основе этого ответа поведение не определено.

N1570. 6.5.6 p8:

Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, и массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что разность индексов результирующих и исходных элементов массива равна целочисленному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P) + N (эквивалентно, N + (P)) и (P) -N (где N имеет значение n) соответственно, i + n-й и i-й-элементы элемента массива, если они существуют. Более того, если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, и если выражение Q указывает один за последним элементом объекта массива, выражение (Q) -1 указывает на последний элемент объекта массива. Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае поведение не определено. Если результат указывает один за последним элементом объекта массива, он не должен использоваться как операнд унарного * оператора, который оценивается.

Может кто-нибудь объяснить это подробно?

Массив, который является базовым адресом (указатель на первый элемент) p имеет тип int[2] . Это означает, что адрес в p можно условно разыменовать только в местах *p и *(p+1) , или если вы предпочитаете нотацию подстроки, p[0] и p[1] . Кроме того, p+2 гарантируется юридически оцениваемой как адрес и сопоставимой с другими адресами в этой последовательности, но не может быть разыменован. Это адрес один за другим.

Код, который вы опубликовали, нарушает правило одного прошлого путем разыменования p только он передает последний элемент в массиве, в котором он находится. То, что массив, в котором он находится, подкрепляется другим массивом аналогичного измерения, не имеет отношения к цитированному формальному определению.

Тем не менее, на практике это работает, но, как часто говорят. наблюдаемое поведение не является и не должно рассматриваться как определенное поведение . Просто потому, что он работает, не делает это правильно.

Объектное представление указателей непрозрачно, в C. Нет запрета на указатели с кодировкой границ. Это одна возможность иметь в виду.

Более практично, реализации также могут достичь определенных оптимизаций, основанных на предположениях, которые утверждены такими правилами: Aliasing.

Тогда есть защита программистов от несчастных случаев.

Рассмотрим следующий код внутри тела функции:

 struct { char c; int i; } foo; char * cp1 = (char *) &foo; char * cp2 = &foo.c; 

Учитывая это, cp1 и cp2 будут сравниваться как равные, но их границы, тем не менее, различны. cp1 может указывать на любой байт foo и даже на «одно прошлое» foo , но cp2 может указывать только на «одно прошлое» foo.c , самое большее, если мы хотим поддерживать определенное поведение.

В этом примере может быть отступы между элементами foo.c и foo.i. В то время как первый байт этого дополнения сочетается с «одним прошлым» членом foo.c , cp2 + 2 может указывать на другое дополнение. Реализация может заметить это во время перевода и вместо того, чтобы создавать программу, она может посоветовать вам, что вы, возможно, делаете то, что не думаете, что делаете.

Напротив, если вы читаете инициализатор для указателя cp1 , он интуитивно предполагает, что он может получить доступ к любому байту структуры foo , включая дополнение.

Таким образом, это может привести к неопределенному поведению во время перевода (предупреждение или ошибка) или во время выполнения программы (путем кодирования информации о границах); нет никакой разницы, по умолчанию: поведение не определено.

Вы можете направить указатель на указатель на массив, чтобы обеспечить правильную семантику массива.

Этот код действительно не определен, но представлен как расширение C в каждом компиляторе, используемом сегодня.

Однако правильным способом сделать это было бы преrotation указателя в указатель на массив следующим образом:

((int (*) [2]) p) [0] [0]

получить нулевой элемент или сказать:

((int (*) [2]) p) [1] [1]

чтобы получить последнее.

Чтобы быть строгим, он считает, что это незаконно, так это то, что вы нарушаете строгий псевдоним, указатели на разные типы могут не указывать на один и тот же адрес (переменная).

В этом случае вы создаете указатель на массив ints и указатель на int и указываете на то же значение, это не допускается стандартом, поскольку единственным типом, который может быть псевдоним другого указателя, является char * и даже это редко используется должным образом.