Указатели представляют собой объекты, значением которых служат адреса других объектов (переменных, констант, указателей) или функций. Как и ссылки, указатели применяются для косвенного доступа к объекту. Однако в отличие от ссылок указатели обладают большими возможностями.
Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *:
тип_данных* название_указателя;
Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.
Например, определим указатель на объект типа int:
int* p;
Такой указатель может хранить только адрес переменной типа int, но пока данный указатель не ссылается ни на какой объект и хранит случайное значение. Мы его даже можем попробовать вывести на консоль:
#include <iostream> int main() { int* p; std::cout << p << std::endl; }
Например, в моем случае консоль вывела "0x8" - некоторый адрес в шестнадцатеричном формате (обычно для представления адресов в памяти применяется шестнадцатеричная форма). Но также можно инициализировать указатель некоторым значением:
int* p{};
Поскольку конкрентное значение не указано, указатель в качестве значения получает число 0. Это значение представляет специальный адрес, который не указывает не на что. Также можно явным образом инициализировать нулем, например, используя специальную константу nullptr:
int* p{nullptr};
Хотя никто не запрещает не инициализировать указатели. Однако в общем случае рекомендуется все таки инициализировать, либо каким-то конкретным значением, либо нулем, как выше. Так, к примеру, нулевое значение в будущем позволит определить, что указатель не указывает ни на какой объект.
Cтоит отметить что положение звездочки не влияет на определение указателя: ее можно помещать ближе к типу данных, либо к имени переменной - оба определения будут равноценны:
int* p1{}; int *p2{};
Также стоит отметить, что размер значения указателя (хранимый адрес) не зависит от типа указателя. Он зависит от конкретной платформы. На 32-разрядных платформах размер адресов равен 4 байтам, а на 64-разрядных - 8 байтам. Например:
#include <iostream> int main() { int *pint{}; double *pdouble{}; std::cout << "*pint size: " << sizeof(pint) << std::endl; std::cout << "*pdouble size: " << sizeof(pdouble) << std::endl; }
В данном случае определены два указателя на разные типы - int и double. Переменные этих типов имеют разные размеры - 4 и 8 байт соответственно. Но размеры значений указателей будут одинаковы. В моем случае на 64-разрядной платформе размер обоих указателей равен 8 байтам.
С помощью операция & можно получить адрес некоторого объекта, например, адрес переменной. Затем этот адрес можно присвоить указателю::
int number {25}; int *pnumber {&number}; // указатель pnumber хранит адрес переменной number
Выражение &number
возвращает адрес переменной number
. Поэтому переменная pnumber
будет хранить адрес переменной number
.
Что важно, переменная number имеет тип int, и указатель, который указывает на ее адрес, тоже имеет тип int. То есть должно быть соответствие по типу. Однако также можно использовать
ключевое слово auto:
int number {25}; auto *pnumber {&number}; // указатель pnumber хранит адрес переменной number
Если мы попробуем вывести адрес переменной на консоль, то увидим, что он представляет шестнадцатиричное значение:
#include <iostream> int main() { int number {25}; int *pnumber {&number}; // указатель pnumber хранит адрес переменной number std::cout << "number addr: " << pnumber << std::endl; }
Консольный вывод программы в моем случае:
number addr: 0x1543bffc74
В каждом отдельном случае адрес может отличаться и при разных запусках программы может меняться. К примеру, в моем случае машинный адрес переменной number - 0x1543bffc74
.
То есть в памяти компьютера есть адрес 0x1543bffc74, по которому располагается переменная number. Так как переменная x представляет тип int,
то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом,
переменная типа int последовательно займет ячейки памяти с адресами 0x1543bffc74, 0x1543bffc75, 0x1543bffc76, 0x1543bffc77.
И указатель pnumber будет ссылаться на адрес, по которому располагается переменная number, то есть на адрес 0x1543bffc74.
Итак, указатель pnumber хранит адрес переменной number, а где хранится сам указатель pnumber? Чтобы узнать это, мы также можем применить к переменной pnumber операцию &:
#include <iostream> int main() { int number {25}; int *pnumber {&number}; // указатель pnumber хранит адрес переменной number std::cout << "number addr: " << pnumber << std::endl; std::cout << "pnumber addr: " << &pnumber << std::endl; }
Консольный вывод программы в моем случае:
number addr: 0xe1f99ff7cc pnumber addr: 0xe1f99ff7c0
Здесь мы видим, что переменная number располагается по адресу 0xe1f99ff7cc
, а указатель, который хранит этот адрес, - по адресу 0xe1f99ff7c0
. Из вывода видно,
что обе переменные хранятся совсем рядом в памяти
Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной number. Для этого применяется операция * или операция разыменования ("indirection operator" / "dereference operator"). Результатом этой операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной number:
#include <iostream> int main() { int number {25}; int *pnumber {&number}; std::cout << "Address = " << pnumber<< std::endl; std::cout << "Value = " << *pnumber << std::endl; }
Пример консольного вывода:
Address = 0x44305ffd4c Value = 25
Значение, которое получено в результате операции разыменования, можно присвоить другой переменной:
int n1 {25}; int *pn1 {&n1}; // указатель pn1 хранит адрес переменной n1 int n2 { *pn1}; // n2 получает значение, которое хранится по адресу в pn1 std::cout << "n2 = " << n2 << std::endl; // n2=25
И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:
int x = 10; int *px = &x; *px = 45; std::cout << "x = " << x << std::endl; // 45
Так как по адресу, на который указывает указатель, располагается переменная x, то соответственно ее значение изменится.