# Разыменование нулевых указателей. Самая крутая ошибка с самыми жуткими последствиями. `null` вообще называют [ошибкой на миллиард долларов](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/). От них страдает куча кода, на самых разных языках программирования. Но если в условной `Java` при обращении по `null`-ссылке вы получите исключение с вполне предсказуемыми последствиями (ну, упало и упало), то в великом и ужасном C++, а также в C за вами придет неопределенное поведение. И оно будет действительно неопределенным! Но для начала, конечно, надо отметить, что, после всех обсуждений туманных формулировок стандарта, до июля 2024 года было некоторое [соглашение](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#315), что все-таки не сама по себе конструкция `*p`, где `p` — нулевой указатель, вызывает неопределенное поведение. А `lvalue-to-rvalue` преобразование. Ну или менее формально, кратко и не совсем правильно: пока нет чтения или записи значения по этому самому нулевому адресу — все нормально. Так, сейчас совершенно законно вы можете вызвать статические методы класса через `nullptr`. ```C++ struct S { static void foo() {}; }; S *p = nullptr; p->foo(); ``` А также можно писать вот такую ерунду ```C++ S* p = nullptr; *p; ``` Причем эту ерунду можно было писать только в C++. В C это безобразие все-таки запретили [(см. 6.5.3.2, сноска 104)](https://web.archive.org/web/20181230041359if_/http:/www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf). И в C применять оператор разыменования к невалидным и нулевым указателями нельзя нигде. А у C++ свой особый путь. И эти странные примеры [собираются](https://godbolt.org/z/zPx31e) в `constexpr` контексте (напоминаю, в нем запрещено UB и компилятор проверяет). Но вот уж совсем недавно было принято решение всё-таки это безобразие пресечь. И все примеры выше теперь содержат неопределённое поведение. Тем не менее никто не запрещает разыменовывать `nullptr` в невычисляемом контексте (внутри `decltype`): ```C++ #define LVALUE(T) (*static_cast(nullptr)) struct S { int foo() { return 1; }; }; using val_t = decltype(LVALUE(S).foo()); ``` Но, несмотря на то что так делать _можно_, совершенно не значит, что так делать _нужно_. Потому что последствия от разыменования `nullptr` там, где этого делать нельзя, могут быть печальными. Лезвие тонкое, острое, можно легко оступиться и что-нибудь взорвать. Если разыменовать `nullptr`, может быть исполнен код, который никак [не вызывался](https://godbolt.org/z/hPje47): ```C++ #include typedef int (*Function)(); static Function Do = nullptr; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); } ``` Компилятор обнаруживает разыменование `nullptr` (вызов функции `Do`). Это неопределенное поведение. Такого быть не может. Компилятор находит, что есть одно место, где этому указателю присваивается ненулевое значение. И раз нуля быть не может, то, значит, именно это значение он и использует. Как результат — исполняется код функции, которую мы не вызывали. Или вот совершенно дурная программа. ```C++ void run(int* ptr) { int x = *ptr; if (!ptr) { printf("Null!\n"); return; } *ptr = x; } int main() { int x = 0; scanf("%d", &x); run(x == 0 ? nullptr : &x); } ``` Из-за разыменования указателя `ptr`, проверка на `nullptr` после разыменования [может быть удалена](https://godbolt.org/z/c7YW9b). Вы, конечно же, почти наверняка никогда не напишете такой странный код. Но что если разыменование указателя будет спрятано за вызовом функции? ```C++ void run(int* ptr) { try_do_something(ptr); // если функция разыменует указатель, // и оптимизатор это увидит, проверка ниже // может быть удалена if (!ptr) { printf("Null!\n"); return; } *ptr = x; } ``` Такая ситуация уже куда ближе к реальности. В стандартной библиотеке C, например, есть функции, от которых можно было бы, по неопытности, ожидать проверки на `nullptr`, но они этого не делают. `strlen`, `strcmp`, другие строковые функции, а в C++ еще конструктор `std::string(const char*)` — их вызов с `nullptr` в качестве аргумента ведет к неопределенному поведению (и удалению нижерасположенных проверок, если вам не повезет). Еще есть особо мерзкие в этом смысле `memcpy` и `memmove`. Которые, несмотря на принимаемые в аргументах размеры буферов, все равно приводят к неопределенному поведению, если передать в них `nullptr` и нулевой размер! И точно также это может проявиться в удалении ваших проверок. ```C++ int main(int argc, char **argv) { char *string = NULL; int length = 0; if (argc > 1) { string = argv[1]; length = strlen(string); if (length >= LENGTH) exit(1); } char buffer[LENGTH]; memcpy(buffer, string, length); // при передаче nullptr // length будет нулевым, // но это не спасает от UB buffer[length] = 0; if (string == NULL) { printf("String is null, so cancel the launch.\n"); } else { printf("String is not null, so launch the missiles!\n"); } } ``` На одних и тех же входных данных (вернее, их отсутствии), этот код завершается с [разными результатами](https://godbolt.org/z/zc4xGz) в зависимости от компилятора и уровня оптимизаций. Если вы еще недостаточно напуганы, то вот еще замечательная [история](https://devblogs.microsoft.com/oldnewthing/?p=97635) о том, как весело и задорно падала функция вида ```C++ void refresh(int* frameCount) { if (frameCount != nullptr) { ++(*frameCount); // прямо вот тут грохалась из-за разыменования nullptr } ... } ``` просто потому что где-то совершенно в не связанном с ней классе написали: ```C++ class refarray { public: refarray(int length) { m_array = new int*[length]; for (int i = 0; i < length; i++) { m_array[i] = nullptr; } } int& operator[](int i) { // разыменование указателя без проверки на null return *m_array[i]; } private: int** m_array; }; ``` И вызвали функцию так: ```C++ refresh(&(some_refarray[0])); ``` А деятельный компилятор, зная что ссылки нулевыми не бывают, заинлайнил и удалил проверку. Здорово, неправда ли? Не забывайте проверять на `nullptr`. Иначе оно взорвется. ## Полезные ссылки 1. https://habr.com/ru/company/pvs-studio/blog/250701/ 2. https://habr.com/ru/post/513058/ 3. https://news.ycombinator.com/item?id=12002746