Перегрузка операторов в языке C++
- Перегрузка через обычные функции
- Перегрузка через дружественные функции
- Перегрузка через методы класса
- Перегрузка операторов ввода и вывода
- Перегрузка унарных операторов
- Перегрузка операторов инкремента и декремента
- Перегрузка оператора индексации
- Перегрузка оператора ()
- Перегрузка операторов преобразования типов
Перегрузка операторов в языке C++ позволяет переопределить действия операторов языка для работы с различными (в том числе и пользовательскими) типами, например такими как классы.
При обработке выражения, содержащего оператор, компилятор использует следующий алгоритм:
- Если все операнды являются фундаментальных типов данных, то вызывать следует встроенные соответствующие версии операторов (если таковые существуют). Если таковых не существует, то компилятор выдаст ошибку.
- Если какой-либо из операндов является пользовательского типа данных (например, объект класса или перечисление), то компилятор будет искать версию оператора, которая работает с таким типом данных. Если компилятор не найдет ничего подходящего, то попытается выполнить конвертацию одного или нескольких операндов пользовательского типа данных в фундаментальные типы данных, чтобы таким образом он мог использовать соответствующий встроенный оператор. Если это не сработает — компилятор выдаст ошибку.
Перегрузить можно только те операторы, которые уже определены в C++. Создать новые операторы нельзя.
Также нельзя изменить количество операндов, их ассоциативность и приоритет.
Операторы, которые перегружать нельзя:
- тернарный оператор ( ? : )
- оператор sizeof
- оператор разрешения области видимости ( :: )
- оператор выбора члена класса "."
- оператор разыменования указателя на член класса ".*"
Синтаксис перегрузки операторов очень похож на определение функции с именем operator@, где @ — это идентификатор оператора (например +, -, ==).
bool operator==(const MyClass& v1, const MyClass& v2) { return v1.val == v2.val; }
В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы.
Существует три основных способа перегрузки операторов:
- через обычные функции
- через функции дружественные для класса
- через методы класса.
Перегрузка через обычные функции
Для перегрузки оператора через обычную функцию достаточно объявить эту функцию вне тела класса.
Перегрузка через обычную функцию не может обращаться к членам класса напрямую, поэтому обращение к членам классов происходит через геттеры.
class MyVal { int m_val; public: MyVal(int val) { m_val = val; } int getVal() { return m_val; } }; MyVal operator+(MyVal& v1, MyVal& v2) { return MyVal(v1.getVal() + v2.getVal()); } MyVal val1(10), val2(20); std::cout << "Val 1 = " << val1.getVal() << std::endl; // Val 1 = 10 std::cout << "Val 2 = " << val2.getVal() << std::endl; // Val 2 = 20 std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl; // Sum Val = 30
Используйте перегрузку операторов через обычные функции, вместо дружественных, если для этого не требуется добавление дополнительных геттеров в класс.
Перегрузка через дружественные функции
Перегрузка через дружественные функции похожа на перегрузку через обычные функции, но при этом используется функция, дружественная классу.
Это позволяет функции обращаться к членам класса напрямую, без использования геттеров.
class MyVal { int m_val; public: MyVal(int val) { m_val = val; } int getVal() { return m_val; } friend MyVal operator+(MyVal& v1, MyVal& v2); }; MyVal operator+(MyVal v1, MyVal v2) { return MyVal(v1.m_val + v2.m_val); } MyVal val1(10), val2(20); std::cout << "Val 1 = " << val1.getVal() << std::endl; std::cout << "Val 2 = " << val2.getVal() << std::endl; std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl;
Если необходимо, дружественные функции могут быть определены внутри класса.
class MyVal { int m_val; public: MyVal(int val) { m_val = val; } int getVal() { return m_val; } friend MyVal operator+(MyVal& v1, MyVal& v2) { return MyVal(v1.m_val + v2.m_val); } }; MyVal val1(10), val2(20); std::cout << "Val 1 = " << val1.getVal() << std::endl; std::cout << "Val 2 = " << val2.getVal() << std::endl; std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl;
Перегрузка через методы класса
При перегрузке через методы класса у функции-метода вместо первого операнда будет неявный параметр - указатель на объект класса.
class MyVal { int m_val; public: MyVal(int val) { m_val = val; } int getVal() { return m_val; } MyVal operator+(MyVal& v); }; MyVal MyVal::operator+(MyVal& v) { return MyVal(m_val + v.m_val); } MyVal val1(10), val2(20); std::cout << "Val 1 = " << val1.getVal() << std::endl; std::cout << "Val 2 = " << val2.getVal() << std::endl; std::cout << "Sum Val = " << (val1 + val2).getVal() << std::endl;
Дополнительная информация
Операторы могут работать с операндами разных типов. В этом случае нужно писать две функции для перегрузки оператора - первая для случая (Type1, Type2) и вторая для случая (Type2, Type1), так как разный порядок типов может давать разный результат.
Операторы присваивания (=), индекса ([]), вызова функции (()), и выбора члена (->) могут перегружаться только через методы класса.
Перегрузка операторов через методы класса не используются, если первый операнд не является классом (например int), или это класс, который нельзя изменять (например std::ostream). Соответственно не получится через методы класса переопределить оператор <<, так как первый его операнд - специальный класс std::ostream.
Перегрузка операторов ввода и вывода
Для удобного вывода сложных структур в поток вывода можно использовать перегрузку оператора вывода <<.
class MyCoord { int m_x; int m_y; int m_z; public: MyCoord(int x, int y, int z) { m_x = x, m_y = y, m_z = z; } friend std::ostream& operator<<(std::ostream& out, MyCoord& point); }; std::ostream& operator<<(std::ostream& out, MyCoord& v) { out << "Coord(" << v.m_x << "," << v.m_y << "," << v.m_z << ")"; return out; } MyCoord val(10, 20, 30); std::cout << val << std::endl; // Coord(10,20,30)
Так же можно перегрузить и оператор ввода, используя std::istream вместо std::ostream.
Перегрузка унарных операторов
Перегрузка унарных операторов отличается от бинарных операторов тем, что функция перегрузки принимает на вход только один операнд. Поэтому следует осуществлять такую перегрузку через метод класса.
class MyVal { int m_val; public: MyVal(int val) { m_val = val; } int getVal() { return m_val; } MyVal operator-() const; }; MyVal MyVal::operator-() const { return MyVal(-m_val); } MyVal val1(10); MyVal val2 = -val1; std::cout << val1.getVal() << std::endl; std::cout << val2.getVal() << std::endl;
Также, поскольку объект класса не меняется, а возвращается новый экземпляр класса, метод следует объявлять константным.
Перегрузка операторов инкремента и декремента
У операторов инкремента (++) и декремента (--) существует две версии - префиксная и постфиксная. Чтобы при перегрузке операторов различать эти версии у постфиксной версии введен фиктивный параметр типа int. При его наличии компилятор понимает, что происходит перегрузка постфиксной версии оператора. При его отсутствии - префиксной.
class MyVal { int m_val; public: MyVal(int val) { m_val = val; } int getVal() { return m_val; } MyVal& operator++(); // Префиксная версия MyVal operator++(int); // Постфиксная версия friend std::ostream& operator<<(std::ostream &out, const MyVal &v); }; std::ostream& operator<< (std::ostream &out, const MyVal &v) { out << v.m_val; return out; } MyVal& MyVal::operator++() // Префиксная версия { ++m_val; return *this; } MyVal MyVal::operator++(int) // Постфиксная версия { MyVal tmp(m_val); ++(*this); return tmp; } MyVal val(10); std::cout << val << std::endl; // 10 std::cout << ++val << std::endl; // 11 std::cout << val++ << std::endl; // 11 std::cout << val << std::endl; // 12
Операторы префиксной версии возвращают объект после того, как он был увеличен или уменьшен. В постфиксной версии нам нужно возвращать объект до того, как он будет увеличен или уменьшен. Из-за этого в постфиксной версии используется временный объект для возврата значения до его изменения.
Также из-за этого при возврате невозможно использовать ссылку, так как временный объект будет уничтожен при выходе из функции. Поэтому в постфиксной версии возвращаемое значение - объект класса, что дает меньшую эффективность.
Перегрузка оператора индексации
class MyArr { int m_arr[10]; public: int& operator[](const int index); }; int& MyArr::operator[](const int index) { assert(index >= 0 && index < 10); return m_arr[index]; } MyArr arr; arr[0] = 10; arr[1] = 20; std::cout << arr[0] << std::endl; // 10 std::cout << arr[1] << std::endl; // 20
Для константных объектов, когда нельзя изменять их содержимое можно использовать константную версию функции.
class MyArr { int m_arr[10] = {0}; public: int& operator[](const int index); const int& operator[](const int index) const; }; int& MyArr::operator[](const int index) { assert(index >= 0 && index < 10); return m_arr[index]; } const int& MyArr::operator[](const int index) const { assert(index >= 0 && index < 10); return m_arr[index]; } MyArr arr1; arr1[0] = 10; const MyArr arr2; // arr[0] = 20; - Ошибка! Нельзя по константной ссылке присваивать значение std::cout << arr1[0] << std::endl; // 10 std::cout << arr2[0] << std::endl; // 0
Также при перегрузке оператора индексации можно использовать проверку передаваемого индекса на корректность.
В качестве индекса может использоваться как целое число, так и любой другой тип данных, например double, string и т.п..
Перегрузка оператора ()
Перегрузка оператора ( ) позволяет задать произвольные тип и количество параметров при вызове оператора ( ).
class Matrix { int m_data[5][5] = {0}; public: int& operator()(int row, int col); const int& operator()(int row, int col) const; void operator()(); }; int& Matrix::operator()(int row, int col) { assert(row >= 0 && row < 5); assert(col >= 0 && col < 5); return m_data[row][col]; } const int& Matrix::operator()(int row, int col) const { assert(row >= 0 && row < 5); assert(col >= 0 && col < 5); return m_data[row][col]; } void Matrix::operator()() { for (int row = 0; row < 5; ++row) for (int col = 0; col < 5; ++col) m_data[row][col] = 55; } Matrix m1; m1(0, 0) = 10; const Matrix m2; std::cout << m1(0, 0) << std::endl; // 10 std::cout << m2(0, 0) << std::endl; // 0 m1(); std::cout << m1(2, 3) << std::endl; // 55
Перегрузка операторов преобразования типов
class MyVal { int m_val = 0; public: MyVal(int v) { m_val = v; } operator int(); }; MyVal::operator int() { return m_val; } MyVal v(10); std::cout << (int)v << std::endl; // 10
Также можно перегружать преобразование в свои типы данных.
class MyVal { public: int m_val = 0; MyVal(int v) { m_val = v; } }; class MyClass { int m_val = 100; public: operator MyVal(); }; MyClass::operator MyVal() { return MyVal(m_val); } MyClass val; MyVal v = (MyVal)val; std::cout << v.m_val << std::endl; // 100
Комментарии (0)