Перевантаження операторів: методи класу та спеціальні оператори
Перевантаження операторів: методи класу та спеціальні оператори
Від бінарних до унарних: нова категорія операторів
У попередній статті ми вивчили перевантаження бінарних операторів — операторів із двома операндами (a + b, a < b, std::cout << a). Ці оператори природно реалізовувати як friend-функції, бо обидва операнди мають рівну роль.
Але C++ має і унарні оператори — оператори з одним операндом. Також є спеціальні оператори, що працюють зовсім інакше: оператор індексації [], оператор виклику функції (), оператори присвоювання =, +=, -=. Ці оператори тісно пов'язані з об'єктом, що стоїть зліва, тому їх природно реалізовувати як методи класу.
Розглянемо клас Counter — простий лічильник, що демонструє всі категорії операторів:
#include <iostream>
class Counter
{
private:
int value;
public:
Counter(int v = 0) : value(v) {}
int getValue() const
{
return value;
}
void print() const
{
std::cout << "Counter: " << value;
}
};
int main()
{
Counter count(5);
count.print();
std::cout << "\n";
// ❌ Хочемо писати:
// count++; // Інкремент
// -count; // Унарний мінус
// count[0]; // Індексація (якщо це масив лічильників)
// count(10); // Виклик як функції
// Але без перевантаження це не працює
return 0;
}
Давайте навчимо клас Counter розуміти ці оператори.
this. Правий операнд (якщо є) передається як параметр. Це природно для операторів, де лівий операнд є «головним» — він модифікується або до нього звертаються.Унарні оператори: -, !, ~
Унарні оператори мають один операнд. Найпоширеніші:
- Унарний мінус (
-a) — негація числа - Логічне НЕ (
!a) — інверсія булевого значення - Побітове НЕ (
~a) — інверсія бітів (рідко використовується для класів)
Унарні оператори перевантажуються як const-методи без параметрів:
#include <iostream>
class Counter
{
private:
int value;
public:
Counter(int v = 0) : value(v) {}
int getValue() const { return value; }
// Унарний мінус: повертає новий Counter із від'ємним значенням
Counter operator-() const
{
return Counter(-value);
}
// Логічне НЕ: повертає true, якщо лічильник дорівнює 0
bool operator!() const
{
return value == 0;
}
void print() const
{
std::cout << "Counter: " << value;
}
};
int main()
{
Counter count(5);
Counter zero(0);
std::cout << "Original: ";
count.print();
std::cout << "\n";
// Унарний мінус
Counter negated = -count;
std::cout << "Negated: ";
negated.print();
std::cout << "\n";
// Логічне НЕ
if (!zero)
{
std::cout << "Zero counter is empty (evaluates to true)\n";
}
if (!count)
{
std::cout << "This won't print\n";
}
else
{
std::cout << "Non-zero counter is not empty\n";
}
return 0;
}
Рядки 14–17: operator-() — const-метод без параметрів. Створює новий Counter із від'ємним значенням. Вираз -count компілятор перетворює на count.operator-().
Рядки 20–23: operator!() повертає bool. Логіка: лічильник дорівнює нулю — це «порожній» стан, що логічно інтерпретується як false. Тому !zero дає true.
Рядок 43: вираз -count створює анонімний тимчасовий об'єкт Counter(-5), який присвоюється змінній negated.
int, double) унарний мінус змінює знак. Для користувацьких типів семантика залежить від контексту. Для Counter — змінює знак значення. Для Vector2D — інвертує напрямок вектора. Для Money — змінює знак (борг vs актив).Інкремент та декремент: ++ та --
Оператори інкременту (++) та декременту (--) існують у двох формах:
- Префіксна (
++a,--a) — збільшує/зменшує значення, повертає посилання на змінений об'єкт - Постфіксна (
a++,a--) — збільшує/зменшує значення, повертає копію старого стану
Компілятор розрізняє ці форми за сигнатурою:
// Префіксна форма: без параметрів, повертає посилання
Counter& operator++(); // ++count
// Постфіксна форма: фіктивний параметр int, повертає копію
Counter operator++(int); // count++
Фіктивний параметр int у постфіксній формі — це маркер для компілятора, а не реальний аргумент. Його значення не використовується.
#include <iostream>
class Counter
{
private:
int value;
public:
Counter(int v = 0) : value(v) {}
int getValue() const { return value; }
// === Префіксний інкремент (++count) ===
Counter& operator++()
{
++value; // Збільшуємо значення
return *this; // Повертаємо посилання на себе
}
// === Постфіксний інкремент (count++) ===
Counter operator++(int)
{
Counter temp(*this); // Зберігаємо копію поточного стану
++(*this); // Викликаємо префіксну версію
return temp; // Повертаємо старий стан
}
// === Префіксний декремент (--count) ===
Counter& operator--()
{
--value;
return *this;
}
// === Постфіксний декремент (count--) ===
Counter operator--(int)
{
Counter temp(*this);
--(*this);
return temp;
}
void print() const
{
std::cout << "Counter: " << value;
}
};
int main()
{
Counter count(5);
std::cout << "Initial: ";
count.print();
std::cout << "\n";
// Префіксний інкремент: збільшує і повертає нове значення
++count;
std::cout << "After ++count: ";
count.print();
std::cout << "\n";
// Постфіксний інкремент: збільшує, але повертає старе значення
Counter old = count++;
std::cout << "After count++, old: ";
old.print();
std::cout << ", current: ";
count.print();
std::cout << "\n";
// Префіксний декремент
--count;
std::cout << "After --count: ";
count.print();
std::cout << "\n";
// Постфіксний декремент
Counter old2 = count--;
std::cout << "After count--, old: ";
old2.print();
std::cout << ", current: ";
count.print();
std::cout << "\n";
return 0;
}
Рядки 14–18: префіксний operator++() збільшує value і повертає *this (посилання на поточний об'єкт). Це ефективно — без копіювання.
Рядки 21–26: постфіксний operator++(int) зберігає копію старого стану у temp, викликає префіксну версію для збільшення, повертає temp. Це менш ефективно через копіювання.
Рядок 24: ++(*this) викликає префіксну версію operator++() для поточного об'єкта. Уникаємо дублювання коду.
Рядки 67–68: Counter old = count++ демонструє різницю: count збільшується до 7, але old отримує старе значення 6.
int, double) компілятор оптимізує це, але для класів копіювання може бути дорогим (особливо якщо клас керує динамічною пам'яттю). Правило: якщо вам не потрібно старе значення, використовуйте префіксну форму ++count замість count++.Оператор індексації: []
Оператор індексації [] дозволяє звертатися до елементів контейнера як до масиву: array[i]. Щоб підтримувати як читання, так і запис, потрібно дві версії оператора:
- Не-const версія — повертає
тип&(посилання), дозволяє модифікацію - Const версія — повертає
const тип&аботип, дозволяє лише читання
#include <iostream>
#include <cassert>
class IntArray
{
private:
int data[10];
int size;
public:
IntArray() : size(10)
{
for (int i = 0; i < size; ++i)
{
data[i] = 0;
}
}
int getSize() const { return size; }
// Не-const версія: дозволяє запис array[i] = value
int& operator[](int index)
{
assert(index >= 0 && index < size && "Index out of bounds");
return data[index];
}
// Const версія: дозволяє читання з const об'єктів
const int& operator[](int index) const
{
assert(index >= 0 && index < size && "Index out of bounds");
return data[index];
}
};
int main()
{
IntArray array;
// Запис через не-const версію
array[0] = 10;
array[1] = 20;
array[5] = 50;
std::cout << "array[0] = " << array[0] << "\n";
std::cout << "array[1] = " << array[1] << "\n";
std::cout << "array[5] = " << array[5] << "\n";
// Const об'єкт — викликається const версія operator[]
const IntArray constArray;
// constArray[0] = 10; // ❌ ПОМИЛКА: не можна змінювати const об'єкт
std::cout << "constArray[0] = " << constArray[0] << "\n"; // ✅ Читання дозволено
return 0;
}
Рядки 22–26: не-const operator[] повертає int& — посилання на елемент. Це дозволяє писати array[0] = 10 (використання як l-value).
Рядки 29–33: const operator[] повертає const int& — константне посилання. Це дозволяє читати елемент, але не змінювати його.
Рядок 24, 31: assert() перевіряє коректність індексу під час виконання. У Debug-режимі це викличе зупинку програми при виході за межі. У Release-режимі (з -DNDEBUG) assert вимикається.
Рядок 41–43: вирази array[i] = value компілятор перетворює на array.operator[](i) = value. Оскільки operator[] повертає посилання, присвоювання працює.
Рядок 53: для constArray викликається const версія operator[], яка повертає const int&. Спроба запису (рядок 52, закоментований) викликає помилку компіляції.
operator[] залежно від того, чи є об'єкт константним. Для const IntArray викликається const-метод. Це частина системи const-коректності C++ — механізму, що гарантує, що константні об'єкти не можуть бути змінені.Оператор виклику функції: () та функтори
Оператор виклику функції () дозволяє об'єкту поводитися як функція. Об'єкти з перевантаженим operator() називаються функторами (functors) або об'єктами-функціями (function objects).
Чому це корисно? Функтори можуть зберігати стан між викликами, на відміну від звичайних функцій. Це робить їх потужнішими за функції у певних сценаріях (наприклад, у алгоритмах STL).
#include <iostream>
// Функтор-акумулятор: зберігає суму між викликами
class Accumulator
{
private:
int total;
public:
Accumulator() : total(0) {}
// Оператор виклику: додає значення до суми
int operator()(int value)
{
total += value;
return total;
}
void reset()
{
total = 0;
}
};
// Функтор-множник: зберігає коефіцієнт
class Multiplier
{
private:
int factor;
public:
Multiplier(int f) : factor(f) {}
// Оператор виклику: множить аргумент на коефіцієнт
int operator()(int value) const
{
return value * factor;
}
};
int main()
{
// Акумулятор: зберігає суму
Accumulator sum;
std::cout << "Accumulating:\n";
std::cout << "sum(10) = " << sum(10) << "\n"; // 10
std::cout << "sum(20) = " << sum(20) << "\n"; // 30
std::cout << "sum(5) = " << sum(5) << "\n"; // 35
sum.reset();
std::cout << "After reset, sum(15) = " << sum(15) << "\n"; // 15
// Множник: створюємо кілька функторів із різними коефіцієнтами
Multiplier timesTwo(2);
Multiplier timesTen(10);
std::cout << "\nMultiplying:\n";
std::cout << "timesTwo(5) = " << timesTwo(5) << "\n"; // 10
std::cout << "timesTwo(8) = " << timesTwo(8) << "\n"; // 16
std::cout << "timesTen(3) = " << timesTen(3) << "\n"; // 30
return 0;
}
Рядки 13–17: operator() приймає один параметр int value, додає його до total, повертає суму. Вираз sum(10) компілятор перетворює на sum.operator()(10).
Рядки 48–50: кожен виклик sum(...) збільшує внутрішній стан total. Це демонструє збереження стану — ключову перевагу функторів над звичайними функціями.
Рядки 57–58: створюємо два різні функтори Multiplier із різними коефіцієнтами. Кожен має власний стан (factor). Це як створити кілька «параметризованих функцій».
Рядки 61–63: виклики timesTwo(5) та timesTen(3) демонструють, що функтори можуть мати різну поведінку залежно від їхнього стану.
std::sort, std::find_if, std::transform) активно використовують функтори. Наприклад, std::sort(vec.begin(), vec.end(), std::greater<int>()) сортує за спаданням, де std::greater<int> — це функтор. У C++11 з'явилися лямбда-вирази, що є синтаксичним цукром для анонімних функторів.Оператори присвоювання: =, +=, -=, *=, /=
Оператори присвоювання модифікують лівий операнд і повертають посилання на нього для підтримки ланцюгових присвоювань (a = b = c). Ці оператори завжди реалізуються як методи класу.
Базовий оператор присвоювання =
Оператор присвоювання = викликається при a = b, коли a вже існує (на відміну від конструктора копіювання, що викликається при ініціалізації Counter a = b).
#include <iostream>
class Counter
{
private:
int value;
public:
Counter(int v = 0) : value(v) {}
int getValue() const { return value; }
// Оператор присвоювання
Counter& operator=(const Counter& other)
{
// Перевірка самоприсвоювання (self-assignment check)
if (this == &other)
{
return *this;
}
// Копіюємо дані
value = other.value;
// Повертаємо посилання на себе для ланцюгових присвоювань
return *this;
}
void print() const
{
std::cout << "Counter: " << value;
}
};
int main()
{
Counter a(10);
Counter b(20);
Counter c(30);
std::cout << "Initial values:\n";
std::cout << "a = "; a.print(); std::cout << "\n";
std::cout << "b = "; b.print(); std::cout << "\n";
std::cout << "c = "; c.print(); std::cout << "\n\n";
// Просте присвоювання
a = b;
std::cout << "After a = b:\n";
std::cout << "a = "; a.print(); std::cout << "\n\n";
// Ланцюгове присвоювання: a = b = c виконується справа наліво
a = b = c;
std::cout << "After a = b = c:\n";
std::cout << "a = "; a.print(); std::cout << "\n";
std::cout << "b = "; b.print(); std::cout << "\n";
// Самоприсвоювання — має бути безпечним
a = a;
std::cout << "\nAfter a = a (self-assignment):\n";
std::cout << "a = "; a.print(); std::cout << "\n";
return 0;
}
Рядки 14–27: operator= повертає Counter& — посилання на поточний об'єкт. Це дозволяє ланцюгові присвоювання.
Рядки 17–20: перевірка самоприсвоювання if (this == &other). Якщо a = a, одразу повертаємо *this. Це оптимізація та захист від помилок (важливо для класів із динамічною пам'яттю).
Рядок 26: return *this; — повертаємо посилання на поточний об'єкт. Компілятор перетворює a = b на a.operator=(b), що повертає a.
Рядок 53: ланцюгове присвоювання a = b = c виконується справа наліво: спочатку b = c (повертає b), потім a = b. Завдяки поверненню *this це працює.
Рядок 59: самоприсвоювання a = a — рідкісний сценарій, але перевірка гарантує коректність.
std::string, що містить char*) самоприсвоювання без перевірки може призвести до катастрофи. Якщо operator= спочатку звільняє стару пам'ять (delete[]), а потім копіює з other (що насправді this), ви копіюєте зі звільненої пам'яті — undefined behavior. Перевірка if (this == &other) запобігає цьому.Складені оператори присвоювання: +=, -=, *=, /=
Складені оператори присвоювання модифікують об'єкт і повертають посилання на нього:
#include <iostream>
class Counter
{
private:
int value;
public:
Counter(int v = 0) : value(v) {}
int getValue() const { return value; }
// Оператор +=
Counter& operator+=(int amount)
{
value += amount;
return *this;
}
// Оператор -=
Counter& operator-=(int amount)
{
value -= amount;
return *this;
}
// Оператор *=
Counter& operator*=(int multiplier)
{
value *= multiplier;
return *this;
}
// Оператор /=
Counter& operator/=(int divisor)
{
if (divisor != 0)
{
value /= divisor;
}
else
{
std::cerr << "Error: Division by zero\n";
}
return *this;
}
void print() const
{
std::cout << "Counter: " << value;
}
};
int main()
{
Counter count(10);
std::cout << "Initial: ";
count.print();
std::cout << "\n";
// Використання складених операторів
count += 5;
std::cout << "After += 5: ";
count.print();
std::cout << "\n";
count -= 3;
std::cout << "After -= 3: ";
count.print();
std::cout << "\n";
count *= 2;
std::cout << "After *= 2: ";
count.print();
std::cout << "\n";
count /= 4;
std::cout << "After /= 4: ";
count.print();
std::cout << "\n";
// Ланцюгові операції
count += 10;
count *= 2;
std::cout << "After += 10, *= 2: ";
count.print();
std::cout << "\n";
return 0;
}
Рядки 14–18: operator+= додає значення до value і повертає *this. Вираз count += 5 еквівалентний count.operator+=(5).
Рядки 35–46: operator/= містить перевірку ділення на нуль. Якщо divisor == 0, виводить помилку і залишає значення незмінним.
Рядки 88–90: складені оператори можна викликати послідовно. Кожен модифікує об'єкт і повертає посилання, що дозволяє наступний виклик.
+ та +=: якщо у вас є operator+=, можна реалізувати operator+ через нього:Counter operator+(Counter left, int right)
{
left += right; // Модифікуємо копію
return left; // Повертаємо модифіковану копію
}
- через -=, * через *= тощо.Які оператори ОБОВ'ЯЗКОВО методи класу?
Не всі оператори можна перевантажувати як вільні функції. Деякі оператори обов'язково мають бути методами класу:
= Присвоювання
operator= — базовий оператор присвоювання. Компілятор генерує версію за замовчуванням, але для класів із динамічною пам'яттю потрібна власна реалізація.
Чому метод? Лівий операнд завжди є об'єктом класу, що модифікується.
[] Індексація
operator[] — доступ до елементів контейнера. Повертає посилання для модифікації або const-посилання для читання.
Чому метод? Індексація тісно пов'язана зі структурою об'єкта.
() Виклик
operator() — функтори, об'єкти-функції. Дозволяє об'єкту поводитися як функція.
Чому метод? Виклик завжди застосовується до конкретного об'єкта.
-> Доступ
operator-> — доступ до членів через об'єкт (використовується у розумних покажчиках, як std::unique_ptr).
Чому метод? Операція доступу до членів завжди застосовується до об'єкта.
=, [], (), -> будуть методами класу. Це жорстке обмеження мови C++.Повний приклад: клас Money з усіма операторами
Об'єднаємо все, що ми вивчили, у розширений клас Money із унарними операторами, інкрементом та складеними присвоюваннями:
#include <iostream>
class Money
{
private:
int dollars;
int cents;
void normalize()
{
if (cents >= 100)
{
dollars += cents / 100;
cents = cents % 100;
}
else if (cents < 0)
{
int dollarsBorrowed = (-cents / 100) + 1;
dollars -= dollarsBorrowed;
cents += dollarsBorrowed * 100;
}
}
public:
Money(int d = 0, int c = 0) : dollars(d), cents(c)
{
normalize();
}
int totalCents() const
{
return dollars * 100 + cents;
}
// === Унарні оператори ===
Money operator-() const
{
return Money(-dollars, -cents);
}
bool operator!() const
{
return dollars == 0 && cents == 0;
}
// === Інкремент/декремент ===
Money& operator++() // Префікс: додає $1
{
++dollars;
return *this;
}
Money operator++(int) // Постфікс
{
Money temp(*this);
++(*this);
return temp;
}
Money& operator--() // Префікс: віднімає $1
{
--dollars;
return *this;
}
Money operator--(int) // Постфікс
{
Money temp(*this);
--(*this);
return temp;
}
// === Складені присвоювання ===
Money& operator+=(const Money& other)
{
dollars += other.dollars;
cents += other.cents;
normalize();
return *this;
}
Money& operator-=(const Money& other)
{
dollars -= other.dollars;
cents -= other.cents;
normalize();
return *this;
}
// === Вивід ===
friend std::ostream& operator<<(std::ostream& out, const Money& money);
};
std::ostream& operator<<(std::ostream& out, const Money& money)
{
out << "$" << money.dollars << ".";
if (money.cents < 10) out << "0";
out << money.cents;
return out;
}
int main()
{
Money wallet(50, 75);
std::cout << "Initial wallet: " << wallet << "\n";
// Унарний мінус
Money debt = -wallet;
std::cout << "Debt (negated): " << debt << "\n";
// Логічне НЕ
Money empty;
if (!empty)
{
std::cout << "Empty wallet evaluates to true with !\n";
}
// Інкремент
++wallet;
std::cout << "After ++wallet: " << wallet << "\n";
Money old = wallet++;
std::cout << "After wallet++ (old): " << old << ", current: " << wallet << "\n";
// Декремент
--wallet;
std::cout << "After --wallet: " << wallet << "\n";
// Складені присвоювання
Money bonus(10, 50);
wallet += bonus;
std::cout << "After += bonus: " << wallet << "\n";
Money expense(5, 25);
wallet -= expense;
std::cout << "After -= expense: " << wallet << "\n";
return 0;
}
Тепер клас Money підтримує повний набір операторів: унарні, інкремент/декремент, складені присвоювання. Код виглядає природно і читабельно.
Практичне завдання: клас Matrix2x2
Закріпимо матеріал, реалізувавши клас Matrix2x2 для матриць 2×2 із повним набором операторів.
Вимоги:
- Поля: масив
data[2][2] - Оператори:
+,-(бінарні),-(унарний),*(множення на скаляр),[](індексація рядків) - Оператори присвоювання:
+=,-= - Вивід:
operator<<
class Matrix2x2
{
private:
double data[2][2];
public:
Matrix2x2(); // Нульова матриця
Matrix2x2(double a11, double a12, double a21, double a22);
// Доступ до рядка (повертає покажчик на рядок)
double* operator[](int row);
const double* operator[](int row) const;
// Унарний мінус
Matrix2x2 operator-() const;
// Арифметичні оператори (friend)
friend Matrix2x2 operator+(const Matrix2x2& a, const Matrix2x2& b);
friend Matrix2x2 operator-(const Matrix2x2& a, const Matrix2x2& b);
friend Matrix2x2 operator*(const Matrix2x2& m, double scalar);
friend Matrix2x2 operator*(double scalar, const Matrix2x2& m);
// Складені присвоювання
Matrix2x2& operator+=(const Matrix2x2& other);
Matrix2x2& operator-=(const Matrix2x2& other);
// Вивід
friend std::ostream& operator<<(std::ostream& out, const Matrix2x2& m);
};
#include <iostream>
#include <iomanip>
class Matrix2x2
{
private:
double data[2][2];
public:
Matrix2x2()
{
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 2; ++j)
{
data[i][j] = 0.0;
}
}
}
Matrix2x2(double a11, double a12, double a21, double a22)
{
data[0][0] = a11; data[0][1] = a12;
data[1][0] = a21; data[1][1] = a22;
}
// Індексація рядків (повертає покажчик на рядок)
double* operator[](int row)
{
return data[row];
}
const double* operator[](int row) const
{
return data[row];
}
// Унарний мінус
Matrix2x2 operator-() const
{
return Matrix2x2(-data[0][0], -data[0][1],
-data[1][0], -data[1][1]);
}
// Складені присвоювання
Matrix2x2& operator+=(const Matrix2x2& other)
{
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 2; ++j)
{
data[i][j] += other.data[i][j];
}
}
return *this;
}
Matrix2x2& operator-=(const Matrix2x2& other)
{
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 2; ++j)
{
data[i][j] -= other.data[i][j];
}
}
return *this;
}
// Арифметичні оператори
friend Matrix2x2 operator+(const Matrix2x2& a, const Matrix2x2& b);
friend Matrix2x2 operator-(const Matrix2x2& a, const Matrix2x2& b);
friend Matrix2x2 operator*(const Matrix2x2& m, double scalar);
friend Matrix2x2 operator*(double scalar, const Matrix2x2& m);
// Вивід
friend std::ostream& operator<<(std::ostream& out, const Matrix2x2& m);
};
Matrix2x2 operator+(const Matrix2x2& a, const Matrix2x2& b)
{
Matrix2x2 result;
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 2; ++j)
{
result.data[i][j] = a.data[i][j] + b.data[i][j];
}
}
return result;
}
Matrix2x2 operator-(const Matrix2x2& a, const Matrix2x2& b)
{
Matrix2x2 result;
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 2; ++j)
{
result.data[i][j] = a.data[i][j] - b.data[i][j];
}
}
return result;
}
Matrix2x2 operator*(const Matrix2x2& m, double scalar)
{
Matrix2x2 result;
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 2; ++j)
{
result.data[i][j] = m.data[i][j] * scalar;
}
}
return result;
}
Matrix2x2 operator*(double scalar, const Matrix2x2& m)
{
return m * scalar;
}
std::ostream& operator<<(std::ostream& out, const Matrix2x2& m)
{
out << "[ " << std::fixed << std::setprecision(2)
<< m.data[0][0] << " " << m.data[0][1] << " ]\n"
<< "[ " << m.data[1][0] << " " << m.data[1][1] << " ]";
return out;
}
int main()
{
Matrix2x2 m1(1, 2, 3, 4);
Matrix2x2 m2(5, 6, 7, 8);
std::cout << "Matrix m1:\n" << m1 << "\n\n";
std::cout << "Matrix m2:\n" << m2 << "\n\n";
Matrix2x2 sum = m1 + m2;
std::cout << "m1 + m2:\n" << sum << "\n\n";
Matrix2x2 diff = m1 - m2;
std::cout << "m1 - m2:\n" << diff << "\n\n";
Matrix2x2 scaled = m1 * 2.0;
std::cout << "m1 * 2.0:\n" << scaled << "\n\n";
Matrix2x2 negated = -m1;
std::cout << "-m1:\n" << negated << "\n\n";
// Індексація
std::cout << "m1[0][0] = " << m1[0][0] << "\n";
std::cout << "m1[1][1] = " << m1[1][1] << "\n\n";
// Модифікація через індексацію
m1[0][0] = 10;
m1[1][1] = 20;
std::cout << "After modifying m1:\n" << m1 << "\n\n";
// Складені присвоювання
m1 += m2;
std::cout << "After m1 += m2:\n" << m1 << "\n";
return 0;
}
Пояснення:
- Рядки 28–36:
operator[]повертає покажчик на рядок (double*), що дозволяє використовувати подвійну індексаціюm[i][j]. Компілятор перетворює це на(m.operator[](i))[j]. - Рядок 165:
m1[0][0] = 10працює, боoperator[]повертає не-const покажчик, що дозволяє модифікацію. - Рядки 39–43: унарний мінус створює нову матрицю з інвертованими знаками всіх елементів.
Резюме: розширений арсенал операторів
У цій статті ми вивчили перевантаження операторів через методи класу — категорію операторів, що тісно пов'язані з об'єктом і природно реалізуються як його методи.
➖ Унарні оператори
-, !, ~ — const-методи без параметрів. Повертають новий об'єкт (для -) або bool (для !). Використовуються для негації, інверсії, перевірки «порожності».⏫ Інкремент/декремент
++, -- — дві форми: префіксна (без параметрів, повертає *this) та постфіксна (фіктивний int, повертає копію). Префіксна ефективніша через відсутність копіювання.🔢 Індексація
operator[] — дві версії: не-const (повертає Type&, дозволяє запис) та const (повертає const Type&, лише читання). Обов'язково метод класу. Дозволяє синтаксис array[i].📞 Виклик функції
operator() — функтори, об'єкти-функції. Зберігають стан між викликами. Використовуються у алгоритмах STL, є основою для лямбда-виразів C++11.➕= Складені присвоювання
+=, -=, *=, /= — модифікують об'єкт, повертають *this для ланцюгових операцій. Базовий operator= вимагає перевірки самоприсвоювання для класів із динамічною пам'яттю.Анонс наступної статті
У цій статті ми завершили вивчення базових та розширених операторів, що роблять користувацькі класи природними у використанні. Але є ще одна категорія операторів, що заслуговує на окрему статтю: приведення типів та спеціальні механізми контролю.
У наступній статті (стаття 65) ми розглянемо:
- Оператори приведення типів (
operator int(),operator double()) — автоматична конвертація класу в інші типи - Ключове слово
explicit— заборона неявних конвертацій, захист від тонких помилок - Ключове слово
delete— явна заборона викликів конструкторів або операторів - Конструктори конвертації — як конструктори з одним параметром стають операторами перетворення
Ці механізми критично важливі для створення безпечних класів, що захищені від випадкових неявних конвертацій — одного з найнебезпечніших джерел помилок у C++.
a = a), ділення на нуль, вихід за межі індексу, переповнення. Використовуйте assert() для перевірки передумов у Debug-режимі. Це допоможе виявити помилки на етапі розробки, а не у продакшені.Перевантаження операторів: основи та арифметика
Як зробити користувацькі класи природними у використанні через перевантаження операторів. Арифметичні оператори (+, -, *, /), оператори порівняння, вводу/виводу. Friend-функції vs методи класу.
Приведення типів, explicit та delete
Оператори приведення типів (user-defined conversions), конструктори-конвертери, ключові слова explicit та delete. Захист від неявних конвертацій.