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

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

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

350

Часть II • Обьектно-ориентированное програм1М1ирова^

Можно, однако, реализовать все или некоторые функции-члены в заголовочном файле. Этот заголовочный файл должен включаться в каждый файл, где упомина­ ется имя класса, например в исходный файл клиента и даже в исходный файл, где реализуются функции-члены (так как имя класса применяется в операции области действия).

Поскольку компоновщик должен видеть определение функции только один раз, спецификации класса следует ограничивать директивами препроцессора для условной компиляции (см. примеры в главе 2 и 5). Например, заголовочный файл для класса Cylinder может выглядеть так:

#ifnclef

CYLINDER_H

 

 

 

 

/ /

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

#define CYLINDER_H

 

 

 

 

 

 

 

 

#include

<iostream>

 

 

 

 

 

 

 

 

using

namespace std;

 

 

 

 

 

 

 

struct

Cylinder

{

 

 

 

 

 

/ /

начало области действия

класса

double

radius,

height;

 

 

 

 

/ /

поля данных для доступа

 

void setCylinder(double

r,

double

h)

/ /

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

{

radius

= r; height

= h; }

 

 

 

 

 

 

double

getVolumeO

 

 

 

 

 

/ /

вычислить объем

 

{ return height * radius * radius

*

3.141593;

}

 

void scaleCylinder(double

factor)

 

 

/ /

масштабировать размеры

 

{

radius

*= factor;

height

*= factor;

}

 

 

void printCylinderO

 

 

 

 

 

/ /

вывести состояние объекта

{

cout

«

"радиус: "

«

radius «

"

высота: "

« height « endl; },

 

}

;

 

 

 

 

 

 

 

 

/ /

конец области действия

класса

#endif

В соответствии с общепринятым соглашением этот исходный код нужно по­ местить в файл Cylinder, h и использовать имя CYLINDER. Н для условной ком­ пиляции. Реализация функций-членов в отдельном файле — важный вклад в повышение модульности программы. Логически функции-члены, например Cylinder: :setCylinder(), определяются в фигурных скобках класса, независимо от того, находятся они в фигурных скобках класса или нет. Вот почему для до­ ступа к элементам данных radius и height эти функции не нуждаются в квалификаторе (операции ::).

В отдельной реализации квалификатор в имени функции обязателен. Без него функция setCylinder() ссылалась бы на глобальные переменные radius и height, а не на компонентные данные radius и height.

inline

void setCylinder(double г,

double h)

/ /

пропущенная операция

 

 

 

 

 

 

/ /

области действия

{ radius

= r; height

= h; }

 

/ /

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

inline

double Cylinder::getVolume()

/ /

вычислить объем

{ return height * radius * radius * 3.141593;

}

 

inline

void Cylinder::scaleCylinder(double

factor)

 

{ radius

*= factor;

height *= factor; }

/ /

масштабировать размеры

inline

void Cylinder::printCylinderO

/ /

вывести состояние объекта

{ cout

«

"радиус: "

« radius «

" высота:

"

« height « endl; }

Компилятор без проблем пропустит такое глобальное определение функции — в C+-f это допустимо. Если в области действия файла не описаны переменные radius и height, то компилятор будет жаловаться на то, что они не определены, а вовсе не на отсутствие операции ::. Он даст сообщение об ошибке, вводящее

Глава 9 • Классы С4«+ как единицы модульности программы

351

в заблуждение. Возможно, вы сначала ему не поверите. Неужели компилятор не видит, что radius и height определены вот здесь, в спецификации класса? Навер­ ное это еще одна ошибка в компиляторе! Но компилятор не может знать, что вы забыли применить для определения глобальных переменных операцию ::. Кстати, если переменные с такими именами определены в данной области действия для каких-то других целей, то компилятор будет молча генерировать код, ссылаюш,ийся на эти глобальные переменные, а не на элементы данных.

О с т о р о ж н о ! программисты иногда забывают указывать перед именем функции операцию ::. Компилятор считает при этом, что реализуется

глобальная функция, и обвиняет программиста в отсутствии определения глобальных переменных с именами применяемых в функции элементов данных.

Определение объектов классов с разными классами памяти

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

Что касается переменных любого типа, то объекты класса (его экземпляры, переменные) могут определяться в C++ автоматически (см. главу 6, где расска­ зывается о классах памяти).

Для автоматических и глобальных (extern или static) переменных память распределяется неявно, в результате определения объекта. Все предыдуш.ие при­ меры определения экземпляров классов — это примеры автоматических пере­ менных. Они создаются, когда при выполнении программы достигается определение. Например, переменные с1 и с2 в листинге 9.2 создаются, когда выполнение дохо­ дит до строки функции mainO, содержащей определения этих переменных.

Если переменная класса определяется как глобальная, то память для такого объекта выделяется перед началом выполнения функции main(). Это же относится

кслучаю, когда переменная класса определяется как статическая (глобальная

вфайле или локальная в некоторой функции).

Объединяет все такие экземпляры объектов то, что на них можно ссылаться по именам. Для доступа к полям данных и функциям-членам объектов (и ссылки на них) можно использовать имя объекта с точкой. Такой способ применяется в клиенте при обращении к компонентам класса:

Cylinder х; x.setCylincler(50,80);

double volume = x.getVolumeO; x. radius = 100;

Для динамических переменных память распределяется явно с помощью опера­ ции new. Объект не определяется по имени — к нему можно обращаться только через указатель. Для доступа к данным и функциям объекта функция-клиент будет использовать имя указателя (а не имя объекта, так как экземпляры объектов не имеют имен) и обозначение-стрелку. Ниже создается имя указателя на объект Cylinder, а затем — неименованный объект класса Cylinder, с которым выполня­ ются операции:

Cylinder* р;

// объект еще несоздан

р = new Cylinder;

// объект пока несуществует

p->set Cylinder(50,80);

// доступ к неименованному объекту

double volume = p->getVolume();

// то жеобозначение

p->radius = 100;

 

352

Часть II« Объектно-ориентированное програ1^11У!ирование но C-^-t-

 

Если нужно избежать операции стрелки, можно использовать операцию разы­

 

менования и селектор-точку:

 

 

 

(*p).setCylinder(50,80);

/ / то же,

что p->set Cylincler(50, 80);

 

Аналогично, когда объект передается функции-клиенту по указателю (а не по зна­

 

чению или по ссылке), используется стрелка (а не точка):

 

void CopyData(Cylinder *to, const

Cylinder &from)

/ / копирование Cylinder

 

{ to->radius=f rom. radius; to->height=f rom. height;

} //обозначение-стрелка

Cylinder X, у; x. radius=3.0; x. height=7.0; CopyData(&y,x);

/ / клиент для CopyDataO / / передача объекта по указателю

Автоматические переменные уничтожаются, когда завершается область дей­ ствия, в которой они определены. Программисту не требуется возвращать зани­ маемую ими память для повторного использования системой. Это же правило действует для глобальных и статических переменных. Они уничтожаются при завершении области действия, т. е. непосредственно после завершения функции main(). Никаких программных операций не нужно.

С динамическими переменными все по-другому. Их нужно удалять явно, так как система не знает, когда программист захочет вернуть для повторного исполь­ зования динамически распределяемую память.

Cylinder* р = new Cylinder;

/ /

создается неименованный объект

p->setCylinder(50, 80);

/ /

доступ к неименованному объекту

cout «

"Объем: «

p->getVolume() «

endl;

/ / операция-стрелка

delete

р;

/ / неименованный Cylinder

уничтожается, а указатель - нет

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

Управление доступом к компонентам класса

В предыдуш^ем разделе был создан класс Cylinder, соединяюш^ий свои компо­ нентные данные и функции в один синтаксический блок. Такой синтаксис позволяет решить две проблемы использования глобальных функций для доступа к данным при объектно-ориентированном программировании.

Во-первых, применение глобальных функций для доступа к данным не обязы­ вает указывать, что данные и операции с ними связаны. Следовательно, можно разделить функции, которые соотносятся друг с другом, и разнести их по разным частям исходного кода программы (в результате затрудняется ее понимание при сопровождении и модификации). Во-вторых, имена глобальных функций действи­ тельно глобальны. Чтобы избежать потенциальных конфликтов имен, программи­ стам придется координировать свою деятельность, даже когда разрабатываемые ими части программы не связаны друг с другом непосредственно. Синтаксис клас­ са явно показывает, что функции и данные — одно целое, а область действия класса исключает потенциальные конфликты имен функций.

В начале данной главы уже упоминали две цели введения классов в C+ + : перенос обязанностей с клиента на функции-серверы и управление доступом к компонентам класса.

Перенос обязанностей с клиента на сервер в классе выполняется путем пра­ вильного выбора функций-членов. (Иногда важен также выбор элементов дан­ ных.) В главе 8 приводился пример (листинг 8.8), где выбор функций-членов

Глава 9 • Классы C++ как единицы модульности программы

353

setRadiusO, getRadiusO, setHeihgtO и getHeightO вынуждал клиента самому выполнять операции, а не обращаться для этого к функциям-серверам. С этой точки зрения выбор функций-членов в листинге 9.2 предпочтительнее. Вместо получения значения radius и height для масштабирования, печати или вычисле­ ния объема клиент просит объекты класса Cylinder масштабировать цилиндр и выводить состояние либо вычислять объем.

Перенос обязанностей на функции-серверы — очень важная концепция. Что здесь достаточно, а что нет — часто является субъективным. Можно, конечно, отмахнуться от примера из листинга 8.8, но и он будет полезен, если класс исполь­ зуется как библиотечная утилита, обслуживаюш^ая большое число пользователей. Для некоторых пользователей подход, применяемый в листинге 9.2, будет слиш­ ком обш,им. Им может потребоваться вычисление поверхности цилиндра, а не численное значение его объема, но они могут быть заинтересованы и в сравнении объектов-цилиндров (как в листинге 8.9).

О переносе обязанностей на серверные классы часто заходит речь при обсуж­ дении построения классов. В данном разделе рассказывается о некоторых методах,

позволяющих разработчику класса

управлять доступом

к элементам

данных

и функциям класса.

На рис. 9.2 показана взаимосвязь клас­

 

 

 

 

v^ элементам

са Cylinder с клиентом main(). Здесь класс

.с-^^'Л

'-^Л^н^

имеет три

компонента: данные, функции

Класс Cylinder

 

 

 

 

и границу, отделяющую все, что находится

(сервер)

 

 

внутри класса, от того, что находится вне

 

 

 

 

 

 

его. Он показывает, что данные располо­

 

 

 

жены внутри класса, а функции частично

 

 

 

находятся внутри класса (их реализация),

 

 

 

а частично — снаружи (интерфейсы, из­

 

 

Код клиента

вестные клиенту). Этот рисунок демонст­

 

 

рирует также, что когда клиенту нужны

 

 

Сообщения

значения полей

Cylinder

(например, для

Локальный доступ

 

вычисления

объема

цилиндра, масштаби­

Функции-члены экземплярам

к элементам данных

класса

класса

рования, печати

или

присваивания

значе­

(рекомендуется)

 

(рекомендуются)

ний полям), он использует функции-члены

 

^ Граница класса

 

getVolumeO, scaleCylinderO и т.д., а не

Рис. 9 . 2 . Класс Cylinder и его

взаимосвязь

обращается

к

значениям

полей

radius

с клиентом

main()

 

и , height.

Вот,

что

означает пунктирная

линия. Она показывает, что прямой доступ к данным исключен.

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

Вторая причина предотвращения прямого доступа клиента к элементам данных заключается в том, что если клиентский код выражается в терминах вызова функций-членов, а не в терминах детальных вычислений со значениями полей, его проще понять. Кроме того, именно это и предполагает перенос обязанностей на функции-члены, которые выполняют работу для клиента, а не просто считыва­ ют и присваивают значения полей подобно функциям getHeightO и setHeight().

Для достижения названных преимуществ все, что находится внутри класса, должно быть закрытым (private), недоступным извне. Тем самым предотвраща­ ется создание зависимостей от серверных данных класса. Не забывайте, что

i

354 1

Часть II * Объектно-ориентированное riporpa^fvinpoeaHne но C-f-t-

 

 

в программировании слово ^'зависимость'' сродни ругательству. Зависимости

 

 

между разными частями программы могут означать:

 

 

 

Необходимость координации работ и тесной кооперации

 

 

 

между программистами при разработке программы

 

 

 

Необходимость изучения и изменения программного кода

 

 

 

в процессе сопровождения программы

 

 

 

 

Трудности повторного использования программного кода

 

 

 

в том же или в аналогичном проекте

 

 

 

 

Между тем конструкция класса в листинге 9.2 не предусматривает никакой

 

 

защиты от доступа к данным. Клиент может обращаться к полям экземпляров

 

 

объектов Cylinder, создавая зависимости от архитектуры данных Cylinder. При

 

 

этом теряются наиболее важные преимущества использования классов.

 

 

Cylinder

с1, с2;

 

/ / определение данных программы

 

 

c1.setCylinder(10,30); с2. setCylinder(20,30);

/ /

использование функции

 

 

 

 

 

 

/ /

доступа

 

 

с1.radius

= 10; с1.height = 20; . . .

/ /

все равно работает!

 

 

С++

позволяет разработчику детально управлять правами доступа к компо­

 

 

нентам класса. С помощью ключевых слов public, private и protected можно за­

 

 

дать доступ ддя каждого компонента (данных или функций). Вот еще одна версия

 

 

класса Cylinder:

 

 

 

 

 

struct

Cylinder {

/ /

начало области действия класса

 

 

private:

 

 

 

 

 

double radius, height;

/ /

данные являются закрытыми

 

 

public:

 

 

 

 

 

 

void setCylinder(double г, double h);

 

 

 

 

double getVolumeO;

/ /

вычисление объема

 

 

void

scaleCylinder(double

factor);

 

 

 

 

void

printCylinderO;

/ /

вывод состояния объекта

 

 

} ;

 

 

/ /

конец области действия класса

Ключевые слова делят область действия класса на сегменты. Все данные или функции, следующие, например, за ключевым словом private, имеют закрытый режим доступа. В этом примере элементы данных radius и height — закрытые (private), а все функции-члены — общедоступные (public).

Сегментов public, private и protected может быть сколько угодно, и следо­ вать они могут в любом порядке. В приведенном ниже примере элементы данных radius определены как private, две функции — как public, затем элементы данных height — как private и еще две функции — как public:

struct Cylinder {

/ /

начало области действия класса

private:

 

 

double radius,

 

 

public:

 

 

 

void setCylinder(double r, double h);

 

double getVolumeO;

/ /

вычисление объема

private:

 

 

double height;

 

 

public:

 

 

 

void

scaleCylinder(double

factor);

 

void

printCylinderO;

/ /

вывод состояния объекта

} ;

 

/ /

конец области действия класса

Глава 9 • Классы C++ кок единицы модульности программы

355

Это предоставляет дополнительную гибкость, но программисты обычно груп­ пируют все компоненты класса с одинаковыми правами доступа в один сегмент.

В общем случае компоненты класса (данные или функции) в сегментах public доступны для остальной части программы (как в предыдущих примерах).

Компоненты класса (также данные и функции) в сегментах private доступны только для функций-членов данного класса (и для функций с правами доступа friend, о чем будет рассказано в главе 10). Использование имени закрытого компонента класса вне области действия класса (или функции friend) даст син­ таксическую ошибку.

Отметим, что эти правила не запрещают объявлять закрытыми данные и де­ лать функции общедоступными. Однако обычно в C-f + элементы данных объяв­ ляются как private, а функции-члены — как public.

Компоненты класса в сегментах protected доступны для функций-членов дан­ ного класса и функций-членов классов, являющихся их наследниками (прямо или косвенно). Обсуждать наследование пока слишком рано, это намного более широ­ кая тема, чем синтаксис. Мы вернемся к ней позднее.

Функции-клиенты (глобальные или функции-члены других классов) могут об­ ращаться к закрытым элементам данных только через функции в части public (если они имеются).

Cylinder с1, с2;

 

/ /

определение данных программы

c1.setCylinder(10,30); c2.setCylinder(20,30);

/ /

использование функции

 

 

 

 

/ /

доступа

/ /

с1. radius = 10;

cl.heihgt = 20;

/ /

это не синтаксическая ошибка

i f

(d.getVolumeO

< c2.getVolume())

/ /

еще одна функция доступа

 

c1.scaleCylinder(1.2);

/ /

масштабирование

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

При применении закрытого доступа к элементам данных класса детали реализа­ ции класса Cylinder будут скрытыми. Если изменяются имена или поля Cylinder, то на клиента это не влияет (если, конечно, интерфейс Cylinder остается тем же). В программном коде клиента не создаются зависимости от архитектуры данных класса Cylinder. Занимающийся разработкой или сопровождением клиента про­ граммист может не изучать ее.

Обычно изменяются именно данные. Вот почему в типичном классе элементы данных объявляются как private, а функции-члены — как public. Тем самым улучшается модифицируемость программы и облегчается повторное использова­ ние класса. Заметим, что функции-члены класса (public или private) могут обра­ щаться к элементам данных того же класса (public или private).

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

Класс изолирован от других частей программы. Его закрытые элементы нахо­ дятся вне пределов досягаемости других функций (подобно локальным перемен­ ным функции или блока).

Это свойство уменьшает необходимость координации ме>кду разработчиками ПО и снижает вероятность неверного понимания при таком взаимодействии. В итоге улучшается качество ПО.

356

Часть II • Объектно-ориентированное г1рогра1^1^мрование но С-^Ф

Во всех предыдущих примерах для определения класса С-Н+ применялось ключевое слово struct. C++ позволяет также использовать для этой цели клю­ чевое слово class. Вот пример класса Cylinder, где вместо struct применяется ключевое слово class:

class Cylinder {

/ /

начало области действия класса

private:

 

 

double radius, height;

/ /

данные являются закрытыми

public:

 

 

 

void setCylinder(double r, double h);

 

double getVolumeO;

/ /

вычисление объема

void

scaleCylinder(double

factor);

 

void

printCylinderO;

 

 

} ;

 

/ /

конец области действия класса

Какая разница между этим определением класса и предыдущим? По существу никакой. Здесь определяется в точности такой же класс с теми же объектами. Различия лишь в ключевых словах struct и class. Одно из различий в том, что ключевое слово имеет в C++ только один смысл и используется только для этой цели (для введения в программе определяемого программистом типа, как это делалось в предыдущих примерах). Еще одно отличие между этими ключевыми словами в назначаемых по умолчанию правах доступа. В struct (и union) доступом по умолчанию будет public. В классе это private. Вот и все.

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

struct Cylinder {

/ /

некоторые предпочитают

перечислять

 

 

/ /

сначала функции-члены

public

void setCylinder(double г, double h);

 

 

double getVolumeO;

 

 

 

void

scaleCylinder(double

factor);

 

 

void

printCylinderO;

 

 

 

private:

 

 

 

double radius, height;

/ /

данные являются закрытыми

} ;

 

/ /

конец области действия

класса

Другие полагают, что для понимания выполняемых функциями операций важно сначала разобраться в данных, следовательно, нет ничего плохого в том, чтобы описывать в первую очередь данные. Кроме того, сокрытие информации не имеет ничего общего с секретами в стиле КГБ, когда имеется в виду знание (или не­ знание) чего-либо. В программировании сокрытие информации и инкапсуляция состоит в предотвращении использования информации. В таком случае, если нужно применить назначенные по умолчанию права доступа, то ключевое слово class лучше, чем struct.

class Cylinder {

/ /

некоторые предпочитают перечислять

 

/ /

сначала элементы данных

double radius, height;

/ /

данные являются закрытыми

Глава 9 • Классы C-i-4- как единицы модульности программы

357

public:

 

 

void setCylincler(double г, double h);

 

double getVolumeO;

 

void

scaleCylinder(double factor);

 

void

printCylinderO;

 

} ;

/ / конец области действия

класса

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

struct

Cylinder {

/ /

используются права доступа по умолчанию

double radius, height;

/ /

данные не защищаются от доступа из клиента

void

setCylinder(double г,

double h);

/ / методы общедоступные

double getVolumeO;

 

 

 

void

scaleCylinder(double

factor);

 

void

printCylinderO;

 

 

 

} ;

 

/ /

конец области действия класса

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

class

Cylinder {

/ /

используются права доступа по умолчанию

double radius, height;

/ /

данные защищаются от доступа из клиента

void

setCylinder(double г,

double h);

/ / методы недоступны

double getVolumeO;

 

 

 

void

scaleCylinder(double

factor);

 

 

void

printCylinderO;

 

 

 

} ;

 

/ /

конец области действия класса

Такой класс вообще нельзя использовать. Да, поля данных теперь закрыты (и это превосходно), но недоступны и функции-члены, и клиент не может вызы­ вать их. Определенно, не очень хорошая конструкция.

Вероятно, предпочтительнее не полагаться на права по умолчанию и назначать права доступа явно. Так что будем называть вещи своими именами.

Инициализация экземпляров объекта

Когда компилятор обрабатывает определение переменной, он использует для выделения требуемого объема памяти определение типа. Память выделяется из динамически распределяемой области (для переменных static и extern или для динамических переменных) или из стека (для локальных автоматических пере­ менных).

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

Cylinder с1;

/ /

элементы данных не инициализируются

double vol = с1.getVolumeO;

/ /

нет, нехорошо

Такой прием программирования может не подойти, если в вычислениях нужно использовать некоторые значения по умолчанию. С++ инициализирует только статические и глобальные переменные (нулями соответствующего типа). Дина­ мические и автоматические переменные остаются без начальных значений.

358

Чость II ^ Объектно-ориентированное riporpaf^f^NpOBOHiie на С+-!-

 

Иногда желательно определить значения по умолчанию. Хорошо было бы ини­

 

циализировать элементы данных в определении, подобно обычным переменным,

 

но в C++ определения элементов данных не могут содержать инициализатор.

 

class Cylinder

{

 

 

 

double radius

= 100, heihgt

= 0; .. .

/ / нет, в C++ это недопустимо

 

Класс может предусматривать функцию-член, позволяющую клиенту задать

 

начальное состояние объекта:

 

 

 

class Cylinder

{

 

 

 

double radius, heihgt;

 

 

 

public:

 

 

 

 

void setCylinder(double r, double h);

. . . } ;

 

С помош,ью этой функции клиент мог бы передавать сообщение setCylinder()

 

объектам Cylinder.

 

 

 

Cylinder с1;

 

 

 

 

c1.setCylinder(100.0,0.0);

/ /

присваивает radius значение 100,

 

 

 

/ /

а height ноль

Конечно, это перебор. Такой код позволяет задавать любые начальные значения, а не указанные по умолчанию. Здесь становятся полезными конструкторы.

Конструкторы как функции-члены

Объекты класса могут инициализироваться неявно, с помош^ью конструктора. Конструктор — это функция-член класса, но она имеет более строгий синтаксис, чем другие функции-члены. Конструктор не может иметь произвольное имя. Оно должно соответствовать имени класса. Интерфейс конструктора не может специ­ фицировать возвраш,аемый тип (даже void) и возвраш,ать значения, даже если содержит оператор return.

class

Cylinder {

 

 

double radius,

height;

 

public:

 

 

Cylinder ()

/ / т о

же имя, что и у класса, нет возвращаемого типа

{

radius=1.0;

heihgt=0.0; }

/ / нет оператора возврата

..•

} ;

 

 

Когда клиент создает объект, вызывается конструктор, заданный по умолчанию.

Cylinder с1; / / конструктор по умолчанию; нет параметров

Он называется конструктором по умолчанию, так как не имеет параметров. Странная причина, но это так.

Конструктор нельзя вызывать явно, как любую другую функцию-член.

d . C y l i n d e r O ; / / синтаксическая ошибка: явно вызывать конструктор нельзя

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

В общем случае экземпляр объекта можно создавать:

• В начале программы (объекты extern и static)

На входе в область действия, содержащую определение объекта (автоматические объекты)

Глава 9 • Классы C++ кок единицы модульности программы

| 359 |

• Когда переменная передается функции (или возвращается из функции) по значению

Когда переменная создается динамически

спомощью операции new (но не malloc)

Теперь видно, почему конструкторы не возвращают значений. Они вызываются неявно сгенерированным компилятором программным кодом, и никто не сможет использовать возвращаемое значение.

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

class Cylinder

{

 

 

 

 

 

double raduis,

heihgt;

 

 

/ /

инициализируются в конструкторах

public:

 

 

 

 

 

 

Cylinder(double

г, double

h);

/ /

прототип компонентной функции

void setCylinder(double г, double h);

 

 

Cylinder: :Cylinder(doiible

r,

double h)

/ /

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

{ radius = г;

heihgt = г;

}

 

 

 

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

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

Вот несколько примеров вызова конструктора в клиенте. Эти разные синтакси­ ческие формы вызывают один и тот же общий конструктор с двумя параметрами. Обратите внимание, что операция присваивания во втором параметре не означает выполнения присваивания. Несмотря на кажущееся использование присваивания, это не то, что вы думаете (что весьма типично для C+ + ). Здесь вовсе нет никакого присваивания. Просто так выглядит еще одна форма вызова конструктора.

Cylinder

с1 (3.0, 5.0);

/ /

вызов конструктора для именованного объекта

Cylinder

с2 = Cylinder(3,5);

/ /

все равно вызов

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

Cylinder

*г = new Cylinder(3.0,5.0);

/ / неименованный объект

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

int х(20);

/ / то же, что х1=20

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