mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 13:14:41 +03:00
111 lines
9.8 KiB
Markdown
111 lines
9.8 KiB
Markdown
# Указатель Шредингера: валиден и невалиден одновременно.
|
||
|
||
Что вообще такое указатель?
|
||
Когда их пытаются объяснить новичкам в C++, часто говорят, что это число, адрес, указывающий на номер ячейки в памяти, где что-то лежит.
|
||
|
||
Это в каком-то смысле справедливо на очень низком уровне — в ассемблере, в машинных кодах. Но В C/С++ указатель это не просто адрес. И тем более не число, которое как-то просто по-особому используется.
|
||
Более того, в C++ (не в C), есть указатели, которые вообще не являются адресами в памяти — указатели на поля и методы классов. Но о них мы говорить сейчас не будем.
|
||
|
||
Указатель — это ссылочный тип данных. Нечто, с помощью чего, можно получить доступ к другим объектам. И, в отличие от C++-ссылок, объекты-указатели являются настоящими объектами, а не странными псевдонимами для существующих значений. С числами и адресами в памяти указатели связаны только деталями реализации.
|
||
|
||
Для указателей в стандарте C++ подробно расписано, откуда они могут появляться.
|
||
Если коротко, то:
|
||
1. Как результат применения операции взятия адреса (`&x` или `std::addressof(x)`)
|
||
2. Как результат вызова оператора `new` (возможно, _placement new_)
|
||
3. Как результат неявного преобразования имени массива или имени функции в указатель.
|
||
4. Как результат некоторой _допустимой_ операции над другим указателем.
|
||
5. Копирование существующего указателя. В частности — копирование `nullptr`.
|
||
|
||
Все остальные источники указателей — implementation defined или вообще undefined.
|
||
|
||
Главная операция, выполняемая над указателями, — разыменование, то есть получение доступа
|
||
к объекту, на который этот указатель ссылается. И вместе с этой операцией приходит главная проблема — ее не ко всем указателям применять можно. Есть и другие операции, которые также применимы не к любому указателю.
|
||
Но, конечно, есть единственная операция, допустимая (почти) всегда — сравнение на равенство (не равенство).
|
||
|
||
В идеальном светлом мире, от типа объекта зависит множество допустимых над ним операций. Но в случае указателей, и это очень печально, применимость или неприменимость зависит не только от значения указателя, но еще и от того, откуда этот указатель взялся. А также откуда взялись другие указатели!
|
||
|
||
```C++
|
||
int x = 5;
|
||
auto x_ptr = &x; // валидный указатель, его МОЖНО разыменовывать
|
||
|
||
auto x_end_ptr = (&x) + 1; // валидный указатель, но его НЕЛЬЗЯ разыменовывать
|
||
|
||
auto x_invalid_ptr = (&x) + 2; // невалидный указатель,
|
||
// само его существование недопустимо.
|
||
```
|
||
|
||
Сравнение указателей на больше или меньше определено только для указателей на элементы одного и того же массива.
|
||
Для произвольных указателей — unspecified.
|
||
|
||
Арифметика указателей допустима только в пределах одного и того же массива (от указателя на первый элемент до указателя на элемент за последним) Иначе — undefined behavior.
|
||
Особый только случай `(&x) + 1` — любой объект считается массивом из одного элемента.
|
||
|
||
Пример кода, который валится с UB именно на арифметике указателей, найти сложно, зато можно привести пример с итераторами (которые разворачиваются в указатели).
|
||
|
||
```C++
|
||
std::string str = "hell";
|
||
str.erase(str.begin() + 4 + 1 - 3);
|
||
```
|
||
|
||
Этот код [упадет](https://rextester.com/GPVRKM58250) в отладочной сборке под msvc. `str.begin() + 4` — указатель на элемент за последним. И еще `+1`
|
||
выводит за пределы строки. Это UB. И не важно, что дальше вычитание возвращает внутренний указатель обратно в границы строки.
|
||
|
||
Не стоит выполнять сложные вычисления с указателями. Прибавлять к ним или вычитать лучше всегда конечный числовой результат. В данном конкретном примере рассчет отступа (4 + 1 - 3) нужно выполнить отдельно — расставить скобки или (еще лучше) безопаснее и понятнее, вынести в отдельную переменную.
|
||
|
||
-----------
|
||
|
||
Помимо выхода за границы объектов, невалидные указатели могут появляться после отрабатывания некоторых функций.
|
||
Наиболее яркий пример такого UB представил Nick Lewycky для [Undefined Behavior Consequences Contest](https://blog.regehr.org/archives/767). Немного переделанная и под C++ (чтобы в ней было только одно UB, а не два) версия выглядит так:
|
||
|
||
```C++
|
||
int* p = (int*)malloc(sizeof(int));
|
||
int* q = (int*)realloc(p, sizeof(int));
|
||
if (p == q) {
|
||
new(p) int (1);
|
||
new(q) int (2);
|
||
std::cout << *p << *q << "\n"; // print 12
|
||
}
|
||
```
|
||
|
||
Этот код, собранный clang, [выводит](https://godbolt.org/z/31av9f) результат, противоречащий здравому смыслу (если вы не знаете, что в коде UB!). И этот же пример демонстрирует, что указатели это не просто число-адрес.
|
||
|
||
Указателем, переданным в сишную функцию `realloc`, при успешной реаллокации, пользоваться более нельзя. Его можно только перезаписать (а потом уже использовать).
|
||
|
||
Данный пример, конечно, искусственный, но в него можно легко влететь, если, например, по какой-то причине писать свою версию вектора, используя `realloc`, и захотеть немного «соптимизировать».
|
||
|
||
|
||
```C++
|
||
template <class T>
|
||
struct Vector {
|
||
static_assert(std::is_trivially_copyable_v<T>);
|
||
|
||
size_t size() const {
|
||
return end_ - data_;
|
||
}
|
||
|
||
private:
|
||
T* data_;
|
||
T* end_;
|
||
size_t capacity_;
|
||
|
||
void reallocate(size_t new_cap){
|
||
auto ndata = realloc(data_, new_cap * sizeof(T));
|
||
if (!ndata) {
|
||
throw std::bad_alloc();
|
||
}
|
||
capacity_ = new_cap;
|
||
if (ndata != data_) {
|
||
const auto old_size = size(); // !access to invalidated data_!
|
||
data_ = ndata;
|
||
end_ = data_ + old_size;
|
||
} // else — "ok", noop
|
||
}
|
||
}
|
||
```
|
||
|
||
Этот код с неопределенным поведением. Скорее всего, оно никак не проявится сейчас, но не значит, что так будет и в будущем.
|
||
Возможно, вызов `reallocate` заинлайнится в неподходящем месте и все пойдет вверх дном.
|
||
|
||
Однако, начиная любые попытки реализовать свой собственный вектор (стандартный вполне может кого-то не устраивать тем, что он по умолчанию инициализирует память), надо иметь в виду следующий печальный факт: это [невозможно](https://stackoverflow.com/questions/60481204/dynamic-arrays-in-c-without-undefined-behavior) сделать без неопределенного поведения (формального или реального). Основная причина — арифметика указателей внутри сырой памяти. В сырой памяти формально нет C++ массивов, только внутри которых арифметика и определена.
|
||
|