
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfГлава 12 • Преимущества и недостатки составнь1х классов |
501 |
Листинг 12.1. Пример создания составного объекта с лишними вызовами конструкторов
#inclucle <lostream> |
|
|
|
|
|
|
|
|
|
|
||||
using |
namespace std; |
|
|
|
|
|
|
|
|
|
|
|||
class |
Point |
{ |
|
|
|
|
|
|
|
|
|
|
|
|
private: |
|
|
|
|
|
|
|
|
|
|
|
// закрытые |
координаты |
|
int X, |
y; |
|
|
|
|
|
|
|
|
|
|
|||
public: |
|
|
|
|
|
|
|
|
|
|
|
// общий конструктор |
||
Point |
(int |
a=0, b=0) |
|
|
|
|
|
|
|
|||||
{ |
X = a; |
у = b; |
|
|
|
|
|
« |
у « |
endl; } |
|
|||
|
cout |
« |
"Создан: x= " « |
x « |
" |
|
||||||||
Point |
(const Point& |
pt) |
|
|
|
|
|
|
// конструктор копирования |
|||||
{ |
X = pt.x; |
у = pt.y; |
|
|
|
|
" у=" « |
у « endl; } |
|
|||||
|
cout |
« |
" Скопирован: x= " « |
x « |
присваивания |
|||||||||
void operator = (const Point& |
pt) |
|
|
|
// операция |
|||||||||
{ |
X = pt.x; |
у = pt.y; |
|
|
|
|
|
« |
у « endl; } |
|
||||
|
cout |
« |
" |
Присвоен: x= " |
« |
x « " у |
|
|||||||
void set |
(int a, int |
b) |
|
|
|
|
|
|
// функция-модификатор |
|||||
|
{ X += a; |
у += b; |
} |
|
|
|
|
|
|
|
||||
void move ( i n t a, |
int |
b) |
|
|
|
|
|
|
// функция-модификатор |
|||||
|
{ X += a; у += b; } |
|
|
|
|
|
|
|
||||||
void get (int& a, int& b) const |
|
|
|
|
// функция-селектор |
|||||||||
|
{ a = x; b = y; } } |
; |
|
|
|
|
|
|
||||||
class Rectangle { |
|
|
|
|
|
|
|
|
// верхний левый и нижний правый угол |
|||||
Point pt1, |
pt2; |
|
|
|
|
|
|
|
|
|||||
int thickness |
|
|
|
|
|
|
|
|
|
// толщина границы прямоугольника |
||||
public: |
|
|
|
|
|
|
|
|
|
|
int width=1); |
|
||
Rectangle (const Point& p1, const Point& p2, |
|
|||||||||||||
void move(int a, intb); |
|
|
|
|
|
|
// перемещение обеих точек |
|||||||
bool pointIn(const Point& pt) const; |
|
|
|
// точка в прямоугольнике? |
||||||||||
} ; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rectangle::Rectangle(const Point& р1, const Point p2, int width) |
||||||||||||||
{ pt1 = p1; pt2 = p2; thickness = width; } |
|
|
// установка значений данных |
|||||||||||
void Rectangle::move(int a, int b) |
|
|
|
|
// передача работы в Point |
|||||||||
{ pt1.move(a,b); pt2.move(a,b); } |
|
|
|
|||||||||||
bool Rectangle:.:pointIn(const Point& pt) const |
|
// точка внутри? |
||||||||||||
{ int X, y,x1,y1,x2, у2; |
|
|
|
|
|
|
|
// координаты pt углов |
||||||
pt.get(x,y); |
|
|
|
|
|
|
|
|
|
// получение координат параметра |
||||
pt1.get(x1,y1); pt2.get(x2,у2); |
|
|
|
|
// получение данных угловых точек |
|||||||||
bool xIsBetweenBorders = (xKx |
&& x<x2 |
| |(x2<x Лх<х1); |
|
|||||||||||
bool ylsBetweenBorders = (y>y1 &&у<у2) | |(y<y1 &&у>у2); |
|
|||||||||||||
return (xIsBetweenBorders && ylsBetweenBorders); } |
|
|||||||||||||
int mainO |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
// левый верхний, правый нижний углы |
|
Point р1(20,40), р2(70,90); |
|
|
|
|
|
|
||||||||
Point point(100,120); |
|
|
|
|
|
|
|
// точка для отлова в прямоугольнике |
||||||
Rectangle |
rec(p1,p2,4); |
|
|
|
|
|
|
|
// напрасные вызовы конструктора |
|||||
point.move(-25,-15); |
|
|
|
|
|
|
|
|
// перемещение точки по экрану |
|||||
rec.move(10,20); |
|
|
« |
|
|
|
|
|
// на 10пикселей вправо, на 20 - вниз |
|||||
if (rec.pointln(point)) cout |
|
Точка внутри\п" |
// точка внутри? |
|||||||||||
return 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
Глава 12 • Преимущества и недостатки составных классов |
с503 I |
Как видно из примера, список инициализации располагается на отдельной строке. Это распространенный вариант синтаксиса. При использовании такого расши ренного инициализатора возникает непростая ситуация. К моменту выполнения тела конструктора делать уже ничего не нужно. Вот почему тело конструктора здесь пустое. Однако оно все равно должно присутствовать, поскольку тело функции опускать нельзя. Инициализация отдельных элементов данных в списке инициализации не дает никаких преимуществ, но по некоторым причинам пустое тело конструктора — популярный прием среди программистов, использующих язык C-f- + .
Список инициализации может создать впечатление, что элемент данных thick ness инициализируется перед pt1 и pt2. Это не так. Не важно, в каком порядке следуют элементы в списке инициализации. Они получают значение в обратном порядке их следования в спецификации класса. Это тот случай, когда внешность обманчива.
А) |
20 |
|
Point pi(20,40); |
Р2 |
70 |
Point р1 (70,90); |
|
р1 |
40 |
1 |
90 |
||||
|
|
|
|
|
|||
В) |
20 |
|
а) размещение объекта pt1 |
|
|
|
|
pt1 |
40 |
|
б) вызов конструктора копирования |
|
|
|
|
|
|
c) размещение объекта pt2 |
|
|
|
||
|
70 |
|
|
|
|
||
pt2 |
|
d) вызов конструктора копирования |
|
|
|
||
90 |
|
|
|
|
|||
|
|
e) создание thickness |
|
|
|
||
thickness |
^1 |
|
|
|
|||
f) инициализация thickness (width) |
|
|
|
||||
С) |
20 |
1 |
{} |
|
|
Выполнение |
|
pt1 |
40 |
|
|
|
|
конструктора Rectangle |
|
|
|
|
|
|
|||
pt2 |
70 |
|
|
|
|
|
|
90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
thickness |
4 |
1 |
|
|
|
|
|
Рис. 12.3. Шаги |
создания |
объекта Rectangle со |
списком |
|
|
||
|
инициализации |
элементов |
|
|
|
На рис. 12.3 показана последовательность событий, происходящих при созда нии объекта со списком инициализации в данном примере.
|
|
|
Point р1(20,40), р2(70,90) |
// верхний левый и нижний правый углы |
||
|
|
|
Rectangle гес(р1,р2,4); |
// ненужен конструктор |
Point по умолчанию |
|
|
|
|
После выделения памяти для р1 и р2 создается объект |
гее типа Rectangle. |
||
|
|
|
Сначала задается элемент данных pt1, затем для него объектом р1 в аргументе |
|||
|
|
|
вызывается конструктор копирования Point. В результате элемент данных pt1 |
|||
|
|
|
|
инициализируется: х равно 20, у — 40. Далее создается элемент данных |
||
Создан: х=20 |
у=40 |
|
|
pt2. Для него вызывается конструктор копирования Point с передачей |
||
Создан: х=70 |
у=90 |
|
|
объекта р2 в аргументе. В результате инициализируется элемент данных |
||
Создан: х=100 |
у=120 |
|
pt2: X равно 70, у — 90. |
После этого создается |
и инициализируется |
|
Скопирован: х=20 |
у=40 |
|
||||
Скопирован: х=70 |
у=90 |
|
значением 4 элемент данных thickness. Лишние вызовы используемого |
|||
Точка внутри |
|
|
|
по умолчанию конструктора класса Point исчезли. |
|
|
Рис. 12.4. |
|
|
|
В листинге 12.2 показана такая же программа, что и в листинге 12.1, |
||
выполнения |
но с измененной архитектурой класса Rectangle. Однако эта программа |
|||||
Результат, |
реализует конструктор Rectangle со списком инициализации элементов. |
|||||
программы |
|
|
|
|||
|
|
|
Результат выполнения данной программы показан на рис. 12.4. |
|||
из листпинга 12.2 |
|
504 |
JIN Программирование с агрегированием и наследованием |
Листинг 12.2. Создание составного объекта без лишних вызовов конструкторов
#inclucle <iostream> |
|
|
|
|
|
|
|
|
||||
using |
namespace std; |
|
|
|
|
|
|
|
|
|||
class |
Point |
{ |
|
|
|
|
|
|
|
|
|
|
|
private: |
|
|
|
|
|
|
|
|
// закрытые |
координаты |
|
|
int X, |
y; |
|
|
|
|
|
|
|
|||
|
public: |
|
|
|
|
|
|
|
|
// общий конструктор |
||
|
Point |
(int a=0, |
b=0) |
|
|
|
|
|
||||
|
{ |
X = a; у = b; |
|
|
|
|
" y=" « у « |
endl; } |
|
|||
|
|
cout |
« |
"Создан: x= " « |
x « |
|
||||||
|
Point |
(const Point& |
pt) |
|
|
|
|
// конструктор копирования |
||||
|
{ |
X = pt.x; у = |
pt.y; |
|
|
|
x « " у=" « |
у « endl; } |
|
|||
|
|
cout |
« |
"Скопирован: x= " |
« |
присваивания |
||||||
|
void operator = (const Point& |
pt) |
|
// операция |
||||||||
|
{ |
X = pt.x; у = |
pt.y; |
|
|
|
|
|
|
|
||
|
|
cout |
« |
"Присвоен: x= " |
« |
x « " y=•" « |
у « endl; } |
|
||||
|
void set |
(int a, |
int |
b) |
|
|
|
|
// функция-модификатор |
|||
|
|
{ X = a; |
у = b; |
} |
|
|
|
|
|
|||
|
void move (int a, int |
b) |
|
|
|
|
// функция-модификатор |
|||||
|
|
{ X +=a; у +=b; } |
|
|
|
|
|
|||||
|
void get (int& a, int& b) const |
|
|
// функция-селектор |
||||||||
|
|
{ a = x; b = y; } } |
; |
|
|
|
|
|||||
class Rectangle { |
|
|
|
|
|
|
// верхний левый и нижний правый угол |
|||||
|
Point pt1, |
pt2; |
|
|
|
|
|
|
||||
int thickness |
|
|
|
|
|
|
// толщина |
границы прямоугольника |
||||
public: |
|
|
|
|
|
|
|
int width=1); |
|
|||
Rectangle (const Point& p1, const Point& p2, |
|
|||||||||||
void move(int a, intb); |
|
|
|
|
// перемещение обеих точек |
|||||||
bool pointIn(const |
Point& pt) const; |
|
// точка в прямоугольнике? |
|||||||||
|
} ; |
|
|
|
|
|
|
|
|
|
|
|
Rectangle::Rectangle(const Point& p1, const Point& p2, int w) |
|
|||||||||||
: thickness(w), pt1(p1), pt2(p2) |
|
|
|
// список инициализации |
||||||||
{ } |
|
|
|
|
|
|
|
|
|
// пустое тело конструктора |
||
void Rectangle::move(int a, int b) |
|
|
// передача работы в Point |
|||||||||
{ pt1.move(a,b); pt2.move(a,b); } |
|
|||||||||||
bool Rectangle::pointIn(const |
Point& pt) const |
|
// точка внутри? |
|||||||||
{ |
int X,y,x1,y1,x2,y2; |
|
|
|
|
|
// координаты pt углов |
|||||
|
pt.get(x,y); |
|
|
|
|
|
|
// получение координат параметра |
||||
- pt1.get(x1,y1); pt2.get(x2,y2); |
|
|
// получение данных угловых точек |
|||||||||
|
bool xIsBetweenBorders = (хКх &&х<х2 | |(х2<х &&х<х1); |
|
||||||||||
|
bool ylsBetweenBorders = (у>у1 &&у<у2) | |(у<у1 &&у>у2); |
|
||||||||||
|
return (xIsBetweenBorders &&ylsBetweenBorders); } |
|
||||||||||
int mainO |
|
|
|
|
|
|
|
|
// левый верхний, правый нижний углы |
|||
{ |
Point p1(20,40), p2(70,90); |
|
|
|
|
|||||||
Point point(100,120); |
|
|
|
|
|
// точка для отлова в прямоугольнике |
||||||
Rectangle |
rec(p1,p2,4); |
|
|
|
|
|
// напрасные вызовы конструктора |
|||||
point.move(-25,-15); |
|
|
|
|
|
// перемещение точки по экрану |
||||||
rec.move(10,20); |
|
|
|
|
|
|
// на 10пикселей вправо, на 20 - вниз |
|||||
if (rec.pointln(point)) cout << "Точка внутри\п"; |
// точка внутри? |
|||||||||||
return 0; |
|
|
|
|
|
|
|
|
|
|
506 |
Чость III * Программирование с агрегированием и наследованием |
Обратите внимание, что применение литеральных значений в списке инициа лизации представляет собой вид переноса обязанностей с клиента на серверный класс. В данной версии программист класса Rectangle определяет толщину линии, равную единице. Можно заменить литеральное значение дополнительным пара метром, как в следующей версии конструктора:
Rectangle: :Rectangle (int х1, int у1, |
int х2, |
int |
у2), int width) |
: pt1 (x1,y1), pt2(x2,y2), thickness |
(width) |
{ |
} |
В таком случае задавать толщину линии, равную единице, должен клиент.
Rectangle г(20,40,70,90,1); |
// обязанности переносятся на клиента |
Третий конструктор класса Rectangle, конструктор по умолчанию, также демонстрирует использование литералов в списке аргументов. При применении этого конструктора каждый объект Rectangle инициализируется таким образом, что его верхним левым углом будет точка с координатами (100,100). Конструктор по умолчанию не получает от клиента никаких данных. Таким образом, объекты, инициализируемые используемым по умолчанию конструктором, устанавливаются в одинаковое состояние.
Класс Point предусматривает конструктор по умолчанию (с нулевыми значе ниями координат), поэтому список инициализации Rectangle для него можно записать следующим образом:
Rectangle::Rectangle () : pt1(), pt2(100,100), thickness(l) { }
Что произойдет, если вы пропустите список инициализации для элементов данных pt1? Вспомните: назначение списка инициализации элементов состоит в том, чтобы избежать вызова используемого по умолчанию конструктора класса компонента перед вызовом конструктора составного класса. Список инициализа ции элементов заменяет вызов применяемого по умолчанию конструктора класса компонента на вызов конструктора, заданного в списке инициализации.
Таким образом, в данном примере конструктора Rectangle вы пытаетесь избе жать вызова для компонента pt1 используемого по умолчанию конструктора Point и заменить его на вызов конструктора по умолчанию для компонента pt 1! Следова тельно, вызовы конструкторов по умолчанию в списке инициализации компонен тов можно опустить. Это не повлияет на последовательность событий при создании объекта составного класса. Последнюю версию конструктора Rectangle можно переписать так:
Rectangle::Rectangle () : pt2(100,100), thickness(l) { } |
/ / то же самое |
Синтаксис списка инициализации выглядит странно. Похоже, ничего подобно го вам раньше не встречалось. У многих программистов при изучении такого син таксиса возникают сложности.
С о в е т у е м изучите синтаксис списка инициализации. Это очень полезно и позволяет инициализировать компоненты составных объектов
без лишних вызовов конструкторов. Такой метод весьма популярен в C++. Позднее будут приведены примеры его использования в наследовании. Чем скорее вы его изучите, тем лучше для вас.
Вы можете изучать этот синтаксис постепенно. Во-первых, вам надо избежать синтаксических ошибок, когда отсутствует используемый по умолчанию конструк тор компонента. Во-вторых, вам необходимо избежать отрицательного влияния на производительность, когда в конструкторе составного класса состояние компонен та сбрасывается. Таким образом, целью является предотвраш^ение синтаксических ошибок и повышение производительности. Синтаксических ошибок можно избе жать, используя в классе компонента конструктор по умолчанию. Для констант и ссылочных элементов данных список инициализации обязателен.
Глава 12 • Преимущества и недостатки составных классов |
507 |
Элементы данных со специальными свойствами
Возможно, прежде вы никогда не думали об использовании констант и ссылоч ных элементов данных. Когда в конце главы 8 перечислялись вопросы, которые удается разрешить в C + + с помощью классов, говорилось о связывании данных и операций, введении области действия класса для имен элементов данных и функ ций-членов, управлении доступом к компонентам классов, переносе обязанностей с клиентов на серверы.
Все это направлено на поддержку принципов объектно-ориентированного про граммирования. В главах 9—11 говорилось о других целях применения классов, которые также стали частью программирования: автоматическая инициализация объектов, управление динамической памятью, одинаковая интерпретация объек тов и переменных встроенных типов. Теперь перейдем к определению элементов данных как констант или ссылок.
Константы как элементы данных
Идея констант или постоянных элементов данных проста, В классе C+ + увязываются вместе элементы данных и функции. Функции реализуют доступ к элементам данных от лица клиента. Часто клиенту требуется изменить состояние объекта (например, сумму на счете, адрес сотрудника, цену за прокат видеофильма и т.д.). Однако некоторые характеристики объекта не предусматривают измене ний (например, номер счета, дата рождения сотрудника, сумма, выплаченная за прокат видеофильма).
Разработчик класса знает, что функции-члены класса не изменяют отдельные элементы данных. Программисту, сопровождающему приложение, придется по нять это, изучив функции-члены и "дружественные" функции класса. Было бы неплохо явно указать замыслы разработчика в классе: значение этого элемента данных не изменяется ни одной функцией-членом или "дружественной" функцией. Вот еще одна работа для ключевого слова const.
Как инициализировать такой компонент-константу? Делать это в конструкторе класса, вероятно, слишком поздно. Вспомните схему на рис. 12.1. Конструктор класса вызывается только после того, как объект уже создан. Для компонентовконстант присваивание не допускается. Постоянный элемент данных должен инициализироваться немед/1енно после создания этого компонента, поэтому C + + требует включать имя компонента-константы в список инициализации. Любое присваивание этому компоненту он помечает как синтаксическую ошибку.
Постоянный элемент данных может иметь тип, определяемый программистом, или встроенный тип. Это не важно для инициализации после создания элементов данных, перед вызовом конструктора.
В качестве примера применения компонента-константы добавим к классу Rectangle дополнительный элемент данных, показывающий вес единицы объема прямоугольника. Материал, из которого изготовлен прямоугольник, не меняется за время существования прямоугольника, и этот элемент данных не будет моди фицироваться после создания объекта Rectangle. Чтобы сопровождающий при ложение программист не перебирал в подтверждение данного факта все функциичлены и "дружественные" функции класса, элемент данных weight надо опреде лить как константу. Следовательно, его необходимо инициализировать в списке инициализации конструктора Rectangle.
class Rectangle { |
|
Point pt1; |
|
Point pt2; |
|
int thickness; |
|
const double weight; |
/ / вес единицы объема многоугольника |
508 |
Часть III • Прогрог^иутровоние с arpemposaHHeivi и наследованиерл |
||
|
public: |
|
|
|
Rectangle (const Point& |
p1, const Point& p2, double wt, int width = 1 ) ; |
|
|
void move(int a, int b); |
|
|
|
void |
setThickness(int w=1); |
|
|
int |
pointIn(const Point& |
pt) const; |
. . . . } ; |
/ / |
остальная |
часть класса Rectangle |
Rectangle::Rectangle(const Point& |
p1, const |
Point& p2, |
double wt, int width) |
: pt1(p1), pt2(p2), weight(wt) |
|
/ / вес здесь не обязателен |
|
{ thickness = width; } |
|
|
|
Обратите внимание, что дополнительный параметр добавлен не в конце списка параметров конструктора, а в середине. Вспомните правило, по которому значе ния по умолчанию допускаются только для самых правых параметров. Если доба вить четвертый параметр в конец списка (как в следуюш,ей строке), это правило нарушается.
Rectangle (const Point& р1, const Point& p2, int width = 1, double wt);
/ / неверно
Для клиента Rectangle не потребуется существенных изменений.
Point |
р1(20,40), р2(70,90); |
/ / |
левый верхний, |
правый нижний углы |
||
Point |
point(100,120); |
/ / |
точка для отлова в прямоугольнике |
|||
Rectangle гес(р1,р2,0.01,4); |
/ / |
напрасные вызовы конструктора |
||||
гее. setThicknessO; |
/ / |
толщина линии - |
1 пиксель (по умолчанию) |
|||
point.move(-25,-15); |
/ / |
перемещение точки по экрану |
|
|||
rec.move(10, 20); |
/ / |
на 10 пикселей |
вправо, |
на 20 |
- вниз |
|
i f (rec.pointln(point)) cout « |
"Точка внутри\п"; |
/ / |
точка |
внутри? |
||
p1.fTiove(30, 35); |
/ / |
изменяется ли объект Rectangle? |
Подобно другим случаям использования ключевого слова const, применение постоянных элементов данных необходимо, чтобы сделать исходный код более понятным. Если элементы данных не имеют ключевого слова const, это должно означать их модификацию во время существования объекта одной из связанных с классом функций. Похоже, что лишь немногие программисты придерживаются правил использования компонентов-констант. Следовательно, отсутствие ключе вого слова const в определении элементов данных не свидетельствует об их из менении. Это может говорить лишь о том, что программист был занят другими аспектами программы и не утруждал себя сообщением информации о своих за мыслах.
Ссылочные элементы данных
Рассмотрим пример применения ссылок на объекты, используемые как эле менты данных других объектов. Это хорошая программная реализация связи между объектами, где несколько клиентских объектов ассоциируются с одним серверным объектом.
Впредыдущих примерах данной главы каждый объект Rectangle имел соб ственную копию своих угловых точек. Если перемещается р1 (как в последней строке предыдущего примера), то объект гее типа Rectangle не изменяется. Во многих приложениях объекты так себя и ведут. В главе 11 был представлен по добный подход с использованием семантики значений.
Вдругих приложениях желательно совместно использовать угловые точки прямоугольников. Таким образом, если клиент перемещает угловую точку прямо угольника, он должен изменяться. В главе 11 такой подход поддерживался с по мощью семантики ссылок. Во многих программах, разработанных с применением