Синтаксис си/с++

Функции: объявление, определение, передача параметров по значению/ссылке/указателю

Введение

Мы уже рассматривали разделение объявления и определения функции на примере square() — в этой статье разберём функции подробнее, фокусируясь на ключевом практическом вопросе: три способа передачи параметров (по значению, по ссылке, по указателю) и принципиальное различие их поведения.

Концепция

Передача параметра по значению создаёт копию аргумента внутри функции — изменения этой копии никак не влияют на оригинальный аргумент в вызывающем коде, что безопасно, но может быть неэффективно для крупных объектов (требующих дорогого копирования) и не подходит, если функция должна изменить сам оригинал. Передача по ссылке (Type &param) даёт функции прямой доступ к оригинальной переменной вызывающего кода без копирования — изменения внутри функции напрямую отражаются на оригинале, и в отличие от указателя, ссылка не может быть «нулевой» и не требует отдельного синтаксиса разыменования внутри функции. Передача по указателю (Type *param) даёт похожую возможность изменения оригинала, но через явный, видимый в коде механизм указателя, который теоретически может быть нулевым (nullptr) — требуя дополнительной проверки в функции, если отсутствие переданного объекта является легитимным, ожидаемым случаем.

Пример кода

#include <iostream>

void byValue(int value)
{
    value = 999; // изменяет ТОЛЬКО локальную копию — оригинал в вызывающем коде НЕ изменится
}

void byReference(int &value)
{
    value = 999; // изменяет ОРИГИНАЛЬНУЮ переменную напрямую, без копирования
}

void byPointer(int *value)
{
    if (value != nullptr) { // указатель ТЕОРЕТИЧЕСКИ может быть нулевым — проверка часто необходима
        *value = 999; // разыменование (*) для доступа к значению, на которое указывает указатель
    }
}

int main()
{
    int original = 1;

    byValue(original);
    std::cout << original << std::endl; // 1 — НЕ изменилось

    byReference(original);
    std::cout << original << std::endl; // 999 — изменилось через ссылку

    original = 1;
    byPointer(&original); // явное взятие адреса через & при передаче в функцию, ожидающую указатель
    std::cout << original << std::endl; // 999 — изменилось через указатель

    return 0;
}
// Передача по константной ссылке — избегает копирования БЕЗ возможности изменения оригинала
#include <string>
#include <iostream>

void printLength(const std::string &text) // const & — эффективно (без копии) И безопасно (нельзя изменить)
{
    std::cout << "Длина: " << text.length() << std::endl;
    // text = "изменено"; // ОШИБКА КОМПИЛЯЦИИ — нельзя изменить объект через const-ссылку
}

void byValueCopy(std::string text) // копирование ВСЕГО содержимого строки при каждом вызове — неэффективно
{
    std::cout << "Длина: " << text.length() << std::endl;
}

Пояснения к коду

Три функции byValue/byReference/byPointer с одинаковым телом (= 999), но разным способом получения параметра, наглядно демонстрируют разное итоговое поведение — только версии со ссылкой и указателем реально влияют на переменную original вызывающего кода, тогда как версия по значению работает исключительно с собственной, независимой копией, не видимой за пределами самой функции. byPointer явно показывает дополнительный синтаксис, нужный для указателей — взятие адреса (&original) при вызове и разыменование (*value) внутри функции для доступа к реальному значению, а также проверку на nullptr, отсутствующую (и невозможную) для ссылок, которые по своей природе не могут быть нулевыми. Сравнение printLength(const std::string &) и byValueCopy(std::string) показывает практическую мотивацию использования константной ссылки — для крупных объектов (как строки потенциально большой длины) передача по значению означает дорогое, полное копирование всего содержимого при каждом вызове, тогда как константная ссылка даёт доступ к оригинальным данным без этого копирования, одновременно гарантируя (через const) невозможность их случайного изменения внутри функции.

Подводные камни

  • Передача больших объектов по значению «по умолчанию», без рассмотрения константной ссылки — это частая, легко допускаемая неэффективность, особенно для разработчиков, переносящих привычки из языков, где передача по ссылке менее распространена или работает иначе; для параметров, которые функция только читает, но не должна изменять, константная ссылка (const Type &) почти всегда предпочтительнее передачи по значению для любых нетривиальных по размеру типов (хотя для простых, «дешёвых» в копировании типов, как int/double, передача по значению остаётся вполне нормальной и часто даже более эффективной из-за избегания индирекции ссылки).
  • Возврат ссылки или указателя на локальную переменную функции, чьё время жизни заканчивается при выходе из функции — это классическая, серьёзная ошибка, приводящая к «висячей» ссылке/указателю, обращение к которой после возврата из функции является неопределённым поведением, поскольку память, на которую указывает эта ссылка/указатель, могла уже быть переиспользована для совершенно иных целей к моменту попытки её использования.
  • Использование указателя там, где ссылка более уместна и безопасна — если параметр функции концептуально не может (и не должен) быть «отсутствующим» (нулевым), использование ссылки вместо указателя устраняет необходимость в проверке на nullptr и явно, через сам синтаксис типа параметра, сообщает читателю кода, что отсутствие реального объекта не является ожидаемым, легитимным случаем для этой конкретной функции.
  • Передача указателя без проверки на nullptr внутри функции, ожидающей реальный, валидный объект, в ситуации, где вызывающий код теоретически может передать нулевой указатель (по ошибке или в редком, но возможном легитимном случае) — это может привести к попытке разыменования нулевого указателя, что является неопределённым поведением (на большинстве платформ практически проявляющимся как немедленный крах программы), и функции, принимающие указатель, должны либо явно документировать предположение о его ненулевом значении (и полагаться на корректность вызывающего кода), либо выполнять явную проверку перед разыменованием.