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] + nm - 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(&quot;\n&quot;);
}

}

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(&quot;\n&quot;);
}

}

printf("\n\n\n"); return 0; }

Harry
  • 221,325
  • int занимает 4 байта в данном случае, поэтому и 44. – TigerTV.ru Feb 25 '21 at 00:20
  • 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:48
  • @wololo, по-моему, Вы немного напутали. 11*4 это же уже и есть 11*sizeof(int), а так у Вас получается, что к адресу прибавляется 44*sizeof(int) == 44*4 == 176, вместо 44 как указал ТС. – V-Mor Feb 25 '21 at 01:25
  • @wololo, подскажите, возможно я в виду малого опыта в программировании не понимаю этого - но откуда берется приведение типа к char если изначально я объявлял массив типа int? – Reiji Akkerman Feb 25 '21 at 04:22
  • @V-Mor, a[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:12
  • @ReijiAkkerman преобразование к char* написал я ;). Приведённый пример показывает равенство адресов, но не цепочку преобразований, которая происходит при вычислении выражения a + 11. Приведение к типу char* нужно, чтобы при арифметике указателей увеличение адреса происходило на заданное количество байтов, а не sizeof(какой_то_тип). Тип char всегда занимает один байт. – wololo Feb 25 '21 at 12:18

2 Answers2

3

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

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.

Harry
  • 221,325
  • Проверил на других числах - логика работает, но почему "a" определяется как указатель если я определяю ее как массив? – Reiji Akkerman Feb 25 '21 at 04:54
  • @ReijiAkkerman потому что в Си массив - это указатель на первый элемент массива. – insolor Feb 25 '21 at 05:11
  • Потому что что такое type[]? Массив типа type. Так что в int[][] что выступает типом массива? int[]... – Harry Feb 25 '21 at 05:41
3

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

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

Если 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.

wololo
  • 6,221