Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf/сюнструкторы^ ^ / ^ и деструкторы:
потенциальные проблемы
Темы данной главы
^Передача объектов по значению
%^ Перегрузка операций для нечисловых классов
^Конструктор копирования
*^ Перегрузка операции присваивания ^ Практические аспекты: способы реализации |/ Итоги
^^^^^анее рассматривались проблемы, связанные с одинаковой интерпре-
J ^ ^ ^ Г тацией в программе C + + встроенных типов и типов, определяемых
Шпрограммистом.
Вглаве 10 обсуждались вопросы конструирования числовых классов, таких как Complex и Rational. Объекты данных классов — это полноценные экземпляры объектов. К ним применимо все, что возникает при работе с объектами, объявле ние класса, управление доступом к компонентам класса, проектирование функцийчленов, определение объекта, его инициализация и передача объекту сообщений
Обсуждавшиеся ранее типы, определяемые программистом, были числовыми В отличие от целых чисел и чисел с плавающей точкой, они имеют более сложную внутреннюю структуру. Однако клиент обрабатывает такие объекты аналогично целым числам и числам с плавающей точкой. В клиентской программе их можно складывать, перемножать, сравнивать и т. д.
Обратите внимание на это утверждение. Под словами "такие объекты" пони маются объекты определяемых программистом типов. Что означает "целые числа и числа с плавающей точкой"? Здесь они сравниваются с типами, определяемыми программистом, а не с переменными целого типа или типа с плавающей точкой Лучше было бы сказать "встроенные" типы. Что же в таком случае означает "их" в последней фразе? Вероятно, то же, что и "такие объекты" выше. Но это не так, поскольку речь идет о работе с ними в коде клиента. Клиент не может работать с типами, определяемыми программистом: он манипулирует объектами подобного типа. Это экземпляры объектов или переменные. Они перемножаются, сравнива ются и т. д.
Глава 11 • Конструкторы и деструкторы: потенциальные проблемы |
441 |
Объекты определяемых программистом классов могут обрабатываться в коде клиента аналогично переменным встроенных типов. Таким образом, имеет смысл поддерживать для них перегруженные операции присваивания. Принцип С++, предусматриваюш^ий интерпретацию встроенных и определяемых программистом типов, работает и для этих классов. В данной главе рассматриваются перегружен ные операции д/ш классов, объекты которых можно складывать, перемножать, вы читать или делить. Например, для операций со строками текста в памяти может использоваться класс St ring. Из-за нечислового характера таких классов перегру женные операторные функции для них становятся искусственными. Например, с помош^ью перегруженной операции сложения можно либо реализовать конкате нацию строк, либо сравнить строки. Однако сложно найти способы для сложения или деления объектов String. Тем не менее, нужно знать, как работать с перегру женными операторными функциями для нечисловых классов.
Эти нечисловые классы имеют важное отличие: объекты одного класса могут использовать разные объемы данных. Объекты числовых классов всегда применя ют одни и те же объемы памяти. Например, в классе Rational присутствуют два элемента данных одного размера — числитель и знаменатель.
В классе String объем текста, хранимый в одном объекте, отличается от объе ма текста в другом объекте. Ес/ш класс зарезервирует для каждого объекта один и тот же объем памяти, то вы столкнетесь с такими проблемами: непроизводи тельной тратой памяти (когда фактический, используемый объем памяти меньше зарезервированного места) или переполнения памяти (когда объект должен хранить слишком много текста). Эти две опасности подстерегают разработчиков классов, выделяюш,их для каждого объекта один и тот же объем памяти.
C++ разрешает эту проблему. В динамически распределяемой области или в стеке он выделяет фиксированный объем памяти для каждого объекта. Кроме того, при необходимости в динамически распределяемой области выделяется до полнительный объем памяти, который для каждого объекта может быть разным и даже может изменяться для одного и того же объекта за время его суш^ествования. Например, объект String может получить дополнительную память для сохра нения текста, добавляемого к суилествуюш,ему объекту в результате конкатенации.
Для динамического управления памятью используются конструкторы и дест рукторы. Неквалифицированное их применение может отрицательно повлиять на производительность программы. Кроме того, в этом случае может быть испорче но содержимое памяти и нарушена целостность программы, что несвойственно никакому другому языку, кроме C++. Каждый программист, работаюнлий с C+ + , должен знать об этих опасностях.
Введем дополнительные концепции для класса Rational фиксированного раз мера (см. главу 10). В этой главе к классу String применяются концепции дина мического управления памятью, что даст читателям возможность лучше понять связи между объектами и между переменными встроенных типов. Несмотря на все усилия интерпретировать их эквивалентно, они различаются.
Не пропустите материал данной главы. Опасность, связанная с конструкторами и деструкторами C+ + , вполне реальна, и нужно знать, как заш,иш,ать себя, своего начальника и своих пользователей от собственных программ.
Передача объектов по значению
в главе 7 приводились аргументы против передачи объектов функциям как значений параметров или как параметров-указателей и рекомендовалась передача параметров по ссылке.
Говорилось о том, что передача по ссылке почти столь же проста, как передача по значению, но выполняется быстрее (для входных параметров, не модифицируе мых функцией). Передача по ссылке происходит так же быстро, как передача по указателю, но синтаксис намного прош,е (для выходных параметров, не изменяе мых при выполнении функции).
442 |
Часть I! о Объектно-ориентировонное програ1^1У1ирование на С-^^ |
Отмечалось также, что при передаче по ссылке для входных и выходных пара метров используется одинаковый синтаксис. Рекомендовалось для выходных пара метров указывать ключевое слово const, показывая, что в результате выполнения функции параметр не изменяется. Если модификаторы не используются, то это должно показывать, что при выполнении функции параметр модифицируется.
Вглаве 7 можно было видеть, что не всегда следует возврандать значенияобъекты, если это не вызвано необходимостью передать сообщения возврандаемому объекту (синтаксис цепочки выражений).
Всоответствии с данным подходом, передача по значению должна быть
ограничена передачей встроенных типов как входных параметров функций и возвратом из функций параметров встроенных типов. Почему это приемлемо для входных значений встроенных типов? Их передача по указателю усложняет программу и может ввести в заблуждение при ее чтении (можно подумать, что данный параметр изменяется в функции). Передача параметров по ссылке (с ключевым словом const) не очень трудна. Таким образом, для встроенных типов используйте передачу параметров по значению.
В последней главе много рассказывалось о методах программирования, с по мощью которых были показаны не только достоинства или недостатки различных режимов передачи параметров, но и фактическая последовательность вызовов.
Кроме того, ранее говорилось об инициализации и присваивании. Хотя в том
ив другом случае используется знак равенства, интерпретируются они по-разному. В этом разделе для демонстрации различий применяются отладочные операторы.
Из всех функций класса Rational оставим только эти три: normalizeO, show()
иoperator+(). Обратите внимание, что перегруженная операция operator+() не является функцией-членом класса Rational. Это "дружественная" функция. Именно поэтому в начале абзаца говорится: "Из всех функций класса Rational",
ане "из всех функций-членов класса Rational". Таким образом, подчеркивается, что функция friend во всех отношениях эквивалентна функции-члену класса. Она реализуется в том же файле, что и другие функции-члены, имеет те же права доступа к закрытым элементам данных и бесполезна для работы с любым другим классом, кроме Rational. От функций-членов ее отличает только синтаксис вызова, но для перегруженных операций у функций-членов и у функций friend синтаксис будет одинаков.
Листинг 11.1. Пример передачи параметров-объектов по значению
#include <iostream.h> |
|
|
|
|
|
|
|
|
|
|
|
||||
class Rational { |
|
|
|
|
|
|
|
|
/ / |
закрытые данные |
|||||
long |
nmr; |
dnm; |
|
|
|
|
|
|
|
|
|||||
void |
normalizeO; |
|
|
|
|
|
|
|
|
/ / |
закрытая |
функция-член |
|||
public: |
|
|
|
|
|
|
|
|
|
/ / |
конструктор: общий, преобразования по умолчанию |
||||
Rational(long |
n=0, long |
d=1) |
|
|
|
|
|||||||||
{ |
nmr = n; |
|
dnm = d; |
|
|
|
|
|
|
|
|
|
|
|
|
|
this->normalize(); |
|
|
|
|
" « |
dnm « |
endl; |
|
|
|||||
|
cout « |
" |
создан: " |
« |
nmr « |
" |
|
|
|||||||
Rational(const Rational |
&r) |
|
|
|
|
|
|
|
|
|
|||||
{ |
nmr = r. nmr; dnm = |
r.dnm; |
|
|
|
|
|
|
|
|
|
||||
|
cout « |
" |
скопирован: " « |
nmr |
« |
« |
dnm « |
endl; } |
|
||||||
void |
operator |
= (const Rational |
&r) |
|
|
|
/ / |
операция |
присваивания |
||||||
{ |
nmr = r.nmr; dnm = r.dnm; |
|
|
|
« |
dnm « |
endl; } |
|
|||||||
|
cout « |
" |
присвоен: " « |
|
nmr « |
" |
|
||||||||
"Rationale) |
|
|
|
|
|
|
|
" « |
dnm « |
// деструктор |
|||||
{ |
cout « |
" |
уничтожен: " |
« |
nmr |
« |
endl; } |
|
|||||||
friend Rational operator + (const Rational x, const Rational y); void showO const;
// конец спецификации класса
|
Глава 11 • KoHcrpyi^Topbi и деструкторы: потенциальные проблемы |
I 443 1 |
||
void Rational::show() const |
|
|
||
{ cout « " " « nmr « V " « dnm; } |
|
|
||
void Rational::normalize() |
// закрытая функция-член |
|||
{ if (nmr ==0) { dnm = 1; return; } |
|
|
||
|
int sign = 1; |
|
// сделать оба положительными |
|
|
if (nmr < 0) { sign = -1; nmr = -nmr; } |
|||
|
if (dnm < 0) { sign = -sign; snm = -dnm; |
// наибольший общий делитель (НОД) |
||
|
long gcd = nmr, value = dnm; |
|||
|
while (value !=gcd) { |
// стоп, если найден НОД |
||
|
if (gcd > value |
|
// вычесть меньшее из большего |
|
|
gcd = gcd - value; |
|||
|
else value = value - gcd; } |
// сделать dnm положительным |
||
|
nmr = sing * (nmr/gcd); dnm = dnm/gcd; } |
|||
Rational operator + (const Rational x, const Rational y) |
|
|||
{ |
return Rational(y.nmr*x.dnm + X. nmr*y.dnm, y.dnm*x.dnm); } |
|
||
int mainO |
b(3,2), c; |
|
|
|
{ |
Rational a(1.4), |
|
|
|
|
cout « endl; |
|
|
|
|
с = a + b; |
" +"; b.showO; cout « |
" ="; c.showO; |
|
|
a.showO; cout « |
|
||
cout « endl « endl; return 0;
В обобщенном конструкторе Rational добавлен отладочный оператор. Он будет выполняться при каждом создании и инициализации объекта Rational в начале функции mainO и в функции operator+().
Rational::Rational(long |
n=0, long |
d=1) |
/ / |
значения no умолчанию |
|
{ nmr = n; |
dnm = d; |
|
|
/ / |
инициализация данных |
this->normalize(); |
|
|
|
|
|
cout « |
"создан: " « |
nmr « " |
" « dnm « |
endl; } |
|
Отладочный оператор вывода добавлен также в конструктор копирования. Этот оператор выполняется при инициализации объекта Rational с помощью элементов данных другого объекта Rational, например при передаче функции operator+() параметров по значению или когда эта функция возвращает объект Rational.
Rational::Rational(const Rational &r) |
/ / |
конструктор копирования |
{ nmr = r.nmr; dnm = r.dnm; |
/ / |
копирование элементов данных |
cout « " скопирован: " << nmr << " |
« dnm « endl; } |
|
Данный конструктор вызывается, когда аргументы типа Rational передаются по значению дружественной операторной функции operator+(). Между тем этот конструктор копирования не вызывается, когда operator+() возвращает значение объекта, так как перед возвратом из функции operator+() вызывается общий конструктор с двумя аргументами.
Конструктор не выполняет никаких осмысленных операций для класса Rational. Он добавлен только в целях отладки. Деструктор вызывается всякий раз при унич тожении объекта Rational.
Самая интересная функция здесь — это перегруженная операторная функция. Ее задача состоит в копировании элементов данных одного объекта Rational в ком поненты данных другого объекта Rational. Чем она отличается от конструктора
г |
446 |
Часть II • Объектно-ориентированное программирование на C++ |
|
Перегрузка операций дая нечисловых классов
Расширение встроенных операций числовых классов — естественный процесс. Перегруженные операторные функции для этих классов аналогичны встроенным операциям. Неверная интерпретация их смысла программистами, занимающимися клиентом или сопровождающими программу, маловероятна. Похоже, что идея одинаковой интерпретации значений встроенных и определяемых программистом типов ведет к простой реализации.
Операции сложения, вычитания и другие могут применяться также к объектам нематематических классов, но их применение может показаться искусственными. Все это напоминает значки-пиктограммы для активизации команд в графическом пользовательском интерфейсе.
Сначала был интерфейс командной строки, и пользователям приходилось на бирать длинные команды с параметрами, ключами, переключателями и т. д. Для упрощения ввода придумали также меню с текстовыми пунктами. Выбирая пункт меню, пользователь мог инициировать команду, не набирая ее. Другой вариант — оперативные клавиши. Если пользователь будет нажимать последовательность таких клавиш, то он активизирует команду непосредственно. Ему не надо отры ваться от клавиатуры и проходить несколько меню с подменю. Наконец, появи лась инструментальная панель с графическими командными кнопками. Нажимая на такую кнопку, пользователь активизирует команду. Ему не надо запоминать комбинацию клавиш. Значки на этих кнопках понятны: Open, Close, Cut, Print. При добавлении новых подобных значков они становятся менее понятными. Появляются кнопки New, Paste, Output, Execute и т. д.
Чтобы пользователь смог выучить значки, добавляются всплывающие под сказки. Пользовательский интерфейс становится более сложным, для приложений требуется больше места на диске и в оперативной памяти, для их создания необхо димо активно работать по программированию. Перейдем к изучению операторных функций для нечисловых классов. Познакомимся с дополнительными правилами, написанием программного кода, текстом программы. Возможно, в коде клиента лучше применять вызовы обычных функций, чем "новомодных" перегруженных операций.
Класс String
Рассмотрим пример использования перегруженных операторных функций для нечисловых классов: операторную функцию для конкатенации текста.
Введем класс String с двумя элементами данных: указателем на динамически распределяемый массив символов и целым, задающим максимальное число допус тимых символов, которые можно вставлять в массив в динамически распределяе мой памяти. Библиотека C + + Standard Library содержит класс String (с первой буквой в нижнем регистре). Это более мощный класс, чем String в данных приме рах, однако он сложнее.
Клиент может создавать объекты этого класса двумя способами: определяя максимальное число допустимых символов и задавая текстовое содержимое стро ки. Для спецификации числа символов требуется целочисленный параметр, для спецификации текста — текстовый массив. Типы данных параметров различают ся, поэтому их нужно использовать в разных конструкторах. Каждый из этих конструкторов имеет один параметр, который отличается от типа класса, но пре образуется к значению типа класса, они называются конструкторами преобразо вания.
Первый конструктор преобразования с параметром, задающим длину строки, имеет по умолчанию нулевое значение аргумента. Если объект String создается с помощью этого значения по умолчанию (параметры не указываются), то длина текста для такого объекта будет равна нулю. В этом случае первый конструктор преобразования применяется как конструктор по умолчанию, например String s;.
Глава 11 • Конструкторь! и деструкторы: потенциальные проблемы |
449 |
его использовать, если длина требуется нечасто и не хочется добавлять для каж |
|
дого строкового объекта дополнительный завершающий символ. |
|
Поскольку динамическая память распределяется для каждого объекта |
String |
индивидуально, многие программисты чувствуют, что ее нужно рассматривать как часть объекта. При таком подходе объекты String представляются в клиенте как объекты переменной длины (в зависимости от размера выделенной динамической области памяти). Такая точка зрения имеет право на существование, но затрудня ет объяснение работы конструкторов и деструкторов.
Используйте подход, проиллюстрированный диаграммами на рис. 11.4 и 11.5. Он отражает принцип С + -Ь, согласно которому класс — это схема, шаблон экземпляра объекта. Такая схема одинакова для всех объектов String. Каждый объект String имеет два элемента данных, а размер любого объекта String будет одним и тем же. В клиенте выполняется следующий оператор:
String t(20); / / для двух элементов данных распределяется память в стеке
Здесь для двух элементов данных объекта t вьщеляется память в стеке. Динами ческая память распределяется функциями-членами String, выполняемыми для конкретного объекта. Для разных объектов String могут выделяться или осво бождаться разные объемы динамической памяти, или они получают больше памя ти без изменения своей "индивидуальности".
При таком подходе сами объекты String с памятью, выделяемой в динамиче ской области, не изменяются.
String *р; |
/ / |
нет объекта String, создается |
|
/ / |
указатель в динамической области |
р = new String ("Привет!"); |
/ / |
в динамической области два |
|
/ / |
элемента данных, плюс 4 символа |
Здесь неименованный объект String, на который ссылается указатель р, получает числовой и символьный указатели в динамически распределяемой области. После создания объекта конструктор выделяет в динамической области место еще для 4 символов и устанавливает указатель str на эту память.
Данный подход удобен тем, что позволяет рассматривать все объекты как объекты одного класса и размера. При создании объекта реализуются два отдель ных процесса: собственно создание объекта, причем всегда одного размера, и вызов конструктора. Он инициализирует элементы данных объекта, включая указатели, ссылающиеся на память в динамической области.
Деструктор освобо>вдает память, выделяемую в динамической области. Он вы зывается непосредственно перед уничтожением объекта. Когда объект уничтожа ется, освобождается для повторного использования память, выделенная для его элементов данных str и 1еп. Если память для объекта выделялась в стеке, как для объектов U и V в функции main() из листинга 11.2, она возвращается в стек, если в динамически распределяемой области, как для неименованного объекта, на ко торый ссылается указатель р, она снова возвращается в динамическую область. Во всех случаях память, освобождаемая деструкторами (на нее ссылается указа тель str), возвращается в динамически распределяемую область до того, как исчезнут элементы данных 1еп и str. В противном случае оператор delete str; был бы недопустимым.
Функция modifyC) изменяет содержимое динамически распределяемой области памяти. Чтобы содержимое памяти не оказалось запорченным, она использует библиотечную функцию strncpyO. В случае переполнения strncpyO не заверша ет строку нулевым символом. Это делается в конце функции. Если строка короче выделенной области памяти, такая операция кажется лишней. Нужно иметь в виду, что функция StrncpyO в любом случае заполняет остаток строки нулями, и запись еще одного нуля не повлияет значительно на скорость выполнения про граммы.
