10.9 – Нулевые указатели
Нулевые значения и нулевые указатели
Как и обычные переменные, указатели не инициализируются при создании экземпляров. Если указателю значение не присвоено, он по умолчанию будет указывать на какой-то мусорный адрес.
Помимо адресов памяти, есть еще одно дополнительное значение, которое может содержать указатель: нулевое значение. Нулевое значение – это специальное значение, которое означает, что указатель ни на что не указывает. Указатель, содержащий нулевое значение, называется нулевым указателем.
В C++ мы можем присвоить указателю нулевое значение, инициализировав или присвоив ему литерал 0:
float* ptr { 0 }; // ptr теперь является нулевым указателем
float* ptr2; // ptr2 не инициализирован
ptr2 = 0; // ptr2 теперь нулевой указатель
Указатели преобразуются в логическое значение false
, если они равны нулю, и в логическое значение true
, если они не равны нулю. Следовательно, мы можем использовать условное выражение, чтобы проверить, является ли указатель нулевым или нет:
double* ptr { 0 };
// указатели преобразуются в логическое значение false, если они равны нулю,
// и в логическое значение true, если они не равны нулю
if (ptr)
std::cout << "ptr is pointing to a double value.";
else
std::cout << "ptr is a null pointer.";
Лучшая практика
Если при создании вы не присваиваете указателям какое-либо значение, инициализируйте их нулевым значением.
Косвенное обращение через нулевые указатели
В предыдущем уроке мы отметили, что косвенное обращение через мусорный указатель приведет к неопределенным результатам. Косвенное обращение через нулевой указатель также приводит к неопределенному поведению. В большинстве случаев это приведет к сбою вашего приложения.
Концептуально в этом есть смысл. Косвенное обращение через указатель означает «перейти по адресу, на который указывает указатель, и получить доступ к значению там». У нулевого указателя нет адреса. Что делать, когда вы пытаетесь получить доступ к значению по этому адресу?
Макрос NULL
В C++ есть специальный макрос препроцессора под названием NULL
(определен в заголовке <cstddef>
). Этот макрос был унаследован от C, где он обычно используется для обозначения нулевого указателя.
#include <cstddef> // для NULL
double* ptr { NULL }; // ptr - нулевой указатель
Значение NULL
определяется реализацией, но обычно определяется как целочисленная константа 0. Примечание. Начиная с C++11, NULL
можно определить как nullptr
(что мы обсудим позже).
Лучшая практика
Поскольку NULL
– это макрос препроцессора со значением, определяемым реализацией, избегайте использования NULL
.
Опасности использования 0 (или NULL
) для нулевых указателей
Обратите внимание, что значение 0 не является типом указателя, поэтому присвоение 0 (или NULL
, до C++11) указателю для обозначения того, что указатель является нулевым, немного противоречиво. В редких случаях, когда ноль используется в качестве литерального аргумента, это может даже вызвать проблемы, потому что компилятор не может определить, имеет ли мы в виду нулевой указатель или целое число 0:
#include <iostream>
#include <cstddef> // для NULL
void print(int x)
{
std::cout << "print(int): " << x << '\n';
}
void print(int* x)
{
if (!x)
std::cout << "print(int*): null\n";
else
std::cout << "print(int*): " << *x << '\n';
}
int main()
{
int* x { NULL };
print(x); // вызывает print(int*), потому что x имеет тип int*
print(0); // вызывает print(int), потому что 0 - целочисленный литерал
print(NULL); // скорее всего, вызывает print(int), хотя мы, вероятно, хотели print(int*)
return 0;
}
В вероятном случае, когда NULL
определяется как значение 0, print(NULL)
вызовет print(int)
, а не print(int*)
, как вы могли бы ожидать от литерала нулевого указателя.
nullptr
в C++11
Для решения вышеуказанных проблем в C++11 введено новое ключевое слово nullptr
. nullptr
– это ключевое слово, очень похожее на логические ключевые слова true
и false
.
Начиная с C++11, когда нам нужен нулевой указатель, следует отдавать предпочтение ему, а не нулю:
int* ptr { nullptr }; // примечание: ptr по-прежнему является указателем int,
// просто он установлен в нулевое значение
C++ неявно преобразует nullptr
в любой тип указателя. Итак, в приведенном выше примере nullptr
неявно преобразуется в указатель int
, а затем значение nullptr
присваивается ptr
. Это приводит к тому, что ptr
, указатель int
, становится нулевым указателем.
Мы также можем вызвать функцию с литералом nullptr
, который будет соответствовать любому параметру, принимающему значение указателя:
#include <iostream>
void print(int x)
{
std::cout << "print(int): " << x << '\n';
}
void print(int* x)
{
if (!x)
std::cout << "print(int*): null\n";
else
std::cout << "print(int*): " << *x << '\n';
}
int main()
{
int* x { nullptr };
print(x); // вызывает print(int*)
print(nullptr); // вызывает print(int*), как и ожидалось
return 0;
}
Для продвинутых читателей
Функция со списком других параметров является новой функцией, даже если функция с таким же именем существует. Мы рассмотрели это в уроке «8.9 – Перегрузка функций».
Лучшая практика
Используйте nullptr
для инициализации указателей нулевым значением.
std::nullptr_t
В C++11 также представлен новый тип std::nullptr_t
(в заголовке <cstddef>
). std::nullptr_t
может содержать только одно значение: nullptr
! Хотя это может показаться странным, в одной ситуации это полезно. Если мы хотим написать функцию, которая принимает только аргумент nullptr
, какого типа мы сделаем параметр? Ответ – std::nullptr_t
.
#include <iostream>
#include <cstddef> // для std::nullptr_t
void doSomething(std::nullptr_t ptr)
{
std::cout << "in doSomething()\n";
}
int main()
{
doSomething(nullptr); // вызываем doSomething с аргументом типа std::nullptr_t
return 0;
}
Возможно, вам никогда не понадобится это использовать, но на всякий случай знать полезно.