Дружественные функции и классы

Перегрузим для класса time_range операцию '<<' (вывод в поток). В качестве первого аргумента данной операции выступает объект класса std::ostream (именно такой тип имеют стандартные объекты cout и cerr).

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

std::ostream& operator<< (std::ostream &stream, const time_range &t) {

stream << t.hour << ":" << t.minute;

return stream;

}

Однако, приведённый код не скомпилируется. Проблема в том, что поля hour и minute в классе time_range являются скрытыми (private), и у функции operator<< нет к ним доступа. Чтобы решить эту проблему, можно сказать, что эта функция является дружественной для класса time_range. Для этого нужно добавить прототип функции в объявление класса, поставив перед ним ключевое слово friend:

class time_range {

friend std::ostream& operator<< (std::ostream&,

const time_range&);

};

Аналогично дружественным функциям, можно создавать и дружественные классы. Если класс B является дружественным для класса A, то из методов класса B можно обращаться даже к скрытым и защищенным полям класса A. Рекомендуется не злоупотреблять данной возможностью, так как в хорошо спроектированной программе классы должны быть как можно более независимыми друг от друга: так их легче разрабатывать и отлаживать.

Статические элементы класса

Элементы класса могут объявляться со спецификатором static. Смысл использования этого спецификатора рассмотрим отдельно для полей и методов.

Статические поля. Такие поля существуют в одном экземпляре независимо от экземпляров класса. Память под такие поля выделяется в области глобальных и статических переменных до начала выполнения программы. Часто такие поля используются для хранения констант.

Например, предположим, что в некоторых методах класса complex_t требуется знать значение числа Пи. Тогда можно добавить в класс статическое константное поле, которое будет хранить это число. Вычислить число Пи можно, например, взяв арккосинус от минус единицы:

#include <cmath>

struct complex_t {

static const double PI;

};

const double complex_t::PI = acos(-1.0);

Внутри описания класса добавилось объявление статического поля PI. Инициализация этого поля производится в отдельной строке за пределами класса (если объявление класса обычно находится в файле с расширением .h, то инициализация − в файле с расширением .cpp). Для обращения к статическому полю пишется имя класса и операция ::, например:

cout << complex_t::PI;

Статические методы. Статический метод − это метод, который можно вызывать без создания экземпляра класса. Статические методы не имеют неявного параметра this и не могут обращаться к нестатическим элементам класса. В качестве примера добавим в класс time_range статический метод cur_time, возвращающий текущее время (для получения времени используются функции из заголовочного файла ctime):

#include <ctime>

class time_range {

...

static time_range cur_time() {

time_t t = time(0);

tm *ts = localtime(&t);

time_range res;

res.set(ts->tm_hour, ts->tm_min);

return res;

}

};

int main() {

time_range t = time_range::cur_time();

std::cout << t;

}

Наследование и полиморфизм

Основные понятия

Одной из главных целей ООП является многократное использование кода. Библиотеки C++, основанные на классах, предоставляют удобные механизмы достижения этой цели.

Наследование – свойство классов порождать своих потомков, которые могут наследовать данные и функции своих родителей. Из этого определения выделяются две сущности:

Класс-потомок Класс-родитель

(наследник, производный класс) (предок, базовый класс)

Класс-потомок может пользоваться многими элементами классов-родителей без их повторного определения (но не всеми – этот вопрос мы далее подробно объясним). У каждого родителя может быть сколько угодно потомков. В С++, в отличие от многих других языков программирования, разрешена и противоположная ситуация – класс-потомок может иметь более одного родителя (этот случай называется множественным наследованием, его также далее поясним).

С помощью наследования можно строить иерархии классов – набор классов, связанных отношением наследования. На схемах иерархий классов наследование классов обозначается в виде стрелки, направленной от класса-потомка к классу-родителю. Пример иерархии классов будет далее приведён.

Поскольку различных тонких моментов в реализации наследования на С++ довольно много, будем разбирать их по порядку. Начнём с основного случая наследования, который обычно называют одиночным наследованием.

Одиночное наследование

Чаще всего у класса наследника бывает всего один предок – это и есть одиночное наследование. В С++ одиночное наследование реализовано так:

Сlass|struct имя_класса : тип_наследования имя_предка

{ новые поля и методы, переопределённые методы предка };

Тип наследования влияет на область видимости элементов родительского класса в классе-потомке. Имеется три типа наследования: public, protected и private.

Запишем различные виды наследования в виде таблицы доступа к элементам в классе-потомке.

Тип наследования public private protected
Доступ к элементу
public public private protected
private недоступно недоступно недоступно
protected protected private protected

Рассмотрим типы наследования подробнее. При наследовании public открытые элементы предка остаются открытыми. При наследовании protected и private область видимости сужается, то есть если элементы были открытыми, то стали скрытыми или защищенными.

К элементам типа private в базовом классе нельзя получить доступ из класса наследника. Поэтому, если доступ к каким-то скрытым элементам базового класса потомку всё-таки нужен, их область видимости private в базовом классе надо заменить на protected.

Если тип наследования не указан, то по умолчанию он считается private для типа class, и public для struct. Можно считать это правило ещё одним отличием в типах class и struct.

Пример реализации одиночного наследования.Для тех примеров, что у нас уже есть, необходимость в создании классов-потомков весьма сомнительна. Поэтому сменим предметную область – будем рассматривать классы различных геометрических фигур. Здесь отношения одиночного наследования возникают естественным образом, поскольку геометрические фигуры имеют и общие свойства, которые можно определить в базовом классе, и специфические черты, которые реализуются в наследниках.

Пример, который представлен далее, содержит определение базового класса coord, представляющего координаты точки (x и y), и его потомка point, который дополнен информацией о цвете точки (r, g, b – интенсивность красного, зелёного и синего цветов).

// Пример 3.4 реализация одиночного наследования

#include <iostream>

class coord { // класс Координаты точки - предок

protected:

int x, y;

public:

void set_xy (int _x, int _y) {x=_x; y=_y;}

void add (int dx, int dy){x += dx; y += dy;}

void show() {std::cout << x << ' ' << y << std::endl;}

};

class point : public coord { // класс Точка - потомок

protected:

int r, g, b;

public:

void set_color(int _r, int _g, int _b){r = _r; g = _g; b = _b;}

void show() {

coord::show();

std::cout << r << ' ' << g << ' ' << b << std::endl;

}

};

int main() {

point p;

p.set_xy(100, 70);

p.set_color(200, 0, 0);

p.show();

// эксперимент с указателями есть ошибка!

coord *cor = &p;

cor-> show();

point *pp = cor; //ошибка - надо point *pp = (point *)cor;

}

В функции main продемонстрировано применение указателей для объектов наших классов.

В С++ есть правило, при котором присваивать указатели и ссылки классов-потомков указателям и ссылкам классов-родителей можно без явного приведения типов. Это приведение вверх – преобразование указателя или ссылки класса-потомка к указателю или ссылке класса родителя. Почему так? Потому что все свойства базового класса у нас унаследованы, и при работе с указателем или ссылкой базового класса мы можем работать только с элементами данного класса, а класс потомок уже имеет все эти элементы, следовательно, мы можем работать с ним как с базовым классом.

Для того чтобы привести указатель базового класса к указателю на класс потомок необходимо использовать явное приведение типов. Это приведение вниз – преобразование указателя или ссылки базового класса в указатель или ссылку класса потомка.

Множественное наследование

Мы уже говорили, что в С++ у класса-потомка может быть несколько родителей. Такая необходимость возникает довольно редко, и поэтому во многих языках, поддерживающих ООП, возможность множественного наследования не предусмотрена. Но в С++ такая возможность есть и реализуется следующим синтаксисом:

сlass имя_класса: тип_наследования имя_предка1, тип_наследования имя_предка2,…., тип_наследования имя_предкаN

{ тело класса-потомка };

Самый распространенный пример, где используется множественное наследование, – это потоковая библиотека ввода/вывода iostream в стандартном C++. Приведём иерархию классов этой библиотеки в немного упрощенном виде (рисунок 3.2)

Дружественные функции и классы - student2.ru

Рисунок 3.2 Иерархия классов библиотеки iostream

Два основных видимых пользователю класса этой библиотеки – istream (для ввода) и ostream (для вывода). В число их общих атрибутов входят:

· информация о форматировании (представляется ли целое число в десятичной, восьмеричной или шестнадцатеричной системе счисления, число с плавающей точкой – в нотации с фиксированной точкой или в научной нотации и т.д.);

· информация о состоянии (находится ли потоковый объект в нормальном или ошибочном состоянии и т.д.);

· информация о параметрах локализации (отображается ли в начале даты день или месяц и т.д.);

· буфер, где хранятся данные, которые нужно прочитать или записать.

Эти общие атрибуты вынесены в отдельный класс ios, для которого istream и ostream являются классами-потомками. Пока речь идёт об одиночном наследовании.

А пример реализации множественного наследования – класс iostream. Он предоставляет поддержку для чтения и записи в один и тот же файл; его предками являются классы istream и ostream.

На рисунке легко выделить геометрическую фигуру – ромб, ограниченную классами ios, istream, ostream, iostream. При такой топологии класс iostream унаследует два различных экземпляра базового класса ios (по одному от istream и ostream), что может привести к дублированию и неоднозначности при вызове методов. Чтобы избежать этих проблем, язык C++ предоставляет специальный вид множественного наследования – виртуальное наследование. Синтаксис изменяется незначительно – в нашем случае достаточно добавить слово virtual в определение классов-родителей класса iostream. Исходя из нашей упрощенной схемы классов ввода-вывода, это может выглядеть так:

class istream : public virtual ios {…}

class ostream : public virtual ios {…}

class iostream : public istream, public ostream {…}

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

Наши рекомендации