Урок 8. Указатели и память
Указатели пугают новичков сильнее всего остального в C++. На деле идея простая, а сложности начинаются только при небрежном обращении. В этом уроке — что такое указатель, как им пользоваться и как не выстрелить себе в ногу.
Адрес переменной
Память компьютера — длинный ряд пронумерованных байтов. Номер байта называется адресом. Каждая переменная лежит где-то в памяти, и у неё есть адрес — номер её первого байта. Узнать его можно оператором &:
#include <iostream>
using namespace std;
int main() {
int x = 10;
cout << &x << "\n";
return 0;
}Программа выведет что-то вроде 0x7ffd5e8c3a4c — адрес в шестнадцатеричной записи. Он разный на разных компьютерах и даже при каждом запуске, и само число обычно не важно. Важно, что по адресу можно найти переменную.
Указатель
Указатель — переменная, в которой хранится адрес. Тип указателя — тип того, на что он указывает, со звёздочкой: int* — указатель на int.
#include <iostream>
using namespace std;
int main() {
int x = 10;
int* p = &x; // p хранит адрес x
cout << *p << "\n"; // значение по адресу
*p = 25; // меняем x через указатель
cout << x << "\n";
cout << (p == &x) << "\n";
return 0;
}10
25
1Звёздочка * здесь встречается в двух ролях, и их легко спутать:
- в объявлении
int* pона говорит:p— указатель; - в выражении
*pона разыменовывает указатель: «то, что лежит по адресу изp».
& тоже двуликий: в выражении &x — взять адрес, а в объявлении int& r = x — ссылка из прошлого урока.
nullptr — указатель в никуда
Указатель, который пока ни на что не указывает, приравнивают к nullptr. Разыменовывать такой указатель нельзя, поэтому перед использованием его проверяют:
#include <iostream>
using namespace std;
void reset(int* p) {
if (p != nullptr) {
*p = 0;
}
}
int main() {
int a = 7;
reset(&a);
reset(nullptr); // ничего не случится: функция проверяет
cout << a << "\n";
return 0;
}0В старом коде вместо nullptr пишут NULL или 0 — в новом коде берите nullptr.
Указатель без начального значения (int* p; внутри функции) содержит случайный адрес. Запись через него (*p = 5) портит случайную память: программа падает с segmentation fault или, хуже, тихо работает неправильно. Всегда задавайте указателю значение — адрес или nullptr.
Указатели и массивы
Имя массива в выражениях превращается в указатель на его первый элемент. К указателю можно прибавить число — он сдвинется на столько элементов (а не байтов):
#include <iostream>
using namespace std;
int main() {
int a[4] = {5, 6, 7, 8};
int* p = a; // то же, что &a[0]
cout << *p << " " << *(p + 2) << " " << p[3] << "\n";
p++;
cout << *p << "\n";
return 0;
}5 7 8
6a[i] — это сокращение для *(a + i). Отсюда же причина, по которой выход за границы массива не ловится: a[10] — просто адрес на 10 элементов дальше начала, и C++ не знает, что массив кончился раньше.
Из-за этого же массив, переданный в функцию, не знает своей длины: функция получает только указатель на первый элемент. Длину приходится передавать отдельным параметром — ещё одна причина брать vector.
Динамическая память: new и delete
Обычные локальные переменные живут в стеке — небольшой области памяти (обычно несколько мегабайт), которая освобождается сама при выходе из функции. Большие данные и данные, которые должны жить дольше функции, берут из кучи — общей памяти программы. Её выдаёт new и возвращает delete:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int* arr = new int[n]; // массив из n чисел в куче
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
long long sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
cout << sum << "\n";
delete[] arr; // вернуть память
arr = nullptr;
return 0;
}530Правила:
new int— одно число, освобождается черезdelete p;;new int[n]— массив, освобождается черезdelete[] p;(с квадратными скобками);- каждому
new— ровно одинdelete.
Ошибки здесь самые коварные:
- утечка памяти —
deleteзабыли, и программа, которая работает долго, занимает всё больше памяти; - висячий указатель — память уже освобождена, а указатель всё ещё хранит её адрес; обращение по нему непредсказуемо. Поэтому после
deleteуказателю присваиваютnullptr; - двойной
deleteодной и той же памяти — программа падает.
Как обойтись без new и delete
В современном C++ new и delete вручную пишут редко:
- для массива —
vector: он сам берёт память в куче и сам её возвращает, знает свой размер и умеет расти; - чтобы функция меняла переменную — ссылка
int&: она не бывает пустой, и её не нужно разыменовывать; - если одиночный объект в куче всё-таки нужен — умный указатель
unique_ptrиз<memory>, который сам вызоветdelete, когда станет не нужен.
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p = make_unique<int>(42);
cout << *p << "\n";
*p += 1;
cout << *p << "\n";
return 0;
} // здесь память освободится сама42
43Указатели при этом никуда не деваются: на них построены vector, string и вся стандартная библиотека, ими пользуются для связных списков и деревьев, без них не обойтись в языке C и при работе с железом. Понимать их нужно, но писать каждый день — не обязательно.
Указатель или ссылка
Указатель int* | Ссылка int& | |
|---|---|---|
| Может быть пустым | да, nullptr | нет |
| Можно перенаправить на другую переменную | да | нет, навсегда связана с первой |
| Доступ к значению | *p | просто r |
Арифметика (p + 1) | есть | нет |
Коротко
&x— адрес переменной,int* p = &x— указатель на неё,*p— значение по адресу.nullptr— пустой указатель; разыменовывать его нельзя, перед использованием проверяйте.- Имя массива — указатель на первый элемент,
a[i]— это*(a + i). new/delete,new[]/delete[]— память из кучи; забытыйdelete— утечка.- Вместо ручного
new:vector, ссылки иunique_ptr.