Однако копирование может быть полезным во многих ситуациях! Просто взгляните на функцию
push_back
; без копирования было бы трудно использовать векторы (функция
push_back
помещает в вектор копию своего аргумента). Почему надо беспокоиться о непредвиденном копировании? Если операция копирования по умолчанию может вызывать проблемы, ее следует запретить. В качестве основного примера такой проблемы рассмотрим функцию
my_fct
. Мы не можем копировать объект класса
Circle
в вектор
v
, содержащий объекты типа
Shape
; объект класса
Circle
имеет радиус, а объект класса
Shape
— нет, поэтому
sizeof(Shape) <sizeof(Circle)
. Если бы мы допустили операцию
v.push_back(c)
, то объект класса
Circle
был бы “обрезан” и любое последующее использование элемента вектора
v
привело бы к краху; операции класса
Circle
предполагают наличие радиуса (члена
r
), который не был скопирован.
Конструктор копирования объекта
op2
и оператор присваивания объекту
op
имеют тот же самый недостаток. Рассмотрим пример.
Marked_polyline mp("x");
Circle c(p,10);
my_fct(mp,c); // аргумент типа Open_polyline ссылается
// на Marked_polyline
Теперь операции копирования класса
Open_polyline
приведут к “срезке” объекта
mark
, имеющего тип
string
.
В принципе иерархии классов, механизм передачи аргументов по ссылке и копирование по умолчанию не следует смешивать. Разрабатывая базовый класс иерархии, заблокируйте копирующий конструктор и операцию копирующего присваивания, как мы сделали в классе
Shape
.
Срезка (да, это технический термин) — не единственная причина, по которой следует предотвращать копирование. Существует еще несколько понятий, которые лучше представлять без операций копирования. Напомним, что графическая система должна помнить, где хранится объект класса
Shape
на экране дисплея. Вот почему мы связываем объекты класса
Shape
с объектами класса
Window
, а не копируем их. Объект класса
Window
ничего не знает о копировании, поэтому в данном случае копия действительно хуже оригинала.
Если мы хотим скопировать объекты, имеющие тип, в котором операции копирования по умолчанию были заблокированы, то можем написать явную функцию, выполняющую это задание. Такая функция копирования часто называется
clone
. Очевидно, что функцию
clone
можно написать, только если функций для чтения данных достаточно для реализации копирования, как в случае с классом
Shape
.
14.3. Базовые и производные классы
Посмотрим на базовый и производные классы с технической точки зрения; другими словами, в этом разделе предметом дискуссии будет не программирование, проектирование и графика, а язык программирования.
Разрабатывая нашу библиотеку графического интерфейса, мы использовали три основных механизма.??
• Вывод. Это способ построения одного класса из другого так, чтобы новый класс можно было использовать вместо исходного. Например, класс
Circle
является производным от класса
Shape
, иначе говоря, класс
Circle
является разновидностью класса
Shape
или класс
Shape
является базовым по отношению к классу
Circle
. Производный класс (в данном случае
Circle
) получает все члены базового класса (в данном случае
Shape
) в дополнение к своим собственным. Это свойство часто называют наследованием (inheritance), потому что производный класс наследует все члены базового класса. Иногда производный класс называют подклассом (subclass), а базовый — суперклассом (superclass).
• Виртуальные функции. В языке С++ можно определить функцию в базовом классе и функцию в производном классе с точно таким же именем и типами аргументов, чтобы при вызове пользователем функции базового класса на самом деле вызывалась функция из производного класса. Например, когда класс Window вызывает функцию
draw_lines
из класса
Circle
, выполняется именно функция
draw_lines
из класса
Circle
, а не функция
draw_lines
из класса
Shape
. Это свойство часто называют динамическим полиморфизмом (run-time polymorphism) или динамической диспетчеризацией (run-time dispatch), потому что вызываемые функции определяются на этапе выполнения программы по типу объекта, из которого они вызываются.
• Закрытые и защищенные члены. Мы закрыли детали реализации наших классов, чтоб защитить их от непосредственного доступа, который может затруднить сопровождение программы. Это свойство часто называют инкапсуляцией (encapsulation).
Наследование, динамический полиморфизм и инкапсуляция — наиболее распространенные характеристики объектно-ориентированного программирования (object-oriented programming). Таким образом, язык C++ непосредственно поддерживает объектно-ориентированное программирование наряду с другими стилями программирования. Например, в главах 20-21 мы увидим, как язык C++ поддерживает обобщенное программирование. Язык C++ позаимствовал эти ключевые механизмы из языка Simula67, первого языка, непосредственно поддерживавшего объектно-ориентированное программирование (подробно об этом речь пойдет в главе 22).
Довольно много технической терминологии! Но что все это значит? И как на самом деле эти механизмы работают? Давайте сначала нарисуем простую диаграмму наших классов графического интерфейса, показав их отношения наследования.
Стрелки направлены от производного класса к базовому. Такие диаграммы помогают визуализировать отношения между классами и часто украшают доски программистов. По сравнению с коммерческими пакетами эта иерархия классов невелика и содержит всего шестнадцать элементов. Причем в этой иерархии только класс
Open_polyline
имеет несколько поколений наследников. Очевидно, что наиболее важным является общий базовый класс (
Shape
), несмотря на то, что он представляет абстрактное понятие о фигуре и никогда не используется для ее непосредственного воплощения.
14.3.1. Схема объекта
Как объекты размещаются в памяти? Как было показано в разделе 9.4.1, схема объекта определяется членами класса: данные-члены хранятся в памяти один за другим. Если используется наследование, то данные-члены производного класса просто добавляются после членов базового класса. Рассмотрим пример.
Класс
Circle
имеет данные-члены класса
Shape
(в конце концов, он является разновидностью класса
Shape
) и может быть использован вместо класса
Shape
. Кроме того, класс
Circle
имеет свой собственный член
r
, который размещается в памяти после унаследованных данных-членов.