Арифметика указателей
К указателям можно прибавлять, вычитать целочисленные значения. Смысл таких операций состоит в следующем.
Если P — указатель, который указывает на i-тый элемент массива из n элементов (или гипотетический элемент, следующий непосредственно за последним элементом массива), и j — некоторое целочисленное значение, то указатель P + j указывает на i+j-тый элемент массива (или гипотетический элемент, следующий непосредственно за последним элементом массива), при условии, что 0 <= i+j <= n, в противном случае поведение программы не определено.
Также указатель P - j указывает на i-j-тый элемент массива (или гипотетический элемент, следующий непосредственно за последним элементом массива), при условии, что 0 <= i-j <= n, в противном случае поведение программы не определено.
В приведённом коде
int arr[2];
int* p0 = &arr[0];
int* p1 = p0 + 1;
указатель p0 указывает на элемент массива arr с индексом 0, а указатель p1 указывает на элемент массива arr с индексом 1.
Каждый элемент массива arr занимает sizeof(int) байт (обычно четыре байта). Хоть мы и прибавляем к указателю единицу, но в байтах, его значение увеличилось на sizeof(int) байт.
Это особенность арифметики указателей. Если указатель p имеет тип T*, то результат выражений p + j и p - j есть указатель «модифицированный» на j * sizeof(T) байт.
Похожим образом, с учётом размеров типа, вычисляется разность двух указателей.
Если указатели P и Q указывают на i и j-тый элементы одного и того же массива (или гипотетический элемент, следующий непосредственно за последним элементом массива), то результат выражения P - Q есть знаковое целочисленное значение, равное i - j, и имеющее тип ptrdiff_t (определён в заголовочном файле <stddef.h>).
Если P и Q не указывают на элементы одного и того же массива (или гипотетический элемент, следующий непосредственно за последним элементом массива) или величина i-j не представима типом ptrdiff_t, то поведение программы не определено.
В приведённом коде
int arr[2];
int* p0 = &arr[0];
int* p1 = &arr[1];
ptrdiff_t diff = p1 - p0; //1
значение diff равно 1. Однако, разница между указателями p1 и p0 в байтах равна sizeof(int) (обычно четыре). Это особенность арифметики указателей.
Если некоторый объект не является массивом, то для целей арифметики указателей он считается массивом, состоящим из одного элемента.
int obj;
int* p = &obj;
//OK, obj считается массивом из одного элемента.
//Указывать на гипотетический элемент за последним можно.
p = p + 1;
//А вот получить значение гипотетического элемента нельзя.
obj = *p; //Поведение не определено.
Массивы и указатели
Массив — это самостоятельная сущность. Массив не эквивалентен указателю на свой первый элемент.
Оператор sizeof, применённый к массиву, возвращает размер массива в байтах, но не размер указателя на первый элемент массива.
int arr[7];
printf("%zu\n", sizeof(arr)); //7 * sizeof(int)
printf("%zu\n", sizeof(&arr[0])); //sizeof(int*)
Пусть есть массив из n элементов типа T.
Оператор взятия адреса &, применённый к такому массиву, возвращает указатель на массив из n элементов типа T, т.е. указатель имеет тип T (*)[n] Но оператор взятия адреса, применённый к первому элементу массива, возвращает указатель на T, т.е. указатель имеет тип T*.
Указатель на весь массив и указатель на первый элемент массива представляют одинаковый адрес, но имеют разный тип. Это напрямую влияет на арифметику указателей, т.к. она зависит от размера типа, на который указывает указатель.
int arr[7];
int (*p_arr)[7] = &arr;
int* p_int = &arr[0];
p_arr = p_arr + 1; //Добавили sizeof(arr) == 7 * sizeof(int) байт.
p_int = p_int + 1; //Добавили sizeof(int) байт.
В некоторых ситуациях массив может быть неявно преобразован в указатель на свой первый элемент.
Например, если массив участвует в арифметике указателей, то он неявно преобразуется в указатель на свой первый элемент.
int arr[2];
int* p1 = arr + 1;
ptrdiff_t diff1 = p1 - arr; //1
ptrdiff_t diff0 = arr - arr; //0
int* p1 = arr + 1 — массив arr неявно преобразован в указатель на свой первый элемент (с индексом ноль), следовательно arr + 1 — это указатель на второй элемент (с индексом один) массива arr.
ptrdiff_t diff1 = p1 - arr — массив arr снова неявно преобразован в указатель на свой первый элемент, следовательно разность p1 - arr — это разность между указателями на элементы одного и того же массива с индесами 1 и 0 соответственно, следовательно результат выражения p1 - arr — это 1.
ptrdiff_t diff0 = arr - arr — опять же массив arr преобразуется в указатель на свой первый элемент.
Многомерные массивы
k-мерный массив, k > 1 — это одномерный массив, элементами которого являются k-1 мерные массивы.
int arr[7][4][3];
В приведённом коде arr — это одномерный массив из семи элементов типа T1, где T1 — это одномерный массив из четырёх элементов типа T2, где T2 — это одномерный массив из трёх элементов типа T3, где T3 — это int.
arr имеет тип int [7][4][3].
arr[0] имеет тип int [4][3].
arr[0][0] имеет тип int [3].
arr[0][0][0] имеет тип int.
С учётом всего написанного выше ваш код
int a[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
&a[0][0] + 11;
a[0] + 11;
a + 11;
работает следующим образом.
Выражение &a[0][0] имеет тип int*. Выражение &a[0][0] + 11 означает к указателю int*, указывающему на объект arr[0][0], прибавить одиннадцать раз sizeof(int) байт.
Выражение a[0] имеет тип int [4], т.е. это массив. Так как к массиву прибавляется целочисленное значение, то массив неявно преобразуется к указателю на свой первый элемент, т.е. к указателю int*. Таким образом, выражение a[0] + 11 означает к указателю int*, указывающему на объект arr[0][0], прибавить одиннадцать раз sizeof(int) байт.
Выражение a имеет тип int [3][4], т.е. это массив. Так как к массиву прибавляется целочисленное значение, то массив неявно преобразуется к указателю на свой первый элемент, т.е. к указателю int (*)[4]. (да, элементами массива a являются массивы, а не int'ы). Таким образом, выражение a + 11 означает к указателю int (*)[4], указывающему на объект arr[0], прибавить одиннадцать раз sizeof(int[4]) == 4 * sizeof(int) байт.
Про выход за границу массива
В начале ответа сказано, что если выражение P + j, где P — указатель, а j — целое число не указывает на элемент массива (или гипотетический элемент за последним элементом массива), то поведение программы не определено.
Приведённый код содержит неопределённое поведение:
int a[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
a + 11; //Поведение не определено.
Массив a содержит ровно три элемента типа int [4], следовательно выражение a + 11 не указывает ни на элемент массива a, ни на гипотетический элемент за массивом, следовательно поведение программы не определено.
Более того, код ниже также содержит неопределённое поведение:
int a[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
&a[0][0] + 11; //Поведение не определено.
a[0] + 11; //Поведение не определено.
Выражение &a[0][0] указывает на первый элемент массива arr[0], состоящего из четырёх элементов типа int, следовательно выражение &a[0][0] + 11 не указывает ни на элемент массива arr[0], ни на гипотетический элемент за массивом, следовательно поведение программы не определено.
Да, мы знаем, что непосредственно за массивом arr[0] следует массив arr[1], а за ним следует массив arr[2], однако, стандарт языка не делает никаких послаблений для такого случая.
Здесь на сайте было сломано немало копий в обсуждениях допустимости выхода за подмассивы многомерного массива. Однозначного ответа нет. Основных позиций две:
- Стандарт суров, но это стандарт. Выходить за границы подмассивов многомерного массива при арифметике указателей нельзя и точка.
- Так как мы знаем, что многомерный массив — это, по сути, большой одномерный массив, и все существующие компиляторы корректно обрабатывают выход за подмассивы многомерного массива, то так делать можно. А если завтра выйдет компилятор некорректно обрабатывающий выход за границы подмассивов, то его нужно немедленно удалить, авторов компилятора расстрелять.
Связанный вопрос: Можно ли обращаться к многомерным массивам как к одномерным?
Про printf и спецификаторы преобразования
С помощью спецификаторов преобразования вы обещаете передать функции printf аргумент определённого типа. И вы обязаны выполнить данное обещание. Если вы этого не делаете, то стандарт языка со своей стороны вам больше ничего не обещает — поведение программы не определено.
printf("size of a = %d\n", sizeof(a));
Спецификатор преобразования %d говорит, что должен быть передан аргумент типа int, но оператор sizeof возвращает значение беззнакового целочисленного типа size_t. Поведение программы не определено. Используйте спецификатор %zu для вывода значений типа size_t.
printf("address a = %p\n", a);
printf("address a[0] = %p\n", a[0]);
printf("address a[0][0] = %p\n\n\n", &a[0][0]);
Спецификатор преобразования %p говорит, что должен быть передан аргумент типа void*, но ни один из передаваемых аргументов (ни a, ни a[0], ни &a[0][0]) не имеет указанный тип, и не приводится неявно в данном конкретном случае к указанному типу. Поведение программы не определено. Используйте явное приведение типа: (void*)a, (void*)a[0], (void*)&a[0][0].
Связанный вопрос: printf and pointers.
Статья на cppreference.com о функции printf с большой табличкой по спецификаторам преобразования: https://en.cppreference.com/w/c/io/fprintf.
a + 11 == &a[0] + 11 == (char*)&a[0] + 11*sizeof(a[0]) == (char*)&a[0] + 11*4*sizeof(int) == (char*)&a[0] + 44*sizeof(int). – wololo Feb 25 '21 at 00:4811*4это же уже и есть11*sizeof(int), а так у Вас получается, что к адресу прибавляется44*sizeof(int) == 44*4 == 176, вместо 44 как указал ТС. – V-Mor Feb 25 '21 at 01:25a[0]имеет типint [4], поэтомуsizeof(a[0]) == 4 * sizeof(int). Вот почему... + 11*sizeof(a[0]) == ... + 11*4*sizeof(int).вместо 44 как указал ТСТС имел ввиду, что 44 раза прибавляетсяsizeof(int), т.е. прибавляется44 * 4 == 176байт. Ну, и собственно пример — все адреса одинаковые. – wololo Feb 25 '21 at 12:12char*написал я ;). Приведённый пример показывает равенство адресов, но не цепочку преобразований, которая происходит при вычислении выраженияa + 11. Приведение к типуchar*нужно, чтобы при арифметике указателей увеличение адреса происходило на заданное количество байтов, а неsizeof(какой_то_тип). Типcharвсегда занимает один байт. – wololo Feb 25 '21 at 12:18