Контейнерные классы: реализация конструкторов

Следует особо остановиться на реализации конструкторов контейнерных классов. Дело в том, что их реализация отличается от реализации конструкторов обычных классов.

Рассмотрим инициализирующий конструктор класса Point.

Point::Point(int x0, int y0)

{ x = x0;

y = y0; }

По аналогии с ним, можно было бы реализовать один из инициализирующих конструктор контейнерного класса Circle следующим образом:

Circle::Circle(Point p, int r)

{ center = p;

rad = r; }

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

1. Определен ли оператор присваивания для вложенного класса (в данном случае, для класса Point)? Он может быть не определен, тогда такой способ инициализации неприменим.

2. Выполнение присваивания может потребовать каких-то дополнительных, не всегда корректных действий. Например, если класс Point использует свободную память, тогда перегруженный для класса оператор присваивания начинает свою работу с того, что освобождает память, занятую операндом, указанным слева от присваивания. Но мы как раз хотим инициализировать значение этого операнда! Попытка освободить память, еще не распределенную, может привести к большим неприятностям на этапе выполнения программы.

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

Эта возможность реализуется с помощью следующей конструкции:

имя_контейнерного_класса :: имя_контейнерного_класса (параметры):

имя_члена-данного1_класса (аргументы),

имя_члена-данного2_класса (аргументы),

. . .

{ инициализация других членов-данных класса }

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

Рассмотрим на примере класса Circle. Реализованный выше инициализирующий конструктор может быть реализован любым из приведенных ниже двух способов:

//------------------------------

Circle::Circle(Point p, int r):

center(p)

{ rad = r; }

//------------------------------

Circle::Circle(Point p, int r):

center(p), rad(r)

{}

И хотя оба способа равноценны, тем не менее, второй предпочтительнее.

В С++ действуют (немногие!) правила по умолчанию. Одно из них гласит: если в конструкторе контейнерного класса не указан явно вызов требуемого конструктора, для соответствующего объекта будет использован пустой конструктор. В соответствии с этим правилом, следующие реализации пустого и одноаргументного конструкторов класса Circle эквивалентны.



  Вариант с явным указанием вызова пустого конструктора Эквивалентный вариант с вызовом по умолчанию пустого конструктора
Пустой конструктор Circle::Circle(): center(), rad(0) {} Circle::Circle(): rad(0) {}
Одноаргументный конструктор Circle::Circle(int r): center(), rad(r) {} Circle::Circle(int r): rad(r) {}

Тем не менее, пользоваться правилами по умолчанию не рекомендуется.

В заключение приведем реализацию всех указанных в определении класса Circle конструкторов.

Circle::Circle(): center(), rad(0){}

Circle::Circle(int r): center(), rad(r){}

Circle::Circle(Point p, int r): center(p), rad(r){}

Circle::Circle(int x, int r): center(x), rad(r){}

Circle::Circle(int x, int y, int r): center(x, y), rad(r){}

10. Производные классы: простое наследование, основные понятия и определения. Правила определения производного класса, типы наследования, видимость членов класса. Реализация и использование конструкторов и деструкторов базового и производных классов. Использование экземпляров базового и производных классов. Указатели на базовый и производные классы.

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

Контейнерные классы: реализация конструкторов - student2.ru

Рис. 6-1. Базовый и производный классы

В производном классе имена методов (и членов – данных) могут совпадать с именами из базового класса.

Отношение между базовым и производным классами можно сформулировать следующим образом:

Производный класс ЕСТЬ базовый ПЛЮС ОБЯЗАТЕЛЬНО что-то свое.

В соответствии с приведенной формулировкой, производный класс наследует от базового все его свойства (и состояние, и методы) и расширяет и уточняет их своими свойствами.

Простое наследование: правила определения производного класса

Правила объявления производных классов. Две проблемы: могут ли (и как) методы производного класса обращаться к членам базового класса (доступность изнутри производного класса) и как члены базового класса, унаследованные производным классом, могут быть доступны извне производного класса.

Первая проблема решается уровнем видимости членов в базовом классе:

class B{

private:

// закрытая часть класса; не доступна никому, кроме методов класса (в том числе

// не доступна и из производного класса)

protected:

// защищенная часть класса; доступна из методов базового и производного класса

// (но не доступна извне класса)

public:

// открытая часть класса; доступна везде

};

Вторая проблема решается способом наследования:

class D: тип_наследования B{

. . . };

тип_наследования: одно из private, protected, public.

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

Уровень видимости в базовом классе Тип наследования
private protected public
protected private protected protected
public private protected public

Пример производного класса

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

Окружность ЕСТЬ Точка, имеющая некоторый (возможно, нулевой) размер.

В такой ситуации класс Точка является базовым для класса Окружность. В результате класс Окружность:

- наследует состояние и методы, определенные в базовом классе Точка;

- должен определить собственные состояние и/или методы, отсутствующие в базовом классе;

- может переопределить какие-то методы базового класса.

Так, класс Окружность наследует часть состояния класса Точка (координаты) и метод вычисления расстояния между точками (центрами двух окружностей); определяется собственная часть состояния (радиус) и свой собственный метод, отсутствовавший в классе Точка (пересечение окружностей); наконец, переопределяет метод вывода.

Рассмотрим определение и реализацию указанных классов.

Определение класса Точка:

class Point{

protected:

int x, y;

public:

// конструкторы

Point():x(0), y(0){}

Point(int x0):x(x0),y(x0){}

Point(int x0, int y0):x(x0), y(y0){}

Point(const Point &p):x(p.x), y(p.y){}

void print();

float distance(Point p);

};

Реализация класса:

void Point::print()

{ cout << '(' << p.x << ", " << p.y << ')'; }

float Point::distance(Point p)

{ float dx = x - p.x;

float dy = y - p.y;

return sqrt(dx * dx + dy * dy); }

Определение производного класса Окружность:

class Circle: public Point{

private:

int rad;

public:

// конструкторы

Circle();

Circle(int r);

Circle(int x, int r);

Circle(int x, int y, int r);

Circle(Point p, int r);

// методы

void print();

int intersect(Circle c);

};

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

Теперь рассмотрим реализацию методов. Начнем с конструкторов.

Проблема, возникающая здесь, заключается в следующем. Конструктор должен инициализировать состояние экземпляра класса. Доступ к собственной части состояния проблем не вызывает. Но как быть с частью состояния, унаследованной от базового класса? Если состояние базового класса включено в область protected, оно доступно из производного класса, и тогда конструктор может быть реализован следующим образом:

Circle::Circle(){x = 0; y = 0; rad = 0;} // пустой конструктор

Если же состояние базового класса включено в private область, оно не доступно из производного класса, но также должно быть инициализировано. Состояние класса инициализируют конструкторы класса. Следовательно, необходимо иметь возможность из конструктора производного класса вызвать конструктор базового класса. Эта возможность реализуется следующим образом:

<имя_производного_класса>::<имя_производного_класса>(<параметры>):

<имя_базового_класса>(<аргументы>) // вызов конструктора базового класса

{< дополнительные операции >}

Инициализация собственного состояния может быть реализована в соответствии с правилами контейнерных классов, тогда конструктор производного класса может выглядеть и так:

<имя_производного_класса>::<имя_производного_класса>(<параметры>):

<имя_базового_класса>(<аргументы>),

<имя_члена_данного1_класса>(<значение>),

...

{< дополнительные операции>}

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

В соответствии с изложенным, реализация конструкторов класса Окружность может быть следующей:

Circle::Circle():rad(0){} // Вызывается пустой конструктор базового класса

Circle::Circle(int r):rad(r){}

Circle::Circle(int x, int r):Point(x), rad(r){}

Circle::Circle(int x, int y, int r):Point(x, y), rad(r){}

Circle::Circle(Point p, int r):Point(p), rad(r){}

Унаследованные от базового класса методы не требуют реализации. Так, метод distance()из базового класса Point наследуется классом Circle и будет определен в производном классе без каких-либо дополнительных усилий.

В реализации собственных методов производного класса может возникнуть необходимость использовать (вызвать) метод базового класса. Поскольку он наследуется производным классом, его вызов ничем не отличается от вызова собственных методов класса. Так, например, метод определения, пересекаются ли окружности, должен вызвать метод определения расстояния между точками базового класса. Реализация метода может выглядеть следующим образом:

int Circle::intersect(Circle c)

{ return distance(c) < rad + c.rad; }

Проблемы могут возникнуть, если вызываемый метод базового класса имеет такое же имя, как и использующий его метод производного класса. Например, в методе вывода print() производного класса используется метод с таким же именем базового класса.

Если метод будет реализован следующим образом:

void Circle::print()

{ print(); // (!) Метод какого класса вызывается?

cout << " with rad " << rad; }

В этом случае возникает вопрос: метод какого класса будет вызван в предложении (!)? В соответствии с правилами языка С++, использование имени члена класса предполагает использование неявного параметра this, т.е. конструкция (!) интерпретируется как this->print(). Так как рассматривается реализация производного класса, параметр this определяет экземпляр производного класса; следовательно, здесь вызывается метод производного класса – и мы получаем бесконечную рекурсию! Чтобы вызвать метод базового класса, нужно это указать явно, используя оператор :: и имя базового класса. Тогда корректная реализация метода будет выглядеть следующим образом:

void Circle::print()

{ Point::print(); // Вызывается метод print() базового класса

cout << " with rad " << rad; }

Использование классов:

Circle c1(3), c2(2, 2);

cout << "distance between " << c1 << " and " << c2 << " is " << c1.distance(c2) << endl;

cout << c1 << " and " << c2;

if(c1.intersect(c2))

cout << " are intersected " << endl;

else

cout << " are not intersected " << endl;

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