Ссылки и указатели в C++: отличие, примеры и лучшие практики

Ссылки и указатели в C++: отличие, примеры и лучшие практики

Ссылки и указатели в C++: отличие, примеры и лучшие практики

Запрос, на который отвечает статья: «Ссылки и указатели в C++: отличие и когда что использовать».

Ключевые различия

  • Ссылка — это псевдоним уже существующего объекта. Она всегда должна быть инициализирована и не может быть «пустой».
  • Указатель — это переменная, хранящая адрес. Может быть нулевым (nullptr), его можно переназначать, поддерживает арифметику указателей.
  • int x = 10;
    int& ref = x;         // ссылка на x, обязана быть инициализирована
    int* ptr = &x;         // указатель на x, может стать nullptr
    ref = 20;              // меняем x через ссылку (x == 20)
    *ptr = 30;             // меняем x через указатель (x == 30)
    ptr = nullptr;         // допустимо для указателя
    // ref = nullptr;    // так нельзя: ссылки не бывают пустыми
    

    Передача параметров в функции

    Общее правило: обязательные параметры — по ссылке (либо по значению, если недорого копировать); опциональные и «выходные» параметры — через указатели или возвращаемое значение.

    // Обязательный входной параметр — по константной ссылке
    void print_user(const std::string& name);
    
    // Опциональный выход — через указатель (может быть nullptr)
    bool find_price(const std::string& item, double* out_price);
    
    // Современная альтернатива «выходным указателям» — возврат значений/структур
    std::optional<double> try_find_price(const std::string& item);
    

    Ссылка vs указатель: swap

    void swap_ref(int& a, int& b) {
      int t = a; a = b; b = t;
    }
    
    void swap_ptr(int* a, int* b) {
      if (!a || !b) return; // указатели надо проверять
      int t = *a; *a = *b; *b = t;
    }
    
    int main() {
      int x = 1, y = 2;
      swap_ref(x, y);     // удобно, нельзя вызвать «пусто»
      swap_ptr(&x, &y);   // более шумно, нужна проверка
    }
    

    Const-корректность: частые сочетания

  • const T& — не копируемый, но неизменяемый параметр.
  • T* const — «константный указатель» (сам указатель нельзя переназначить, но по адресу можно менять объект).
  • const T* — «указатель на константу» (объект менять нельзя, переназначать указатель можно).
  • const T* const — и объект менять нельзя, и указатель переназначать нельзя.
  • void foo(const std::string& s);   // читать без копии
    void bar(const int* p);            // читать через указатель
    void baz(int* const p);            // p фиксирован, *p можно менять
    

    Время жизни и «висячие» ссылки

    Ссылка должна указывать на объект, который живёт дольше ссылки. Нельзя возвращать ссылку на локальную переменную функции.

    const std::string& bad() {
      std::string s = "temp";
      return s;           // ОПАСНО: возвращаем ссылку на уничтоженный объект
    }
    
    const std::string& good(const std::string& input) {
      return input;       // корректно: ссылка ссылается на внешний объект
    }
    

    Если нужно вернуть «что-то похожее на ссылку», но без риска по времени жизни — возвращайте по значению или используйте std::string_view (только если гарантирован срок жизни исходных данных).

    nullptr вместо NULL

    Используйте nullptr (C++11+) — типобезопасный нулевой указатель.

    void process(int* p);
    
    process(nullptr);   // корректно
    // process(NULL);  // может быть неоднозначно (макрос целочисленного типа)
    

    Массивы, указатели и потеря размера

    Массив «деградирует» до указателя при передаче в функцию — информация о размере теряется. Используйте шаблон/референс или std::span.

    void bad(int* a) { // размер неизвестен }
    
    // Размер известен через ссылку на массив
    template<size_t N>
    void good(int (&a)[N]) {
      static_assert(N > 0);
    }
    
    // Современный способ — std::span (C++20)
    #include <span>
    void process(std::span<const int> data) {
      for (int v : data) { /* ... */ }
    }
    
    int arr[3] {1,2,3};
    process(arr);                 // не теряем размер
    std::vector<int> v{1,2,3};
    process(v);                   // тоже работает
    

    Когда выбирать ссылку, а когда указатель

  • Ссылка: параметр обязателен, объект точно существует, вы не хотите проверок на nullptr.
  • Константная ссылка: большой объект «для чтения» без копии.
  • Указатель: параметр опционален, нужен «выходной» параметр, работа с массивами/буферами на низком уровне.
  • Владение динамической памятью: используйте умные указатели (unique_ptr/shared_ptr) — они управляют временем жизни. В этой статье мы сосредоточены на базовых указателях, но для владения предпочтительнее RAII-инструменты.
  • Типичные ошибки и как их избежать

    1. Висячие ссылки/указатели: не храните ссылку/указатель на объект, который скоро уничтожится.
    2. Неправильный delete: для массивов используйте delete[]; не вызывать delete на памяти, которой не владеете.
    3. Неинициализированные указатели: всегда инициализируйте nullptr и проверяйте перед разыменованием.
    4. Потеря размера массива: используйте std::span или шаблон со ссылкой на массив.
    int* p; *p = 42;         // неопределённое поведение (p не инициализирован)
    int* q = nullptr;        // хорошо: явная инициализация
    if (q) *q = 42;          // ничего не делаем, безопасно
    

    Инструменты контроля качества

  • Компилятор с флагами: -Wall -Wextra -Wpedantic.
  • Sanitizers: -fsanitize=address,undefined — ловят выход за границы, U.B. и т.п.
  • Статический анализ: clang-tidy, cppcheck.
  • Мини-чеклист

  • Обязательный параметр? — ссылка (T& или const T&).
  • Опциональный параметр или out-параметр? — указатель (T*) или возврат значения.
  • Нужна «пустота»? — только указатель (nullptr).
  • Следите за временем жизни объектов и используйте средства анализа.
  • Хотите системно прокачать основы и практику? Посмотрите пошаговый курс «C++ с Нуля до Гуру» — там много практики, разбор типичных ошибок и домашние задания.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий