
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf360 |
Часть II • Обьвктно-ориентированное програ1У1мироеание но С^4» |
Когда память для объекта выделяется с помощью вызова mallocO, конструк тор не вызывается. Следовательно, клиенту нужно явно инициализировать объек ты класса.
Cylinder р* = (Cylinder*)malloc(sizeof(Cylincler));
|
/ / |
нет вызова конструктора |
p->setCylinder(3, 5); |
/ / |
полям объекта присваиваются значения |
Вызов malloc() — единственный способ в СН- + , позволяющий создать объект без помош>1 вызова конструктора. Создание других объектов (именованных и ди намических) сопровождается вызовом конструктора. Итак, мы незаметно мино вали точку возврата. Теперь невозможна ситуация, когда вы просто создаете экземпляр объекта и выделяете ему область памяти. Любое создание объекта будет сопровождаться вызовом функции-конструктора. Это опять требует опреде ленной смены мышления. Каждый раз, видя создание объекта, нужно напоминать себе, что это означает вызов конструктора. Но какого?
Конструкторы^ используемые по умолчанию
Многие классы не нуждаются в конструкторах, так как объекты этих классов не требуют инициализации по умолчанию. Если разработчик не включает в класс никаких конструкторов, то система подставляет для класса конструктор по умол чанию (который просто ничего не делает).
class |
Cylinder { |
/ / |
OK, |
если нет |
конструкторов/деструкторов |
double radius, height; |
/ / |
данные защищены от доступа из клиента |
|||
public: |
|
|
|
|
|
void |
setCylinder(double г, |
double |
h); |
/ / |
конструкторы доступны |
double getVolumeO; |
|
|
|
|
|
void |
scaleCylinder(double |
factor); |
|
|
|
void |
printCylinderO; |
|
|
|
|
} ; |
|
/ / |
конец области действия класса |
Все версии класса Cylinder, обсуждавшиеся в предыдущем разделе, использу ют конструктор, по умолчанию подставляемый системой. О нем специально ничего не упоминалось, чтобы не усложнять дискуссию. Когда клиент создает объект Cylinder, вызывается этот конструктор по умолчанию.
Cylinder с1; |
/ / вызывается конструктор по умолчанию, нет инициализации |
Зачем нужно знать об этом? Весь такой конструктор ничего не делает. Но следует иметь в виду, что если класс определяет конструктор явно (конструктор с пара метрами), то конструктор по умолчанию не подставляется.
Это нужно знать, т. к., если разработчик определяет переменные и массивы, ну>вдающиеся в конструкторе по умолчанию, возникает синтаксическая ошибка.
Данная версия класса Cylinder не имеет определенных программистом конст рукторов. Следовательно, система назначает этому классу конструктор по умолча нию (ничего не делающий). При создании переменной с1 был вызван такой конструктор. Откуда это известно? Но ведь какие-то конструкторы должны вызы ваться. (Создание объекта без вызова конструктора невозможно.) Так какой же конструктор вызывается? Зависит от числа аргументов. Переменная с1 не преду сматривает никаких аргументов. Это говорит о том, что вызывается конструктор без аргументов, т. е. конструктор по умолчанию. Предусматривает ли класс конст руктор по умолчанию? Нет. Следовательно, конструктор по умолчанию подставля ется системой. Он ничего не делает. Все замечательно.
Давайте рассмотрим версию класса Cylinder, который предусматривает общий определенный программистом конструктор. Это означает, что система не будет использовать конструктор по умолчанию.
Глава 9 • Классы С^-ь как вАиницы моАУ^ьност программы |
361 |
||||
class |
Cylinder { |
|
|
|
|
double radius, |
height; |
|
|
|
|
public: |
|
|
/ / этого недостаточно |
|
|
Cylinder(double |
r, double |
h) |
|
||
{ |
radius = r; height = h; |
} |
|
|
|
. |
. . } ; |
|
|
|
|
Когда клиент пытается создать объекты Cylinder, возникают проблемы.
Cylinder с1(3.0,5.0); |
//OK |
|
||
Cylinder |
с2, с[1000]; |
/ / |
1001 синтаксическая |
ошибка |
Cylinder |
*р = new Cylinder; |
/ / |
одна синтаксическая |
ошибка |
Здесь создается 1001 экземпляр объекта Cylinder без указания аргументов. Помните, что создание объекта без вызова конструктора не возможно? Поэтому компилятор пытается сгенерировать 1001 вызов конструктора. Какого именно? Так как аргументов нет, то он вызывает конструктор без аргументов, т. е. конст руктор по умолчанию Cylinder: :Cylinder(). Но в данной версии класса Cylinder не определен конструктор по умолчанию, а определен общий конструктор. Таким образом система вызывает общий конструктор, а конструктор по умолчанию не использует. Что будет, если клиент вызовет 1001 раз функцию-член класса и со здаст 1001 объект Cylinder? Поскольку эту функцию в спецификации класса найти не удается, генерируется сообщение о синтаксической ошибке. Научитесь быстро делать такие логические выводы.
Проблему можно устранить, предложив клиенту конструктор по умолчанию, определяемый программистом. Подобно конструктору, подставляемому по умол чанию системой, этот конструктор может не выполнять никаких операций, либо инициализировать элементы данных объекта, присваивая им какие-то разумные значения.
class |
Cylinder { |
|
|
|
|
|
|
double radius, |
height; |
|
|
|
|||
public: |
|
|
|
|
|
|
|
Cylinder |
() |
|
/ / определенный программистом конструктор по умолчанию |
||||
{ |
radius |
= 100.0; |
height |
= 0.0; } |
/ / |
разумные значения |
|
Cylinder(double |
г, |
double h) |
/ / |
общий конструктор |
|||
{ |
radius |
= г; height = h; |
} |
|
|
||
. |
. . } ; |
|
|
|
|
|
|
В клиенте:
Cylinder |
с1(3.0,5.0); |
//OK |
|
Cylinder |
с2, с[1000]; |
/ / |
тоже OK |
Cylinder |
*р = new Cylinder; |
/ / |
нет синтаксической ошибки |
Отметим еще раз, что создание каждого объекта сопровождается здесь по крайней мере одним вызовом функции. Конструкторы — это встраиваемые функ ции (inline). Тем не менее они могут влиять на производительность. В C-f + не бывает ситуации, когда объект создается без вызова функции.
Внимание Создание объекта в C++ всегда сопровождается
вызовом конструктора. Если конструктор в классе не определяется, то за созданием объекта следует вызов конструктора по умолчанию,
поставляемого системой. Если в классе определен какой-либо конструктор, система не подставляет конструктор по умолчанию. В этом случае нельзя создавать массивы или "объекты объектов" без аргументов. Система дала, система взяла.
щ 362 I |
Часть II« Объектно-ориентировонное орогра1\^мировани0 но O^-t |
Конструкторы копирования
Одно из важных понятий, составляющих основу философии объектов C+ + , является то, что классы — это типы. Определение классов в программе расширя ет систему встроенных типов C+ + . Идея в том, чтобы интерпретировать в C+ + встроенные типы как объекты, а определяемые программистом типы интерпрети ровать как встроенные.
Например, можно определять переменные встроенных типов без специфика ции их начальных значений. Следовательно, можно делать это и с объектными переменными.
i nt х; Cylinder с1; |
/ / неинициализированные переменные |
Синтаксис тот же, но смысл другой. Определение переменной встроенного типа просто выделяет память для этой переменной. Определение переменной со зданного программистом типа приводит к выделению памяти для этой переменной и вызову конструктора по умолчанию. Если класс не определяет конструкторы, то определение переменной класса даст синтаксическую ошибку. Чтобы этого не произошло, в классе должен быть определен конструктор по умолчанию. Этот конструктор может не выполнять никаких действий или инициализировать поля объекта какими-то значениями по умолчанию.
Аналогично может потребоваться инициализация отличной от класса перемен ной встроенного типа другой переменной того же типа. C++ поддерживает по добный синтаксис, позволяюндий клиенту инициализировать один объект класса значениями другого объекта того же класса.
int |
х(20); Cylinder с1(50,70); |
/ / объекты создаются и инициализируются |
|
int |
у=х; Cylinder с2=с1; |
/ / |
инициализация с помощью существующих объектов |
Пусть вас не вводят в заблуждение операции присваивания во второй строке. Никакого присваивания здесь нет. Не забывайте, что когда после имени перемен ной указывается тип, то речь идет об инициализации. Если имя типа отсутствует, а указано только имя переменной, то мы имеем дело с присваиванием. Для чего это знать? Как вы увидите далее, в каждом случае вызываются разные функции.
Какая же функция вызывается в этом случае? Ответ прост. Так как объект создается и инициализируется, здесь вызывается конструктор. Какой именно? Как уже говорилось выше, это зависит от контекста, т. е. числа и типа фактиче ских аргументов, подставляемых при создании объекта.
В этом примере для инициализации объекта с2 используется один аргумент — объект с1. Он имеет тип Cylinder. Следовательно, вызываемый конструктор име ет один параметр типа Cylinder. Вывод ясен? Такая цепочка рассуждений должна иметь место ка>вдь1Й раз, когда вы анализируете операторы создания объекта.
Конструктор с одним параметром того же типа, что и класс, имеет специальное название — конструктор копирования. Он называется так потому, что копирует значения из имеющегося источника в поля только что созданного целевого объекта. Как видно, последняя версия класса Cylinder не содержит конструктора с одним параметром типа Cylinder. Она имеет общий конструктор с двумя параметрами double и конструктор по умолчанию без параметров. Означает ли это, что приве денные операторы ошибочны, подобно ситуации, когда вводилась концепция ис пользуемого по умолчанию конструктора? Нет, и это еще одно подтверждение, что изучение С+Н нескучное занятие.
Если конструкторы в классе не определены, C++ предусматривает собствен ный конструктор по умолчанию. Этот конструктор копирует элементы данных по битам из объекта-источника в целевой объект. В отличие от подставляемого сис темой конструктора по умолчанию, такой подставляемый системой конструктор копирования не игнорируется, даже когда в классе определены другие конструкто ры. Следовательно, всегда нужно учитывать его существование.
364 |
Честь И • ОбъектнО'Ориеитированное програттыроваитв на C++ |
Еш,е один общий комментарий по поводу вызова конструктора. Для всех кон структоров, за исключением конструктора по умолчанию, можно использовать синтаксис вызова функции (со скобками). Вот примеры общего конструктора и конструктора копирования для именованных и динамических переменных.
Cylinder |
с1(50,70); |
/ / |
вызывается общий конструктор |
||
Cylinder |
с2=с1; |
/ / |
вызывается конструктор копирования |
||
Cylinder |
*р |
= new Cylinder(50,70); |
/ / |
вызывается |
общий конструктор |
Cylinder |
*q |
= new Cylinder(*p); |
/ / |
вызывается |
конструктор копирования |
Для используемых по умолчанию конструкторов такой синтаксис недоступен. Применение круглых скобок при вызове в клиенте конструктора по умолчанию даст синтаксическую ошибку.
Cylinder |
с1(); |
/ / |
синтаксическая ошибка |
|
|
Cylinder |
с2; |
/ / |
вызывается конструктор |
по умолчанию |
|
Cylinder |
*р |
= new CylinderO; |
/ / |
синтаксическая ошибка: |
круглые скобки |
Cylinder |
*q |
= new Cylinder; |
/ / |
вызывается конструктор |
по умолчанию |
Почему же такая несогласованность? Это чтобы легче было написать ком пилятор. Взгляните на первую строку последнего примера. Как узнать, что предполагается вызов конструктора, а не прототипа функции с именем с1() и возвращаемым типом Cylinder? Неизвестно. Разработчик компилятора тоже этого не знает. Один из способов избежать подобной неоднозначности состоит
взапрете использования прототипа везде, кроме начала файла исходного кода. Вполне разумно, поскольку именно там обычно находятся прототипы. Между тем в С прототипы разрешается использовать повсеместно, а в C++ слишком ценится обратная совместимость, чтобы можно было позволить генерировать
вэтом случае синтаксическую ошибку. В Java не преследуется задача обратной
совместимости с языком С, и синтаксис вызова конструктора по умолчанию в клиенте там согласован с синтаксисом вызова других конструкторов.
Конструкторы преобразования
Конструктор с одним параметром какого-то другого типа (не обязательно типа класса) называется конструктором преобразования. Часто он имеет тип одного из элементов данных класса. Конструктор преобразования полезен, когда клиент хочет задавать при создании каждого объекта значение только одного конкретного поля, а для других использовать значения по умолчанию.
Например, в программе моделирования может потребоваться создать объекты Cylinder с разными значениями радиуса. Первоначально — с нулевой высотой, которая потом растет, отражая процесс моделирования (рост артерий, связующих электронных компонентов, теплообмен через трубы отопления и т.д.).
Cylinder |
с1(50.0); |
/ / |
вызывается |
конструктор |
преобразования |
Cylinder |
с2 = 30.0; |
/ / |
вызывается |
конструктор |
преобразования |
Вновь, несмотря на разный синтаксис, обаоператора имеют один смысл — вызов конструктора преобразования.
В отличие от конструкторов по умолчанию и конструкторов копирования, кон структоры преобразования системой не подставляются. Если в классе определен конструктор преобразования с одним параметром типа double, то оба приведенных выше оператора дадут ошибку. Конструктор преобразования задает, что делать, если в качестве параметра указывается только одно значение, и какие значения использовать для других полей объекта. В следующем примере класс Cylinder определяет четыре конструктора: конструктор по умолчанию, конструктор копиро вания, конструктор преобразования и общий конструктор с двумя параметрами.
Глава 9 • Классы C++ кок единицы модульности программы |
365 |
|||||||||
class Cylinder { |
|
|
|
|
|
|
|
|
|
|
double |
radius, |
height; |
|
|
|
|
|
|
||
public: |
|
|
|
|
|
|
|
|
|
|
Cylinder |
() |
/ / |
предусмотренный |
программистом конструктор |
по умолчанию |
|||||
{ radius =1 . 0 . |
height |
= 0.0; } |
|
|
|
|||||
Cylinder |
(const |
Cylinder |
&c) |
|
/ / |
конструктор копирования |
||||
{ radius = с. radius, |
height |
= с |
height; } |
|
|
|||||
Cylinder |
(double r, double |
h) |
|
|
|
|
||||
{ radius = r, |
height |
= h; |
} |
|
/ / |
общий конструктор |
||||
Cylinder |
(double |
|
r) |
|
|
|
|
|
|
|
{ radius = r, |
height |
= 0.0; |
} |
/ / |
конструктор преобразования |
|||||
. . . . } ; |
|
|
|
|
|
|
|
|
|
|
Конструктор преобразования — первый удар по системе строгого контроля типов в C++. Как уже упоминалось, все современные языки поддерживают стро гий контроль типов. Если в каком-то контексте ожидается значение одного типа, то подстановка значения другого типа даст синтаксическую ошибку. Рассмотрим, к примеру, такой оператор:
Cylinder с2 = 30.0; / / вызывается конструктор преобразования
Если Cylinder — это просто структура С, то такой оператор синтаксически ошибочен. Компилятор сообилает об этом и говорит, что у вас есть шанс подумать и решить, что вы хотите сделать. Если Cylinder — класс C++ без конструктора преобразования, тоже возникает синтаксическая ошибка. У вас также не будет возможности выполнить программу и проанализировать ее результаты. Когда Cylinder — класс C++ без конструктора преобразования, то синтаксической ошибки не будет. Если это сделано намеренно, то все замечательно. Если же нет, то компилятор не заидитит от такой ошибки. Система строгого контроля типов здесь дает сбой.
В качестве следуюш^его примера рассмотрим функцию CopyDataO из этой гла вы (предполагая, что элементы данных radius и height объявлены как public).
void CopyData(Cylinder *to, |
const Cylinder &from) |
|
/ / копирование данных Cylinder |
{ to->radius=from.radius; |
to->height=from.height; } / / запись со стрелкой |
Для простой структуры с или для класса C++ без конструктора преобразова ния этот вызов функции в клиенте даст синтаксическую ошибку:
CopyData(&c2,70.0); / / здесь пропущено FROM Cylinder
Если доступен конструктор преобразования, компилятор будет генерировать программный код, создаюш,ий временный неименованный объект Cylinder, вызываюш,ий для этого временного объекта конструктор преобразования (с фактиче ским аргументом 70,0) и передаюш,ий временный неименованный объект функции CopyDataO как второй аргумент.
Если в клиенте используется значение числового типа, отличного от double, это не проблема. Компилятор генерирует код, преобразуюш,ий данное числовое значение в double, а затем передающий это значение как фактический аргумент конструктору преобразования.
Cylinder с2 = 30; |
/ / |
30 преобразуется |
в double |
CopyData(&c2.70); |
/ / |
70 преобразуется |
в double |
Конечно, если это именно то, что требовалось написать, то можно отметить такое положительное свойство C++ как гибкие возможности реализации наме рений программиста, но, если такой код написан по ошибке, остается лишь по жалеть, что компилятор не сообш,ил о ней, чтобы дать возможность исправить программу еще до ее выполнения.
366 Часть il • Обьектно-ориентирОБОнное программирование на C^-f
Деструкторы
Объект C++ уничтожается в конце выполнения программы (для объектов static или extern), при достижении закрывающей фигурной скобки, завершающей об ласть действия (для автоматических объектов), при выполнении операции delete (для динамических объектов с памятью, выделенной через new) или при вызове библиотечной функции fгее() (для объектов с памятью, выделенной через malloc()).
Когда уничтожается объект класса (за исключением вызова freeO), непо средственно перед уничтожением вызывается деструктор класса. Если деструктор в классе не определен, то вызывается деструктор, подставляемый системой по умолчанию (как и конструктор по умолчанию, он ничего не делает).
Деструктор, определяемый программистом, аналогичен конструктору. Это функция-член класса. Синтаксис деструктора еще более строгий, чем синтаксис конструктора. Указывать возвращаемый функцией тип в ее интерфейсе недопус тимо, а в теле функции не может присутствовать оператор return. Деструктор имеет то же имя, что и имя класса, но ему предшествует тильда (~), например ''Cylincler(). Деструкторы в отличие от конструкторов не могут иметь параметров.
Конструкторы и деструкторы — хороши для размещения операторов отладки. class Cylinder {
double |
radius, height; |
|
|
|
|
|
public: |
|
|
|
|
|
|
"Cylinder |
() |
|
/ / |
определяемый программистом деструктор: |
||
|
|
|
|
/ / |
нет возвращаемого типа |
|
{ cout |
« |
"Cylinder (" « |
radius |
« |
", " « |
height |
|
« |
") уничтожен" « |
endl; |
} |
/ / |
нет возвращаемого значения |
Когда деструктор реализуется вне области действия класса, используется опе рация области действия. Обратите внимание, что тильда — это часть имени функ ции, а не часть операции области действия.
Cylinder::"Cylinder |
( ) |
/ / |
деструктор класса: нет возвращаемого типа |
||
{ cout « |
"Cylinder |
(" « |
radius |
« ", " « |
height |
« |
") уничтожен" « |
endl; |
} |
/ / нет возвращаемого типа |
Посколыо^ деструкторы не могут иметь параметров, перегрузка имен для них не возможна (т. к. функции сперегрузкой имен должны различаться по списку пара метров). Следовательно, каждый класс может иметь не более одного деструктора.
Определяемый программистом конструктор нужен в том случае, когда объект использует динамическую память или другие ресурсы (файлы, блокировки базы данных и пр.). Чтобы избежать утечек ресурсов, деструктор должен возвращать эти ресурсы системе. Для таких последовательностей, как выделение и освобож дение памяти, открытие и закрытие файлов и т. д., функции-деструкторы допол няют конструкторы.
Рассмотрим пример класса, где может быть полезен деструктор. Класс Name содержит строку символов — фамилию человека. Конструктор инициализирует символьный массив. (Это конструктор преобразования, так как он имеет один параметр с типом, отличным от Name.) Для простоты данные объявлены как public, и предусматривается только один метод show_name(), отображающий на экране содержимое объекта.
struct Name { |
// фиксированный размер объекта, открытые данные |
char contents[30]; |
|
Name (char* name); |
// или Name(char name[]); |
void show_name(); |
// деструктор еще не нужен |
} ; |
Глава 9 « Классы C-f-f кок единицы моАудьности программы |
367 |
|||
Name::Name(char* name) |
// конструктор |
преобразования |
||
{ strcpy(contents, name); } |
// стандартное действие: копирование |
|||
void Name::show_name() |
// данных аргумента |
|
||
|
|
|
|
|
{ cout « contents « "\n"; } |
|
|
|
|
Клиент может определять объекты данного типа и отображать их содержимое |
||||
на экране. |
|
|
|
|
Name п1("Джонс"); |
/ / |
вызывается |
конструктор |
преобразования |
Name *р = new NameC'CMHT"); |
/ / |
вызывается |
конструктор |
преобразования |
n1.show_name(); p->show_name(); |
|
|
|
|
delete р; |
/ / |
удаляется неименованный |
объект |
В этой конструкции, независимо от длины фамилии, выделяется один и тот же объем памяти. Когда фамилия короткая, память теряется напрасно, а если длин ная — возможна порча содержимого памяти.
Популярным решением этой проблемы является динамическое распределение памяти. Вместо использования в качестве элементов данных фиксированного массива этот класс определяет только один указатель символьного типа. Объем выделяемой динамической памяти зависит от длины фамилии, передаваемой клиентом. В конструкторе для определения требуемого объема памяти вызыва ется функция strlenO (дополнительный символ — это завершающий ноль). Затем эта память выделяется и для инициализации динамически распределяемой области вызывается функция strcpyO.
struct Name { |
|
// указатель надинамически распределяемую |
|
char *contents; |
|
||
Name (char* name); |
|
// память: все равно public |
|
} ; |
// или Name(char name []); |
||
void show_name(); |
// теперь деструктор нужен |
||
Name::Name(char* name) |
// конструктор |
преобразования |
|
{ int len = strlen(name); |
// аргумент - число символов |
||
contents = new char[len+1]; |
// выделение динамической памяти для аргумента |
||
if(contents ==NULL) |
// 'new' выполнена неуспешно |
||
{ cout « "Нет памяти\п"; exit(1); } |
// отказ |
||
strcpy(contents, name); } |
//успех: копирование данных аргумента |
||
void Name::show_name() |
|
|
|
{ cout « contents « |
"\n"; } |
|
|
Чтобы обсудить, что происходит при использовании новой версии класса Name, поместим программный код клиента в глобальную функцию.
void ClientO |
|
|
|
|
{ Name n1("Джонс"); |
/ / |
вызывается |
конструктор |
преобразования |
Name *р = new Name("CMHT"); |
/ / |
вызывается |
конструктор |
преобразования |
n1.show_name(); p->show_name(); |
|
|
|
|
delete р; } |
/ / |
удаляется |
неименованный объект |
Когда в функции ClientO выполняется оператор delete р;, освобождается память, на которую ссылается указатель р. Эта память содержит только указатель contents. Память по указателю contents не удаляется и становится недоступной. Это — утечка памяти. Обратите внимание, что оператор delete р; не удаляет указатель р. Он удаляет то, на что этот указатель указывает. Указатель р удаля ется в соответствии с правилами области действия (когда завершается та область действия, где он определен). Это происходит, когда завершается выполнение функции ClientO (достигается закрываюш.ая фигурная скобка).