
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf
|
|
|
11 # Конструкторы и деструкторы: потенциальные проблемы |
471 |
||||||||
|
|
|
|
Вызывается функция enterData(). Она запрашивает у пользователя название |
||||||||
|
|
|
города и передает его как аргумент конструктору преобразования String. На |
|||||||||
|
|
|
экране появляется выводимое конструктором сообщение "Создан". Поскольку |
|||||||||
|
|
|
объект U в функции main() создается только при вызове enterData(), вызываемый |
|||||||||
|
|
|
в enterData() конструктор используется как конструктор для объекта и в функции |
|||||||||
|
|
|
main(). Конструктор копирования не вызывается. Объекты String работают с ди |
|||||||||
|
|
|
намической памятью, однако целостность программы сохраняется. Конструктор |
|||||||||
|
|
|
преобразования выделяет для объекта и в функции main() индивидуальную память |
|||||||||
|
|
|
в динамически распределяемой области. |
|
|
|
|
|
||||
|
|
|
|
Измените функцию enterData(). Добавьте еще один локальный объект для |
||||||||
|
|
|
хранения данных пользователей. |
|
|
|
|
|
|
|||
|
|
|
String |
enterDataO |
|
|
|
|
|
|
|
|
|
|
|
{ cout |
« "Введите название города для поиска: "; |
|
/ / |
запрос пользователю |
|||||
|
|
|
char data[200]; |
|
/ / |
грубое решение |
|
|||||
|
|
|
cin » |
data; |
|
/ / |
принять ввод от пользователя |
|||||
|
|
|
String X = data; |
|
/ / |
конструктор преобразования |
||||||
|
|
|
return х; } |
|
/ / |
конструктор копирования |
||||||
|
|
|
Изменения незначительны. Если бы х была переменной встроенного типа, то |
|||||||||
|
|
|
все осталось бы по-прежнему. Для объектов с динамически |
распределяемой |
||||||||
|
|
|
памятью все иначе. При создании объекта х вызывается конструктор преобразо |
|||||||||
|
|
|
вания. Между тем, когда функция завершает работу, объект и в функции main() |
|||||||||
|
|
|
инициализируется с помощью конструктора копирования. Если определяемый |
|||||||||
|
|
|
программистом конструктор копирования не реализован, то используется сис |
|||||||||
|
|
|
|
|
|
темный конструктор копирования. Он копирует |
||||||
Исходный: " |
|
|
|
|
элементы данных объекта х |
в элементы |
данных |
|||||
|
|
|
|
объекта и и не выделяет динамическую память. |
||||||||
Исходный: '' |
|
|
|
|
||||||||
Исходный: '' |
|
|
|
|
Указатели str объектов и и х ссылаются на одну |
|||||||
Исходный: '' |
|
|
|
|
область динамически |
распределяемой |
памяти. |
|||||
Создан: |
'Атланта' |
|
|
|
Когда функция enterDataO |
завершает |
работу |
|||||
Создан: |
'Бостон' |
|
|
|
||||||||
Создан: |
'Чикаго' |
|
|
|
и объект X исчезает, вызывается деструктор String. |
|||||||
Создан: |
'Денвер' |
|
|
|
Он |
освобождает динамическую |
область |
памяти, |
||||
Введите название города для поиска: Москва |
на |
которую ссылается указатель |
str в объекте х. |
|||||||||
Создан: |
'Москва' |
|
|
|
||||||||
Скопирован: |
'Москва' |
|
|
Динамическая память |
объекта и освобождается |
|||||||
Город Москва |
найден |
|
|
при создании объекта. |
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|||
Рис. 11.17. |
Результат |
выполнения |
|
В результате такого подхода программа аварий |
||||||||
но завершает работу. Однако она уже некорректна. |
||||||||||||
|
|
программы |
из листпипга 11.6 |
Нужен определяемый программистом конструктор |
||||||||
|
|
с |
модифицированной |
копирования. |
|
|
|
|
|
|||
|
|
функцией |
enterDataO |
|
|
|
|
|
||||
|
|
и |
конструктором |
|
Все работает нормально, когда подставляется |
|||||||
|
|
копирования |
|
определяемый программистом конструктор копиро |
||||||||
|
|
|
|
|
|
вания. Пример результата выполнения программы |
||||||
|
|
|
|
|
|
представлен на рис. 11.17. |
|
|
|
|||
|
|
|
Вывод показывает, что после ввода строки пользователем для локального |
|||||||||
|
|
|
объектах в функции main() вызывается конструктор преобразования, затем |
|||||||||
|
|
|
конструктор копирования для локального объекта |
и в функции main(). Данная |
||||||||
|
|
|
версия работает медленнее предыдущей. Однако важно то, что она ведет себя |
|||||||||
|
|
|
не так, как предыдущая. И еще важнее, что, если бы переменные х и и были |
|||||||||
|
|
|
встроенного типа, то такие изменения не повлияли бы на поведение программы. |
|||||||||
|
|
|
Подобные эксперименты со встроенными типами помогают программисту найти |
|||||||||
|
|
|
свое решение. Несмотря на все усилия, встроенные и определяемые программи |
|||||||||
|
|
|
стом типы интерпретируются |
в C + + по-разному. Работа с объектами требует |
изменения интуиции программиста. Вот почему важно проследить за последова тельностью событий. Нужно уметь связывать структуры клиента с неявно вызы ваемыми функциями класса.
I 472 I |
Часть II « Объектно-ориентированное програттшроваитв на С^-^ |
шшяшя^шшшшшшшшшшшшшшшшшшш/шшшшшшшшшшшшшшшш^шшшшшшшшшшшшшшш^шшшшшшшшшш^^
Ограничения для эффективности конструктора копирования
Хотелось бы внести в программу еще одно небольшое изменение, на этот раз в клиенте. Вместо определения объекта и в функции main() и немедленной его инициализации после определения этого объекта (с помощью конструктора по умолчанию) присвоим ему то, что вводит пользователь при вызове функции enterDataO.
i nt |
mainO |
|
|
|
|
{ enum { MAX = 4) ; |
|
|
|
||
/ / |
создание базы данных с названиями |
городов |
|
||
/ / |
String |
U = enterDataO; |
/ / |
аварийно завершается без |
|
|
|
|
/ / |
конструктора |
копирования |
String и; |
|
/ / |
конструктор |
по умолчанию |
|
U = enterDataO; |
/ / |
аварийно завершается: конструктор |
|||
|
|
|
/ / |
копирования не поможет |
|
/ / |
поиск |
города, вывод результатов |
|
|
|
return 0; |
|
|
|
|
|
} |
|
|
|
|
|
После внесения изменений моя система аварийно завершает работу. Не стоит приводить еще одно диалоговое окно с бесполезной информацией об источнике проблемы. Кроме того, это был бы пример выполнения на конкретной машине под конкретной ОС. Важно то, что программа некорректна. Хотя она правильно компилируется, она не должна выполняться. Так как компилятор не сообщает о некорректности программы, именно интуиция программиста должна подсказать ему, что происходит в ее недрах.
Перегрузка операции присваивания
в некоторых случаях инициализация и присваивание объекта в C++ различа ются. Если приходится иметь дело со встроенными типами данных, такое различие часто бывает академическим. Рассмотрим пример:
int V = 5; int u = v; / / переменная инициализируется
Сравните его с таким примером:
int V = 5; int u; u = v; / / присваивание переменной u
В первом примере переменная и инициализируется при определении. Во втором происходит присваивание переменной и после ее определения. Для встроенных переменных конечный результат будет одним и тем же, но когда эти вычислитель ные объекты представляют собой объекты определяемых программистом типов, работающих со своей собственной памятью, разница важна.
String |
V = |
"Hello"; |
String |
u = v; |
/ / |
объект |
u |
инициализируется |
String |
V = |
"Hello"; |
String |
u; u = v; |
/ / |
объект |
u |
присваивается |
Если в классе нет конструктора копирования, первая строка может быть для вас неожиданной. Вторая строка приведет к проблемам при отсутствии в классе совмещенной операции присваивания. Конструктор копирования для второй стро ки не вызывается.
474 |
Часть II * Объектно-:-', |
Операция присваивания должна копировать отличные от указателей элементы данных из объекта-параметра в целевой объект, выделять область памяти доста точного размера и копировать содержимое динамически распределяемой памяти параметра в динамическую память целевого объекта. Эти действия аналогичны действиям конструктора копирования.
1.Копирование длины символьного массива (параметра) в элемент 1еп целевого объекта.
2 . Выделение динамической памяти и установка на нее указателя str целевого объекта.
3 . Проверка на успешное распределение памяти. Отказ, если в системе нет памяти.
,4 . Копирование символов из объекта-параметра
ввыделенную область памяти.
Внимание Если нужно присвоить один объект другому и объект работает с динамически распределяемой памятью, то следует убедиться, что класс содержит перегруженную операцию присваивания. Конструктора копирования здесь недостаточно.
Ниже приведена версия операции присваивания.'Хотя она работает медленнее, чем операция присваивания, предусмотренная системой, но сохраняет семантику значений.
void |
String::operator = (const |
String& s) |
||
{ len = s.len; |
|
/ / |
копирование данных, отличных от указателя |
|
str |
= new char [len |
+ 1]; |
/ / |
выделение собственной области |
|
|
|
/ / |
в динамической памяти |
i f |
(str == NULL) exit(1); |
/ / |
проверка на успех |
|
strcpy(str,S . str); |
} |
/ / |
копирование данных в динамически |
|
|
|
|
/ / |
распределяемую область |
Такая операция присваивания интерпретирует целевой объект точно так же, как конструктор копирования, т. е. как будто это новый объект, не имеющий предыстории. В случае конструктора копирования так оно и есть, однако в случае операции присваивания это не так. Целевой объект и уже был создан ранее, т. е. конструктор вызывался при создании объекта и указатель st г был установлен на область в динамически распределяемой памяти. Операция присваивания игнори рует эту динамически распределяемую память. Она устанавливает указатель str на другую область в динамической памяти, а память, выделенная ранее для объек та, теряется. Следовательно, данная операция присваивания приводит к утечке памяти. Это вторая опасность в программе C+ + . Первая неприятность связана с повторным освобождением памяти.
Каков же выход? В отличие от конструктора копирования, операция присваи вания должна предварительно освобождать ресурсы (память), используемые целе вым объектом. Ниже — улучшенная версия перегруженной операции присваивания:
void |
String::operator - (const |
String& s) |
||
{ delete str; |
|
/ / |
это нельзя сделать в конструкторе копирования |
|
len = s.len; |
|
/ / |
копирование данных, отличных от указателя |
|
str |
= new char[len |
+ 1 ] ; |
/ / |
выделение собственной области |
|
|
|
/ / |
в динамической памяти |
i f |
(str == NULL) exit(1); |
/ / |
проверка на успех |
|
strcpy(str,S . str); |
} |
/ / |
копирование данных в динамически |
|
|
|
|
/ / |
распределенную область |
476 |
Часть li * Объектно-ориентированное програ1М1^ироеание но С'^^ |
В клиенте всегда можно указать последовательность двухместных операций:
v; |
//двухместная |
операция: |
и. operator=(v); |
и; |
//двухместная |
операция: |
t. operator=(u); |
Между тем снова встает вопрос об одинаковой интерпретации встроенных типов и типов, определяемых программистом. Для переменных встроенных типов такая цепочка в программе C++ вполне законна. Следовательно, она должна допускаться для всех переменных определяемых программистом типов.
Операция присваивания ассоциативна справа налево, поэтому смысл цепочки следуюидий:
t = (и = v); |
/ / возвращает тип void - это не поддерживается |
|
|
Листинг 11.7 представляет модифицированную версию программы из листин |
||||
|
|
га 11.6. Здесь добавлена перегруженная операция присваивания. Для запроса |
||||
|
|
динамически распределяемой памяти и проверки ее успешного выделения вызыва |
||||
|
|
ется закрытая функция allocate(). Чтобы сократить объем отладочного вывода, |
||||
|
|
здесь удалены операторы вывода сообщений "Исходный" из используемого по |
||||
|
|
умолчанию конструктора. Кроме того, исключен вызов операции конкатенации |
||||
|
|
|
|
operator+=() в цикле клиента, загружающий на |
||
Создан: |
'Атланта' |
|
звания городов в базу данных. Она заменена на |
|||
Присвоен: 'Атланта' |
|
операцию присваивания. Результат программы по |
||||
Скопирован: |
'Атланта' |
|
казан на рис. 11.18. |
|
||
Создан: |
'Бостон' |
|
Как можно видеть, проблема целостности ис |
|||
Присвоен: 'Бостон' |
|
|||||
Скопирован: |
'Бостон' |
|
чезла. С объектами типа String можно работать |
|||
Создан: |
'Чикаго' |
|
точно так же, как с объектами встроенных число |
|||
Присвоен: 'Чикаго' |
|
|||||
Скопирован: |
'Чикаго' |
|
вых типов. Допускается их создание без инициали |
|||
Создан: |
'Денвер' |
|
зации, инициализация из символьного массива или |
|||
Присвоен: 'Денвер' |
|
|||||
Скопирован: |
'Денвер' |
поиска: Денвер |
из другого, ранее созданного объекта String. Обра |
|||
Введите |
название города для |
тите внимание, что C++ не позволяет делать это |
||||
Создан: |
'Денвер' |
|
с массивами: массивы C + + |
реализуют семантику |
||
Присвоен: 'Денвер' |
|
|||||
Скопирован: |
'Денвер' |
|
ссылок, а не семантику значений. |
|||
Город Денвер |
найден |
|
Можно добавить в класс столько арифметиче |
|||
Рис. 11-18- |
Результат |
выполнения |
ских операций, сколько требуется (например, для |
|||
сложения |
объектов String, |
их вычитания, умно |
||||
|
|
программы |
из лист>ипга 11.7 |
жения и |
т.д.). Между тем |
не нужно забывать |
|
|
|
|
о приложении программиста.
478 |
|
Часть II • Объектно-ориентированное программирование на C-t-i- |
|||||||
bool String::operator==(const String& s) const |
// сравнение содержимого |
||||||||
{ |
return strcmp(str,s.str)==0: } |
|
// при совпадении strcmp возвращает О |
||||||
const char* String::show() const |
|
// защита данных отизменений |
|||||||
{ |
return str; } |
|
|
|
|
|
|
||
void String: :moclify(const char a[]) |
// передача позначению |
||||||||
{ |
strcpy(str,a,len-1); |
|
|
// защита отпереполнения |
|||||
|
str[len-1] = 0; } |
|
|
|
|
||||
String enterDataO |
|
|
|
// запрос пользователю |
|||||
{ |
cout « |
"Введите название города для поиска: |
|||||||
|
char clata[200]; |
|
|
|
|
// грубое решение |
|||
|
cin » data; |
|
|
|
|
|
// принять ввод от пользователя |
||
|
return String(data); } |
|
|
// вызов конструктора |
|||||
int mainO |
|
|
endl; |
|
|
|
|||
{ |
cout «endl « |
|
|
|
|||||
|
enum {MAX = 4 } |
; |
|
|
|
// база данных объекта |
|||
|
String data[4]; |
|
|
|
|
||||
|
char |
*c[4] = { |
"Атланта", 'Бостон", "Чикаго" |
"Денвер" }; |
|||||
|
for |
(int |
j=0; |
j<MAX; |
j++) |
|
|
// присваивание: |
|
|
{ data[j] |
= c [ j ] ; } |
|
|
|
||||
data[j].operator+=(c[j]); |
|
|
|
||||||
|
String u; |
int |
i ; |
|
|
|
|
// нужно присваивание, нет |
|
|
u = enterDataO; |
|
|
|
|||||
|
for (i=0; i < MAX; i++) |
|
|
// конструктора копирования |
|||||
|
|
|
// i определено вне цикла |
||||||
|
{ if (data[i] == u) break; } |
|
|
// выход, если строка найдена |
|||||
(data[i].operator==(u)) |
|
|
|
||||||
|
if (i == MAX) |
|
|
u.showO |
« |
' не найден\п" |
|
||
|
cout « |
"Город « |
|
||||||
|
else |
|
" Город « |
U.showO |
« |
' найден\п"; |
|
||
|
cout « |
|
|||||||
|
return 0; |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
Вопросы производительности
Если нужно инициализировать один объект с помоидью другого объекта (при определении, передаче, параметров по значению или возврате значения из функции), то следует предусмотреть конструктор копирования. Если требуется присвоить один объект другому, используйте перегруженную операцию присваивания.
Проблемы целостности программы, которые могут возникать из-за динамиче ского управления памятью, настолько опасны, что многие программисты реали зуют конструктор копирования и операцию присваивания для каждого класса, работающего с динамической памятью. Часто они это делают даже для класса, не управляющего памятью динамически.
Предотвратить появление проблем можно следуюш^1м образом. Разработчики должны изучить требования клиентской программы и понять последствия различ ных решений.
Существует также ряд проблем, связанных с реализацией в классе избыточ ного числа функций-членов. Одна из них — слишком объемный исходный код класса. Когда программист, сопровождающий программу (или занимающийся клиентской частью), просматривает все эти бесполезные функции, он отвлекается от более важных деталей. Еще одна проблема — производительность. Как видно
Глава 11 • Конструкторы и деструкторы: потенциальные проблемы |
479 |
на рис. 11.18, могут возникнуть проблемы с производительностью. Каждому при сваиванию вводимой строки в цикле соответствуют два вызова функций и опера ция присваивания:
1. Вызов конструктора преобразования для параметра функции operator=()
2. Вызов самой функции operator=()
3. Вызов конструктора копирования для возврата по значению из операции присваивания
Несмотря на все усилия, сохраняется большая разница между объектами клас са и встроенными значениям. В данном цикле, если массивы clata[] и с[] имеют компоненты встроенных типов, оператор будет только один. Для класса String все иначе: тело цикла представляет три вызова функции:
for (int j=0; j<MAX; |
j++) |
{ clata[j]=c[j]; } |
//присваивание: data[j] . operator=(String(c[[j])); |
Обратите внимание, что все эти операции обходятся достаточно дорого. Кроме самого вызова функции, каждая из них влечет за собой копирование параметрастроки в динамически распределяемую область памяти, вызов конструктора, возврат системе динамически распределяемой памяти. Однократное выполнение таких операций неизбежно для присваивания, поддерживающего для раздельных областей динамической памяти операндов семантику значений. Многократное их повторение для параметра операции присваивания и ее возвращаемого значения кажется чрезмерным. К тому же объект, генерируемый конструктором копирова ния, не используется клиентом и уничтожается после вызова деструктора.
Первое решение:
больше перегруженных операций
Есть два способа, позволяющих повысить производительность перегруженной операции присваивания. Изменение типа параметра операции присваивания со String на символьный массив дает возможность исключить вызов конструктора преобразования.
String String::operator=(const char s [ ] ) |
/ / массив как параметр |
||
{ delete |
str; |
/ / этого не следует делать в конструкторе копирования |
|
1еп = strlen(s); |
// выделение памяти, копирование входящего текста |
||
str = allocate(s); |
|||
cout « |
"Присвоен: '" « |
str << " '\п"; |
// для отладки |
return *this; } |
|
|
|
|
|
Если нужно поддерживать операцию присваи |
|
|
|
вания для символьных массивов и для объектов |
|
|
|
St riag, следует дважды перегрузить операцию при |
|
|
|
сваивания: для объекта String и для символьного |
|
|
|
массива, который выступает в роли параметра. |
|
|
|
Результат программы из листинга 11.7 со второй |
|
|
|
операцией присваивания |
показан на рис. 11.19. |
|
|
В отладочном сообщении второй операции присва |
|
|
|
ивания добавлено несколько пробелов. Вы можете |
|
|
|
различать сообщения, выведенные первой опера |
|
|
|
цией присваивания (с параметром типа String), |
|
|
|
и второй операцией присваивания (с параметром |
|
|
|
типа символьного массива). |