Указатель – самая частая причина, по которой C++ считают сложным, и одновременно то, ради чего его выбирают. По сути это просто переменная, в которой лежит адрес, но вокруг неё выстроен целый слой правил: арифметика, связь с массивами, отдельная разновидность строк.
Что такое указатель
int value = 42;
int* p = &value; // & – взять адрес
std::cout << p; // напечатает адрес, что-то вроде 0x7ffd4c8a1b2c
std::cout << *p; // 42 – разыменование: «то, что лежит по адресу»
*p = 100; // теперь value == 100
Тип указателя не украшение: он говорит компилятору, сколько байт читать при разыменовании и с
каким шагом двигаться при арифметике. int* и double* – разные типы, между собой неявно не
преобразуются.
Читать объявления удобно справа налево: int* p – «p есть указатель на int», const char* s – «s есть указатель на константный char». Позиция звёздочки (int* p или int *p) – вопрос стиля, компилятору всё равно. Но int* a, b; объявляет указатель и обычный int: звёздочка относится к переменной, а не к типу.
Нулевой указатель
Указатель может не указывать никуда. В современном C++ это записывается nullptr:
int* p = nullptr;
if (p != nullptr) {
*p = 1; // разыменовывать можно только после проверки
}
Старый код пишет NULL или просто 0. Оба варианта – это буквально ноль, из-за чего вызов
Foo(NULL) мог уйти в перегрузку Foo(int). nullptr имеет собственный тип и такой
неоднозначности не создаёт.
Разыменование нулевого указателя – неопределённое поведение; на практике почти всегда мгновенное падение, и это лучший из возможных исходов: ошибка видна сразу.
Массивы
Массив в C++ – это непрерывный участок памяти под N элементов одного типа. Размер – часть типа и должен быть известен на компиляции.
int a[5] = {1, 2, 3, 4, 5};
int b[] = {1, 2, 3}; // размер выведет компилятор: 3
int c[10] = {}; // все элементы – нули
std::cout << sizeof(a); // 20 – именно байты массива, а не указателя
Проверок границ нет. a[7] скомпилируется и прочитает то, что лежит за массивом, – в лучшем
случае мусор, в худшем испортит соседние данные.
Массив и указатель – не одно и то же
Имя массива в большинстве контекстов распадается (decay) в указатель на первый элемент:
int a[5] = {1, 2, 3, 4, 5};
int* p = a; // распад: p == &a[0]
std::cout << *(a + 2); // 3
std::cout << a[2]; // 3 – ровно то же самое, a[i] означает *(a + i)
std::cout << 2[a]; // тоже 3: сложение коммутативно. Так писать не надо
Но массив и указатель – разные типы, и разница видна там, где распада не происходит:
int a[5];
int* p = a;
sizeof(a); // 20 – размер массива
sizeof(p); // 8 – размер указателя
Особенно важно это при передаче в функцию:
void Foo(int a[3]) { /* ... */ } // размерность 3 компилятор игнорирует
int x[1] = {};
Foo(x); // компилируется, хотя элементов не три
Параметр-массив всегда превращается в указатель, поэтому размер внутрь функции не попадает. Отсюда стандартный приём: передавать длину отдельным параметром.
void Print(const int* data, size_t count);
Арифметика указателей
int a[5] = {10, 20, 30, 40, 50};
int* p = a;
++p; // сдвиг на sizeof(int) байт, теперь p смотрит на a[1]
p += 2; // на a[3]
--p; // на a[2]
int* end = a + 5; // указатель «за последним» – так делать законно
std::ptrdiff_t n = end - a; // 5: разность указателей даёт число элементов
Правила, которые стоит помнить:
- шаг арифметики – размер типа, поэтому у
void*арифметики нет; - законно получить адрес «на один за последним элементом», но разыменовывать его нельзя;
- вычитать друг из друга можно только указатели внутри одного массива;
- сравнение
<,>тоже определено только внутри одного массива.
Перебор массива через указатели – идиома, на которой построена вся стандартная библиотека:
for (int* it = a; it != a + 5; ++it) {
std::cout << *it << ' ';
}
Именно так устроены итераторы: begin() и end() – обобщение указателя на начало и «за конец».
C-style строки
Отдельного строкового типа в C не было. Строка – это массив char, заканчивающийся символом
'\0' (символ с кодом ноль). C++ совместимость сохранил.
const char* s = "C-style string"; // тип литерала – const char[15]: 14 символов и ноль
char copy[] = "C-style string"; // копия в собственном массиве, её менять можно
Литерал константен не для красоты: он лежит в области памяти, доступной только для чтения, и запись в него роняет программу.
Длина строки нигде не хранится – её каждый раз вычисляют проходом до нуля:
size_t Length(const char* s) {
size_t n = 0;
while (s[n] != '\0') {
++n;
}
return n;
}
Отсюда два классических источника ошибок. Первый: забыли завершающий ноль – и любая функция работы со строкой уходит читать за границу массива. Второй: strcat и strcpy не знают, сколько места в приёмнике, и радостно пишут за его пределы. Это буквально самая эксплуатируемая уязвимость в истории. Предпочитайте варианты с ограничением длины (strncat) или, ещё лучше, std::string.
Что уже есть в стандартной библиотеке
Функции работы с C-строками объявлены в <cstring>:
#include <cstring>
char str[100] = "left part";
std::strcat(str, " right part");
size_t length = std::strlen(str);
const char* found = std::strstr(str, "right"); // nullptr, если не найдено
А для массивов вообще – <algorithm>:
#include <algorithm>
int a[100] = { /* ... */ };
std::sort(a, a + 100); // по возрастанию
int* minIt = std::min_element(a, a + 100); // указатель на элемент, а не сам элемент
Обратите внимание на пару «начало, за концом» – так выглядит почти каждая функция стандартной
библиотеки. Писать свою сортировку не нужно; писать свой strlen – тоже, хотя один раз это
полезно сделать, чтобы понять, откуда берётся его цена.