Функция – способ дать имя куску работы. В C++ у неё несколько особенностей, которых нет в языках попроще: перегрузка, три способа передать аргумент и препроцессорный двойник, который выглядит как функция, но ею не является.
Объявление и определение
int Power(int base, int exp); // объявление: как вызывать
int Power(int base, int exp) { // определение: что делает
int result = 1;
for (int i = 0; i < exp; ++i) {
result *= base;
}
return result;
}
Компилятор читает файл сверху вниз и о функции узнаёт только когда до неё дойдёт. Поэтому вызвать функцию, определённую ниже, можно лишь если выше есть объявление – это и есть основное назначение заголовочных файлов.
Функция без возвращаемого значения объявляется как void. Функция с типом, отличным от void,
обязана вернуть значение по всем путям выполнения – пропущенный return в одной из веток даёт
неопределённое поведение, а компилятор об этом лишь предупреждает.
Три способа передать аргумент
Здесь C++ отличается от большинства языков, и выбор осмысленный.
По значению – функция получает копию:
void Increment(int x) { x += 1; } // снаружи ничего не изменится
По ссылке – функция получает псевдоним оригинала:
void Increment(int& x) { x += 1; } // изменит переменную вызывающего
По константной ссылке – псевдоним, но менять нельзя:
void Print(const std::string& text); // без копии и без права изменить
Для int копия ничего не стоит. Для строки на мегабайт или вектора на миллион элементов передача
по значению означает полное копирование при каждом вызове.
| Что передаём | Как | Почему |
|---|---|---|
Встроенный тип (int, double, указатель) |
по значению | копия дешевле ссылки |
| Объект, который читаем | const T& |
без копии, без права изменить |
| Объект, который меняем | T& |
изменения видны вызывающему |
| Объект, который забираем себе | по значению + std::move |
вместо копии – перенос |
Практическое правило: const T& по умолчанию для всего, что больше указателя. Если в сигнатуре видно & без const – это сигнал читателю: «функция изменит ваш объект».
Значения по умолчанию
void Connect(const std::string& host, int port = 8080, bool secure = false);
Connect("example.com"); // port = 8080, secure = false
Connect("example.com", 9000); // secure = false
Аргументы со значениями по умолчанию идут последними – иначе непонятно, какой из них пропущен. Указывать значение нужно в объявлении, в заголовке; повторять его в определении нельзя.
Перегрузка
Несколько функций с одним именем и разными параметрами:
void Print(int value);
void Print(double value);
void Print(const std::string& value);
Print(42); // int
Print(3.14); // double
Print("hello"); // const std::string&
Компилятор выбирает по типам аргументов – это разрешение перегрузки, и приоритет такой:
точное совпадение, затем продвижение типа (float → double), затем стандартное преобразование
(int → double), затем пользовательское.
Различать перегрузки только по возвращаемому типу нельзя: в момент выбора компилятор ещё не знает, куда попадёт результат.
int GetValue();
double GetValue(); // ошибка: сигнатуры не различаются
Отсюда искажение имён (name mangling) в сообщениях компоновщика: чтобы Print(int) и Print(double) не совпали в объектном файле, типы аргументов кодируются прямо в имени. Из-за этого же C++ нельзя напрямую вызвать из C – там перегрузок нет и имена не искажаются. Для этого пишут extern "C".
Макросы
Макрос – текстовая замена, которую делает препроцессор до всякой компиляции.
#define MAX_USERS 100
#define SQR(x) x * x
Выглядит как константа и как функция, но не является ни тем, ни другим. И именно это создаёт проблемы.
Проблема первая: приоритет операций
#define SQR(x) x * x
int a = SQR(3 + 0); // подставится как 3 + 0 * 3 + 0, то есть 3
Функция вычислила бы аргумент до вызова. Препроцессор просто вставляет текст, а дальше действуют обычные приоритеты. Лечится скобками – всеми сразу:
#define SQR(x) ((x) * (x))
Проблема вторая: двойное вычисление
Скобки не спасают от второй беды:
int i = 5;
int a = SQR(i++); // ((i++) * (i++)) – i увеличится дважды, результат неопределён
Аргумент подставляется столько раз, сколько раз упомянут в теле. Побочный эффект выполняется столько же.
Проблема третья: макрос не знает про типы
#define MAX(a, b) ((a) > (b) ? (a) : (b))
MAX("abc", "abd"); // сравнит указатели, а не строки – и молча
Ошибки внутри макроса компилятор показывает в месте вызова, в развёрнутом виде: сообщение описывает код, которого вы не писали. Отладчик по макросу тоже не пройдёт – для него это одна строка.
Чем заменить
Современный C++ обходится без макросов почти везде:
constexpr int kMaxUsers = 100; // вместо #define MAX_USERS
constexpr int Sqr(int x) { return x * x; } // вместо SQR, с проверкой типов
template <typename T>
const T& Max(const T& a, const T& b) { // вместо MAX, для любого типа
return a > b ? a : b;
}
Всё это – настоящие сущности языка: у них есть типы, области видимости, их видит отладчик, а
constexpr при этом вычисляется на этапе компиляции ровно как макрос.
Макрос не подчиняется областям видимости: он действует от места определения до конца единицы трансляции и переписывает всё, что совпало по имени, – включая имена в чужих заголовках. Именно поэтому макросы называют капсом: MAX_USERS трудно случайно повторить, max – легко.
Полностью макросы не исчезли: без них не сделать include guard и не написать #ifdef для сборки
под разные платформы. Но там, где справляется функция или константа, макрос – это лишний риск,
ничего не дающий взамен.