Почему сглаживание многомерного массива в C незаконно?

Моя книга (Указатели на C Кеннетом Реком) гласит, что следующее незаконно, хотя оно отлично работает.

  int arr[5][5];
  int *p=&arr[2][2];
  p=p+3; // As array is stored in row major form I think this 
         //should make p point to arr[3][0]

В книге говорится, что оставить одну строку в следующей строке незаконной. Но я не понимаю, почему.

Ответ 1

Я надолго набросился на это, и я постараюсь изо всех сил объяснить, откуда я думаю, что он исходит, хотя и не читая книгу, это будет наилучшая гипотеза.

Во-первых, технически приращение, которое вы предлагаете (или он предложил), не является незаконным; разыменование это. Стандарт позволяет вам направить указатель на один последний элемент последовательности массива, из которого он был получен для оценки, но не для разыменования. Измените его на p = p + 4, и оба они являются незаконными.

В стороне, линейный след массива, который не выдерживает, ar[2] имеет тип, а int[5]. Если вы не верите в это, рассмотрите следующее, все из которых правильно напечатаны:

int ar[5][5];
int (*sub)[5] = ar+2;   // sub points to 3rd row
int *col = *sub + 2;    // col points to 3rd column of third row.
int *p = col + 3;       // p points to 5th colum of third row.

Не относится ли это к ar[3][0]. Вы превысили объявленную величину измерения, участвующую в указателе-математике. Результат не может быть разыменован на законных основаниях и был бы больше, чем 3-смещение, и он не мог бы быть даже юридически оценен.

Помните, что массив, адресуемый, ar[2]; а не только ar, а said-same объявлен как size = 5. То, что оно подкрепляется двумя другими массивами одного и того же типа, не имеет отношения к решению, которое в настоящее время выполняется. Я считаю, Кристоф ответ на вопрос, предложенный как дубликат, должен был быть выбран для прямого решения. В частности, ссылка на C99 §6.5.6, p8, которая, хотя и многословная, появляется ниже:

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

Извините за спам, но выделенные жирным шрифтом - это то, что, по моему мнению, имеет отношение к вашему вопросу. Обращаясь так же, как и вы, вы оставляете массив адресованным и, таким образом, ходите в UB. Короче говоря, он работает (обычно), но не является законным.

Ответ 2

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

arr - это массив из 5 элементов, в котором каждый элемент представляет собой массив из 5 целых чисел. Таким образом, теоретически, если вы хотите иметь указатели на элементы массива в arr[i], вы можете сделать только арифметику указателя, которая дает указатели в диапазоне &arr[i][0..4] или arr[i]+5 , сохраняя i константу.

Например, представьте, что arr был одномерным из 5 целых чисел. Тогда указатель p может указывать только на каждый из &arr[0..4] или arr+5 (один за концом). Это также происходит с многомерными массивами.

С помощью int arr[5][5]; вы можете выполнять арифметику указателей, так что всегда есть указатель, который находится в диапазоне &arr[i][0..4] или arr[i]+5, - это то, что говорят правила. Это просто путают, потому что это массивы внутри массивов, но правило такое же, что бы ни было. Концептуально arr[0] и arr[1] представляют собой разные массивы, и хотя вы знаете, что они смежны в памяти, незаконно выполнять арифметику указателя между элементами arr[0] и arr[1]. Помните, что концептуально каждый элемент в arr[i] является другим массивом.

В вашем примере, однако, p+3 будет указывать один за конец arr[2][2], поэтому он выглядит так, как будто он действителен. Это плохой выбор примера, потому что он будет p указывать точно на один конец, делая его по-прежнему действительным. Если бы автор выбрал p+4, пример был бы правильным.

В любом случае, у меня никогда не было проблем с выравниванием многомерных массивов на C с использованием аналогичных методов.

Также см. этот вопрос, он получил другую полезную информацию: Одномерный доступ к многомерному массиву: четко определенный C?

Ответ 3

Да. Это незаконно в C. Фактически, делая это, вы кладете свой компилятор. p указывает на элемент arr[2][2] (и имеет указатель на тип int), т.е. третий элемент третьей строки. Оператор p=p+3; будет увеличивать указатель p до arr[2][5], что эквивалентно arr[3][0].
Но это не удастся, если память выделяется как мощность 2 (2n) на некоторой архитектуре. Теперь в этом случае распределение памяти будет округлено до 2 n т.е. В вашем случае каждая строка будет округлена до 64 байтов.
См. Тестовую программу, в которой выделенная память представляет собой 5 распределений из 10 целых чисел. На некоторых машинах распределение памяти составляет не более 16 байт, поэтому запрошенные 40 байтов округляются до 48 байт на выделение:

#include <stdio.h>
#include <stdlib.h>

extern void print_numbers(int *num_ptr, int n, int m);
extern void print_numbers2(int **nums, int n, int m);

int main(void)
{
    int **nums;
    int n = 5;
    int m = 10;
    int count = 0;

    // Allocate rows
    nums = (int **)malloc(n * sizeof(int *));

    // Allocate columns for each row
    for (int i = 0; i < n; i++)
    {
        nums[i] = (int *)malloc(m * sizeof(int));
        printf("%2d: %p\n", i, (void *)nums[i]);
    }

    // Populate table
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            nums[i][j] = ++count;

    // Print table
    puts("print_numbers:");
    print_numbers(&nums[0][0], n, m);
    puts("print_numbers2:");
    print_numbers2(nums, n, m);
    return 0;
}

void print_numbers(int *nums_ptr, int n, int m)
{
    int (*nums)[m] = (int (*)[m])nums_ptr;

    for (int i = 0; i < n; i++)
    {
        printf("%2d: %p\n", i, (void *)nums[i]);
        for (int j = 0; j < m; j++)
        {
            printf("%3d", nums[i][j]);
        }
        printf("\n");
    }
}


void print_numbers2(int **nums, int n, int m)
{
    for (int i = 0; i < n; i++)
    {
        printf("%2d: %p\n", i, (void *)nums[i]);
        for (int j = 0; j < m; j++)
            printf("%3d", nums[i][j]);
        printf("\n");
    }
}

Пример вывода в Mac OS X 10.8.5; GCC 4.8.1:

 0: 0x7f83a0403a50
 1: 0x7f83a0403a80
 2: 0x7f83a0403ab0
 3: 0x7f83a0403ae0
 4: 0x7f83a0403b10
print_numbers:
 0: 0x7f83a0403a50
  1  2  3  4  5  6  7  8  9 10
 1: 0x7f83a0403a78
  0  0 11 12 13 14 15 16 17 18
 2: 0x7f83a0403aa0
 19 20  0  0 21 22 23 24 25 26
 3: 0x7f83a0403ac8
 27 28 29 30  0  0 31 32 33 34
 4: 0x7f83a0403af0
 35 36 37 38 39 40  0  0 41 42
print_numbers2:
 0: 0x7f83a0403a50
  1  2  3  4  5  6  7  8  9 10
 1: 0x7f83a0403a80
 11 12 13 14 15 16 17 18 19 20
 2: 0x7f83a0403ab0
 21 22 23 24 25 26 27 28 29 30
 3: 0x7f83a0403ae0
 31 32 33 34 35 36 37 38 39 40
 4: 0x7f83a0403b10
 41 42 43 44 45 46 47 48 49 50  

Пример вывода на Win7; GCC 4.8.1:

enter image description here