Я хочу поделиться своим любимым фрагментом «запрещенного кода», потому что, когда вы его поймете, вы больше никогда не будете путаться в указателях и массивах, и вы оцените, сколько пользы разработчики C выжали из нескольких тонких кусочков. синтаксиса.

Я надеюсь заставить вас усомниться в том, что в C вообще есть массивы, а затем доказать вам, что это действительно так, но не так, как вы могли подумать. Взглянем:

#include <stdio.h>

struct {
    int zero_squared;
    int one_squared;
    int two_squared;
    int three_squared;
    int four_squared;
    int five_squared;
    int six_squared;
} squares = {0,1,4,9,16,25,36};

int main()
{
    printf("4 squared is %d\n", 
            4[(&squares.zero_squared)] );
}
4 squared is 16

Когда вы впервые читаете его, это выражение кажется полным бредом:

4[(&squares.zero_squared)]

Здесь нет массивов, так что же делают эти скобки? И даже если вы на мгновение приостановите недоверие и притворитесь, что это массив, то почему 4, который должен быть индексом в массиве, находится снаружи квадратных скобок? Разве это не просто куча синтаксических ошибок?

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

Оператор скобок и арифметика указателя

Во-первых, рассмотрим обычный старый массив в стеке:

int squares[5] = {0, 1, 4, 9, 16};

Обычный способ доступа к элементам массива — скобки:

int one_squared = squares[1];

Но вы можете быть знакомы с другим способом доступа к элементам:

int two_squared = *(squares + 2);

Это работает, выполняя простые арифметические действия с указателями. squares — это «просто указатель» на начало массива. Итак, squares[2] всего на sizeof(int)*2байта позже. Поскольку сложение коммутативно, это также работает:

int three_squared = *(3 + squares);

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

К настоящему моменту вы можете подумать, зачем вообще все это делать, когда у нас есть оператор скобок? Разве это не очень окольный способ сделать это? Что ж, оказывается…

int four_squared = 4[squares];

*(a+i) неокольный способ сделать a[i]. Вместо этого a[i] является сокращением для *(a+i)!

Итак, теперь мы знаем, что массивы в C работают, потому что:

  • Скобки — это сокращение для +, применяемого к внутреннему и внешнему выражениям, за которым следует унарное *, применяемое к результату.
  • + неявно выполняет sizeof(), когда один операнд является указателем.

Вернуться к началу

Я написал пример в начале этой статьи, чтобы доказать, что скобки расширяются до арифметики указателей вообще и что это происходит даже в коде, полностью лишенном массивов; то есть, чтобы доказать, что это действительно просто условное обозначение, а не особенность массивов. Теперь нам ясно, что это:

4[(&squares.zero_squared)]

точно совпадает с этим:

*(4 + (&squares.zero_squared))

Что выглядит совершенно правильно; он говорит: «Возьмите адрес первого элемента структуры, переместите пространство из четырех целых чисел дальше в память и дайте мне там значение». Мы видим, что скобки действительно не имеют ничего общего с массивами. Они действительно просто сокращение.

Массивы в куче

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

int* squares = malloc(squares, sizeof(int) * 6);

с обычными старыми скобками:

squares[i]

Но помните, C ничего здесь не видит как массив — он просто применяет сокращение!

Так что же такое массив?

Значит ли это, что массивы — это «просто указатели»? Являются ли массивы вообще особенностью C?

Это правда, что символ, который мы носим в качестве имени массива, является «просто указателем» для целей арифметических выражений. Верно и то, что между массивами и скобками на самом деле нет никакой особой связи.

Но не будем слишком волноваться; C все еще имеет массивы.

Во-первых, и это наиболее очевидно, вы объявляете массив со специальным синтаксисом, который действительно встроен в язык:

int squares[5] = {0, 1, 4, 9, 16};

Поскольку вы возвращаете «просто указатель», это может быть неубедительно. Но посмотрите, как размеры массива sizeof()respects:

int foo[100];
printf("sizeof(foo) is %d\n", sizeof(foo));
sizeof(foo) is 400

Хотя простые указатели не могут вести себя так:

int* foo = malloc(sizeof(int)*100);
printf("sizeof(foo) is %d\n", sizeof(foo));
sizeof(foo) is 8

Это доказывает, что массивы действительно являются типом C, отличным от указателей.

Разве это не позор? Мы могли бы сказать, что массивы — это просто элегантное эмерджентное свойство арифметики указателей. Так зачем заставлять sizeof() относиться к этим двоим по-разному? Разве мы не можем избавиться от этой разницы?

Давайте посмотрим на более сложный пример, чтобы выяснить это.

Многомерные массивы

Заполните простой двумерный массив:

int foo[10][12];
for (unsigned int i=0; i < 10; i++)
    for (unsigned int j=0; j < 12; j++)
        foo[i][j] = i*100 + j;

Давайте проверим, что раскрытие скобок в математике указателей все еще имеет смысл:

printf("foo[3][4]     = %d\n", foo[3][4]     );
printf("*(foo[3]+4)   = %d\n", *(foo[3] + 4) );
printf("*(*(foo+3)+4) = %d\n", *(*(foo+3)+4) );
foo[3][4]     = 304
*(foo[3]+4)   = 304
*(*(foo+3)+4) = 304

Конечно делает! Но обратите внимание, что это работает, только если sizeof(foo[i]) достаточно, чтобы перейти к следующей целой строке массива. Другими словами, он должен быть таким же большим, как один из целых массивов следующего уровня. Проверять:

printf("sizeof(*foo) = %d\n", sizeof(*foo));
printf("sizeof(**foo) = %d\n", sizeof(**foo));
sizeof(*foo) = 48
sizeof(**foo) = 4

Это 48 — это целочисленный размер (4), умноженный на их количество в массивах следующего уровня (12). Если бы sizeof() не работало таким образом, многомерные массивы развалились бы.

Давайте обновим наш список синтаксических приемов, благодаря которым массивы работают в C:

  • Скобки — это сокращение для +, применяемого к внутреннему и внешнему выражениям, за которым следует унарное *, применяемое к результату.
  • + неявно выполняет sizeof(), когда один операнд является указателем.
  • sizeof() учитывает размер массивов, не считая их указателями.

Массив массивов или массив указателей?

Посмотрите еще раз внимательно на это выражение:

*(*(foo+3)+4)

Разве это не означает, что 2d-массивы на самом деле являются массивами указателей, причем каждый указатель указывает на другой массив?

В конце концов, это именно то, что представляет собой многомерный массив в куче — помните, что в этом контексте нет типов массивов, и вам действительно нужно собрать его с этой структурой явно следующим образом:

int** numbers = malloc(sizeof(int*)*10);
for (unsigned int i = 0; i < 10; i++)
    numbers[i] = malloc(sizeof(int)*12);

Раскрывая скобки в поиске 2D-массива на основе стека, вы видите два шага разыменования указателя с помощью *, и похоже, что объявление массива должно делать что-то вроде:

  1. Поместите массив из 10 указателей в стек
  2. Поместите в стек 10 массивов из 12 целых чисел.
  3. Убедитесь, что исходные 10 указателей указывают на начало 10 массивов.

Кажется, что это единственный способ иметь два отдельных разыменования указателя в выражении, но это было бы очень расточительно. Компилятор знает размеры всего, так что не может ли он просто убрать косвенность указателя между слоями массива?

Итак, давайте посмотрим на фактические адреса:

printf("foo           = %lx\n", foo);
printf("foo[3]        = %lx\n", foo[3]);
printf("(foo+3)       = %lx\n", (foo+3));
printf("*(foo+3)      = %lx\n", *(foo+3));
printf("*(foo+3)+4    = %lx\n", *(foo+3)+4);
printf("*(*(foo+3)+4) = %d\n", *(*(foo+3)+4));
foo           = 7ffe362d4590
foo[3]        = 7ffe362d4620
(foo+3)       = 7ffe362d4620
*(foo+3)      = 7ffe362d4620
*(foo+3)+4    = 7ffe362d4630
*(*(foo+3)+4) = 304

Подождите… (foo+3) и *(foo+3) — это один и тот же адрес!

Оказывается, массивы действительно отличаются от указателей еще в одном отношении: оператор * для массива возвращает указатель на начало самого массива, как если бы вы вообще не использовали оператор *.

Итак, вот наш, наконец, полный список синтаксических приемов, благодаря которым массивы работают:

  • Скобки — это сокращение для +, применяемого к внутреннему и внешнему выражениям, за которым следует унарное *, применяемое к результату.
  • + неявно выполняет sizeof(), когда один операнд является указателем.
  • sizeof() учитывает размер массивов, не считая их указателями.
  • *, примененный к массиву, возвращает указатель на начало массива без разыменования.

«Просто указатель»

Ранее я писал, что массив — это «просто указатель». Кавычки здесь не просто так, потому что теперь мы, наконец, готовы понять разницу между ними.

Для указателей:

  • sizeof() сообщает размер указателя
  • * делает настоящий поиск в памяти

Для массивов:

  • sizeof() объединяет размер содержимого
  • Арифметика с массивом работает так же, как арифметика с указателем на начало массива.
  • «Разыменование» массива с * дает вам адрес начала массива так же, как если бы * был проигнорирован

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