почему при увеличении адреса на 11 массива с записью "a" он увеличивается на 44??? вместо положенных 11 как при записи "a[0]" и "a[0][0]"?
Недавно начал учить Си, в коде урока было следующее
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int a[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12}};
int n = sizeof(a)/sizeof(a[0]); // число строк
int m = sizeof(a[0])/sizeof(a[0][0]); // число столбцов
int *final = a[0] + n*m - 1; // указатель на самый последний элемент
for(int *ptr=a[0], i=1; ptr<=final; ptr++, i++)
{
printf("%d \t", *ptr);
// если остаток от целочисленного деления равен 0,
// переходим на новую строку
if(i%m==0)
{
printf("\n");
}
}
return 0;
}
тут указано
int *final = a[0] + n*m - 1;
что значит указатель final должен получить адрес последнего элемента двумерного массива, так оно и происходит. Но я решил проверить работает ли это со всеми способами указания адреса массива в указатель - ну т.е.
- a;
- a[0];
- &a[0][0];
с последними двумя вопросов нет, они увеличивают адрес согласно коду ниже на 11 - как и должно быть, но вот в первом варианте адрес увеличивается на все 44, чего быть не должно по идее. Вопрос почему так происходит, и что более интересно как??? Если все они начинаются с одного адреса.
#include <stdio.h>
int main(void)
{
int a[3][4] = { {1, 2, 3, 4} , {5, 6, 7, 8}, {9, 10, 11, 12} };
int n = sizeof(a) / sizeof(a[0]);
int m = sizeof(a[0]) / sizeof(a[0][0]);
printf("size of a = %d\n", sizeof(a));
printf("size of a[0] = %d\n", sizeof(a[0]));
printf("size of a[0][0] = %d\n", sizeof(a[0][0]));
printf("n = %d m = %d\n\n\n", n, m);
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]); //адрес первого элемента массива
int* fin = a + n * m - 1; //(увеличивается не на 11 адресов а на 44 хотя начальный адрес у всех одинаковый)
int* fin1 = a[0] + n * m - 1; //адрес последнего элемента массива
int* fin2 = &a[0][0] + n * m - 1; //адрес последнего элемента массива
printf("fin: address %p \t value %d\n", fin, *fin); //(увеличивается не на 11 адресов а на 44 хотя начальный адрес у всех одинаковый)
printf("fin1: address %p \t value %d\n", fin1, *fin1); //адрес последнего элемента массива
printf("fin2: address %p \t value %d\n\n\n", fin2, *fin2); //адрес последнего элемента массива
int* ptrd = a;
int diff = fin - ptrd;
printf("fin - ptrd = %d\n\n\n", diff);
for (int* ptr = a[0], i = 1; ptr <= fin2; ptr++, i++)
{
printf("i = %d ptr = %p %d ", i, ptr, *ptr);
if (i % m == 0)
{
printf("\n");
}
}
printf("\n\n");
for (int* ptr = a[0], i = 1; ptr <= fin; ptr++, i++)
{
printf("i = %d ptr = %p %d ", i, ptr, *ptr);
if (i % m == 0)
{
printf("\n");
}
}
printf("\n\n\n");
return 0;
}
Ответы (2 шт):
Арифметика указателей строится на том, что для
type * p
увеличение p на 1 означает переход к следующему элементу, т.е. увеличение значения на sizeof(type).
В ina a[3][4] тип a - указатель на int[4], так что в
int* fin = a + n * m - 1;
к адресу a прибавляется 11*sizeof(int[4])=11*16=176? т.е. в байтах - смещение на 176 байт, в int'ах (fin - указатель на int) - на 176/4=44.
Арифметика указателей
К указателям можно прибавлять, вычитать целочисленные значения. Смысл таких операций состоит в следующем.
Если 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.