Указатель всегда хранит какое-то значение. Когда объекта для указания ещё нет — до присваивания, после освобождения того, на что он указывал, или когда функции нечего вернуть — нужно значение, которое недвусмысленно означает «здесь ничего». Это значение — нулевой указатель, и пишется оно как NULL.
Дисциплина вокруг него несложная и постоянно окупается: присваивайте указателям NULL, когда у них нет цели, проверяйте на NULL перед разыменованием и возвращайте NULL, когда результата нет.
Что такое NULL на самом деле
NULL — это макрос, определённый в <stddef.h> и подтягиваемый заодно из <stdio.h>, <stdlib.h>, <string.h> и других. Он раскрывается в нулевую указательную константу — обычно ((void*)0) или просто 0.
Стандарт C гарантирует главное свойство: нулевой указатель не равен указателю ни на один реальный объект или функцию. Два нулевых указателя одного типа всегда равны друг другу.
Большинство систем выводит для нулевого указателя (nil) или 0x0 — нулевой адрес. Это распространённая реализация, а не правило; стандарт нигде не обещает, что биты нулевые. Он обещает результат сравнения, поэтому пишите p == NULL и никогда не полагайтесь на представление.
Разыменование NULL роняет программу
Причина существования всех проверок на null:
int *p = NULL;
printf("%d\n", *p); // неопределённое поведение - почти наверняка падение
В Windows, macOS и Linux первая страница адресного пространства намеренно оставлена неотображённой, поэтому процессор перехватывает обращение, а операционная система убивает программу. Вы увидите:
Segmentation fault (core dumped) # Linux
zsh: segmentation fault ./program # macOS
Это немедленное падение — достоинство. Оно превращает логическую ошибку в громкий сбой ровно на той строке, которая его вызвала, вместо тихой порчи данных, которую вы обнаружите через три недели. О том, как прочитать такое падение и найти строку, — в статье ошибка сегментации.
Именно поэтому «null pointer» в C — не исключение, которое можно перехватить. Механизма восстановления нет; есть только проверка заранее.
Проверяйте то, что может не получиться
Функции, которые могут завершиться неудачей, сигнализируют об этом, возвращая NULL. Проверка — не факультативная вежливость: альтернатива ей — падение.
Стандартная библиотека полна таких функций: malloc, calloc, realloc, fopen, strchr, strstr, getenv, bsearch. Каждая из них отвечает «результата нет» значением NULL. Полная история про выделение памяти — в статье динамическая память.
Две одинаково распространённые формы проверки:
if (p == NULL) { /* обработка */ } // явная - предпочтительна для ясности
if (!p) { /* обработка */ } // краткая - опирается на ложность NULL
Нулевой указатель в условии считается ложным, а корректный — истинным, поэтому работают обе. Явная форма читается лучше, когда посторонний человек просматривает ваш код в поисках багов.
NULL против 0 и против неинициализированного
Три разные ситуации, которые новички смешивают в одну:
int *a = NULL; "не указывает ни на что" - проверяемо и безопасно
int *b; неинициализирован - хранит мусор, НЕ проверяем
int *c = &x; указывает на реальный объект
Опасен здесь b. Его байты — это то, что осталось в этой ячейке стека: может быть, ноль (и тогда всё выглядит нормально), а может быть, правдоподобный адрес от более раннего вызова. Никакая проверка не отличит его от корректного указателя, а поведение меняется между отладочной и релизной сборками.
Инициализируйте каждый указатель. Если адреса ещё нет, то адрес — это NULL. Собирайте с -Wall -Wextra, и компилятор сам укажет вам на многие случаи использования неинициализированных значений.
Про 0: в указательном контексте целочисленная константа 0 и есть нулевая указательная константа, поэтому p = 0; — корректный C. И всё же предпочитайте NULL. Он выражает намерение, и это важно в вызовах с переменным числом аргументов, где компилятор не может выполнить преобразование за вас:
execl("/bin/ls", "ls", 0); // рискованно - вместо указателя может уйти int
execl("/bin/ls", "ls", (char *)NULL); // правильно
В C23 появился nullptr — ключевое слово с собственным типом, полностью снимающее эту неоднозначность; NULL при этом остаётся корректным и переносимым везде.
Защитные приёмы
Проверяйте в начале функции, принимающей указатели.
Присваивайте NULL после освобождения. free не меняет ваш указатель — он только отдаёт память. Оставшийся устаревший указатель называется висячим, и его использование — неопределённое поведение, которое часто не приводит к падению сразу.
Два факта из этого примера стоит запомнить. free(NULL) по определению ничего не делает, поэтому код очистки никогда не нужно этим озабочивать. А присваивание p = NULL после free превращает использование освобождённой памяти — которое может тихо испортить данные — в немедленное и отлаживаемое падение.
Возвращайте NULL в значении «результата нет» и говорите об этом.
// возвращает указатель на подходящий элемент или NULL, если такого нет
int *find(int *arr, int n, int target);
Напишите это в комментарии над функцией. Вызывающий, который знает о возможном NULL, напишет проверку; тот, кто не знает, — нет.
Короткий чек-лист
- Инициализируйте каждый указатель — настоящим адресом или значением
NULL. - Проверяйте возвращаемое значение
malloc,calloc,reallocиfopenдо его использования. - Проверяйте параметры-указатели в начале любой функции, которую могут вызвать с некорректным вводом.
- Присваивайте указателям
NULLсразу послеfree. - Пишите
NULL, а не0, везде, где подразумевается указатель. - Собирайте с
-Wall -Wextraи, когда есть возможность, запускайте под санитайзером:gcc -fsanitize=address,undefinedловит разыменование null с точным отчётом.
Часто задаваемые вопросы
Что такое NULL-указатель в C?
Это указатель, хранящий нулевую указательную константу — значение, которое гарантированно не равно адресу ни одного реального объекта. Так принято говорить «этот указатель пока никуда не указывает», а NULL — макрос для такого значения, определённый в <stddef.h> и ещё нескольких заголовках.
Что произойдёт, если разыменовать NULL-указатель в C?
Это неопределённое поведение. На практике на настольных и серверных системах программа немедленно падает с ошибкой сегментации, потому что нулевой адрес намеренно оставлен неотображённым. На встраиваемых системах без защиты памяти вместо падения может произойти тихое чтение или порча данных, что намного хуже.
NULL и 0 — это одно и то же в C?
В указательном контексте да: целочисленная константа 0 является нулевой указательной константой, поэтому и p = 0;, и p == NULL работают. Но для указателей пишите NULL, а для чисел — 0: так читателю видно, что вы имели в виду, и это защищает вызовы с переменным числом аргументов, где голый 0 может быть передан как int, а не как указатель.
Чем NULL-указатель отличается от неинициализированного?
NULL-указатель точно не указывает ни на что, и это можно проверить. Неинициализированный указатель хранит те байты, которые случайно оказались в памяти, — возможно, похожие на настоящий адрес, — и никакой проверкой его не поймать. Всегда инициализируйте указатели значением NULL, если ничего лучше у вас нет.