следующие операторы можно перегрузить только в качестве методов:
=
- присваивание;->
- доступ к полям по указателю;()
- вызов функции;[]
- доступ по индексу;->*
- доступ к указателю-на-поле по указателю;следующие операторы можно перегрузить только в виде внешних функций (перегружаем методы класса <iostream>
):
Общие правила перегрузки операторов можно подсматривать на learn.microsoft.com.
Для перегрузки бинарных операций нужно создать метод, принимающий один аргумент - ссылку на второй аргумент операции.
class Complex {
private:
double _re, _im;
public:
Complex(double re, double im) : _re(re), _im(im) {}
Complex operator+(const Complex& c) { return Complex(_re + c._re, _im + c._im); }
};
В результате операции сложения возникает новый объект типа Complex
.
Перегрузим присваивание:
class Complex {
...
Complex& operator=(const Complex& c) {
_re = c._re;
_im = c._im;
return *this;
}
...
};
Теперь можно воспользоваться двумя перегруженными операциями:
Complex a(1.1, 2.2), b(3.3, 4.4), c(0.0, 0.0);
c = a + b;
...
Complex operator+(const Complex& c) { return Complex(_re + c._re, _im + c._im); }
...
Обратите внимание на то, как создается временный объект внутри метода operator+
:
return Complex(_re + c._re, _im + c._im);
Сравните с другим возможным примером:
Complex temp(_re + c._re, Im + c._im);
return temp;
В результате выполнения обоих примеров, будет проведена оптимизация и создаваться лишний объект при копировании будет только один (с суммой).
class Complex {
private:
int _re, _im;
public:
Complex(int re = 0, int im = 0) : _re(re), _im(im) { std::cout << "C(" << re << "," << im << ")" << std::endl; }
Complex(const Complex& c) : _re(c._re), _im(c._im) { std::cout << "CC(" << _re << "," << _im << ")" << std::endl; }
~Complex() { std::cout << "D(" << _re << "," << _im << ")" << std::endl; }
Complex& operator=(const Complex& c) {
_re = c._re; _im = c._im;
return *this;
}
Complex operator+(const Complex& c) {
Complex temp(_re + c._re, _im + c._im);
return temp;
}
};
int main() {
Complex a(1, 2), b;
b = a + a;
}
C(1,2)
C(0,0)
C(2,4)
D(2,4)
D(2,4)
D(1,2)
А как это получилось???
Подробнее про управление памятью:
Поскольку в операции участвует только один операнд, то никаких внешних ссылок методу, реализующему операцию, передавать не нужно.
Complex operator-() {
Complex temp;
temp.Re = -Re;
temp.Im = -Im;
return temp;
}
Для операций ++
и --
также существуют две формы: префиксная и постфиксная, которые реализуются при перегрузке особым образом.
class Coord {
int _x, _y, _z;
public:
Coord(int x, int y, int z) : _x(x), _y(y), _z(z){}
...
// Перегрузка префиксной формы:
Coord& operator++() {
++_x; ++_y; ++_z;
return *this;
}
// Перегрузка постфиксной формы:
Coord operator++(int) {
Coord temp = *this;
++_x; ++_y; ++_z;
return temp;
}
...
};
В этом случае у нас есть также две возможности: создать обычную функцию, или функцию, дружественную классу. В случае унарной операции функция должна принимать ссылку на объект класса, к которому она применяется. Если операция бинарная, то таких ссылок должно быть две: на первый аргумент и на второй.
class Coord {
int _x, _y, _z;
public:
Coord() {}
Coord(int x, int y, int z) : _x(x), _y(y), _z(z) {}
friend Coord operator+(const Coord&, const Coord&);
};
Coord operator+(const Coord& c1, const Coord& c2) {
Coord temp;
temp._x = c1._x + c2._x;
temp._y = c1._y + c2._y;
temp._z = c1._z + c2._z;
return temp;
}
int main() {
Coord a(1, 2, 3), b(4, 5, 6), c;
c = a + b;
}
Операция присваивания (aka копирующая инициализация) определена в любом классе по умолчанию как поэлементное копирование. Если класс содержит поля, память под которые выделяется динамически, необходимо определить собственную операцию присваивания. Чтобы сохранить семантику присваивания, функция должна возвращать ссылку на объект, для которого она вызвана и принимать в качестве параметра ссылку на присваиваемый объект.
=
можно перегружать только методом класса.=
при наследовании не наследуется. Если подумать, то будет видно, что при наследовании могут быть добавлены новые поля или изменено их смысловое значение и следовательно оператор =
может не работать корректно.Функции operator<<
и operator>>
должны быть друзьями класса, описывающего пользовательский тип.
Функции принимают в качестве аргументов ссылку на входной (выходной) поток и ссылку на объект класса, данные из которого необходимо обработать. В качестве возвращаемых значений необходимо также указывать ссылки на поток ввода или вывода (в этом случае можно конструировать сложные потоковые конструкции).
P.S. Будьте аккуратны при использовании using namespace std
. Неосторожное использование может привести к веселому поиску ошибок компиляции при попытке реализовать перегрузку потоков ввода/вывода для вашего пользовательского типа.
class Complex {
...
friend ostream& operator<<(ostream& os, Complex& c);
friend istream& operator>>(istream& is, Complex& c);
...
};
ostream& operator<<(ostream& os, Complex& c) {
return os << '(' << c._re << ',' << c._im << ')';
}
istream& operator>>(istream& is, Complex& c) { return is >> c._re >> c._im; }
int main() {
Complex c(0, 0);
std::cin >> c; // 23,45
std::cout << c; // (23,45)
}
Преобразования типов позволяют задать правила преобразования нашего класса к другим типам и классам.
Также можно указать спецификатор explicit
, который позволит преобразовывать типы только, если программист явно это указал (например static_cast<Point3>(Point(2,3));)
.
Пример:
Point::operator bool() const {
return this->x != 0 || this->y != 0;
}
class A {
int x;
public:
A(int _x) : x(_x) {}
operator int() const { return x; }
};
void foo() {
A a;
int b;
b = a;
}
new
и delete
Существует возможность перегрузить операторы new
, delete
для более эффективного распределения динамической памяти. Всего существует 4 формы операторов выделения и освобождения памяти
new
- работа с одиночными объектамиnew[]
- работа с массивами объектовdelete
- работа с одиночными объектамиdelete[]
работа с массивами объектовnew
и delete
(продолжение)При перегрузке операторов для работы с динамической памятью нужно соблюдать следующие правила:
new
должен передаваться размер объекта size_t
;new
должны иметь тип void*
;delete
, delete[]
должны иметь тип void
;delete
, delete[]
должен иметь тип void*
;new
и delete
class Foo {
...
void* operator new(size_t size);
void operator delete(void* obj);
При вызове оператора new
сначала выделяется память для объекта:
При вызове оператора delete
все происходит в обратном порядке: сначала вызывается деструктор, потом освобождается память. Деструктор не должен выбрасывать исключений.
new[]
и delete[]
Когда оператор new[]
используется для создания массива объектов, то сначала выделяется память для всего массива:
Для удаления массива надо вызвать оператор delete[]
, при этом для всех элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается.
delete
Стандартные функции выделения памяти при невозможности удовлетворить запрос выбрасывают исключение типа std::bad_alloc
Любую форму оператора delete
безопасно применять к нулевому указателю.
new
и delete
class X {
...
public:
void* operator new(std::size_t size) {
std::cout << "X new\n";
return ::operator new(size);
}
void operator delete(void* ptr) {
std::cout << "X delete\n";
::operator delete(ptr);
}
void* operator new[](std::size_t size) {
std::cout << "X new[]\n";
return ::operator new[](size);
}
void operator delete[](void* ptr) {
std::cout << "X delete[]\n";
::operator delete[](ptr);
}
};
new
и delete
при наследованииВ классе (особенно, когда используется наследование) иногда удобно применить альтернативную форму функции освобождения памяти:
void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);
Параметр size
задает размер элемента (даже в варианте для массива). Такая форма позволяет использовать разные функции для выделения и освобождения памяти в зависимости от конкретного производного класса.
new
и delete
void *X::operator new(size_t size) {
void *p;
std::cout << "In overloaded new.";
p = malloc(size);
if (!p) {
throw std::bad_alloc; // Throw directly than with named temp variable
}
return p;
}
void X::operator delete(void *p) {
std::cout << "In overloaded delete.\n";
free(p);
}
void *X::operator new[](size_t size) {
void *p;
std::cout << "Using overload new[].\n";
p = malloc(size);
if (!p) {
throw std::bad_alloc;
}
return p;
}
void X::operator delete[](void *p) {
std::cout << "Free array using overloaded delete[]\n";
free(p);
}
В этом разделе мы обсудим два основных типа отношений между классами: агрегацию и наследование.
Engine
(двигатель). У двигателя есть определенная мощность, рабочее топливо и его расход (литры на 1 км).Tank
- топливный бак, который характеризуется емкостью. мы можем проверят, пуст ли бак, а также уменьшать количество топлива в нем на определенную величину.Engine
#include <iostream>
using namespace std;
typedef unsigned short power_t;
enum Fuel { Petrol, Diesel };
class Engine {
private:
power_t _power;
Fuel _fuel;
double _consume; // расход топлива
public:
Engine(power_t p, Fuel f, double c) {
_power = p;
_fuel = f;
_consume = c;
}
double conFuel(size_t path) { return path * _consume; }
};
Tank
class Tank {
private:
double _capacity;
public:
Tank(double cap) : _capacity(cap) {}
bool isEmpty() const { return _capacity <= 0.0; }
void consume(double value) {
if (!isEmpty()) _capacity -= value;
}
};
При наличии двух узлов автомобиля попробуем создать класс Auto
:
class Auto {
protected:
Engine *_engine;
Tank *_tank;
public:
Auto(Engine *e, Tank *t) : _engine(e), _tank(t) {}
void move(size_t path) {
size_t current = 0;
while (current < path && _tank->isEmpty() == false) {
_tank->consume(_engine->conFuel(1.0));
cout << "Current dist: " << current << "km" << endl;
current++;
}
cout << "Stop!" << endl;
}
};
Auto
Автомобиль содержит двигатель и бак и умеет передвигаться на некоторое расстояние, расходуя при этом горючее. При опустошении бака автомобиль останавливается.
Класс Auto
выступает в качестве базового класса для нескольких типов автомобилей.
Рассмотрим легковой автомобиль (Car
):
class Car : public Auto {
protected:
size_t _passengers;
public:
Car(Engine *e, Tank *t, size_t pass) : Auto(e, t) { _passengers = pass; }
};
Главная особенность нового класса: перевозка пассажиров.
Аналогично можно создать грузовик, перевозящий грузы:
class Lorry : public Auto {
protected:
size_t _cargo;
public:
Lorry(Engine *e, Tank *t, size_t c) : Auto(e, t) { _cargo = c; }
};
В производных классах Carr
и Lorry
мы не определяем заново двигатель и бак - они переходят при наследовании от родительского класса Auto
.
Конструктор производного класса должен вызвать конструктор базового и передать ему необходимые параметры.
Про наследование и не только!
If not, just clap your hands!