10.10 – Указатели и массивы
Указатели и массивы неразрывно связаны в C++.
Разложение массива
На предыдущем уроке вы узнали, как определить фиксированный массив:
int array[5]{ 9, 7, 5, 3, 1 }; // объявляем фиксированный массив из 5 значений int
Для нас это массив из 5 значений типа int
, но для компилятора массив – это переменная типа int[5]
.
Во всех случаях, кроме двух (которые мы рассмотрим ниже), когда в выражении используется фиксированный массив, фиксированный массив будет раскладываться (неявно преобразовываться) в указатель, указывающий на первый элемент массива. Вы можете увидеть это в следующей программе:
#include <iostream>
int main()
{
int array[5]{ 9, 7, 5, 3, 1 };
// выводим адрес первого элемента массива
std::cout << "Element 0 has address: " << &array[0] << '\n';
// выводим значение указателя, до которого раскладывается массив
std::cout << "The array decays to a pointer holding address: " << array << '\n';
return 0;
}
На машине автора эта программа напечатала:
Element 0 has address: 0042FD5C
The array decays to a pointer holding address: 0042FD5C
В C++ распространено заблуждение, что массив и указатель на массив идентичны. Нет. В приведенном выше случае массив имеет тип "int[5]", а его «значение» – это сами элементы массива. Указатель на массив будет иметь тип "int*", а его значением будет адрес первого элемента массива.
Мы скоро увидим, в чем разница.
Все элементы массива по-прежнему доступны через указатель (мы увидим, как это работает в следующем уроке), но информация, полученная на основе типа массива (например, длина массива), не может быть доступна из указателя.
Однако это также эффективно позволяет нам в большинстве случаев обрабатывать фиксированные массивы и указатели одинаково.
Например, мы можем использовать косвенное обращение к массиву, чтобы получить значение первого элемента:
int array[5]{ 9, 7, 5, 3, 1 };
// Косвенное обращение через массив возвращает первый элемент (элемент 0)
std::cout << *array; // напечатает 9!
char name[]{ "Jason" }; // Строка в стиле C (также массив)
std::cout << *name << '\n'; // напечатает 'J'
Обратите внимание, что на самом деле мы не выполняем косвенное обращение через сам массив. Массив (типа int[5]
) неявно преобразуется в указатель (типа int*
), и мы используем косвенное обращение через этот указатель, чтобы получить значение по адресу памяти, который хранит указатель (значение первого элемента массива).
Мы также можем присвоить указателю значение, чтобы тот указывал на массив:
#include <iostream>
int main()
{
int array[5]{ 9, 7, 5, 3, 1 };
std::cout << *array << '\n'; // напечатает 9
int* ptr{ array };
std::cout << *ptr << '\n'; // напечатает 9
return 0;
}
Это работает, потому что массив раскладывается на указатель типа int*
, а наш указатель имеет тот же тип (так же тип int*
).
Различия между указателями и фиксированными массивами
Есть несколько случаев, когда разница при наборе кода между фиксированными массивами и указателями имеет значение. Это помогает проиллюстрировать, что фиксированный массив и указатель – это не одно и то же.
Основное отличие возникает при использовании оператора sizeof()
. При использовании с фиксированным массивом sizeof
возвращает размер всего массива (длина массива * размер элемента). При использовании с указателем sizeof
возвращает размер адреса памяти (в байтах). Следующая программа иллюстрирует это:
#include <iostream>
int main()
{
int array[5]{ 9, 7, 5, 3, 1 };
std::cout << sizeof(array) << '\n'; // напечатает sizeof(int) * длина массива
int* ptr{ array };
std::cout << sizeof(ptr) << '\n'; // напечатает размер указателя
return 0;
}
Эта программа печатает:
20
4
Фиксированный массив знает, какой длины массив, на который он указывает. Указатель на массив – нет.
Второе отличие возникает при использовании оператора адреса (&
). Взятие адреса указателя дает адрес памяти переменной указателя. Взятие адреса массива возвращает указатель на весь массив. Этот указатель также указывает на первый элемент массива, но информация о типе отличается (в приведенном выше примере тип &array
– это int(*)[5]
). Вряд ли вам когда-нибудь это понадобится.
Возвращаясь к передаче фиксированных массивов функциям
Еще в уроке «10.2– Массивы (часть 2)» мы упоминали, что, поскольку копирование больших массивов может быть очень дорогостоящим, C++ не копирует массив, когда тот передается в функцию. При передаче массива в качестве аргумента функции фиксированный массив раскладывается на указатель, и функции передается этот указатель:
#include <iostream>
void printSize(int* array)
{
// здесь массив рассматривается как указатель
std::cout << sizeof(array) << '\n'; // выводит размер указателя, а не размер массива!
}
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << sizeof(array) << '\n'; // напечатает sizeof(int) * длина массива
printSize(array); // здесь аргумент array раскладывается в указатель
return 0;
}
Этот код печатает:
32
4
Обратите внимание, что это происходит, даже если параметр объявлен как фиксированный массив:
#include <iostream>
// C ++ неявно преобразует параметр array[] в *array
void printSize(int array[])
{
// здесь array рассматривается как указатель, а не как фиксированный массив
std::cout << sizeof(array) << '\n'; // выводит размер указателя, а не размер массива!
}
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << sizeof(array) << '\n'; // напечатает sizeof(int) * длина массива
printSize(array); // здесь аргумент array раскладывается в указатель
return 0;
}
Этот код печатает:
32
4
В приведенном выше примере C++ неявно преобразует параметр, использующий синтаксис массива ([]
), в синтаксис указателя (*
). Это означает, что следующие два объявления функций идентичны:
void printSize(int array[]);
void printSize(int* array);
Некоторые программисты предпочитают использовать синтаксис []
, потому что он дает понять, что функция ожидает массив, а не просто указатель на значение. Однако в большинстве случаев, поскольку указатель не знает, насколько массив велик, вам всё равно придется передать размер массива как отдельный параметр (строки являются исключением, потому что они заканчиваются нулем).
Мы настоятельно рекомендуем использовать синтаксис указателя, поскольку он дает понять, что параметр обрабатывается как указатель, а не как фиксированный массив, и что определенные операции, такие как sizeof()
, будут работать так, как если бы параметр был указателем.
Лучшая практика
Для параметра функции, принимающего массив, используйте синтаксис указателя (*
) вместо синтаксиса массива ([]
).
Знакомство с передачей по адресу
Тот факт, что массивы раскладываются на указатели при передаче в функцию, объясняет основную причину, по которой изменение массива в функции изменяет настоящий переданный массив-аргумент. Рассмотрим следующий пример:
#include <iostream>
// параметр ptr содержит копию адреса массива
void changeArray(int* ptr)
{
*ptr = 5; // поэтому изменение элемента массива изменяет _настоящий_ массив
}
int main()
{
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << "Element 0 has value: " << array[0] << '\n';
changeArray(array);
std::cout << "Element 0 has value: " << array[0] << '\n';
return 0;
}
Element 0 has value: 1
Element 0 has value: 5
Когда вызывается changeArray()
, массив раскладывается на указатель, и значение этого указателя (адрес памяти первого элемента массива) копируется в параметр ptr
функции changeArray()
. Хотя значение в ptr
является копией адреса массива, ptr
по-прежнему указывает на настоящий массив (а не на копию!). Следовательно, когда выполняется косвенное обращение через ptr
, элемент, к которому осуществляется доступ, является настоящим первым элементом массива!
Проницательные читатели заметят, что это явление работает и с указателями на значения, не являющиеся массивами. Мы рассмотрим эту тему (называемую передачей по адресу) более подробно в следующей главе.
Массивы в структурах и классах не раскладываются
Наконец, стоит отметить, что массивы, которые являются частью структур или классов, не раскладываются, когда вся структура или класс передается функции. Это дает полезный способ предотвратить разложение, если это необходимо, и будет полезно позже, когда мы будем писать классы, использующие массивы.
В следующем уроке мы рассмотрим арифметику указателей и поговорим о том, как на самом деле работает индексирование массивов.