Menu

NULL-указатели в C: что такое NULL и как его проверять

NULL — это адрес, который хранит указатель, когда он не указывает ни на что. Здесь: что такое NULL на самом деле, почему его разыменование роняет программу, чем он отличается от 0 и от неинициализированного указателя, и какие защитные приёмы избавляют код от null-багов.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Указатель всегда хранит какое-то значение. Когда объекта для указания ещё нет — до присваивания, после освобождения того, на что он указывал, или когда функции нечего вернуть — нужно значение, которое недвусмысленно означает «здесь ничего». Это значение — нулевой указатель, и пишется оно как 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, если ничего лучше у вас нет.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ