Я хочу поделиться своим любимым фрагментом «запрещенного кода», потому что, когда вы его поймете, вы больше никогда не будете путаться в указателях и массивах, и вы оцените, сколько пользы разработчики 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-массива на основе стека, вы видите два шага разыменования указателя с помощью *
, и похоже, что объявление массива должно делать что-то вроде:
- Поместите массив из 10 указателей в стек
- Поместите в стек 10 массивов из 12 целых чисел.
- Убедитесь, что исходные 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()
объединяет размер содержимого- Арифметика с массивом работает так же, как арифметика с указателем на начало массива.
- «Разыменование» массива с
*
дает вам адрес начала массива так же, как если бы*
был проигнорирован
Все эти тонкие различия объединяются в изобретении, так что оператор скобки может использоваться для поиска элементов в массивах кучи или стека в любом количестве измерений, без необходимости запекать оператор скобки в языке как что-то большее, чем просто преобразование текста. Я восхищаюсь таким элегантным дизайном.