Язык программирования C++.

7.3. Дополнительные средства класса.

Хотя класс Sales_data довольно прост, он все же позволил исследовать немало средств поддержки классов. В этом разделе рассматриваются некоторые из дополнительных средств, связанных с классом, которые класс Sales_data не будет использовать. К этим средствам относятся типы-члены (type member), внутриклассовые инициализаторы для типов-членов класса, изменяемые переменные-члены, встраиваемые функции-члены, функции-члены, возвращающие *this, а также подробности определения и использования типов класса и дружественных классов.

7.3.1. Снова о членах класса.

Для исследования некоторых из дополнительных средств определим пару взаимодействующих классов по имени Screen и Window_mgr.

Определение типов-членов.

Класс Screen представляет окно на экране. У каждого объекта класса Screen есть переменная-член типа string, хранящая содержимое окна и три переменные-члена типа string::size_type, представляющие позицию курсора, высоту и ширину окна.

Кроме переменных и функций-членов, класс может определять собственные локальные имена таких типов. Определенные классом имена типов подчиняются тем же правилам доступа, что и любой другой его член, и могут быть открытыми или закрытыми:

class Screen {

public:

 typedef std::string::size_type pos;

private:

 pos cursor = 0;

 pos height = 0, width = 0;

 std::string contents;

};

Тип pos определен в части public класса Screen, поскольку пользователи должны использовать это имя. Пользователи класса Screen не обязаны знать, что он использует класс string для хранения своих данных. Определив тип pos как открытый член, эту подробность реализации класса Screen можно скрыть.

В объявлении типа pos есть два интересных момента. Во-первых, хоть здесь и был использован оператор typedef (см. раздел 2.5.1), с таким же успехом можно использовать псевдоним типа (см. раздел 2.5.1):

class Screen {

public:

 // альтернативный способ объявления типа-члена с использованием

 // псевдонима типа

 using pos = std::string::size_type;

 // другие члены как прежде

};

Во-вторых, по причинам, которые будут описаны в разделе 7.3.4, в отличие от обычных членов, типы-члены определяются прежде, чем используются. В результате типы-члены обычно располагают в начале класса.

Функции-члены класса Screen.

Чтобы сделать наш класс полезней, добавим в него конструктор, позволяющий пользователям задавать размер и содержимое экрана, наряду с членами, позволяющими переместить курсор и получить символ в указанной позиции:

class Screen {

public:

 typedef std::string::size_type pos;

 Screen() = default; // необходим, поскольку у класса Screen есть

                     // другой конструктор

 // внутриклассовый инициализатор инициализирует курсор значением 0

 Screen(pos ht, pos wd, char c) : height(ht), width(wd),

                                  contents(ht * wd, c) { }

 char get() const // получить символ в курсоре

 { return contents [cursor]; }          // неявно встраиваемая

 inline char get(pos ht, pos wd) const; // явно встраиваемая

 Screen &move(pos r, pos с); // может быть сделана встраиваемой позже

private:

 pos cursor = 0;

 pos height = 0, width = 0;

 std::string contents;

};

Поскольку мы предоставляем конструктор, компилятор не будет автоматически создавать стандартный конструктор сам. Если у нашего класса должен быть стандартный конструктор, то придется создать его явно. В данном случае используется синтаксис = default, чтобы попросить компилятор самому создать определение стандартного конструктора (см. раздел 7.1.4).

Стоит также обратить внимание на то, что второй конструктор (получающий три аргумента) неявно использует внутриклассовый инициализатор для переменной-члена cursor (см. раздел 7.1.4). Если бы у класса не было внутриклассового инициализатора для переменной-члена cursor, то мы явно инициализировали бы ее наряду с другими переменными-членами.

Встраиваемые члены класса.

У классов зачастую бывают небольшие функции, которые выгодно сделать встраиваемыми. Как уже упоминалось, определенные в классе функции-члены автоматически являются встраиваемыми ( inline) (см. раздел 6.5.2). Таким образом, конструкторы класса Screen и версия функции get(), возвращающей обозначенный курсором символ, являются встраиваемыми по умолчанию.

Функцию-член можно объявить встраиваемой явно в ее объявлении в теле класса. В качестве альтернативы функцию можно указать встраиваемой в определении, расположенном вне тела класса:

inline // функцию можно указать встраиваемой в определении

Screen &Screen::move(pos r, pos с) {

 pos row = r * width; // вычислить положение ряда

 cursor = row + с;    // переместить курсор к столбцу этого ряда

 return *this;        // возвратить этот объект как l-значение

}

char Screen::get(pos r, pos с) const // объявить встраиваемый в классе

{

 pos row = r * width;      // вычислить положение ряда

 return contents[row + с]; // возвратить символ в данном столбце

}

Хоть и не обязательно делать это, вполне допустимо указать ключевое слово inline и в объявлении, и в определении. Однако указание ключевого слова inline в определении только вне класса может облегчить чтение класса.

Язык программирования C++.По тем же причинам, по которым встраиваемые функции определяют в заголовках (см. раздел 6.5.2), встраиваемые функции-члены следует определить в том же заголовке, что и определение соответствующего класса.

Перегрузка функций-членов.

Подобно функциям, которые не являются членами класса, функции-члены могут быть перегружены (см. раздел 6.4), если они отличаются количеством и/или типами параметров. При вызове функции-члена используется тот же процесс подбора функции (см. раздел 6.4), что и у функций, не являющихся членом класса.

Например, в классе Screen определены две версии функции get(). Одна версия возвращает символ, обозначенный в настоящее время курсором; другая возвращает символ в указанной позиции, определенной ее рядом и столбцом. Чтобы определить применяемую версию, компилятор использует количество аргументов:

Screen myscreen;

char ch = myscreen.get(); // вызов Screen::get()

ch = myscreen.get(0,0);   // вызов Screen::get(pos, pos)

Изменяемые переменные-члены.

Иногда (но не очень часто) у класса есть переменная-член, которую следует сделать изменяемой даже в константной функции-члене. Для обозначения таких членов в их объявление включают ключевое слово mutable.

Изменяемая переменная-член (mutable data member) никогда не бывает константой, даже когда это член константного объекта. Соответственно константная функция-член может изменить изменяемую переменную-член. В качестве примера добавим в класс Screen изменяемую переменную-член access_ctr, используемую для отслеживания частоты вызова каждой функции-члена класса Screen:

class Screen {

public:

 void some_member() const;

private:

 mutable size_t access_ctr; // может измениться даже в константном

                            // объекте

 // другие члены как прежде

};

void Screen::some_member() const {

 ++access_ctr; // сохранить количество вызовов любой функции-члена

               // безотносительно других выполняемых ею действий

}

Несмотря на то что функция-член some_member() константная, она может изменить значение переменной-члена access_сtr. Этот член класса является изменяемым, поэтому любая функция-член, включая константные, может изменить это значение.

Инициализаторы переменных-членов класса.

Язык программирования C++.Кроме класса Screen, определим также класс диспетчера окон, который представляет коллекцию окон на данном экране. У этого класса будет вектор объектов класса Screen, каждый элемент которого представляет отдельное окно. По умолчанию класс Window_mgr должен изначально содержать один объект класса Screen, инициализированный значением по умолчанию. По новому стандарту наилучшим способом определения такого значения по умолчанию является внутриклассовый инициализатор (см. раздел 2.6.1):

class Window_mgr {

private:

 // по умолчанию отслеживающий окна объект класса Window_mgr

 // содержит одно пустое окно стандартного размера

 std::vector<Screen> screens{Screen(24, 80, ' ')};

};

При инициализации переменных-членов типа класса их конструктору следует предоставить аргументы. В этом случае применяется список инициализации переменной-члена типа vector (см. раздел 3.3.1) с инициализатором для одного элемента. Этот инициализатор содержит значение типа Screen, передаваемое конструктору vector<Screen> для создания вектора с одним элементом. Это значение создается конструктором класса Screen, получающим параметры в виде двух размерностей и заполняющего символа, чтобы создать пустое окно заданного размера.

Как уже упоминалось, для внутриклассовой инициализации может использоваться форма инициализации = (как при инициализации переменных-членов класса Screen) или прямая форма инициализации с использованием фигурных скобок (как у вектора screens).

Язык программирования C++.При предоставлении внутриклассового инициализатора это следует сделать после знака = или в фигурных скобках.

Упражнения раздела 7.3.1.

Упражнение 7.23. Напишите собственную версию класса Screen.

Упражнение 7.24. Добавьте в свой класс Screen три конструктора: стандартный; получающий высоту, ширину и заполняющий содержимое соответствующим количеством пробелов; получающий высоту, ширину и заполняющий символ для содержимого экрана.

Упражнение 7.25. Может ли класс Screen безопасно полагаться на заданные по умолчанию версии операторов копирования и присвоения? Если да, то почему? Если нет, то почему?

Упражнение 7.26. Определите функцию Sales data::avg_price как встраиваемую.

7.3.2. Функции, возвращающие указатель *this. 7.3. Дополнительные средства класса. Глава 7. Классы. Часть I. Основы. Язык программирования C++.7.3.2. Функции, возвращающие указатель *this.

Теперь добавим функции, устанавливающие символ в курсоре или в заданной области:

class Screen {

public:

 Screen &set(char);

 Screen &set(pos, pos, char);

 // другие члены, как прежде

};

inline Screen &Screen::set(char c) {

 contents[cursor] = с; // установите новое значение в текущей позиции

                       // курсора

 return *this;         // возвратить этот объект как l-значение

}

inline Screen &Screen::set(pos r, pos col, char ch) {

 contents[r * width + col] = ch; // установить позицию по данному

                                 // значению

 return *this; // возвратить этот объект как l-значение

}

Как и функция move(), функция-член set() возвращает ссылку на объект, из которого они вызваны (см. раздел 7.1.2). Возвращающие ссылку функции являются l-значениями (см. раздел 6.3.2), а это означает, что они возвращают сам объект, а не его копию. Это позволяет связать несколько их вызовов в одно выражение:

// переместить курсор в указанную позицию и присвоить

// символу значение

myScreen.move(4,0).set('#');

Эти операции выполнятся для того же объекта. В этом выражении сначала перемещается курсор ( move()) в окно ( myScreen), а затем устанавливается ( set()) заданный символ. Таким образом, этот оператор эквивалентен следующему:

myScreen.move(4,0);

myScreen.set('#');

Если бы функции move() и set() возвращали тип Screen, а не Screen&, этот оператор выполнялся бы совсем по-другому. В данном случае он был бы эквивалентен следующему:

// если move возвращает Screen, а не Screen&

Screen temp = myScreen.move(4,0); // возвращаемое значение было

                                  // бы скопировано

temp.set('#'); // содержимое myScreen осталось бы неизменно

Если бы функция move() имела возвращаемое значение не ссылочного типа, то оно было бы копией *this (см. раздел 6.3.2). Вызов функции set() изменил бы лишь временную копию, а не сам объект myScreen.

Возвращение *this из константной функции-члена.

Теперь добавим функцию display(), выводящую содержимое окна. Необходима возможность включать эту операцию в последовательность операций set() и move(). Поэтому, подобно функциям set() и move(), функция display() возвратит ссылку на объект, для которого она выполняется.

Логически отображение объекта класса Screen (окна) не изменяет его, поэтому функцию display() следует сделать константным членом. Но если функция display() будет константной, то this будет указателем на константу, а значение *this — константным объектом. Следовательно, типом возвращаемого значения функции display() будет const Screen&. Однако, если функция display() возвратит ссылку на константу, мы не сможем вставить вызов функции display() в последовательность действий:

Screen myScreen;

// если display возвращает константную ссылку,

// вызов в последовательности будет ошибкой

myScreen.display(cout).set('*');

Хотя объект myScreen неконстантный, вызов функции set() не будет компилироваться. Проблема в том, что константная версия функции display() возвращает ссылку на константу, и мы не можем вызвать функцию set() для константного объекта.

Язык программирования C++.Тип возвращаемого значения константной функции-члена, возвращающей *this как ссылку, должен быть ссылкой на константу.

Перегрузка на основании константности.

Функции-члены вполне можно перегружать исходя из того, являются ли они константными или нет, причем по тем же причинам, по которым функцию можно перегружать исходя из того, является ли ее параметр указателем на константу (см. раздел 6.4). Неконстантная версия неприменима для константных объектов; она применима только для константных объектов. Для неконстантного объекта можно вызвать любую версию, но неконстантная версия будет лучшим соответствием.

В этом примере определим закрытую функцию-член do_display() для фактического вывода окна. Каждая из функций display() вызовет эту функцию, а затем возвратит объект, для которого она выполняется:

class Screen {

public:

 // display перегружена на основании того, является ли

 // объект константой или нет

 Screen &display(std::ostream &os)

 { do_display(os); return *this; }

 const Screen &display(std::ostream &os) const

 { do_display(os); return *this; }

private:

 // функция отображения окна

 void do_display(std::ostream &os) const {os << contents;}

 // другие члены как прежде

};

Как и в любом другом случае, при вызове одной функции-члена другой неявно передается указатель this. Таким образом, когда функция display() вызывает функцию-член do_display(), ей неявно передается собственный указатель this. Когда неконстантная версия функции display() вызывает функцию do_display(), ее указатель this неявно преобразуется из указателя на неконстанту в указатель на константу (см. раздел 4.11.2).

Когда функция do_display() завершает работу, функция display() возвращает объект, с которым они работают, обращаясь к значению указателя this. В неконстантной версии указатель this указывает на неконстантный объект, так что эта версия функции display() возвращает обычную, неконстантную ссылку; константная версия возвращает ссылку на константу.

Когда происходит вызов функции display() для объекта, вызываемую версию определяет его константность:

Screen myScreen(5, 3);

const Screen blank(5, 3);

myScreen.set('#').display(cout); // вызов неконстантной версии

blank.display(cout);             // вызов константной версии

Совет. Используйте закрытые вспомогательные функции. Некоторые читатели могут удивиться: зачем дополнительно создавать отдельную функцию do_display()? В конце концов, обращение к функции do_display() не намного проще, чем осуществляемое в ней действие. Зачем же она нужна? Причин здесь несколько. • Всегда желательно избегать нескольких экземпляров одного кода. • По мере развития класса функция display() может стать значительно более сложной, а следовательно, преимущества одной, а не нескольких копий кода станут более очевидными. • Во время разработки в тело функции display(), вероятно, придется добавить отладочный код, который в финальной версии будет удален. Это будет проще сделать в случае, когда весь отладочный код находится в одной функции do_display(). • Поскольку функция do_display() объявлена встраиваемой ( inline), при создании исполняемого кода компилятор и так вставит ее содержимое по месту вызова, поэтому вызов функции не повлечет за собой никаких потерь времени и ресурсов. Обычно в хорошо спроектированных программах на языке С++ присутствует множество маленьких функций, таких как do_display(), которые выполняют всю основную работу, когда их использует набор других функций.

Упражнения раздела 7.3.2.

Упражнение 7.27. Добавьте функции move(), set() и display() в свою версию класса Screen. Проверьте свой класс, выполнив следующий код:

Screen myScreen(5, 5, 'X');

myScreen.move(4,0).set('#').display(cout);

cout << "\n";

myScreen.display(cout);

cout << "\n";

Упражнение 7.28. Что если бы в предыдущем упражнении типом возвращаемого значения функций move(), set() и display() был Screen, а не Screen&?

Упражнение 7.29. Пересмотрите свой класс Screen так, чтобы функции move(), set() и display() возвращали тип Screen, а затем проверьте свое предположение из предыдущего упражнения.

Упражнение 7.30. Обращение к членам класса при помощи указателя this вполне допустимо, но избыточно. Обсудите преимущества и недостатки явного использования указателя this для доступа к членам.

7.3.3. Типы классов.

Каждый класс определяет уникальный тип. Два различных класса определяют два разных типа, даже если их члены совпадают. Например:

struct First {

 int memi;

 int getMem();

};

struct Second {

 int memi;

 int getMem();

};

First obj1;

Second obj2 = obj1; // ошибка: obj1 и obj2 имеют разные типы

Язык программирования C++.Даже если у двух классов полностью совпадает список членов, они являются разными типами. Члены каждого класса отличны от членов любого другого класса (или любой другой области видимости).

К типу класса можно обратиться непосредственно, используя имя класса как имя типа. В качестве альтернативы можно использовать имя класса после ключевого слова class или struct:

Sales_data item1;       // инициализация значением по умолчанию объекта

                        // типа Sales_data

class Sales_data item1; // эквивалентное объявление

Оба способа обращения к типу класса эквивалентны. Второй метод унаследован от языка С и все еще допустим в С++.

Объявления класса.

Подобно тому, как можно объявить функцию без ее определения (см. раздел 6.1.2), можно объявить (class declaration) класс, не определяя его:

class Screen; // объявление класса Screen

Такое объявление иногда называют предварительным объявлением (forward declaration), оно вводит имя Screen в программу и указывает, что оно относится к типу класса. После объявления, но до определения, тип Screen считается незавершенным типом (incomplete type), т.е. известно, что Screen — это тип класса, но не известно, какие члены он содержит.

Использование незавершенного типа весьма ограниченно. Его можно использовать только для определения указателей или ссылок, а также для объявления (но не определения) функций, которые используют этот тип как параметр или тип возвращаемого значения.

Прежде чем можно будет писать код, создающий объекты некого класса, его следует определить, а не только объявить. В противном случае компилятор не будет знать, в каком объеме памяти нуждаются его объекты. Аналогично класс должен быть уже определен перед использованием ссылки или указателя для доступа к члену класса. В конце концов, если класс не был определен, компилятор не сможет узнать, какие члены имеет класс.

За одним исключением, рассматриваемым в разделе 7.6, переменные-члены могут быть определены как имеющие тип класса, только если класс был определен. Тип следует завершить, поскольку компилятор должен знать объем памяти, необходимый для хранения переменных-членов. Пока класс не определен, пока его тело не создано, у класса не может быть переменных-членов его собственного типа. Однако класс считается объявленным (но еще не определенным), как только его имя стало видимо. Поэтому у класса могут быть переменные-члены, являющиеся указателями или ссылками на ее собственный тип:

class Link_screen {

 Screen window;

 Link_screen *next;

 Link_screen *prev;

};

Упражнения раздела 7.3.3.

Упражнение 7.31. Определите два класса, X и Y, у которых класс X имеет указатель на класс Y, a Y содержит объект типа X.

7.3.4. Снова о дружественных отношениях.

Наш класс Sales_data определил три обычных функции, не являющиеся членом класса, как дружественные (см. раздел 7.2.1). Класс может также сделать дружественным другой класс или объявить дружественными определенные функции-члены другого (определенного ранее) класса. Кроме того, дружественная функция может быть определена в теле класса. Такие функции неявно являются встраиваемыми.

Дружественные отношения между классами.

В качестве примера дружественных классов рассмотрим класс Window_mgr (см. раздел 7.3.1), его членам понадобится доступ к внутренним данным объектов класса Screen, которыми они управляют. Предположим, например, что в класс Window_mgr необходимо добавить функцию-член clear(), заполняющую содержимое определенного окна пробелами. Для этого функции clear() нужен доступ к закрытым переменным-членам класса Screen. Для этого класс Screen должен объявить класс Window_mgr дружественным:

class Screen {

 // члены класса Window_Mgr смогут обращаться к закрытым

 // членам класса Screen

 friend class Window_mgr;

 // ... остальное, как раньше в классе Screen

};

Функции-члены дружественного класса могут обращаться ко всем членам класса, объявившего его другом, включая не открытые члены. Теперь, когда класс Window_mgr является другом класса Screen, функцию-член clear() класса Window_mgr можно переписать следующим образом:

class Window_mgr {

public:

 // идентификатор области для каждого окна на экране

 using ScreenIndex = std::vector<Screen>::size_type;

 // сбросить данное окно, заполнив его пробелами

 void clear(ScreenIndex);

private:

 std::vector<Screen> screens{Screen(24, 80, ' ')};

};

void Window_mgr::clear(ScreenIndex i) {

 // s - ссылка на окно, которое предстоит очистить

 Screen &s = screens[i];

 // сбросить данное окно, заполнив его пробелами

 s.contents = string(s.height * s.width, ' ');

}

Сначала определим s как ссылку на класс Screen в позиции i вектора окон. Затем переменные-члены height и width данного объекта класса Screen используются для вычисления количества символов новой строки, содержащей пробелы. Эта заполненная пробелами строка присваивается переменной-члену contents.

Если бы функция clear() не была дружественной классу Screen, то этот код не компилировался бы. Функция clear() не смогла бы использовать переменные-члены height, width или contents класса Screen. Поскольку класс Screen установил дружественные отношения с классом Window_mgr, для его функций доступны все члены класса Screen.

Важно понимать, что дружественные отношения не передаются. Таким образом, если у класса Window_mgr есть собственные друзья, то у них нет привилегий доступа к членам класса Screen.

Язык программирования C++.Каждый класс сам контролирует, какие классы или функции будут его друзьями.

Как сделать функцию-член дружественной.

Вместо того чтобы делать весь класс Window_mgr дружественным классу Screen, можно предоставить доступ только функции-члену clear(). Когда функция-член объявляется дружественной, следует указать класс, которому она принадлежит:

class Screen {

 // класс Window_mgr::clear должен быть объявлен перед классом Screen

 friend void Window_mgr::clear(ScreenIndex);

 // ... остальное как раньше в классе Screen

};

Создание дружественных функций-членов требует тщательного структурирования программ в соответствии с взаимозависимостями объявлений и определений. В данном случае программу следует упорядочить следующим образом.

• Сначала определите класс Window_mgr, который объявляет, но не может определить функцию clear(). Класс Screen должен быть объявлен до того, как функция clear() сможет использовать члены класса Screen.

• Затем определите класс Screen, включая объявление функции clear() дружественной.

• И наконец, определите функцию clear(), способную теперь обращаться к членам класса Screen.

Перегруженные функции и дружественные отношения.

Хотя у перегруженных функций одинаковое имя, это все же разные функции. Поэтому класс должен объявить дружественной каждую из перегруженных функций:

// перегруженные функции storeOn

extern std::ostream& storeOn(std::ostream &, Screen &);

extern BitMap& storeOn(BitMap &, Screen &);

class Screen {

 // версия ostream функции storeOn может обращаться к закрытым членам

 // объектов класса Screen

 friend std::ostream& storeOn(std::ostream &, Screen &); // ...

};

Класс Screen объявляет другом версию функции storeOn, получающей поток ostream&. Версия, получающая параметр BitMap&, особых прав доступа к объектам класса Screen не имеет.

7.3.4. Снова о дружественных отношениях. 7.3. Дополнительные средства класса. Глава 7. Классы. Часть I. Основы. Язык программирования C++.Объявление дружественных отношений и область видимости.

Классы и функции, не являющиеся членами класса, не следует объявлять прежде, чем они будут использованы в объявлении дружественными. Когда имя впервые появляется в объявлении дружественной, оно неявно подразумевается принадлежащей окружающей области видимости. Однако сам друг фактически не объявлен в этой области видимости (см. раздел 7.2.1).

Даже если мы определим функцию в классе, ее все равно придется объявить за пределами класса, чтобы сделать видимой. Объявление должно уже существовать, даже если вызывается дружественная функция:

struct X {

 friend void f() { /* дружественная функция может быть определена

                      в теле класса */ }

 X() { f(); } // ошибка: нет объявления для f

 void g();

 void h();

};

void X::g() { return f(); } // ошибка: f не была объявлена

void f();                   // объявляет функцию, определенную в X

void X::h() { return f(); } // ok: объявление f теперь в области

                            // видимости

Важно понимать, что объявление дружественной затрагивает доступ, но не является объявлением в обычном смысле.

Язык программирования C++.Помните: некоторые компиляторы не выполняют правил поиска имен друзей (см. раздел 7.2.1).

Упражнения раздела 7.3.4.

Упражнение 7.32. Определите собственные версии классов Screen и Window_mgr, в которых функция clear() является членом класса Window_mgr и другом класса Screen.