почему при увеличении адреса на 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 шт):

Автор решения: Harry

Арифметика указателей строится на том, что для

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.

→ Ссылка
Автор решения: wololo

Арифметика указателей

К указателям можно прибавлять, вычитать целочисленные значения. Смысл таких операций состоит в следующем.

Если 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], однако, стандарт языка не делает никаких послаблений для такого случая.

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

  1. Стандарт суров, но это стандарт. Выходить за границы подмассивов многомерного массива при арифметике указателей нельзя и точка.
  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.

→ Ссылка