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

2 семестр / Литература / Язык программирования С++. Краткий курс. Страуструп

.pdf
Скачиваний:
9
Добавлен:
16.07.2023
Размер:
31.34 Mб
Скачать

90

Глава

4.

Классы

Стрелки

указывают

отношение

наследования.

Например,

класс

Circle

яв­

ляется

производным

от класса

Shape.

Иерархия

классов

обычно

изображает­

ся

растущей

вниз

от

"самого

базового"

класса

-

от

корня

к

(определяемым

позже)

производным

классам.

Для

представления

такой

простой

диаграммы

в

коде

сначала

нужно

создать

класс,

который

определяет

общие

свойства

всех

геометрических

фигур:

class

Shape

{ puЫic:

virtual virtual virtual virtual virtual 11 ...

};

Point

center()

const

=О;

11

void

move(Point

to)

=О;

 

 

void

draw ()

const =

О;

 

//

void

rotate(int

angle)

=О;

-Shape ()

{}

 

 

 

 

/ /

Чисто

виртуальная

 

Вывод

на текущем

"холсте"

Деструктор

 

Естественно,

этот

интерфейс

является

абстрактным

классом;

что

касается

представления,

то

ничто

(кроме

местоположения

указателя

на

vtЫ)

не

яв­

ляется

общим

для

каждого

Shape.

В

этом

определении

мы

можем

написать

только

общие

функции,

управляющие

векторами

указателей

на фигуры:

//

Поворот

элементов

v

на

угол

angle

градусов:

void {

rotate_all(vector<Shape*>&

v,

int

angle)

for

(auto р : v) p->rotate(angle);

Чтобы определить конкретную фигуру, мы должны указать, что это Shape, определить ее конкретные свойства (включая виртуальные функции):

и

class Circle

: puЬlic Shape

{

 

 

puЬlic:

 

 

Circle(Point р,

int rad);

/ /

Конструктор

Point

center()

{

 

 

return

х;

const

override

void

move(Point

(

 

 

х

=

to;

to)

override

void void

draw()

const

rotate(int)

override; override (}

//

Простой

алгоритм

4.5.

Иерархии

классов

91

private:

 

 

Point

х;

int

r;

 

} ;

 

 

// 11

Центр Радиус

Пока что пример Shape и Circle не дает ничего нового по сравнению

мером Container и Vector_container, но мы можем продолжить:

с

при­

11 Используем

 

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

базовый

класс

для

лица:

class Smiley

:

puЫic Circle

 

 

 

 

{

 

 

 

 

puЫic:

 

 

 

 

Smiley(Point

р,

int

rad)

-Smiley ()

 

 

 

 

{

 

 

 

 

delete mouth;

 

 

for (auto

р

:

eyes)

delete

р;

 

 

Circle{p,r},

mouth{nullptr}

{

}

void void void

move(Point

to)

draw()

const

rotate(int)

 

override; override; override;

void

add_eye(Shape*

s)

eyes.push_back(s);

void set_mouth(Shape* virtual void wink(int

11 ...

 

private:

 

vector<Shape*>

eyes;

Shape* mouth;

 

} ;

 

s); i);

//Подмигивание

i-м

глазом

//

Обычно два

глаза

Функция-член

push_

back

()

класса

vector

копирует

свой

аргумент

в

vector

(здесь

-

eyes)

в

качестве

последнего

элемента,

увеличивая

размер

вектора

на единицу.

 

 

Теперь мы

можем определить Smiley:: draw

вов члена draw ()

базового класса и членов:

()

с

использованием

вызо­

void

Smiley::draw()

const

{

Circle:: draw ();

for

(auto р :

eyes)

 

p->draw();

 

mouth->draw();

 

92

Глава

4.

Классы

Обратите

внимание

на

то,

как

Smiley

хранит

глаза

в

vector

стандартной

библиотеки

и

удаляет их

в

своем

деструкторе.

Деструктор

Shape

является

виртуальным,

а

деструктор

Smiley

перекрывает

его.

Виртуальный

деструк­

тор

необходим

для

абстрактного

класса,

потому

что

объект

производного

класса

обычно

обрабатывается

через

интерфейс,

предоставляемый

его

аб­

страктным

базовым

классом.

В

частности,

он

может

быть удален с

помощью

указателя

на базовый

класс,

и

в

этом

случае

механизм

вызова

виртуальной

функции

гарантирует,

что

будет

вызван

правильный

деструктор.

Этот

де­

структор

затем

неявно

вызывает

деструкторы

его

базовых

классов

и

членов.

В

этом

упрощенном

примере

задача

программиста

-

поместить

глаза

и

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

и

то

и

другое,

по­

скольку

мы

определяем

новый

класс

путем

наследования.

Это

дает

большую

гибкость

-

вместе

с

соответствующими

возможностями

для

путаницы

и пло­

хого

дизайна.

4.5.1.

Преимущества

иерархий

Иерархия

классов

предоставляет

две

разновидности

преимуществ.

Наследование интерфейса. Объект производного класса можно исполь­

зовать

везде, где

требуется

объект

базового класса. То есть базовый

класс

действует в

качестве

интерфейса

для производного класса. При­

мерами являются

классы Container и

Shape. Такие классы часто яв­

ляются абстрактными.

 

 

 

Наследование реализации. Базовый

класс предоставляет функции или

данные, что упрощает реализацию

производных классов. Примерами

являются использование классом Smiley конструктора Circle и функ­

ции-члена Circle:: draw ().Такие базовые классы часто имеют члены

данных и конструкторы.

 

 

 

Конкретные

классы

-

особенно

с

небольшими

представлениями

--

очень

похожи

на

встроенные

типы:

мы

определяем

их

как

локальные

переменные,

обращаемся

к

ним

с

помощью

имен,

копируем

их

и

т.д.

Классы

в

иерархии

классов

различаются

-

обычно

для

них

выделяется

память

с

использовани­

ем

оператора

new,

и

доступ

к

ним

выполняется

через

указатели

или

ссылки.

Например,

рассмотрим

функцию,

которая

считывает

данные,

описывающие

фигуры,

из

входного

потока

и

создает

соответствующие

объекты

Shape:

enum

class

Kind

{

circle,

triangle,

smiley

);

11 Чтение описаний фигур из

входного

Shape* read_shape(istream&

is)

потока

is:

{

 

 

 

 

 

 

 

 

4.5. Иерархии классов

11 ...

Чтение

заголовка фигуры

из

is и поиск

его

Kind k ...

switch

(k)

 

 

 

 

 

 

 

case

Kind::circle:

 

 

 

 

 

 

//

Чтение

данных

окружности

{Point,int}

ври

r

 

return

new

Circle{p,rl;

 

 

 

 

case

Kind::triangle:

 

 

 

 

 

 

//

Чтение

вершин

треугольника

{Point,Point,Point}

 

/ /

в pl, р2 и рЗ

 

 

 

 

 

 

return

new

Triangle{pl,p2,p3);

 

 

 

case

Kind::smiley:

 

 

 

 

 

 

//

Чтение

данных

смайлика {Point,int,Shape,Shape,Shape}

 

11

в р,

r,

el, е2

и т

 

 

 

 

 

Smiley*

ps

= new

Smiley{p,r);

 

 

 

 

ps->add_eye(ell;

 

 

 

 

 

 

ps->add_eye(e2);

 

 

 

 

 

 

ps->set_mouth(ml;

 

 

 

 

 

 

return

ps;

 

 

 

 

 

 

93

Программа

может

использовать

функцию

чтения

фигуры

следующим

образом:

void {

user

()

std::vector<Shape*>

v;

while

(cin)

 

v.push_back(read_shape(cin));

draw_all (v); rotate_all(v,45);

/ / Вызов //Вызов

draw(}

каждого элемента

rotate(45)

каждого элемента

for(auto р delete

:

v)

р;

 

11

Не

забудьте

удалить

элементы

Очевидно,

что

пример

упрощен

(особенно

в

отношении

обработки

оши­

бок),

но он

наглядно

иллюстрирует,

что

user

()

не

имеет

абсолютно

никакого

представления

о

том,

с

какими

видами

фигур

он

работает.

Код

user

()

мо­

жет

быть

скомпилирован

один

раз

и

позже

использоваться

для

новых

видов

фигур, добавленных

в

программу.

Обратите

внимание,

что

нет

никаких

ука­

зателей

на

фигуры

вне

user

(),

поэтому

за

освобождение

объектов

отвеча­

ет

сама

функция.

Это

делается

с

помощью

оператора

delete,

полагаясь

при

этом

на

виртуальный

деструктор

Shape.

Поскольку

деструктор

является

вир­

туальным,

delete

вызывает

деструктор

для

наиболее

позднего

производного

класса.

Это

имеет решающее

значение,

поскольку

производный

класс

может

захватывать

всевозможные

ресурсы

(такие,

как

дескрипторы

файлов,

бло­

кировки

и

выходные

потоки),

которые

должны

быть

освобождены.

В

нашем

94

Глава

4.

Классы

случае

Srniley

удаляет

свои

объекты eyes

и

rnouth.

Как

только

это

сделано,

вызывается

деструктор

Circle.

Объекты

строятся

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

"снизу

вверх"

(сначала

-

базовый),

а

уничтожаются

деструкторами

"сверху

вниз"

(сначала

-

производные).

4.5.2.

Навигация

по

иерархии

Функция

read_shape

()

возвращает

Shape*,

так что

мы

можем

рассма­

тривать

все

фигуры

единообразно.

Однако

что

нам

делать,

если

мы

захотим

использовать

функцию-член,

имеющуюся

только

в

определенном

произ­

водном

классе,

как,

например,

wink

()

в

классе

Srniley?

Мы

можем

запро­

сить,

"является

ли

данная

фигура

Srniley?",

воспользовавшись

оператором

dynarnic

cast:

Shape*

ps{read

if

(Smiley*p =

_shape(cin) ); dynamic_cast<Smiley*>(ps))

//

ps

указывает

на

Smiley?

//Да,

это

Smiley;

используем

его

else

11

Нет,

это

не

Smiley,

делаем

что-то

иное

Если

во

время

выполнения

объект,

на

который

указывает

аргумент

dynarnic

_

cast

данном

случае

-

ps)

не

имеет

ожидаемого

типа

данном

случае

-

Srniley)

или

типа

класса,

производного

от

ожидаемого,

то

dynarnic

_ cast

возвращает

nullptr.

Мы

используем

 

приведение dynarnic_ cast к типу

указателя,

когда аргу­

мент

представляет

собой корректный указатель. Затем

мы проверяем, явля­

ется

ли

результат

равным nullptr. Эту проверку часто удобно

помещать в

инициализацию

переменной

в

условии.

Мы можем использовать dynarnic_cast и для приведения типу. Если объект не относится к ожидаемому типу, dynarnic

к

ссылочному cast в этом

случае

генерирует

исключение

bad

_

cast:

Shape*

Smiley&

ps r

{read_shape(cin) ); {dynamic_cast<Smiley&>(*ps)

);

11 11

Может генерировать

исключение std: :bad

cast

Код

будет

более

чистым,

если

dynarnic

_

cast

используется

с

ограничения­

ми.

Если

мы

сможем

избежать

использования

информации

о

типе,

то

сможем

написать

более

простой

и

эффективный

код.

Но

иногда

информация

о

типе

теряется

и

должна

быть

восстановлена.

Обычно

это

происходит,

когда

мы

пе­

редаем

объект

какой-либо

системе,

которая

принимает

интерфейс,

определен­

ный

базовым

классом.

Когда

эта

система

позже

возвращает

объект

обратно,

96

Глава

4.

Классы

В

качестве

приятного

побочного

эффекта

этого

изменения

нам

больше

не

нужно

определять деструктор

для

Smiley.

Компилятор

будет

неявно генери­

ровать

деструктор,

который

выполнит

требуемое

уничтожение

unique

_ptr

(§5.3)

в

векторе.

Код,

использующий

unique

_ptr,

будет

таким

же

эффектив­

ным,

как

и

код

с

обычными

указателями.

Теперь

рассмотрим

пользователей

read

_

shape

()

:

//

Читаем описания фигур из входного

потока is:

 

unique_ptr<Shape> read_shape(istream&

is)

 

{

 

 

 

 

// Читаем заголовок фигуры из is

и находим

ее

 

switch (k)

 

 

 

{

 

 

Kind

k

case

Kind::circle:

 

 

 

 

// Читаем данные

Circle {Point,int}

ври

r

 

return unique_ptr<Shape>{new Circle{p,r));

 

 

11 ...

 

 

 

//

§13.2.1

void {

user

()

vector<unique_ptr<Shape>>

v;

 

while (cin)

 

 

 

v.push_back(read_shape(cin));

 

draw_all (v);

// Вызов

draw()

для

rotate_all(v,45);

//Вызов

rotate(45)

ка'11;l!ого элемента для ка'11;l!ого элемента

11

Все

объекты

Shape

неявно

уничтожаются

Теперь

каждым

объектом

владеет

unique

_ptr,

который

удаляет

свой

объект,

когда

он

больше

не

нужен,

т.е.

когда

его

unique

_ptr

выходит

из

области

ви­

димости.

 

Чтобы версия user

() с использованием unique_ptr была работоспособ­

на, нам

нужны версии

функций draw_all () и rotate_all (),которые при­

нимали

бы в качест.ве

аргументов vector<unique_ptr<Shape>>. Написа­

ние множества таких_all ()-функций может оказаться очень утомительным

занятием; в §6.3.2 показано альтернативное решение.

4.6.

Советы

[1] [2]

[3]

Выражайте идеи

непосредственно в коде; §4.1; [CG:P. I].

Конкретный тип

является

простейшей разновидностью класса. Где это

возможно, предпочитайте

конкретные типы более сложным классам и

простым структурам данных; §4.2; [CG:C. l О].

Для

представления простых концепций используйте конкретные клас­

сы;

§4.2.

 

 

4.6.

Советы

97

[4]

[5]

[6]

[7]

[8]

[9]

[1О]

[ 11] [12)

[13)

[14)

[15)

[16)

[17]

[18)

[19)

[20)

[21)

Для компонентов, критичных с

точки зрения производительности, пред­

почитайте конкретные

классы иерархиям классов; §4.2.

 

 

Для управления

инициализацией

объектов

определяйте конструкторы;

§4.2.1, §5.1.1; [CG:C.40) [CG:C.41

].

 

 

 

 

 

 

 

 

Делайте функцию членом только

тогда,

когда необходим

непосред­

ственный доступ к представлению класса; §4.2.1; [CG:C.4].

 

 

Определяйте

операторы, в первую

очередь,

для

имитации

обычного

применения;

§4.2.1; [CG:C.160).

 

 

 

 

 

 

 

 

 

Используйте свободные функции

для симметричных операторов;

§4.2.1;

[CG:C.161 ].

 

 

 

 

 

 

 

 

 

 

 

 

 

Объявляйте функции-члены, которые

не

модифицируют объект, как

cons t; §4.2.1.

 

 

 

 

 

 

 

 

 

 

 

 

Если конструктор захватывает

ресурс,

необходим

деструктор, освобо­

 

 

2

; §4.2.2; [CG:C.20).

 

 

 

 

 

ждающий этот ресурс

 

 

 

 

 

Избегайте операций с "голыми" new и delete; §4.2.2; [CG:R.11 ].

 

Для управления

ресурсами используйте дескрипторы ресурсов и

идио­

му RAII; §4.2.2; [CG:R. l ].

 

 

 

 

 

 

 

 

 

 

Если класс представляет собой

контейнер,

снабдите его конструктором

на основе списка инициализации; §4.2.3; [CG:C. l 03).

 

 

Когда требуется

полное разделение

интерфейса и

реализации, исполь­

зуйте в качестве

интерфейсов абстрактные

классы;

§4.3; [CG:C.122).

Обращайтесь

к

полиморфным

объектам с

помощью указателей

и ссы­

лок; §4.3.

 

 

 

 

 

 

 

 

 

 

 

 

 

Абстрактный

класс

обычно

не

 

нуждается

в

конструкторе;

§4.3;

[CG:C.126).

 

 

 

 

 

 

 

 

 

 

 

 

 

Используйте

иерархии классов

для

представления

концепций с

иерар­

хической структурой; §4.5.

 

 

 

 

 

 

 

 

 

 

Класс с виртуальной функцией

должен иметь

виртуальный деструктор;

§4.5; [CG:C.127).

 

 

 

 

 

 

 

 

 

 

 

Используйте

ключевое слово overr ide для явного указания

перекры­

тия в больших иерархиях классов;

§4.5.1; [CG:C.128).

 

 

При проектировании иерархии

классов различайте

наследование

реали­

зации и наследование

интерфейса;

§4.5.1; [CG:C.129).

 

 

Используйте

dynamic_cast там,

где

навигация по иерархии классов

неизбежна; §4.5.2; [CG:C.146).

 

 

 

 

 

 

 

 

 

 

2

Так как ресурс может захватываться не только в конструкторе, деструктор

 

 

бождать все захваченные за время жизни объекта ресурсы. -

Примеч. ред.

должен

осво­

98

Глава

4.

Классы

[22]

[23]

[24]

Используйте

dynamic_ cast

для

ссылочного

типа там,

где

невозмож­

ность найти [CG:C.147].

необходимый

класс рассматривается

как

ошибка;

§4.5.2;

Используйте

dynamic_ cast для типа указателя там, где невозмож­

ность найти

необходимый класс рассматривается как корректный вари­

ант; §4.5.2; [CG:C.148].

Используйте unique_ptr или shared_ptr, чтобы не забывать удалять

объекты, созданные с помощью оператора new; §4.5.3; [CG:C.149].

5

Основные

операции

• •

 

Когда кто-то

говорит "Я хочу язык программирования,

 

 

в котором достаточно просто сказать,

 

 

что я хочу сделать", дайте емуледенец.

 

 

-Алан Перлис

Введение

 

 

Основные операции

 

Преобразования типов

 

Инициализаторы членов

 

Копирование и перемещение

 

Копирование контейнеров

 

Перемещение контейнеров

 

Управление

ресурсами

 

Обычные операции

 

Сравнения

 

 

Операции

с контейнерами

 

Операции

ввода-вывода

 

Пользовательские литералы

 

swap()

 

 

hash<>

 

 

Советы

 

 

5.1.

Введение

Некоторые

операции,

такие

как

инициализация,

присваивание,

копирова­

ние

и

перемещение,

являются

фундаментальными

в

том

смысле,

что

правила

языка

делают

о

них

определенные

предположения.

Другие

операции,

такие

как==

и<<,

имеют

обычный

стандартный

смысл,

который

опасно

игнори­

ровать.