Виртуальный конструктор-копировщик
Конструкторы не могут быть виртуальными, из чего можно сделать вывод, что не может быть также виртуального конструктора-копировщика. Но иногда требуется, чтобы программа могла передать указатель на объект базового класса и правильно скопировать его в объект производного класса. Чтобы добиться этого, необходимо в базовом классе создать виртуальный метод Clone(). Метод Clone() должен создавать и возвращать копию объекта текущего класса.
Поскольку в производных классах метод Clone() замещается, при вызове его создаются копии объектов, соответствующие выбранному классу. Программа, использующая этот метод, показана в листинге 11.11.
Листинг 11.11. Виртуальный конструктор-копировщик
1: //Листинг 11.11. Виртуальный конструктор-копировщик
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { cout << "Mammal constructor...\n"; }
9: virtual ^Mammal() { cout << "Mammal destructor...\n"; }
10: Mammal (const Mammal & rhs);
11: virtual void Speak() const { cout << "Mammal speak!\n"; }
12: virtual Mammal* Clone() { return new Mammal(*this); }
13: int GetAge()const { return itsAge; }
14: protected:
15: int itsAge;
16: };
17:
18: Mammal::Mammal (const Mammal & rhs):itsAge(rhs.GetAge())
19: {
20: cout << "Mammal Copy Constructor...\n";
21: }
22:
23: class Dog : public Mammal
24: {
25: public:
26: Dog() { cout << "Dog constructor...\n"; }
27: virtual ~Dog() { cout << "Dog destructor...\n"; }
28: Dog (const Dog & rhs);
29: void Speak()const { cout << "Woof!\n"; }
30: virtual Mammal* Clone() { return new Dog(*this); }
31: };
32:
33: Dog::Dog(const Dog & rhs):
34: Mammal(rhs)
35: {
36: cout << "Dog copy constructor...\n";
37: }
38:
39: class Cat : public Mammal
40: {
41: public:
42: Cat() { cout << "Cat constructor,,,\n"; }
43: ~Cat() { cout << "Cat destructor...\n"; }
44: Cat (const Cat &);
45: void Speak()const { cout << "Meow!\n"; }
46: virtual Mammal* Clone() { return new Cat(*this); }
47: };
48:
49: Cat::Cat(const Cat & rhs):
50: Mammal(rhs)
51: {
52: cout << "Cat copy constructor..,\n";
53: }
54:
55: enum ANIMALS { MAMMAL, D0G, CAT };
56: const int NumAnimalTypes = 3;
57: int main()
58: {
59: Mammal *theArray[NumAnimalTypes];
60: Mammal* ptr;
61: int choice, i;
62: for ( i = 0; i<NumAnimalTypes; i++)
63: {
64: cout << "(1)dog (2)cat (3)Mammal: ";
65: cin >> choice;
66: switch (choice)
67: {
68: case DOG: ptr = new Dog;
69: break;
70: case CAT: ptr = new Cat;
71: break;
72: default: ptr = new Mammal;
73: break;
74: }
75: theArray[i] = ptr;
76: }
77: Mammal *OtherArray[NumAnimalTypes];
78: for (i=0;i<NumAnimalTypes;i++)
79: {
80: theArray[i]->Speak();
81: OtherArray[i] = theArray[i]->Clone();
82: }
83: for (i=0;i<NumAnimalTypes;i++)
84: OtherArray[i]->Speak();
85: return 0;
86: }
Результат:
1: (1)dog (2)cat (3)Mammal: 1
2: Mammal constructor...
3: Dog constructor...
4: (1)dog (2)cat (3)Mammal: 2
5: Mammal constructor...
6: Cat constructor...
7: (1)dog (2)cat (3)Mammal: 3
8: Mammal constructor...
9: Woof!
10: Mammal Copy Constructor...
11: Dog copy constructor...
12: Meow!
13: Mammal Copy Constructor...
14: Cat copy constructor...
15: Mammal speak!
16: Mammal Copy Constructor...
17: Woof!
18: Meow!
19: Mammal speak!
Анализ: Листинг 11.11 похож на два предыдущих листинга, однако в данной программе в классе Mammal добавлен один новый виртуальный метод — Clone(). Этот метод возвращает указатель на новый объект класса Mammal, используя конструктор-копировщик, параметр которого представлен указателем <<this.
Метод Clone() замещается в обоих производных классах — Dog и Cat — соответствующими версиями, после чего копии данных передаются на конструкторы- копировщики производных классов. Поскольку Clone() является виртуальной функцией, то в результате будут созданы виртуальные конструкторы-копировщики, как показано в строке 81.
Пользователю предлагается выбрать объект класса 0og, Cat или Mammal. Объект выбранного типа создается в строках 62-74. В строке 75 указатель на новый объект добавляется в массив данных.
Затем осуществляется цикл, в котором для каждого объекта массива вызываются методы Speak() и Clone() (см. строки 80 и 81). В результате выполнения функции возвращается указатель на копию объекта, которая сохраняется в строке 81 во втором массиве.
В строке 1 вывода на экран показан выбор пользователем опции 1 — создание объекта класса Dog. В создание этого объекта вовлекаются конструкторы базового и производного классов. Эта операция повторяется для объектов классов Cat и Mammal в строках вывода 4-8.
В строке 9 вывода показано выполнение метода Speak() для объекта класса Dog. Поскольку функция Speak() также объявлена как виртуальная, то при обращении к ней вызывается та ее версия, которая соответствует типу объекта. Затем следует обращение еще к одной виртуальной функции Clone(), виртуальность которой проявляется в том, что при вызове из объекта класса Dog запускаются конструктор класса Mammal и конструктор-копировщик класса Dog.
То же самое повторяется для объекта класса Cat (строки вывода с 12—14) и объекта класса Mammal (строки вывода 15 и 16). В результате создается массив объектов, для каждого из которых вызывается своя версия функции Speak().
Цена виртуальности методов
Поскольку объекты с виртуальными методами должны поддерживать v-таблицу, то использование виртуальных функций всегда ведет к некоторому повышению затрат памяти и снижению быстродействия программы. Если вы работаете с небольшим классом, который не собираетесь делать базовым для других классов, то в этом случае нет никакого смысла использовать виртуальные методы.
Объявляя виртуальный метод в программе, заплатить придется не только за v- таблицу (хотя добавление последующих записей потребует не так уж много места), но и за создание виртуального деструктора. Поэтому следует подумать, имеет ли смысл преобразовывать методы программы в виртуальные, а если да, то какие именно.
Рекомендуется: Используйте виртуальные методы только в том случае, если программа содержит базовый и производные классы. Используйте виртуальный деструктор, если в программе были созданы виртуальные методы.
Не рекомендуется: Не пытайтесь создать виртуальный конструктор.
Резюме
Сегодня вы узнали, как наследовать новые классы от базового класса. В этой главе рассматривалось наследование с ключевым словом public и использование виртуальных функций. Во время наследования в производные классы передаются все открыты e и защищенные данные и функции из базового класса.
Защищенные данные базового класса открыты для всех производных классов, но закрыты для всех других классов программы. Но даже производные классы не могут получить доступ к закрытым данным и функциям базового класса.
Конструкторы могут инициализироваться до выполнения тела конструктора. При этом вызывается конструктор базового класса, и туда могут быть переданы данные в виде параметров.
Функции, объявленные в базовом классе, могут быть замещены в производных классах. Если при этом функция объявлена как виртуальная, а обращение к функции от объекта осуществляется с помощью указателя на объект или ссылки, то вызываться будет тот замещенный вариант функции, который соответствует типу текущего объекта.
Методы базового класса можно вызывать явным обращением, когда в строке вызова сначала указывается имя базового класса с двумя символами двоеточия после него. Например, если класс Dog произведен от класса Mammal, то к методу базового класса напрямую можно обратиться следующим выражением: Mammal::walk().
Если в классе используются виртуальные методы, то следует объявить также и виртуальный деструктор. Он необходим для того, чтобы быть уверенным в удалении части объекта, относящейся к производному классу, если удаление объекта осуществлялось с помощью указателя базового класса. Нельзя создать виртуальный конструктор. В то же время можно создать виртуальный конструктор-копировщик и эффективно его использовать с помощью виртуальной функции, вызывающей конструктор-копировщик.
Вопросы и ответы
Наследуются ли данные и функции-члены базового класса в последующие поколения производных классов? Скажем, если класс Dog произведен от класса Mammal, а класс Mammal произведен от класса Animals, унаследует ли класс Dog данные и функции класса Animals?
Да. Если последовательно производить ряд классов, последний класс в этом ряду унаследует всю сумму данных и методов предыдущих базовых классов.
Если в предыдущем примере в классе Mammal будет замещена функция, описанная в классе Animals, то какой вариант функции получит класс Dog?
Если класс Dog наследуется от класса Mammal, то он получит функцию в том виде, в каком она существует в классе Mammal, т.е. замещенную.
Можно ли в производном классе описать как private функцию, которая перед этим была описана в базовом классе как public?
Можно. Функция может быть не только защищена в производном классе, но и закрыта. Она останется закрытой для всех последующих классов, произведенных от этого.
В каких случаях не следует делать функции класса виртуальными?
Описание первой виртуальной функции вызовет создание v-таблицы, что потребует времени и дополнительной памяти. Последующее добавление виртуальных функций будет тривиальным. Многие программисты увлекаются созданием виртуальных функций и полагают, что если в программе есть уже одна виртуальная функция, то и все другие должны быть виртуальными. В действительности это не так. Создание виртуальных функций всегда должно отвечать решению конкретных задач.
Предположим, что некоторая функция без параметров была описана в базовом классе как виртуальная, а затем перегружена таким образом, чтобы принимать один и два целочисленных параметра. Затем в производном классе был замещен вариант функции с одним целочисленным параметром. Что произойдет, если с помощью указателя, связанного с объектом производного класса, вызвать вариант функции с двумя параметрами?
Замещение в производном классе варианта функции с одним параметром скроет от объектов этого класса все остальные варианты функции. Поэтому в случае обращения, описанного в вопросе, компилятор покажет сообщение об ошибке.
Коллоквиум
В этом разделе предлагаются вопросы для самоконтроля и укрепления полученных знаний и приводится несколько упражнений, которые помогут закрепить ваши практические навыки. Попытайтесь самостоятельно ответить на вопросы теста и выполнить задания, а потом сверьте полученные результаты с ответами в приложении Г. Не приступайте к изучению материала следующей главы, если для вас остались неясными хотя бы некоторые из предложенных ниже вопросов.
Тест
1. Что такое v-таблица?
2. Что представляет собой виртуальный деструктор?
3. Можно ли объявить виртуальный конструктор?
4. Как создать виртуальный конструктор-копировщик?
5. Как вызвать функцию базового класса из объекта производного класса, если в производном классе эта функция была замещена?
6. Как вызвать функцию базового класса из объекта производного класса, если в производном классе эта функция не была замещена?
7. Если в базовом классе функция объявлена как виртуальная, а в производном классе виртуальность функции указана не была, сохранится ли функция как виртуальная в следующем произведенном классе?
8. С какой целью используется ключевое слово protected?
Упражнения
1. Объявите виртуальную функцию, которая принимает одно целочисленное значение и возвращает void.
2. Запишите объявление класса Square, произведенного от класса Rectangle, который, в свою очередь, произведен от класса Shape.
3. Предположим, что в предыдущем примере объект класса Shape не использует параметры, объект класса Rectangle принимает два параметра (length и width), а объект класса Square — один параметр (length); запишите конструктор для класса Square.
4. Запишите виртуальный конструктор-копировщик для класса Square, взятого из упражнения 3.
5. Жучки: что неправильно в следующем программном коде?
void SomeFunction(Shape);
Shape * pRect = new Rectangle;
SoneFunction(*pRect);
6. Жучки: что неправильно в следующем программном коде?
class Shape()
{
public:
Shape();
virtual -Shape();
virtual Shape(const Shape&);
};