Не разыменовывайте недействительный указатель
Причина
Разыменование недействительного указателя, такого как nullptr, является неопределённым поведением и, как правило, приводит к немедленному краху программы, неверным результатам или повреждению памяти.
Примечание
Под указателем здесь подразумевается любой вид ссылки на объект, включая итераторы и представления (view).
Примечание
Это очевидное и хорошо известное правило языка, однако соблюдать его бывает непросто. Устранение нарушений без значительных накладных расходов требует хорошего стиля кода, поддержки библиотек и статического анализа. Это ключевая часть обсуждения модели типо- и ресурсобезопасности C++.
Смотрите также:
- Используйте RAII для предотвращения проблем с временем жизни.
- Используйте unique_ptr для предотвращения проблем с временем жизни.
- Используйте shared_ptr для предотвращения проблем с временем жизни.
- Используйте ссылки, когда
nullptrневозможен. - Используйте not_null для раннего обнаружения неожиданных
nullptr. - Используйте профиль bounds для предотвращения выхода за границы диапазона.
Пример
void f()
{
int x = 0;
int* p = &x;
if (condition()) {
int y = 0;
p = &y;
} // p становится недействительным
*p = 42; // ПЛОХО: p может быть недействительным, если ветка была выполнена
}
Чтобы решить проблему, либо продлите время жизни объекта, на который указывает указатель, либо сократите время жизни указателя (переместите разыменование до конца времени жизни указуемого объекта).
void f1()
{
int x = 0;
int* p = &x;
int y = 0;
if (condition()) {
p = &y;
}
*p = 42; // OK: p указывает на x или y, оба ещё в области видимости
}
К сожалению, большинство проблем с недействительными указателями сложнее обнаружить и исправить.
Пример
void f(int* p)
{
int x = *p; // ПЛОХО: как узнать, что p действителен?
}
Такого кода очень много. Большая его часть работает — после обширного тестирования, — но в изоляции невозможно сказать, может ли p быть nullptr. Это также является значительным источником ошибок. Существует множество подходов к решению этой проблемы:
void f1(int* p) // обработать nullptr
{
if (!p) {
// обработать nullptr (выделить память, вернуть, бросить исключение, указать на что-то и т.д.)
}
int x = *p;
}
Проверка на nullptr имеет два потенциальных недостатка:
- не всегда очевидно, что делать при обнаружении
nullptr - проверка может быть избыточной и/или относительно дорогой
- неясно, является ли проверка защитой от нарушения или частью требуемой логики.
<!-- comment needed for code block after list -->
void f2(int* p) // заявить, что p не должен быть nullptr
{
assert(p);
int x = *p;
}
Это несёт затраты только при включённой проверке утверждений и даёт компилятору/анализатору полезную информацию. Это будет работать ещё лучше, если/когда C++ получит прямую поддержку контрактов:
void f3(int* p) // заявить, что p не должен быть nullptr
[[expects: p]]
{
int x = *p;
}
Либо можно использовать gsl::not_null, чтобы гарантировать, что p не является nullptr.
void f(not_null<int*> p)
{
int x = *p;
}
Эти меры решают проблему только с nullptr. Помните, что существуют и другие способы получить недействительный указатель.
Пример
void f(int* p) // старый код, не использует owner
{
delete p;
}
void g() // старый код: использует голый new
{
auto q = new int{7};
f(q);
int x = *q; // ПЛОХО: разыменование недействительного указателя
}
Пример
void f()
{
vector<int> v(10);
int* p = &v[5];
v.push_back(99); // может перераспределить элементы v
int x = *p; // ПЛОХО: разыменование потенциально недействительного указателя
}
Контроль
Данное правило входит в профиль безопасности времени жизни.
- Отмечать разыменование указателя, указывающего на объект, вышедший из области видимости.
- Отмечать разыменование указателя, который мог быть обнулён присваиванием
nullptr. - Отмечать разыменование указателя, который мог стать недействительным после
delete. - Отмечать разыменование указателя на элемент контейнера, который мог стать недействительным при итерации.