Menu

Указатели и массивы в C: приведение к указателю, arr[i] = *(arr+i) и ловушка sizeof

В C имя массива почти везде превращается в указатель на его первый элемент. Здесь разобрано это превращение, почему arr[i] буквально равно *(arr+i), почему размер массива приходится передавать в функцию отдельно и чем указатель на массив отличается от массива указателей.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Массивы и указатели в 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, размещённых в памятиодна переменная, хранящая адрес
sizeof40 — весь блок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) — тот же элемент. Никогда не пишите так в реальном коде, но это доказывает правило.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ