Переменная, которая хранит адрес другой переменной, — и что с этим адресом можно делать
nullptr, неинициализированного и «висячего»a[i] — это *(a + i)g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.В разделе 7 функция меняла переменную вызывающего через параметр-ссылку int&. Зачем ещё один способ? Потому что ссылки есть только в C++, а Windows API, с которым мы будем работать с раздела 14, написан на C. Там результат возвращают по-другому: функции передают адрес переменной, и она записывает результат по этому адресу. Так выглядит чтение из файла или канала (раздел 17):
DWORD bytesRead;
ReadFile(file, buffer, sizeof(buffer), &bytesRead, nullptr); // &bytesRead — «запиши сюда, сколько прочитано»
Кроме того, адреса нужны, чтобы передать в функцию массив (раздел 10), выделить память во время работы программы (раздел 18) и передать данные в поток (раздел 16). Всё это держится на одной идее из этого раздела.
Модель памяти. Оперативная память — длинный ряд байтов, пронумерованных подряд. Номер байта — его адрес. Каждая переменная занимает несколько байтов подряд: int — 4, double — 8. Адрес переменной — адрес её первого байта. Элементы массива лежат вплотную друг за другом: если a[0] начинается с адреса 1000, то a[1] — с 1004, a[2] — с 1008.
Указатель — переменная, в которой хранится адрес. Тип указателя говорит, что лежит по адресу: int* — указатель на int, double* — на double.
// Адрес и указатель: & берёт адрес переменной, * обращается к значению по адресу
#include <iostream>
int main()
{
int x = 5;
int y = 8;
int* p = &x; // p хранит адрес x
std::cout << "x = " << x << ", &x = " << &x << '\n';
std::cout << "p = " << p << ", *p = " << *p << '\n';
*p = 7; // запись по адресу — меняется x
std::cout << "x = " << x << '\n';
p = &y; // тот же указатель — теперь на y
*p += 1;
std::cout << "y = " << y << ", *p = " << *p << '\n';
std::cout << "sizeof(int) = " << sizeof(int) << ", sizeof(p) = " << sizeof(p) << '\n';
return 0;
}
x = 5, &x = 0x5ffe9c
p = 0x5ffe9c, *p = 5
x = 7
y = 9, *p = 9
sizeof(int) = 4, sizeof(p) = 8
Адрес у вас будет другим и может меняться от запуска к запуску: Windows размещает программу в памяти по-разному. Совпадать должны &x и p. Адрес выводится в шестнадцатеричной системе (раздел 3).
| Запись | Что означает |
|---|---|
int* p; | объявление: p — указатель на int |
&x | адрес переменной x; тип — int*, если x — int |
*p | разыменование: переменная, на которую указывает p. Её можно читать и изменять: *p = 7, *p += 1 |
p = &y | указатель теперь хранит другой адрес; x не меняется |
sizeof(p) | размер самого указателя: 8 байт в 64-разрядной программе, какой бы тип ни был по адресу |
int* p звёздочка — часть типа, в выражении *p — «значение по адресу». В объявлении int& r амперсанд — ссылка (раздел 7), в выражении &x — «адрес x». И ещё: int* p, q; объявляет указатель p и обычный int q — звёздочка относится только к ближайшему имени. Правило курса: одно объявление указателя на строку.nullptr и недействительные указателиУказатель полезен, только если в нём адрес существующей переменной. Три случая, когда это не так:
| Случай | Пример | Что будет |
|---|---|---|
| не инициализирован | int* p; *p = 5; | в p случайное значение — запись по случайному адресу. g++ предупреждает: 'p' is used uninitialized |
nullptr | int* p = nullptr; *p = 5; | программа аварийно завершается (Windows: Access violation) |
| «висячий» — переменной уже нет | функция вернула адрес своей локальной переменной | после выхода из функции её переменные исчезают (раздел 7, пункт 05); по адресу — что угодно |
nullptr — специальное значение «ни на что не указывает». В отличие от мусора, его можно проверить: if (p != nullptr). Отсюда правило: указатель инициализируется при объявлении — адресом переменной или nullptr. Если функция ищет элемент и может не найти, nullptr в результате означает «не найдено» (пункт 07).
int* makeValue()
{
int value = 42;
return &value; // ошибка: value исчезнет при выходе из функции
}
warning: address of local variable 'value' returned [-Wreturn-local-addr]
Такая программа может напечатать 42 — и ошибка останется незамеченной, а может мусор или завершиться аварийно: это неопределённое поведение, как выход за границы массива в разделе 8. Современный g++ (проверено на версии 14) вместо адреса мёртвой переменной возвращает nullptr — программа падает на *p при любых ключах. Функция возвращает значение (int), а не адрес своей переменной.
Функция из раздела 7, которая меняет местами две переменные, — теперь через адреса:
// Обмен через параметры-указатели и через параметры-ссылки: результат один, запись вызова разная
#include <iostream>
void swapPtr(int* a, int* b);
void swapRef(int& a, int& b);
int main()
{
int x = 1;
int y = 2;
swapPtr(&x, &y); // адреса передаются явно — видно, что x и y изменятся
std::cout << "after swapPtr: x = " << x << ", y = " << y << '\n';
swapRef(x, y);
std::cout << "after swapRef: x = " << x << ", y = " << y << '\n';
return 0;
}
// Меняет местами значения по двум адресам
void swapPtr(int* a, int* b)
{
const int temp = *a;
*a = *b;
*b = temp;
}
// То же через ссылки — раздел 7
void swapRef(int& a, int& b)
{
const int temp = a;
a = b;
b = temp;
}
after swapPtr: x = 2, y = 1
after swapRef: x = 1, y = 2
Параметр-указатель — тоже параметр по значению: в функцию копируется адрес. Но по копии адреса функция добирается до той же самой переменной x — и меняет её. Внутри функции везде *a: без звёздочки a = b поменяло бы местами только локальные копии адресов, и x с y остались бы прежними.
Указатель умеет то, чего не умеет ссылка: он может быть nullptr. Поэтому через указатель удобно возвращать необязательный результат — так устроены многие функции Windows API:
// Необязательный результат: nullptr вместо адреса — «этот результат не нужен»
#include <iostream>
bool divide(int a, int b, int* quotient, int* remainder);
int main()
{
int q = 0;
int r = 0;
if (divide(17, 5, &q, &r)) {
std::cout << "17 / 5 = " << q << ", remainder " << r << '\n';
}
if (divide(-17, 5, &q, nullptr)) {
std::cout << "-17 / 5 = " << q << '\n';
}
if (!divide(1, 0, &q, &r)) {
std::cout << "1 / 0: division by zero\n";
}
return 0;
}
// Целое деление a на b. Частное и остаток пишутся по адресам, если адрес не nullptr.
// b == 0 — false, по адресам ничего не пишется
bool divide(int a, int b, int* quotient, int* remainder)
{
if (b == 0) {
return false;
}
if (quotient != nullptr) {
*quotient = a / b;
}
if (remainder != nullptr) {
*remainder = a % b;
}
return true;
}
17 / 5 = 3, remainder 2
-17 / 5 = -3
1 / 0: division by zero
Ссылка int& | Указатель int* | |
|---|---|---|
| Вызов | f(x) — по вызову не видно, что x изменится | f(&x) — видно |
| «Нет значения» | невозможно: ссылка всегда связана с переменной | nullptr; функция обязана проверить |
| Внутри функции | a | *a |
| Перенаправить | нельзя | можно: p = &y |
Правило курса: в своих функциях — ссылки, как в разделе 7; указатель — когда нужен nullptr («результат не нужен», «не найдено») и в вызовах Windows API, где выбора нет. В практической работе №13 обмен элементов делает функция с параметрами-указателями — на ней отрабатывается запись *a.
const и указателиУ указателя две вещи, которые можно запретить менять: значение по адресу и сам адрес. Объявление читается справа налево:
int x = 1;
int y = 2;
const int* p = &x; // p — указатель на const int: *p = 5 нельзя, p = &y можно
int* const q = &x; // q — константный указатель на int: *q = 5 можно, q = &y нельзя
const int* const r = &x; // нельзя ни то, ни другое
const int* — «смотреть, но не трогать». Так объявляют параметр функции, которая только читает данные по адресу; в разделе 10 так будут передаваться массивы, которые функция не меняет. Попытка записи: assignment of read-only location '* p'.int* const — адрес закреплён. Так удобно хранить границу массива, которая не должна сдвинуться: int* const end = a + n; (пункт 07).const-переменной можно положить только в const int*: иначе через указатель удалось бы изменить константу.Почти в любом выражении имя массива превращается в адрес его первого элемента: a — то же, что &a[0], и тип у него int*. Поэтому указатель можно направить на массив без амперсанда:
// Имя массива — адрес его первого элемента; a[i] — то же, что *(a + i)
#include <iostream>
int main()
{
int a[5] = {10, 20, 30, 40, 50};
int* p = a; // то же, что p = &a[0]
std::cout << "a = " << a << ", &a[0] = " << &a[0] << '\n';
std::cout << "a + 1 = " << a + 1 << ", &a[1] = " << &a[1] << '\n';
std::cout << "*(a + 3) = " << *(a + 3) << ", a[3] = " << a[3] << ", p[3] = " << p[3] << '\n';
std::cout << "sizeof(a) = " << sizeof(a) << ", sizeof(p) = " << sizeof(p) << '\n';
return 0;
}
a = 0x5ffe80, &a[0] = 0x5ffe80
a + 1 = 0x5ffe84, &a[1] = 0x5ffe84
*(a + 3) = 40, a[3] = 40, p[3] = 40
sizeof(a) = 20, sizeof(p) = 8
a[i] по определению означает *(a + i): «взять адрес начала, отступить на i элементов, взять значение». Поэтому квадратные скобки работают и с указателем: p[3]. Индексация с нуля отсюда же: a[0] — это *(a + 0), отступ ноль.a + 1 больше a не на 1, а на 4 — на размер int (пункт 07).sizeof(a) — 20 байт, весь массив; sizeof(p) — 8, только адрес. Указатель — переменная, его можно перенаправить: p = b, ++p. Имя массива — нет: a = p и ++a не собираются: incompatible types in assignment of 'int*' to 'int [5]' и lvalue required as increment operand.a == b для двух массивов сравнивает адреса их первых элементов, а не содержимое.К указателю на элемент массива можно прибавить целое число: p + i — адрес элемента, стоящего на i позиций правее. Компилятор сам умножает i на размер элемента: для int* адрес растёт на 4 · i, для double* — на 8 · i. Поэтому тип указателя важен не только при разыменовании.
| Операция | Результат | Условие |
|---|---|---|
p + i, p - i | указатель на i элементов правее / левее | результат — внутри массива или сразу за последним элементом |
++p, --p, p += i | то же с изменением p | то же |
q - p | сколько элементов (не байтов) между ними; тип std::ptrdiff_t — целое со знаком | p и q — в одном массиве |
p - a | индекс элемента, на который указывает p | p указывает в массив a |
p + q, p * 2 | не собирается: у суммы двух адресов нет смысла | — |
Обход массива указателем: p идёт от a до a + n, на каждом шаге ++p переводит его на следующий элемент. Индекс не нужен, а когда он всё-таки нужен для вывода, это p - a:
// Обход массива указателем: сумма, наибольший, поиск. Индексов нет — только адреса
#include <iostream>
constexpr int N = 8;
int main()
{
int a[N] = {4, -2, 9, 0, 9, 5, -7, 3};
int* const end = a + N; // адрес за последним элементом
int sum = 0;
for (int* p = a; p < end; ++p) {
sum += *p;
}
int* maxPtr = a; // указатель на первый наибольший
for (int* p = a + 1; p < end; ++p) {
if (*p > *maxPtr) {
maxPtr = p;
}
}
std::cout << "sum " << sum << ", max " << *maxPtr << " at " << maxPtr - a << '\n';
int x;
std::cout << "x: ";
std::cin >> x;
if (std::cin.fail()) {
std::cout << "Error: not a number\n";
return 1;
}
int* found = nullptr; // nullptr — «не найдено»
for (int* p = a; p < end && found == nullptr; ++p) {
if (*p == x) {
found = p;
}
}
if (found != nullptr) {
std::cout << x << " at " << found - a << '\n';
} else {
std::cout << x << " not found\n";
}
return 0;
}
sum 21, max 9 at 2
x: 5
5 at 5
sum 21, max 9 at 2
x: 1
1 not found
Сравните с разделом 8: наибольший хранился как индекс maxIndex, «не найдено» было −1. Здесь то же самое — указатель maxPtr и nullptr. Результат поиска-указателя сразу годится для работы с элементом: *found = 0 обнулит найденный элемент.
| Запись | Что делает | При p на 50 в массиве 40 50 60 |
|---|---|---|
*p++ | значение по p, затем p сдвигается: постфиксный ++ сильнее звёздочки и относится к p | 50; p на 60, массив не изменился |
(*p)++ | увеличивает значение по адресу | 50; массив 40 51 60, p на месте |
++*p | то же, результат — новое значение | 51; массив 40 51 60 |
В своих программах пишите *p и ++p отдельными инструкциями — запись *p++ нужно уметь читать в чужом коде, но легко перепутать с (*p)++.
| Сравнение | Когда имеет смысл |
|---|---|
p == q, p != q | всегда: указывают ли на один и тот же элемент; p == nullptr — пуст ли указатель |
p < q, p <= q, p > q, p >= q | только внутри одного массива (включая адрес за последним элементом): p < q — элемент *p левее *q. Для указателей в разные массивы результат ничего не значит |
Адрес за последним элементом a + n — законная граница: его можно вычислить, хранить и сравнивать, но нельзя разыменовывать — там уже не массив. Поэтому цикл обхода — p < a + n, так же как i < n. А вот адрес левее начала a - 1 нельзя даже вычислять: стандарт C++ разрешает арифметику только в пределах от a до a + n. Из-за этого обход с конца пишется так, чтобы указатель уменьшался только после проверки:
for (int* p = a + n - 1; p >= a; --p) // НЕВЕРНО: последний --p вычисляет a - 1
for (int* p = a + n; p > a; ) { // верно: сравнение с a, потом --p, потом *p
--p;
...
}
Первый вариант на практике обычно работает — поэтому ошибка и живёт в чужом коде годами. Но компилятор вправе считать, что p >= a всегда истинно (ведь p по правилам не может уйти левее a), и с оптимизацией цикл может не закончиться. То же с шагом больше единицы: p += 2 от предпоследнего элемента уводит за a + n. Проверяйте, есть ли куда шагать, до шага: end - p >= 2.
Два указателя навстречу — приём, ради которого сравнение указателей и нужно. Левый идёт от начала, правый от конца, пока left < right:
// Разворот массива двумя указателями навстречу друг другу; обход с конца без выхода за начало
#include <iostream>
constexpr int N = 7;
void swapPtr(int* a, int* b);
int main()
{
int a[N] = {1, 2, 3, 4, 5, 6, 7};
int* left = a;
int* right = a + N - 1;
while (left < right) { // встретились — все пары обменяны
swapPtr(left, right);
++left;
--right;
}
for (int* p = a; p < a + N; ++p) {
std::cout << *p << ' ';
}
std::cout << '\n';
for (int* p = a + N; p > a; ) { // сначала сравнение, потом --p: адрес a - 1 не вычисляется
--p;
std::cout << *p << ' ';
}
std::cout << '\n';
return 0;
}
// swapPtr — из пункта 04
7 6 5 4 3 2 1
1 2 3 4 5 6 7
Почему --right здесь безопасен: цикл идёт, пока left < right, а left не левее a — значит, right правее a, и после уменьшения не уйдёт за начало. При нечётном n указатели встречаются на среднем элементе, при чётном — проходят друг мимо друга; условие left < right ложно в обоих случаях. Тот же приём решает «разделить массив на две группы»: левый ищет элемент, которому место справа, правый — которому место слева, и они меняются.
Двумерный массив int m[3][4] — массив из трёх строк, каждая из которых — массив из четырёх int (раздел 8, пункт 06). Значит, m[i] — имя одномерного массива-строки, и по правилу пункта 06 оно превращается в адрес первого элемента строки &m[i][0]. Строку можно обходить указателем, как обычный массив:
// Строка матрицы — одномерный массив: m[i] — адрес её первого элемента
#include <iostream>
constexpr int ROWS = 3;
constexpr int COLS = 4;
int main()
{
int m[ROWS][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
for (int i = 0; i < ROWS; ++i) {
int* const row = m[i]; // то же, что &m[i][0]
int sum = 0;
for (int* p = row; p < row + COLS; ++p) {
sum += *p;
}
std::cout << "row " << i << ": sum " << sum << '\n';
}
std::cout << "m[2][1] = " << m[2][1] << ", *(m[2] + 1) = " << *(m[2] + 1) << '\n';
return 0;
}
row 0: sum 10
row 1: sum 26
row 2: sum 42
m[2][1] = 10, *(m[2] + 1) = 10
m[i][j] — это *(m[i] + j): сначала выбрана строка, затем в ней — элемент. Строки лежат в памяти подряд (раздел 8), и соблазнительно пройти всю матрицу одним указателем от m[0] до m[0] + ROWS * COLS. На практике это работает, но формально указатель, полученный из строки 0, не должен уходить за её конец. В курсе матрицу обходят построчно: внешний цикл по строкам, внутри — указатель по строке. Как передать матрицу в функцию — раздел 10.
| Ошибка | Признак | Сообщение g++ | Как избежать |
|---|---|---|---|
| указатель не инициализирован | запись по случайному адресу, аварийное завершение | 'p' is used uninitialized | при объявлении — адрес или nullptr |
разыменование nullptr | аварийное завершение (Access violation) | нет | if (p != nullptr) перед *p, если указатель может быть пустым |
| функция возвращает адрес своей локальной переменной | иногда верное значение, иногда мусор | address of local variable 'value' returned | возвращать значение, а не адрес |
int* p, q; | q — обычный int | invalid conversion from 'int*' to 'int' | одно объявление указателя на строку |
| адрес переменной другого типа | не собирается | cannot convert 'double*' to 'int*' in initialization | тип указателя — тип переменной |
запись через const int* | не собирается | assignment of read-only location '* p' | для записи — int*; const — если функция только читает |
| присваивание имени массива | не собирается | incompatible types in assignment of 'int*' to 'int [3]' | завести указатель: int* p = a; |
a = b в функции вместо *a = *b | функция ничего не меняет | нет | в теле — работа со значениями через * |
указатель левее a или правее a + n; разыменование a + n | обычно «работает», иногда мусор или бесконечный цикл | нет | обход с конца — p > a и --p до *p; шаг больше 1 — проверка до шага |
< между указателями в разные массивы | бессмысленный результат | нет | упорядоченно сравнивать только внутри одного массива |
*p++ вместо (*p)++ | сдвинулся указатель, а значение не изменилось | иногда value computed is not used | *p и ++p — отдельными инструкциями |
*p, *(p + 1), *(p - 1) в теле попадает в a … a + n - 1, а сам указатель никогда не выходит за a … a + n. Тесты: n = 1, n = 2, чётное и нечётное n, ответ в первом и в последнем элементе.&x — адрес, *p — значение по адресуnullptr; nullptr проверяется перед разыменованиемnullptr — «не нужно»const int* — нельзя менять значение, int* const — нельзя менять адресa[i] — это *(a + i)p + i — на i элементов дальше; q - p — число элементов между; p - a — индексa … a + n; a + n не разыменовывается, a - 1 не вычисляется< и > между указателями — только внутри одного массива; два указателя навстречу — пока left < rightconst; раздел 7 — параметры по значению и по ссылке, стек вызовов и время жизни локальных переменных; раздел 8 — массивы, матрицы, выход за границы. Нужен для: раздела 10 — массив и строка в функции передаются как адрес; раздела 11 — оператор -> для структур; раздела 12 — this и указатель на базовый класс; разделов 14–19 — все функции Windows API возвращают результаты через указатели, HANDLE и LPVOID — тоже адреса; раздела 18 — память, выделенная во время работы программы, доступна только по адресу.