Что такое перегрузка функций и операторов в C++?

Ответ

Перегрузка (Overloading) в C++ — это возможность определять несколько функций или операторов с одним и тем же именем, но с разными списками параметров (по типам, количеству или порядку). Компилятор выбирает подходящую версию на основе контекста вызова (аргументов).

1. Перегрузка функций

#include <iostream>
#include <string>

// Разное количество параметров
void print(int value) {
    std::cout << "Integer: " << value << std::endl;
}
void print(double value) { // Разный тип параметра
    std::cout << "Double: " << value << std::endl;
}
void print(const std::string& label, int value) { // Разный порядок/набор
    std::cout << label << ": " << value << std::endl;
}

int main() {
    print(42);          // Вызовет print(int)
    print(3.14);        // Вызовет print(double)
    print("Count", 42); // Вызовет print(const std::string&, int)
}

2. Перегрузка операторов

Позволяет определять поведение операторов (+, -, <<, [], (), etc.) для пользовательских типов.

class Vector2D {
public:
    double x, y;
    Vector2D(double x, double y) : x(x), y(y) {}

    // Перегрузка оператора сложения как член класса
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }

    // Перегрузка оператора вывода в поток как дружественная функция
    friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
        os << "(" << v.x << ", " << v.y << ")";
        return os;
    }

    // Перегрузка оператора вызова функции (functor)
    double operator()() const {
        return std::sqrt(x*x + y*y); // Возвращает длину вектора
    }
};

int main() {
    Vector2D v1(1.0, 2.0), v2(3.0, 4.0);
    Vector2D v3 = v1 + v2; // Используется operator+
    std::cout << "v3 = " << v3 << std::endl; // Используется operator<<
    std::cout << "Length of v1: " << v1() << std::endl; // Используется operator()
}

Ключевые правила и ограничения:

  • Перегружать можно только существующие операторы, нельзя создавать новые (например, **).
  • Приоритет и ассоциативность оператора остаются неизменными.
  • Нельзя перегружать по возвращаемому типу. Выбор перегрузки зависит исключительно от типов и количества аргументов.
  • Некоторые операторы можно перегружать только как функции-члены класса (например, operator=), другие — как члены или свободные функции (например, operator+).
  • Перегрузка должна быть интуитивно понятной. Например, operator+ не должен вычитать элементы.

Ответ 18+ 🔞

А, ну вот, перегрузка! Это ж как в жизни — один и тот же чувак, а ведёт себя по-разному, смотря кто к нему пристаёт. Слушай, представь: у тебя есть функция print. И ты ей говоришь: «На, распечатай число». А она тебе: «Окей, братан, целое? Сделаю красиво». А потом ты ей суёшь дробное — она уже другим тоном: «А, double? Ща, по-другому оформлю». А потом ты ей ещё и подпись прилепил — она вообще третью песню заводит. И компилятор не охуевает, он смотрит на то, что ты ей всунул, и сам понимает, какую из трёх одноимённых функций вызывать. Удобно, блядь, до безобразия.

Вот смотри, как это выглядит в коде. Тут всё просто — три разных функции print, но компилятор их не путает, потому что у них списки параметров разные. Умная жопа, одним словом.

#include <iostream>
#include <string>

void print(int value) {
    std::cout << "Integer: " << value << std::endl;
}
void print(double value) {
    std::cout << "Double: " << value << std::endl;
}
void print(const std::string& label, int value) {
    std::cout << label << ": " << value << std::endl;
}

int main() {
    print(42);          // Вызовет print(int) — целое число, ёпта
    print(3.14);        // Вызовет print(double) — дробное, ядрёна вошь
    print("Count", 42); // Вызовет print(const std::string&, int) — с подписью уже
}

А теперь самое интересное — перегрузка операторов. Это вообще ёперный театр! Ты можешь заставить плюс + между двумя твоими объектами делать что угодно. Хоть в космос лететь. Но главное — не обосраться и не сделать так, чтобы a + b вдруг объект удалял. Это уже будет пиздопроебибна, и все коллеги тебя возненавидят.

Вот, например, класс для двумерного вектора. Смотри, как мы ему прописываем свои правила жизни.

class Vector2D {
public:
    double x, y;
    Vector2D(double x, double y) : x(x), y(y) {}

    // Вот тут магия: говорим, что знак + между двумя Vector2D — это сложение их x и y
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }

    // А это чтобы выводить в cout красиво, типа (1, 2)
    friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
        os << "(" << v.x << ", " << v.y << ")";
        return os;
    }

    // А это вообще прикол: если написать v1(), то он тебе длину вектора посчитает
    double operator()() const {
        return std::sqrt(x*x + y*y);
    }
};

int main() {
    Vector2D v1(1.0, 2.0), v2(3.0, 4.0);
    Vector2D v3 = v1 + v2; // Работает наш собственный плюсик!
    std::cout << "v3 = " << v3 << std::endl; // А тут работает наш вывод в поток
    std::cout << "Length of v1: " << v1() << std::endl; // И это тоже наша шалость
}

Но есть, конечно, и правила, нарушать которые — себя не уважать. Запомни раз и навсегда:

  • Выдумывать новые операторы, типа ** для степени, — нельзя. Только существующие. Хуй с горы, а не новый оператор.
  • Нельзя перегружать по возвращаемому значению. Компилятору похуй, что ты там вернёшь — int или string. Он смотрит только на то, что ты в функцию передаёшь. Вот на этом многие обжигаются, а потом волнение ебать — почему не компилируется.
  • Приоритет оператора (то есть что выполняется раньше, * или +) остаётся как у занозы в жопе — неизменным. Не надейся это поменять.
  • И самое главное — делай это с умом. Если твой operator- начинает объекты умножать, то ты или гений, или пидарас шерстяной. Скорее второе. Все будут думать, что твой код писал полупидор, и доверия ебать ноль.

Короче, инструмент мощный, но, как говорится, вы ходите по охуенно тонкому льду. Один неверный шаг — и все твои абстракции накрылись медным тазом. Используй с умом, и будет тебе счастье.