Что такое указатель?
Указатель — это тип переменной, который должен хранить адрес в памяти.
Я говорю здесь “должен”, потому что если указатель правильно инициализирован, то в нем хранится либо nullptr
, либо адрес другой переменной (он даже может хранить адрес другого указателя), но если он не был инициализирован должным образом, то в нем будут содержаться произвольные данные, что довольно опасно, так как это может привести к неопределенному поведению.
Как можно инициализировать указатель?
В нашем распоряжении есть сразу три способа:
- Взять адрес другой переменной:
#include <iostream>
int main(){ int v = 42; int* p = &v;}
- Указать на переменную в куче
#include <iostream>
int main(){ int* p = new int {42}; }
- Или просто взять значение другого указателя
#include <iostream>
int main(){ int* p = new int {42}; int* p2 = p;}
Значения указателей и значения, на которые они указывают
Если вы решите вывести на экран значение указателя, то вы увидите адрес в памяти. Но если вам необходимо получить значение, на которое указатель он указывает, то нужно разыменовать этот указатель с помощью оператора .
#include <iostream>
int main() {
int* p = new int {42};
int* p2 = p;
std::cout << p << " " << *p << '\n';
std::cout << p2 << " " << *p2 << '\n';
std::cout << &p << " " << &p2 << '\n';
}
Вывод:
0x7d1848 42
0x7d1848 42
0x61ff0c 0x61ff08
В данном примере мы видим, что p
и p2
хранят один и тот же адрес в памяти, а это значит, что они указывают на одно и то же значение. Но если мы воспользуемся оператором &
, то мы увидим, что адреса у этих указателей разные.
Деаллокация памяти
Если выделение памяти (аллокация) происходит с помощью оператора new
, другими словами, если вы выделяете память в куче, то кто-то должен впоследствии высвободить (деаллоцировать) выделенную память. Это можно сделать с помощью оператора delete
. Если забыть это сделать, то, когда указатель выйдет за пределы области видимости, произойдет утечка памяти (memory leak). Поэтому обязательно высвобождайте всю выделенную память.
#include <iostream>
int main() {
int* p = new int {42};
std::cout << p << " " << *p << '\n';
delete p;
}
Если вы попытаетесь получить доступ к указателю уже после удаления или же попытаетесь удалить его во второй раз, то это вызовет неопределенное поведение. Простейший способ перестраховаться от такой ситуации — сразу после удаления присвоить p nullptr
. Если попытаться удалить указатель еще раз, то это не даст никакого эффекта, так как удаление nullptr
является no-op.
#include <iostream>
int main(){
int* p = new int {42};
std::cout << p << " " << *p << '\n';
bool error = true;
if (error) {
delete p;
p = nullptr;
}
}
Еще один важный момент — всегда проверяйте пригодность (валидность) указателя перед обращением к нему. Что если указатель уже был удален и не установлен в nullptr
? Неопределенное поведение, потенциальные краши. Или еще хуже…
Итерация по массивам
Еще один важный момент, связанный с указателями, — это операции, которые можно выполнять над ними. Мы часто называем их арифметикой указателей, потому что указатели можно инкрементировать или декрементировать (выполнять сложение и вычитание). Но на самом деле можно складывать и вычитать любые целые числа… Благодаря возможности инкремента/декремента, указатели можно использовать для итерации по массивам и доступа к любому их элементу.
#include <iostream>
int main(){
int numbers[5] = {1, 2, 3, 4, 5};
int* p = numbers;
for(size_t i=0; i < 5; ++i) {
std::cout << *p++ << '\n';
}
for(size_t i=0; i < 5; ++i) {
std::cout << *--p << '\n';
}
std::cout << '\n';
std::cout << *(p+3) << '\n';
}
Это конечно хорошо, но стоит ли использовать указатели для итерации по массивам в 2023 году?
Ответ однозначен — нет. Это небезопасно, указатель может указывать куда угодно, и он не работает со всеми типами контейнеров.
В предыдущем примере вы могли заметить, что в первом цикле мы используем постфиксный инкремент, а во втором — префиксный декремент. После прохода по массиву указатель указывает на недопустимое местоположение, поэтому перед разыменованием его необходимо декрементировать, иначе мы рискуем наткнуться на неопределенное поведение.