Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
267
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

500

Часть III • Программирование с агрегированием и наследованием

 

А)

20

Point p1 (20,40);

P2

70

Point p1 (70,90);

 

р1

40

90

 

 

 

 

 

В)

pt1

pt2 thickness

С)

pt1

pt2 thickness

Рис. 12.1.

0

a) Выделение памяти для объекта pt1

0

b) Вызов используемого по умолчанию конструктора Point

0

c) Выделение памяти для объекта pt2

0d) Вызов конструктора по умолчанию e) Создание thickness

20

1

a)pt1 =р1;

Выполнение

40

 

 

 

 

конструктора Rectangle

70

 

b) pt2 = р2;

 

90

 

 

 

 

 

 

4

1

c) thickness = width;

 

Шаги создания

объекта Rectangle

с помощью вызовов

конструктора

по умолчанию

 

Для объекта pt1 выделяется память. Вызывается конструктор по умолчанию Point, устанавливаюндий pt1. х и pt1. у в 0. Далее выделяется память для объекта pt2 и вызывается конструктор Point, устанавливающий pt2.x и pt2.y в 0. После этого выделяется и остается неинициализированной память для элемента данных thickness. Затем вызывается конструктор Rectangle. Когда выполняется тело конструктора, содержимое аргумента р1 сначала копируется в элемент данных pt1, затем конструктор копирует р2 и pt2, после этого thickness устанавливается

взначение 4.

Врезультате данной последовательности событий создается объект Rectangle, при этом pt 1.x устанавливается в 20, pt1 .у — в 40, pf2.x — в 70, pt2.y — в 90.

Значения, помещенные в pt1 и pt2 конструктором Point, оказались недолго­ вечными. Они были перезаписаны данными из р1 и р2 при вызове конструктора Rectangle. Два конструктора по умолчанию для двух элементов данных отработали напрасно. Какие чувства это у вас вызывает? Негодование? Сожаление по поводу напрасно потраченных при создании объекта Rectangle миллисекунд? Хорошо. Теперь вы знаете, что такое неверное программирование на C+ + .

Внимание Создание экземпляров объектов в C++ всегда влечет за собой вызов функции — конструктора класса. При задании составных объектов в C++ вызывается несколько конструкторов. Конструктор вызывается сразу после создания каждого элемента данных. Нужно научиться видеть эти вызовы конструкторов в любой программе C++.

Создан: х=20 у=40 Создан: х=70 у=90 Создан: х=100 у=120 Создан: х=0 у=0 Создан: х=0 у=0 Присвоен: х=20 у=40 Присвоен: х=70 у=90 Точка внутри

Рис. 12.2.

Результат, выполнения программы из листинга 12.1

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

Листинг 12.1 показывает реализацию составного класса Rectangle и класса-компонента Point с тестовой программой. Чтобы облег­ чить анализ результатов, к классу Point добавлены конструктор копирования и перегруженная операция присваивания с отладоч­ ными сообщениями, позволяющими отследить процесс создания составного объекта Rectangle. Результат выполнения данной про­ граммы представлен на рис. 12.2.

Глава 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;

 

 

 

 

 

 

 

 

 

 

 

 

 

502 Часть III • Программирование с агрегированием и наследованием

Первые три сообщения "Создан" на рис. 12.2 отражают создание объектов р1

ир2 типа Point и объекта Point. Следуюш^ие два сообщения "Создан" означают создание объекта Rectangle: первое описывает создание элементов данных pt1

ивызов конструктора по умолчанию Point, а второе — создание элемента дан­ ных pt2 и вызов используемого по умолчанию конструктора Point. Два сообщения "Присвоен" описывают выполнение конструктора Rectangle после завершения создания объекта. Первое сообщение соответствует первому присваиванию в теле конструктора, а второе — второму.

Это типичная картина создания составного объекта в С+ + . Для крупных со­ ставных объектов данный процесс может быть сложным и связанным с большим числом лишних вызовов конструкторов. Конечно, вы не сможете исключить вызовы конструкторов, активизируемые сразу после создания каждого элемента данных. В C + + не может быть создан объект, за которым сразу не следует вызов конструктора. Однако надо стараться вызывать такой конструктор, который не будет действовать после выполнения конструктора составного объекта.

Использование списка инициализации элементов

Язык C + + позволяет использовать список инициализации элементов в конструкторе составного класса. В нем применяется не вполне обычный синтак­ сис, основанный на включении списка между заголовком и телом конструктора. Вот как выглядит инициализация элементов:

class Rectangle {

 

 

 

 

 

Point pt1, pt2;

 

 

/ /

верхний левый,

нижний правый

int thickness;

 

 

 

 

 

public-

 

 

 

 

 

Rectangle (const

Point& p1, const

Point& p2, int w = 1);

 

. . . . } ;

 

 

/ /

остальная часть

класса Rectangle

Rectangle::Rectangle(const

Point& p1,

 

 

const Point&

p2, int

w) : pt1(p1),pt2(p2)

 

{ thickness = w;

}

 

/ /

это намного лучше!

Список инициализации помещается между закрывающей круглой скобкой списка параметров конструктора и открывающей фигурной скобкой тела конст­ руктора. Он открывается двоеточием и перечисляет имена (а не типы) элементов данных. После каждого имени элемента в круглых скобках указывается соответст­ вующее значение (значения), используемое для инициализации данного объекта — элемента данных. Список не имеет терминатора. Он заканчивается открывающей фигурной скобкой тела класса. Каждая запись в списке аналогична вызову конст­ руктора в определении переменной, например pt1(p1).

Заметим, что список инициализации элементов применяется только к реализа­ ции конструктора. Не путайте список инициализации со значениями параметров по умолчанию. Синтаксис значений по умолчанию применяется только к прото­ типам и не влияет на способ записи реализации конструктора.

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

Фактически любые элементы данных могут инициализироваться в списке. Кон­ структор класса Rectangle, где все элементы данных инициализируются в списке:

Rectangle::Rectangle(const Point& р1, const Point& p2, int w)

:

thickness(w), pt1(p1), pt2(p2)

/ / на отдельной строке

{

}

/ / пустое тело: популярная идиома в C++

Глава 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;

 

 

 

 

 

 

 

 

 

 

Глава 12 • Преимущества и недостатки составных классов

505 1

Первые три сообндения "Создан" на рис. 12.4 соответствуют первым трем со­ общениям на рис. 12.2. Они показывают процесс создания трех объектов Point в функции main О. Следующие два сообщения "Скопирован" демонстрируют про­ цесс создания объекта Rectangle. Первое сообщение появляется, когда после создания элемента данных pt1 вызывается конструктор копирования. Второе сообщение выводится, когда конструктор копирования Point задается после со­ здания элемента данных pt1. Как видно, вызывается именно конструктор копиро­ вания Point, а не конструктор по умолчанию. В результате операция присваивания Point не вызывается при вызове конструктора Rectangle. Тело этого конструктора пустое.

В этих примерах составной класс имел только один конструктор. Если бы конструкторов было несколько, каждый из них создавался бы одинаково. При со­ здании составного объекта сначала задаются его элементы данных. Какой именно конструктор класса вызывается в конце, зависит от числа и типов аргументов, подставляемых клиентом для составного объекта. Если составной класс имеет такой конструктор — замечательно. Если же нет, то при инициализации объекта будет синтаксическая ошибка. Когда вызываемый в итоге конструктор не имеет списка инициализации элементов, за созданием каждого из них следует вызов используемого по умолчанию конструктора класса, к которому принадлежит эле­ мент. Если конструктор составного класса имеет список инициализации, для каж­ дого элемента в нем вызывается соответствующий конструктор класса компонента.

Списки инициализации, реализуемые разными конструкторами, могут разли­ чаться. Вот еще одна версия класса Rectangle, совмещающая три конструктора: общий конструктор, использовавшийся в предыдущих примерах, общий конструк­ тор с четырьмя параметрами (координатами двух точек) и конструктор по умолча­ нию. Каждый конструктор имеет свой собственный список инициализации. Эти списки не обязаны быть одинаковыми.

class Rectangle {

 

 

 

 

 

 

Point pt1, pt2;

 

/ /

верхний левый,

нижний правый

int thickness;

 

 

 

 

 

 

public:

 

 

 

 

 

 

Rectangle (const Point& p1, const Point& p2, int

w = 1);

 

Rectangle (int x1, int y1, int x2, int у2);

 

 

 

Rectangle ();

 

 

 

 

 

 

. . . . } ;

 

/ /

остальная

часть класса Rectangle

Rectangle::Rectangle(const

Point& p1, const

Point& p2,

int

w)

: thickness(w), pt1(p1), pt2(p2) { }

 

 

 

 

Rectangle: :Rectangle

(int

x1, int y1, int x2, int

у2)

 

 

: pt1(x1,y1), pt2(x2,y2), thickness (1) { }

 

 

 

Rectangle::Rectangle

0 : pt1(0,0), pt2(100,100),

thickness(l)

{ }

 

 

 

 

 

 

Первый конструктор демонстрирует, как можно передавать параметры конст­ руктору составного класса для подстановки параметров конструктора класса ком­ понента. В этом примере параметр р1 типа Point перенаправляется как аргумент конструктору копирования для инициализации элемента данных pt 1, а параметр р2 типа Point передается как аргумент конструктору копирования для инициализации элемента данных pt2. Последний параметр конструктора применяется для инициа­ лизации элемента данных типа int.

Второй конструктор показывает, что список инициализации элементов данных не ограничивается использованием параметров, подставляемых клиентом. Здесь клиент подставляет лишь данные, которые используются для вызова общего кон­ структора Point. Значение, инициализирующее элемент данных thickness, опре­ деляется как литеральная константа.

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 такой подход поддерживался с по­ мощью семантики ссылок. Во многих программах, разработанных с применением

Глава 12 • Преимущества и недостатки составных классов

509

объектно-ориентированной методологии, семантика ссылок служит для реализации связей между объектами. Например, данные о владельце счета (фамилия, адрес, номер социального страхования и т. д.) могут быть частью класса Account (счет). Если в приложении необходим класс Owner (владелец), эти данные можно ском­ бинировать в класс и использовать объект Owner как элемент данных класса Account. Если владелец имеет несколько счетов, то в приложении можно исполь­ зовать ддя них только один объект Owner. Тогда изменения в данных владельца автоматически распространяются на все счета.

Для поддержки такой функциональности в клиенте можно применять ссылоч­ ные элементы данных. Эти ссылки обозначают объекты вне составного объекта. Такие внешние объекты могут модифицироваться в клиенте без знания составного

объекта.

 

Как уже говорилось выше, все ссылки в C-fH

это константы. Они не могут

изменяться после инициализации. Следовательно, подобно постоянным элементам данных, ссылочные элементы должны инициализироваться только в своем списке. Никакая инициализация в теле конструктора не допускается. Там ее делать слиш­ ком поздно, так как конструктор вызывается после создания всех элементов дан­ ных и вызовов их конструкторов. Новая архитектура класса Rectangle подобна предыдуш,ей версии. Единственная разница состоит в двух знаках амперсанда пос­ ле типа Point в определениях элементов данных.

class Rectangle {

 

 

 

 

 

 

Point&

p t i ;

 

 

/ /

точки могут использоваться

Point&

pt2;

 

 

/ /

совместно с другими фигурами

int

thickness;

 

 

 

 

 

 

const int

weight;

 

/ /

вес единицы объема многоугольника

public:

 

 

 

 

 

 

 

 

Rectangle

(const

Point& p1, const Point& p2,

 

 

 

 

 

 

int wid = 1. int wt

= 1);

 

 

void nrtove(int a, int b);

 

 

 

 

void

setThickness(int

w=1);

 

 

 

 

int

point(const

Point

&pt) const;

 

 

 

 

. . . . } ;

 

 

 

/ /

остальная часть

класса

Rectangle

Rectangle::Rectangle(const

Point& p1, const Point& p2, int

width,

int wt)

: pt1(p1),

pt2(p2), weight(wt)

/ /

здесь это не обязательно

{ thickness

= width;

}

 

/ /

тот же конструктор, что и выше

Все ссылки — это константы, поэтому данное свойство специально обозначать не нужно. В классе Rectangle компоненты pt1 и pt2 являются ссылками-констан­ тами. Их нельзя "отсоединить" от объектов, на которые они ссылаются, и пере-

. назначить на другие объекты, однако сами объекты константами не являются. Т^х содержимое может изменяться.

^-—У Подобно объектам, нЗ которые ссылаются указатели, объекты по ссылкам также могут определяться как константы. Это означает, что pt1 и pt2 не только обозначают одни и те же объекты Point и не могут переключаться на другие объекты Point, но и сами эти объекты не меняются и сохраняют свое состояние.

class Rectangle {

 

 

const

Point&

pt1;

/ /

точки могут использоваться

 

 

 

/ /

совместно с другими фигурами

const

Point&

pt2;

/ /

точки не могут изменять свои координаты

. . .

. } ;

 

/ /

остальная часть класса Rectangle

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

Соседние файлы в предмете Программирование на C++