Массивы и указатели в C — разные вещи, которые бо́льшую часть времени ведут себя одинаково. Именно это «бо́льшую часть времени» и делает тему скользкой: код, который обращается с массивом как с указателем, обычно работает — до того дня, когда он попадает в одно из двух мест, где разница важна, и ответ молча оказывается неверным.
Эта страница проводит границу точно.
Превращение массива в указатель: одно правило, стоящее за всем
Почти в каждом выражении имя массива автоматически преобразуется в указатель на его первый элемент. Это называется decay (превращение, «распад»). arr становится &arr[0] с типом int *.
Имя массива и адрес его первого элемента печатаются одинаково. Это превращение в действии, и именно поэтому массив можно присвоить указателю без &.
Есть ровно три места, где превращение не происходит: в качестве операнда sizeof, в качестве операнда & и при инициализации символьного массива строковым литералом. Везде в остальном считайте, что превращение есть.
arr[i] — это буквально *(arr + i)
Стандарт C определяет оператор индексации через арифметику указателей: a[b] означает *(a + b). Индексация — не отдельная возможность языка, а сокращённая запись.
Отсюда вытекают два следствия. Во-первых, указатель можно индексировать: p[2] — это *(p + 2), поэтому функции, получающие указатель, всё равно могут пользоваться привычным синтаксисом arr[i]. Во-вторых, 2[arr] компилируется: *(2 + arr) — тот же адрес, что и *(arr + 2). Так никто не пишет за пределами головоломок, но это закрывает вопрос о том, является ли индексация «на самом деле» арифметикой указателей.
Передача массива в функцию
Из-за превращения функция никогда не получает массив. Она получает указатель.
Из этого примера стоит вытащить три вещи.
Размер в int arr[100] — это документация и ничего больше. Компилятор переписывает параметр в int *arr и никогда не проверяет, что вы передали сотню чего-либо.
Длина должна путешествовать отдельно. У функции есть адрес и никакого представления о том, насколько далеко простираются данные. Поэтому практически каждая функция C, принимающая массив, принимает и количество: memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). Строки — исключение только потому, что несут собственную метку конца, '\0'.
Передача указателя дёшева, а данные общие. Копия массива не создаётся, поэтому функция может изменять элементы вызывающей стороны — и часто именно это вам и нужно:
Помечайте параметры только для чтения как const int *, как это делает print_all. Это документирует намерение и позволяет компилятору поймать случайную запись.
Ловушка sizeof
Вот баг, который хоть раз ловит каждого программиста на C. sizeof — одно из двух мест, где превращения не происходит, поэтому он даёт настоящий размер массива там, где массив объявлен, и размер указателя везде, где массив уже превратился.
В main sizeof data равен 40 байтам, и длина вычисляется как 10. Внутри inspect sizeof arr — это размер указателя (8 на 64-битной машине), и «длина» получается равной 2. Код выглядит одинаково и тихо ошибается.
Отсюда правило: вычисляйте длину массива только в той области видимости, где массив объявлен, и передавайте её дальше оттуда.
& от массива даёт другой тип
Второе место, где превращения не происходит, — оператор &. &arr — это указатель на весь массив, а не на его первый элемент. Оба хранят один и тот же адрес; разница в том, что означает «прибавить 1».
Тот же начальный адрес, разный шаг. У arr тип int *, и он шагает на один элемент; у &arr тип int (*)[5], и он шагает на весь массив. Это тот самый механизм, благодаря которому работает передача строки двумерного массива, — о нём рассказано в многомерных массивах.
Указатель на массив против массива указателей
Два объявления, которые выглядят похоже и означают противоположное:
int *p[5]; // массив из 5 указателей на int
int (*q)[5]; // указатель на массив из 5 int
У [] приоритет выше, чем у *, поэтому int *p[5] разбирается как «p — это массив указателей». Скобки в int (*q)[5] заставляют * связаться первым: «q — это указатель на массив».
int *p[5] int (*q)[5]
p[0] -> [ int ] q -> [ int | int | int | int | int ]
p[1] -> [ int ] один непрерывный блок из 5
p[2] -> [ int ]
p[3] -> [ int ]
p[4] -> [ int ]
пять отдельных адресов
Массив указателей встречается чаще, и вы уже им пользовались: char *argv[] — это массив указателей на строки, по одному на каждый аргумент командной строки. Это ещё и способ хранить список строк разной длины, не расходуя память впустую.
Обратите внимание на (*q)[0]: разыменовать указатель, чтобы получить массив, а затем проиндексировать его. Скобки здесь снова обязательны — по той же причине приоритета.
Где массивы и указатели действительно различаются
Держите в голове эту короткую таблицу — и путаница исчезнет:
Массив int arr[10] | Указатель int *p | |
|---|---|---|
| Что это | 10 int, размещённых в памяти | одна переменная, хранящая адрес |
sizeof | 40 — весь блок | 8 — только указатель |
| Можно присваивать | нет: arr = x; — ошибка | да: p = x; — нормально |
& даёт | int (*)[10] | int ** |
| Откуда берётся память | объявлена вместе с массивом | там, куда вы указали |
Имя массива — не переменная, хранящая адрес; оно и есть хранилище, а адрес вычисляется по требованию. Поэтому присвоить ему ничего нельзя.
Часто задаваемые вопросы
Массивы и указатели — это одно и то же в C?
Нет, но их легко перепутать, потому что имя массива почти в каждом выражении превращается в указатель на его первый элемент. Сам массив — это блок элементов известного размера; указатель — одна переменная, хранящая один адрес. sizeof и & — два места, где разница видна.
Почему sizeof(arr) даёт неверный ответ внутри функции?
Потому что параметр не является массивом. void f(int arr[]) молча переписывается в void f(int *arr), поэтому sizeof(arr) измеряет указатель (8 байт на большинстве машин), а не исходный массив. Передавайте длину отдельным параметром.
В чём разница между int *p[5] и int (*p)[5]?
int *p[5] — это массив из 5 указателей на int. int (*p)[5] — один указатель на массив из 5 int. Скобки сначала связывают * с именем; без них побеждает [], потому что у него выше приоритет.
Правда ли, что arr[i] — это то же самое, что *(arr + i)?
Да — стандарт определяет a[b] как *(a + b). Именно поэтому странно выглядящее i[arr] компилируется и работает: сложение коммутативно, поэтому *(i + arr) — тот же элемент. Никогда не пишите так в реальном коде, но это доказывает правило.