В лекции 9 рассматривался следующий пример конструктора класса Date
.
Date(int d, int m, int y)
{
this->d = d;
this->m = m;
this->y = y;
}
Данная реализация является упрощённой («как в Паскале»). В C++ для инициализации полей в конструкторе используются списки инициализации.
class Date
{
int d, m, y;
public:
Date(int d, int m, int y) : d(d), m(m), y(y) {}
};
Здесь
d(d), m(m), y(y)
— список инициализации.
Конструкция d(d)
означает поле(аргумент конструктора)
.
Если список инициализации не используется (как в первой версии), то в случае
полей классовых типов (например, string
) всё равно вначале будут вызваны их
конструкторы, а затем в теле конструктора класса значения полей будут перезаписаны
на основе аргументов конструктора. То есть выполняется двойная работа.
Для больших классов часто разносят описание и реализацию всех функций-членов для улучшения обозримости класса, возможно оставляя определение таких мелких объектов, как конструктор в данном примере.
Как было сказано на прошлой лекции, бинарные операции можно перегружать двумя способами:
При перегрузке бинарной операции, очень важно правильно ответить на вопрос:
Как выбрать между функцией-членом и внешней функцией?
Ключевым моментом этого выбора является то, что функция-член имеет доступ к закрытым полям класса. Для многих операций думать над этим не нужно, а нужно знать стандартный подход, который является наиболее разумным в большинстве случаев.
Рассмотрим перегрузку операции сравнения на равенство на примере класса Date
из лекции 9.
class Date
{
int d, m, y;
public:
//bool operator==(Date d1, Date d2) типичная ошибка
bool operator==(Date const & other)
{
return d == other.d &&
m == other.m &&
y == other.y;
}
};
Замечания. Если операция бинарная, то у нее будет один аргумент (в случае, когда операция определена функцией-членом). Если класс этого аргумента совпадает с текущим классом, то у нас есть доступ к закрытым полям объекта-аргумента.
Аргумент оператора правильнее передавать по ссылке на константу.
Это общее правило C++: сначала формальный параметр описывается
как ссылка на константу, затем, при необходимости, из объявления параметра
убирается const
. В последнюю очередь стоит подумать о передаче объекта по
значению (это нужно довольно редко).
Должен ли operator<<(>>)
быть функцией-членом?
Ответ: нет, он должен быть внешней функцией т.к cout
— объект существующего
класса, в который мы не можем добавить перегрузку для вывода объекта нашего
нового класса.
Если operator<<(>>)
должен быть внешней функцией, тогда как получить доступ к
полям класса Date
?
Для решения это проблемы operator<<
обычно объявляется другом класса:
в начале заголовка добавляется ключевое слово friend
.
/* date.h */
class Date
{
int d, m, y;
public:
// …
friend
ostream & operator<<(ostream & os, Date const & d);
};
/* date.cpp */
ostream & operator<<(ostream & os, Date const & d)
{
os << d.d << '.' << d.m << '.' << d.y << '\n';
return os;
}
Функция operator<<
должна возвращать ссылку на полученный объект потока для
допустимости цепочек типа cout << d1 << d2;
Операция чтения из потока определяется аналогичным образом, только вместо ostream
используется istream
.
Т.к. operator<<
небольшой, то его лучше сделать inline
. Перенесем реализацию
в заголовочный файл.
/* date.h */
class Date {
int d, m, y;
public:
// …
friend
ostream & operator<<(ostream & os, Date const & d)
{
return os << d.d << '.' << d.m << '.' << d.y << '\n';
}
};
Если друг класса определен прямо в классе, то он остается внешней функцией и
становится inline
.
Пусть @
∈ {+
, -
, *
, /
}.
Обычно определяют пару функций: operator@=
, operator@
.
В таком случае operator@=
определяется как функция-член, а operator@
— как
внешняя функция.
Операция @=
называется присваивающей формой операции @
.
Рассмотрим перегрузку арифметических операций на примере класса BigInteger
.
class BigInteger {
int data[1024];
public:
BigIntiger & operator+=(BigInteger cons & other)
{
// Цикл по data: суммирование и перенос
// Возвращаем ссылку на себя
return *this;
}
};
BigInteger operator+(BigInteger const & bi1, BigInteger const & bi2)
// функция не может возвращать ссылку, потому возвращает новый объект
{
BigInteger res(bi1); // копия bi1
res += bi2; // res хранит bi1 + bi2
return res;
}
Так как ссылки на локальные переменные запрещены, а аргументы + должны остаться
неизменными, сумму они хранить не могут, то нам не на что возвращать ссылку и
мы возвращаем новый объект. При этом происходит копирование объекта из кадра
стека функции operator+
в кадр стека вызывающей его функции, что может
иметь чувствительные накладные расходы.
Поскольку @=
является inline
, дополнительных расходов на её вызов не возникает.