Лекция 10

Конструктор со списком инициализации

В лекции 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, дополнительных расходов на её вызов не возникает.

  1. На главную