JavaRush /Курсы /C++ SELF /T& и const T& — «второе имя»

T& и const T& — «второе имя»

C++ SELF
15 уровень , 1 лекция
Открыта

1. Введение

Когда новичок видит & в параметре, первая реакция часто такая: «О нет, это что-то опасное, наверное, из тех страшных тем, где всё падает». На самом деле ссылки в параметрах — это один из самых дружелюбных инструментов C++… если относиться к ним как к контракту, а не как к «трюку».

Представьте, что функция — это маленький цех. Аргументы — это то, что вы заносите внутрь. Есть два способа работать: либо вы приносите копию детали (передача по значению), либо вы приносите саму деталь и говорите «вот она, работай прямо с ней» (передача по ссылке). Ссылка — это как бейджик «я официальный представитель этого объекта».

T& как «второе имя»

Важный мягкий старт: ссылка — это не новый объект. Ссылка — это другое имя для уже существующего объекта.

То есть если у вас есть переменная a, и вы где-то получили int& r = a;, то r и a — это «две таблички на одной двери». Внутри комнаты один и тот же объект.

Мини-пример, чтобы почувствовать руками:

#include <iostream>

int main() {
    int a = 10;
    int& r = a;      // r — второе имя a

    r += 5;
    std::cout << a << '\n';  // 15
}

Здесь нет копирования. Нет «второго int». Есть один int, но к нему два пути обращения.

Параметр T&: функция может изменить аргумент

Теперь переносим идею в функции. Когда параметр записан как T&, это читается так: «Функция работает с реальным объектом вызывающей стороны». А значит, изменения внутри функции будут видны снаружи. Это не «побочный эффект», это и есть смысл такого параметра: вы явно разрешили функции менять ваш объект.

Самый короткий пример — инкремент:

#include <iostream>

void inc(int& x) {
    x += 1;
}

int main() {
    int a = 10;
    inc(a);
    std::cout << a << '\n'; // 11
}

Здесь x — это a. Поэтому x += 1 меняет a.

И вот тут появляется очень взрослое правило читаемости: если вы используете T&, то по названию функции обычно должно быть понятно, что она меняет аргумент. inc, append, normalize, sort_inplace — хорошие названия. calc, get, print — подозрительные, потому что «calc» обычно считают, а не переписывают вам переменную.

2. T& не создаёт копию

Ещё одна причина любить ссылки — они часто убирают лишние копии больших объектов. Но сегодня мы держим фокус не на производительности ради производительности, а на смысле: если функция должна менять строку — логично, что она принимает ссылку.

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

#include <iostream>
#include <string>

void add_exclamation(std::string& s) {
    s += "!";
}

int main() {
    std::string name = "Bob";
    add_exclamation(name);

    std::cout << name << '\n'; // Bob!
}

Важная деталь: если бы параметр был std::string s (по значению), вы бы меняли копию, и снаружи ничего бы не произошло. То есть тип параметра — это не «как удобнее», а что именно обещает функция.

3. Ограничение T&: нельзя привязать к временному значению

Сейчас будет момент, где многие впервые получают ошибку компиляции и думают: «Компилятор сломался». Нет, компилятор вас спас.

Нельзя привязать T& к временному значению (к объекту, который живёт очень недолго).

Пример:

#include <string>

void add_exclamation(std::string& s) {
    s += "!";
}

int main() {
    add_exclamation(std::string("Bob")); // ошибка компиляции
}

Почему? Потому что std::string("Bob") — временный объект. Он появляется «на время выражения», а потом исчезает. Если бы C++ разрешил вам сделать на него «долгоиграющую» ссылку T&, вы могли бы начать менять объект, который вот-вот испарится. Это как пытаться прикрутить полку к воздуху: пока вы держите дрель — вроде есть куда, но потом окажется, что стены не было.

Запомните прикладное правило: T& требует «настоящую переменную» (именованный объект), а не результат выражения «на лету».

4. const T&: читаю без копии и обещаю «не изменяю»

Теперь второй герой лекции: const T&.

Когда вы пишете const T&, вы делаете сразу две вещи:

  1. говорите «не хочу копировать, дай мне доступ к исходному объекту»
  2. говорите «и не дам себе его менять — честное программистское»

Это очень частый вид параметра для функций, которые только читают объект, особенно если объект потенциально большой (строка, вектор).

Пример: функция, которая печатает строку:

#include <iostream>
#include <string>

void print_line(const std::string& s) {
    std::cout << s << '\n';
}

int main() {
    std::string msg = "hello";
    print_line(msg); // печатает: hello
}

Ключевая мысль: print_line не имеет права менять s. Если вы внутри напишете s += "!", компилятор вас остановит. Это прямой, железобетонный способ защитить себя от случайной порчи данных.

Кстати, в стандартной библиотеке идея “reference/const_reference” встречается как устойчивый способ назвать «ссылку на элемент» и «константную ссылку на элемент» (например, в контейнерах), и даже в редакторских заметках к стандарту можно встретить формулировки про (const_)reference как про стандартный паттерн именования.

const T& можно привязать к временному значению

Вот где const T& выигрывает у T& по удобству. const T& можно привязать к временному объекту. И это логично: если вы обещаете «не менять», то ссылаться на временное значение безопаснее, потому что вы его не модифицируете и обычно используете «прямо сейчас».

Пример:

#include <iostream>
#include <string>

std::size_t len(const std::string& s) {
    return s.size();
}

int main() {
    std::cout << len(std::string("abc")) << '\n'; // 3
}

Это очень приятная возможность: вы можете передать результат выражения без лишних переменных, и при этом не копировать строку.

Важно не превращать это в религию. Для маленьких типов (int, double) const T& часто избыточен, и в лекции 71 мы как раз обсуждали, почему «везде const int&» — это шум в коде. Но для строк и векторов это обычно хороший стиль.

5. Мини-таблица: T, T& или const T&

Сейчас полезно собрать картинку в одну компактную таблицу, чтобы перестать «угадывать» и начать выбирать осознанно.

Что вы хотите по смыслу Что писать в параметре Что это означает
«Мне нужна копия, я работаю с независимым значением»
T x
создаётся копия аргумента
«Я изменю ваш объект»
T& x
работаю с исходным объектом, изменения видны снаружи
«Я только читаю, копию не хочу»
const T& x
работаю с исходным объектом, но менять запрещено

Эта таблица — не про «оптимизацию». Она про то, чтобы сигнатуры не превращались в сюрприз.

Схема: как работает «второе имя»

Когда вы только привыкаете к ссылкам, помогает держать визуальную модель: T& — это не коробка, это стрелочка «к тому же объекту».

flowchart LR
    A["Переменная a (реальный объект)"] -->|alias| R["Ссылка r (второе имя)"]
    A -->|alias| P["Параметр x типа T& в функции"]

Смысл: объект один (a), а имён/путей доступа может быть несколько (r, x).

6. Практика: ссылки в консольном TodoList

Сейчас сделаем прикладной шаг: добавим в наше приложение (пусть это будет простейший TodoList в консоли) функции так, чтобы по сигнатурам было видно, где мы меняем список задач, а где только читаем.

Представим, что задачи — это просто std::vector<std::string>. Мы пока не моделируем «задачу» как struct (это будет позже по курсу), поэтому держим всё максимально просто.

Печать задач: const std::vector<std::string>&

Начнём с печати. Печать не должна менять список. Значит — const&.

#include <iostream>
#include <string>
#include <vector>

void print_tasks(const std::vector<std::string>& tasks) {
    std::cout << "Tasks: " << tasks.size() << '\n';
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        std::cout << i << ": " << tasks[i] << '\n';
    }
}

Обратите внимание: мы не копируем tasks, а читаем. Если вы случайно попытаетесь сделать tasks.push_back("..."), компилятор вас остановит — и это хорошо.

Добавление задачи: меняем список (T&), читаем текст (const T&)

Добавление задачи меняет вектор, значит вектор — по ссылке &. А текст задачи мы только читаем, значит строка — по const&.

#include <string>
#include <vector>

void add_task(std::vector<std::string>& tasks, const std::string& text) {
    if (!text.empty()) {
        tasks.push_back(text);
    }
}

Сигнатура читается почти как фраза: «я изменяю tasks, но text не трогаю».

Нормализация строки «на месте»: пример честного std::string&

Частая задача: поправить строку «на месте», например, удалить лишние пробелы по краям. Мы не будем делать идеальный trim на все случаи жизни (это отдельная большая тема), сделаем учебную версию: если строка начинается с пробела — убрать один пробел, если заканчивается — убрать один.

#include <string>

void trim_one_space(std::string& s) {
    if (!s.empty() && s.front() == ' ') {
        s.erase(0, 1);
    }
    if (!s.empty() && s.back() == ' ') {
        s.pop_back();
    }
}

И использование:

#include <iostream>
#include <string>

int main() {
    std::string t = " hello ";
    trim_one_space(t);
    std::cout << t << '\n'; // hello
}

Здесь std::string& абсолютно оправдан: функция именно редактирует строку.

«Выходной параметр» через T&

Иногда вам хочется, чтобы функция не просто «посчитала и вернула», а записала результат в уже существующую переменную. Это называют выходным параметром. Мы сегодня не углубляемся в дизайн «как лучше возвращать несколько значений», но сам приём на уровне ссылки полезно увидеть.

Пример: функция пытается найти первую задачу, содержащую подстроку. Если нашла — записывает индекс в out_index и возвращает true, иначе возвращает false.

#include <string>
#include <vector>

bool find_task(const std::vector<std::string>& tasks,
               const std::string& query,
               std::size_t& out_index) {
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        if (tasks[i].find(query) != std::string::npos) {
            out_index = i;
            return true;
        }
    }
    return false;
}

Использование:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"buy milk", "learn c++", "sleep"};
    std::size_t idx = 0;

    if (find_task(tasks, "c++", idx)) {
        std::cout << idx << '\n'; // 1
    }
}

Смысл std::size_t& out_index здесь простой: «если нашёл — запишу сюда ответ». И снова контракт читается по сигнатуре.

Если перепутать T& и const T&

Очень практичный момент: начинающие часто ставят T& «на всякий случай», а потом неожиданно меняют данные «снаружи» и ловят странные эффекты.

Представьте, что вы написали функцию «печать задач», но случайно забыли const:

#include <iostream>
#include <string>
#include <vector>

void print_tasks_wrong(std::vector<std::string>& tasks) {
    tasks.push_back("oops");          // внезапно изменили список
    std::cout << tasks.size() << '\n';
}

Это пример плохого контракта: функция по названию «печатает», а по факту меняет данные. Такие вещи превращают программу в детектив, где преступник — вы сами две недели назад.

Обратная ошибка тоже бывает: вы хотели изменить объект, но случайно написали const T& — и компилятор вам запретил. Это как раз приятный случай: лучше пусть компилятор ругается сразу, чем программа «молча делает не то».

Мини-скелет main.cpp

Чтобы было легче связать примеры в единое приложение, вот маленький скелет, куда вы можете вставлять функции. Он не «финальная архитектура», но уже похож на аккуратный учебный проект с тонким main.

#include <iostream>
#include <string>
#include <vector>

void print_tasks(const std::vector<std::string>& tasks);
void add_task(std::vector<std::string>& tasks, const std::string& text);

int main() {
    std::vector<std::string> tasks;

    add_task(tasks, "learn references");
    add_task(tasks, "drink tea");
    print_tasks(tasks);

    return 0;
}

И ожидаемый вывод может быть таким:

// Tasks: 2
// 0: learn references
// 1: drink tea

7. Типичные ошибки при работе с T& и const T&

Ошибка №1: ставить T& «на всякий случай», хотя функция не должна менять аргумент.
Это самая распространённая проблема, потому что кажется, что «ссылка быстрее» и «так правильнее». В итоге вы получаете функцию, которая по контракту может менять аргумент, и читатель кода вынужден ожидать подвоха. Если вы не меняете объект — выбирайте const T& (для больших типов) или передачу по значению (для маленьких).

Ошибка №2: пытаться привязать T& к временному значению и удивляться ошибке компиляции.
Например, f(std::string("hi")) при f(std::string&) не скомпилируется. Это не каприз языка, а защита от привязки к объекту, который исчезнет «через мгновение». Если вы хотите принимать временные значения и только читать — используйте const T&.

Ошибка №3: думать, что const — это «для красоты», и снимать его без причины.
const в const T& — это контракт. Он не только для человека, но и для компилятора: он запрещает случайно менять аргумент. Если вы убираете const, вы буквально выключаете ремень безопасности, потому что «так удобнее дотянуться до бардачка».

Ошибка №4: делать const T x вместо const T& x, думая, что так «не будет копирования».
Параметр const T x всё равно создаёт копию — просто «неизменяемую копию». Это иногда полезно, но если цель была не копировать std::string/std::vector, то нужно именно const T&.

Ошибка №5: писать функции с неожиданной модификацией через ссылку и без намёка в названии.
Если функция меняет аргумент через T&, это должно читаться по сигнатуре (там уже видно), но желательно и по названию. Иначе следующий читатель кода (включая вас будущего) будет запускать программу «на всякий случай» и проверять, не прячется ли там очередной сюрприз.

1
Задача
C++ SELF, 15 уровень, 1 лекция
Недоступна
Подкрутить счётчик
Подкрутить счётчик
1
Задача
C++ SELF, 15 уровень, 1 лекция
Недоступна
Счётчик символов
Счётчик символов
1
Задача
C++ SELF, 15 уровень, 1 лекция
Недоступна
Снять по пробелу
Снять по пробелу
1
Задача
C++ SELF, 15 уровень, 1 лекция
Недоступна
Первый совпавший
Первый совпавший
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ