Avatar ·

Ключевое слово nullptr в C++: чем оно лучше NULL и как правильно использовать

Всем привет. Я начал изучать C++ недавно, и у меня возникла путаница с нулевыми указателями. В старых учебниках и на форумах часто пишут NULL, а в новых примерах я вижу nullptr. Вроде бы оба обозначают "ничто" для указателя, но компилятор иногда ругается на NULL.

Вот пример, который меня смутил. Я написал функцию, которая принимает указатель на int:

void print(int* ptr) {
    if (ptr == NULL) {
        std::cout << "Указатель пуст" << std::endl;
    } else {
        std::cout << *ptr << std::endl;
    }
}

void print(int value) {
    std::cout << "Целое число: " << value << std::endl;
}

Когда я вызываю print(NULL), компилятор (GCC с флагом -std=c++17) выдаёт предупреждение или даже ошибку, говоря, что вызов неоднозначен. Но если я пишу print(nullptr), всё работает идеально и вызывается версия с указателем.

Объясните, пожалуйста:

  • Что такое nullptr с точки зрения типа данных?
  • Почему NULL может приводить к неоднозначности?
  • Стоит ли везде заменять NULL на nullptr в современном C++?

Заранее спасибо за разъяснение!

<<>>

Что такое nullptr и почему он лучше NULL?

Отличный вопрос! Вы столкнулись с классической проблемой C++98/03, которую решили в C++11, введя ключевое слово nullptr. Давайте разберём всё по порядку.

1. Проблема NULL

В старом C++ NULL — это макрос, который обычно определён как 0 или (void*)0. Проблема в том, что 0 — это целое число. Из-за этого возникают неоднозначности при перегрузке функций, как в вашем примере:

void print(int* ptr);   // указатель
void print(int value);  // целое число

print(NULL); // Компилятор видит print(0) и не знает, какую версию выбрать!
// Обе функции подходят: 0 можно преобразовать и в int, и в int*

Это приводит к ошибкам компиляции или, что ещё хуже, к вызову не той функции (если перегрузки нет — NULL просто передаст целое число туда, где ожидался указатель).

2. Решение: nullptr

nullptr — это ключевое слово, которое представляет собой литерал нулевого указателя. Его тип — std::nullptr_t. Главные преимущества:

  • Типобезопасность: nullptr можно присвоить только указателю (или умному указателю), но нельзя целому числу. Это исключает путаницу.
  • Однозначность при перегрузке: компилятор точно знает, что nullptr — это указатель.
  • Шаблонная безопасность: в шаблонах nullptr сохраняет свой тип, в отличие от NULL.

Пример корректной работы:

void foo(int* p) { std::cout << "Указатель\n"; }
void foo(int i) { std::cout << "Целое\n"; }

foo(nullptr); // Выведет "Указатель" — однозначно
foo(NULL);    // Ошибка или неопределённое поведение

3. Как использовать nullptr на практике

В современном C++ (C++11 и новее) следует:

  • Всегда использовать nullptr вместо NULL и 0 для указателей.
  • Проверять указатели через if (ptr == nullptr) или просто if (ptr).
  • Возвращать nullptr из функций, которые в случае ошибки должны вернуть нулевой указатель.
  • Инициализировать указатели: int* p = nullptr;

Пример правильного кода:

#include <iostream>

void process(int* data) {
    if (data == nullptr) {
        std::cerr << "Ошибка: нулевой указатель!\n";
        return;
    }
    std::cout << "Данные: " << *data << '\n';
}

int main() {
    int value = 42;
    int* ptr1 = &value;
    int* ptr2 = nullptr;

    process(ptr1); // Работает
    process(ptr2); // Выводит ошибку

    // Сравнение
    if (ptr2 == nullptr) {
        std::cout << "ptr2 — нулевой\n";
    }

    return 0;
}

4. Дополнительные возможности

nullptr также работает с умными указателями (std::unique_ptr, std::shared_ptr) и в шаблонах:

template<typename T>
void safe_delete(T*& ptr) {
    delete ptr;
    ptr = nullptr; // Безопасно: ptr теперь точно нулевой
}

std::shared_ptr<int> sp = nullptr; // Тоже работает!

Вывод: nullptr — это современная, типобезопасная замена NULL. В коде на C++11 и новее всегда используйте nullptr. Это сделает ваш код понятнее и защитит от ошибок, связанных с неявным преобразованием целых чисел в указатели.

<<>>

Дополнение: nullptr и совместимость со старым кодом

Если вы работаете с проектом, который должен компилироваться и под старые стандарты (C++98/03), у вас может не быть выбора — придётся использовать NULL или 0. Однако есть трюк, который позволяет получить "почти nullptr" даже в старом C++:

// Для C++98/03 можно определить свой nullptr
const class nullptr_t {
public:
    template<typename T>
    operator T*() const { return 0; }
    
    template<typename C, typename T>
    operator T C::*() const { return 0; }
private:
    void operator&() const; // Запрещаем взятие адреса
} nullptr = {};

Но это костыль. В современных проектах (C++11+) используйте встроенный nullptr. Если вы поддерживаете старые компиляторы, лучше всего:

  • Написать макрос: #define MY_NULLPTR nullptr (или NULL для C++03).
  • Использовать #if __cplusplus >= 201103L для выбора.

Ещё один важный момент: nullptr неявно преобразуется к любому типу указателя, но не к целым числам. Это значит, что код вроде int x = nullptr; вызовет ошибку компиляции — и это хорошо, так как предотвращает случайное использование нулевого указателя как числа.

В итоге: если ваш проект использует C++11 или новее (а это сейчас стандарт для большинства новых разработок), просто замените все NULL на nullptr — компилятор сам подскажет, если где-то возникнут проблемы.