Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfГлава 11 • Конструкторы и деструкторы: потенциальные проблемы |
453 |
в динамически распределяемой области и копирования настояш.его содержимого объекта и в динамическую память (шаги 1 —4 алгоритма). Часть В показывает со стояние динамически распределяемой памяти после шага 5. Часть С изображает состояние объектов после установки указателя str целевого объекта и на выде ленную в динамической области память (шаг 6).
Если собрать все вместе, то получится:
void |
String::operator += (const String |
s) |
/ / |
параметр-объект |
|
{ char* p; |
|
/ / |
локальный указатель |
||
len |
= strlen(str) |
+ strlen(s . str); |
/ / |
общая длина |
|
p = new char[len |
+ 1 ] ; |
/ / |
распределение динамической памяти |
||
i f |
(p==NULL) exit(1); |
/ / |
проверка на успех |
||
strcpy(p,str); |
|
/ / |
копирование |
первой части результата |
|
strcat ( p . s . s t r ) ; |
|
/ / |
конкатенация |
второй строки |
|
str |
= р; } |
|
/ / |
str указывает на новую память |
|
Возможно, такой подробный разбор этапов алгоритма кажется лишним. Но вряд ли так думает большинство читателей. Многим операции с указателями кажутся сложными и непонятными.
Только опытные программисты могут заметить, что здесь не возвраш,ается дол жным образом память, принадлежаш^ая целевому объекту.
Изображение подобных схем — единственный способ для понимания механиз мов распределения памяти и выявления ошибок. Лучше потратить несколько лишних минут на рисование и планирование, чем потерять часы на работу с отлад чиком и другими сложными инструментами.
Конечно, такие схемы — всего лишь инструменты. Программисты должны использовать их, чтобы добиться понимания каждого оператора.
Предотвращение ^^утечек памяти'^
Как уже упоминалось, на рис. 11.7 показано, что есть проблема с возвратом символьного массива в динамически распределяемой области памяти, на который ссылается целевой указатель str в начале вызова функции. Когда указатель str устанавливается на вновь выделенный сегмент памяти (куда ссылается локальный указатель р), массив становится недоступным. "Утечка памяти" — распростра ненная ошибка при управлении памятью. Чтобы предотвратить ее, нужно вернуть символьный массив до того, как указатель str будет установлен на новый выде ленный массив.
void |
String:-.operator += (const String s) |
/ / |
параметр-объект |
{ char* p; |
/ / |
локальный указатель |
|
len |
= strlen(str) + s t r l e n ( s . s t r ) ; |
/ / |
общая длина |
p = new char[len + 1]; |
/ / |
распределение динамической памяти |
|
i f |
(p==NULL) exit(1); |
/ / |
проверка на успех |
strspy(p,str); |
/ / |
копирование первой части результата |
|
strcpy(p,s . str); |
/ / |
конкатенация второй строки |
|
delete str; |
/ / |
возврат отведенной динамической памяти |
|
str |
= р; } |
/ / s t r указывает на новую память |
|
Рис. 11.8 аналогичен рис. 11.7. Он показывает, что динамически распределя емый символьный массив, на который указывает целевой элемент данных str, исчезает в результате операции delete. Только после этого указатель str устанав ливается на новый массив в динамически распределяемой области памяти.
Поговорим об "ут^ечке памяти". Покажем, какие опасности связаны с исполь зованием различных средств. При написании программы С+-Н никогда не нужно забывать о них. Как часто бывает, источником проблемы здесь является передача объектов в качестве значений параметров.
Глава 11 • Конструкторы и деструкторы: потенциальные проблемы |
|
|
455 |
|||||||||||
str |
п |
W |
Mi \П i |
1 |
|
|
|
|
|
|
|
|
|
|
^ |
П1 \и |
|
|
|
|
|
|
|
|
|
||||
ten |
11^ 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Server code; |
|
|
|
|
||
А) |
|
|
|
|
|
|
|
Ien += s.len; |
|
|
|
|
||
|
Н |
Hi \0 |
|
str |
|
p = new char[len+1]; |
|
|||||||
|
|
|
|
|
||||||||||
|
|
|
1 ^ |
strcpy(p,str); |
|
|
|
|
||||||
|
|
|
|
|
|
ien |
|
|
|
|
||||
|
|
|
|
|
|
strcat(p,s.str); |
|
|
|
|||||
|
|
|
|
|
|
|
|
|
|
|
||||
|
|
["]—>-[ Hi there!\0 |
|
|
|
|
|
|
|
|
|
|||
8) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
str |
^ j |
0 |
|
|
|
str |
|
Server code; |
|
|
|
|
||
|
|
|
|
|
|
deiete str; |
|
|
|
|
||||
ien |
1 |
|
Hi there!\0 |
ien |
1 6 |
str = p; |
|
|
|
|
||||
|
|
D ^ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
П |
|
|
1 |
^ |
• |
1 |
|
|
|
|
|
|
|
str |
1 |
|
N |
/ |
|||
C) |
|
|
|
|
|
|
— ^ |
|
th»epe!\0 |
|
||||
|
|
|
|
|
|
|
|
|
1 |
• •, |
ЧN |
1 |
||
str |
F^J |
|
|
|
str |
ien |
1^ 1 |
|
|
|
|
|||
ien |
|
|
|
ien |
I 6 1 |
Server code; |
|
|
|
|
||||
|
1 Ч[Р^Ёtliere!\0 |
|
|
} // конец функции |
|
|||||||||
Рис. 1 1.9. Диаграмма памяти для передачи объекта |
String |
|
|
|
|
|||||||||
|
по значению |
|
|
|
|
|
|
|
|
|
|
|||
В результате указатели фактического аргумента и его локальной копии ссыла ются на одну и ту же область в динамически распределяемой памяти. Каждый объект считает, что использует эту память эксклюзивно.
Данная ситуация изображена на рис. 11.9, где показан локальный объект с эле ментами данных, инициализированными значениями фактического аргумента v. Рис. 11.9(A) демонстрирует, что этот локальный объекту и фактический аргу мент U совместно используют одну и ту же область динамически распределяемой памяти. На рис. 11.9(B) видно, что после распределения новой области динамиче ской памяти, инициализации и замены суидествующей области динамической па мяти в целевом объекте локальный объект s и аргумент и продолжают совместно использовать обшую область динамически распределяемой памяти.
Теперь — о завершении функции. Когда функция достигает закрывающей фи гурной скобки ее области действия и она завершает работу, локальный объект (String s) уничтожается. Обычно это означает, что исчезает (освобождается) па мять объекта (в данном случае указатель и целое значение). Между тем в С+ + не уничтожается объект. Каждому уничтожению объекта предшествует вызов особой функции — деструктора.
При вызове деструктора происходит то, что написано в его коде: он возвраш,ает сегмент памяти, на который ссылается указатель объекта.
String: |
:"'String() |
|
|
{ delete |
[ ] str; } |
/ / |
возврат динамической памяти, |
|
|
/ / |
на которую ссылается указатель |
456 |
Часть II • Объектно-ориентированное oporpoi^r^^i: |
|
|
|
|
|
|||||||||
|
Рис. 11.9(C) показывает состояние локального объекта s и фактического dyiy- |
||||||||||||||
|
мента V после вызова деструктора и перед уничтожением локального объекта. Он |
||||||||||||||
|
демонстрирует, что локальный объект и фактический аргумент теряют свою дина |
||||||||||||||
|
мически распределяемую память. (Освобождается память, на которую ссылается |
||||||||||||||
|
указатель str.) Конечно, это действие не влияет на состояние целевого объекта, |
||||||||||||||
|
так как он не уничтожается. При завершении работы перегруженной операторной |
||||||||||||||
|
функции целевой объект находится в том же состоянии, что и при предыдущем |
||||||||||||||
|
обсуждении (см. рис. 11.8). Клиент даст корректные результаты. |
|
|
||||||||||||
|
String uC'Hi " ) ; |
String v("there!"); |
|
|
|
|
|
|
|
||||||
|
u += v; |
|
|
|
|
|
|
|
|
/ / |
выводит "Hi |
there!" |
|||
|
cout |
« |
" и = " « |
u.showO |
« |
endl; |
|
|
|||||||
|
Между тем возвращаемая конструктором память при уничтожении формального |
||||||||||||||
|
параметра s уже не принадлежит данному объекту. Она относится к фактическо |
||||||||||||||
|
му аргументу, т. е. к определенному в пространстве клиента объекту v. После вы |
||||||||||||||
|
зова функции объект клиента, который используется как фактический аргумент |
||||||||||||||
|
для передачи по значению, теряет свою динамически распределенную память. |
||||||||||||||
|
Использовать ее после данного вызова в клиенте будет ошибкой. |
|
|
||||||||||||
|
String |
U ("Hi |
'); |
String v("there!"); |
|
// выводит "Hi |
" |
|
|||||||
|
cout |
« |
" |
u = ' |
« |
U.showO |
« |
endl; |
|
|
|||||
|
cout |
« |
" |
V = ' |
« |
v.showO |
« |
endl; |
|
// выводит "there!" |
|||||
|
u += v; |
" u = '« |
U.showO |
« |
endl; |
|
// выводит "Hi there!" |
||||||||
|
cout « |
|
|||||||||||||
|
cout « |
•' V = '« |
V.showO |
« |
endl; |
|
// выводит все, что угодно |
||||||||
|
Не нужно проверять значение объекта v, который только что отображен на экра |
||||||||||||||
|
не и использован как г-значение в вызове функции operator+=(). Это сделано |
||||||||||||||
|
здесь только потому, что проблема сданной реализацией известна заранее. Ясно, |
||||||||||||||
|
что у объекта должно быть то же значение, что и при использовании в качестве |
||||||||||||||
|
операнда |
в выражении и += v. В большинстве случаев (но не во всех) в С+ + |
|||||||||||||
|
работает интуиция программиста. Поэтому нужно выработать альтернативную |
||||||||||||||
|
интуицию. Стоит еще раз повторить это. Даже |
в таком |
невинном |
на |
первый |
||||||||||
bti Chapter |
|
|
|
|
|
|
|
_^^ |
взгляд клиентском коде значением |
||||||
|
|
|
|
|
|
|
i^*^l |
(текстом) в объекте v может быть |
|||||||
u = Это тест. |
|
|
|
|
|
|
|
все, |
что угодно, и любое исполь |
||||||
V = Ничего |
плохого. |
|
|
|
|
|
|
|
зование данного объекта, предпо |
||||||
U = Это тест.Ничего плохого. |
|
|
|
|
|
|
|||||||||
V = Давайте |
надеяться на л1 I |
I |
I I |
|
|
|
|
лагая, |
что |
он имеет |
то же |
||||
|
|
|
|
состояние, что и прежде, будет |
|||||||||||
|
|
|
|
|
|
|
|
|
|||||||
^mcrosoftУi%lmШ*ШovШШ |
|
|
|
|
безрассудным. |
|
|
|
|||||||
|
|
|
|
|
|
|
|
|
Есть еще одна скобка, завер |
||||||
|
Program: .AMICROSOFT VISUAL |
|
|
|
шающая область действия. Обра |
||||||||||
|
|
|
|
щайте внимание на все фигурные |
|||||||||||
|
STUDIO\MYPROJECTS\CHATER\DEBUG\CHAPTER.[ |
||||||||||||||
|
|
|
|
|
|
|
|
|
скобки, |
ограничивающие |
области |
||||
|
Expression: _BLOCK_TYPE_IS__VALID(pHead->nBiockUse) |
действия. Они выполняют немало |
|||||||||||||
|
работы. |
Когда |
клиент |
достигает |
|||||||||||
|
|
|
|
|
|
|
|
|
|||||||
|
For information on how your program can cause an assertion |
закрывающей |
фигурной |
скобки, |
|||||||||||
|
failure, see the Visual C++ documentation on asserts. |
|
и область действия завершается, |
||||||||||||
|
(Press Retry to debug the application) |
|
|
||||||||||||
|
|
|
для |
всех |
локальных |
объектов, |
|||||||||
|
|
|
|
|
|
|
|
|
включая объект v, который исполь |
||||||
|
|
|
|
|
|
|
|
|
зовался |
как |
фактический аргумент |
||||
|
|
|
|
|
|
|
|
|
при обращении к функции, вы |
||||||
Рис. 11.10. Результат |
выполпепия |
программы |
|
зываются деструкторы |
класса. |
||||||||||
|
Деструктор пытается освободить |
||||||||||||||
|
из лист,инга |
11.3 |
|
|
|
|
^ |
rj |
г |
|
|
|
|
||
Глава 11 • Конструкторы и деструкторь!: потенциальные проблемы |
457 |
память, на которую указывает элемент данных str. Между тем, эта память уже возвраш,ена системе. При разработке языка можно было бы оформить такой вызов как операцию "по ор". В С+Н- повторное использование операции delete
стем же указателем запрещено. Это ошибка.
Ксожалению, "ошибка" не означает, что компилятор сообш^ит о синтаксиче ской ошибке, которую можно исправить. Разработчик компилятора не следит за ходом выполнения программы и не выявляет ошибки программиста — анали зируется лишь синтаксическая корректность кода. Это также не означает, что программа компилируется, выполняется и дает повторяющиеся некорректные результаты. Все зависит от платформы. На поведение приложения влияет опера ционная система. Система может аварийно завершить работу.
Листинг 11.3 демострируетет полную программу, реализующую такую плохую архитектуру. Вывод программы показан на рис. 11.10.
Листинг 11.3. Перегруженная функция конкатенации с параметром-значением
#inclucle <iostream> using namespace std;
class String { |
|
|
// динамически распределяемый символьный массив |
||||
|
char |
*str; |
|
|
|||
|
int |
len; |
|
|
|
|
|
public: |
|
|
|
// конструктор преобразования поумолчанию |
|||
|
String |
(int |
length=0); |
|
|||
|
String(const |
char*); |
|
// конструктор преобразования |
|||
|
"String |
0 ; |
|
|
// освобождение памяти |
||
|
void operator += (const String); |
// конкатенация с другим объектом |
|||||
|
void modify(const char*); |
// изменение содержимого массива |
|||||
|
const char* showO const; |
// возврат указателя массива |
|||||
String::String(int length) |
|
|
|
||||
{ |
len = length; |
|
|
|
|||
|
str = new char[len+1]; |
|
// проверка науспех |
||||
|
if (str==NULL) exit(1); |
|
|||||
|
str[0] = 0; } |
|
// пустая строка нулевой длины - ОК |
||||
String::String(const char* s) |
// определение длины входного текста |
||||||
{ |
len = strlen(s); |
|
|||||
|
str = new char[len+1]; |
|
// выделение достаточной памяти вдинамической области |
||||
|
if (str==NULL) exit(1); |
|
// проверка науспех |
||||
|
strcpy(str,s); } |
|
// копирование входной строки в динамически |
||||
|
|
|
|
|
|
// распределяемую память |
|
String: :"'String() |
|
// возврат памяти вдинамической области (не указателя) |
|||||
{ delete str; } |
|
|
|||||
void String::operator += (const String s) |
// передача noзначению |
||||||
{ |
len = strlen(str) + strlen(s.str); |
|
// общая длина |
||||
|
char *p = new char[len + |
1]; |
// выделение достаточного объема памяти |
||||
|
if (p==NULL) exit(1); |
|
// проверка науспех |
||||
|
strcpy(p,str); |
|
// копирование первой части результата |
||||
|
strcat (p.s.str); |
|
// добавление второй части результата |
||||
|
delete str; |
|
|
// важный шаг |
|||
|
str = p; } |
|
|
// теперь р может исчезнуть |
|||
const char* String::ShowO |
const |
// защита данных отизменений |
|||||
{ |
return str; } |
|
|
|
|
||
458 |
Часть II • Объектно-ориентированное профоммирование на C++ |
||
void String: :mociify(const char a[]) |
/ / |
здесь нет управления памятью |
|
{ |
strncpy(str,a,len-1); |
/ / |
защита от переполнения |
|
str[len - 1] = 0; } |
/ / |
правильное завершение строки |
int mainO
{String иС'Проверка");
String у("Ничего плохого не случится");
cout |
« |
' |
u = |
« |
u.showO |
« |
endl; |
/ / |
результат OK |
cout |
« |
' |
V = |
« |
v.showO |
« |
endl; |
/ / |
результат OK |
u += v; |
|
|
|
|
|
|
/ / |
u.operator+=(v); |
|
cout |
« |
'' |
u = |
« |
U.showO |
« |
endl; |
/ / |
результат OK |
cout |
« |
'' |
V = |
« |
V.showO |
« |
endl; |
/ / |
результат не OK |
v.modify("Давайте |
надеяться на лучшее."); |
/ / |
порча содержимого памяти |
||||||
cout |
« |
" V = " |
« |
V.showO |
« |
endl; |
/ / |
???? |
|
return 0; |
|
|
|
|
|
|
|
||
} |
|
|
|
|
|
|
|
|
|
Обратите внимание, что все неприятности происходят при завершении функ ции. Первая проблема возникает, когда завершается серверная перегруженная операторная функция operator+=() и вызывается деструктор для формального параметра — фактический аргумент v теряет свою динамически распределяемую память. Вторая неприятность случается, когда завершает работу клиент main() и объект V оказывается вне области действия. В этом случае память освоболодается повторно.
В С+Ч- повторное освобождение динамически распределяемой области памяти считается ошибкой. Удаление же указателя NULL не будет ошибкой. Это "пустая операция". Некоторые программисты пытаются решить проблему, присваивая в деструкторе указателю на динамическую память значение NULL.
String: i^StringO |
|
|
|
{ delete |
str; |
/ / |
возврат динамически распределяемой памяти |
str = 0; |
} |
/ / |
установить в null, чтобы избежать |
|
|
/ / |
двойное освобождение памяти |
Устанавливаемый в нуль указатель принадлежит объекту, который через не сколько микросекунд будет уничтожен. Вы могли бы установить в нуль второй указатель, ссылаюидийся на ту же память, но он недоступен в деструкторе, выпол няемом в другом объекте. Если бы даже такой способ сработал, то можно было бы лишь предотвратить "ошибку" а не восстановить некорректно удаленную память.
Переход из пункта А в пункт В
Внимательно относитесь к управлению динамической памятью в программах. Даже если программы выполняются на машине правильно, вовсе не очевидно, что программа корректна.
В течение месяца или нескольких лет программа может работать корректно. Однако после установки каких-то других приложений или перехода на следующую версию Windows изменяется характер использования памяти, и программа за вершается аварийно или дает некорректные результаты. Это может остаться незамеченным, т. к. ранее программа всегда работала корректно. Что же делать? Ругать Microsoft, поскольку вы только что модернизировали операционную сис тему? Но Microsoft тут ни при чем! Это ошибка программиста, забывшего включить один символ & в интерфейс перегруженной операторной функции, такой как operator-b=().
Глава 11 • Конструкторы и деструкторы: потенциальные проблемы |
459 |
||||||
Вот как должна выглядеть эта функция. Она не передает свой параметр-объект |
|||||||
по значению. Он передается по ссылке. |
|
|
|
|
|||
void String: .-operator += (const |
String |
&s) |
/ / |
параметр-ссылка |
|||
{ len |
= strlen(str) + s t r l e n ( s . s t r ) ; |
|
/ / |
общая длина |
|||
char |
*p = new char[len + 1]; |
/ / |
выделение достаточного |
объема памяти |
|||
i f (p==NULL) e x i t d ) ; |
/ / |
проверка |
на успех |
|
|||
strcpy(p,str); |
/ / |
копирование первой части результата |
|||||
s t r c a t ( p , s . s t r ) ; |
/ / |
добавить |
вторую часть |
результата |
|||
delete |
str; |
/ / |
важный шаг |
|
|
||
str - p; |
} |
/ / |
str указывает на новую память |
||||
U = Проверка |
|
|
|
На рис. 11.11 представлен результат программы из |
||
|
не случится |
|
листинга 11.3 с функцией конкатенации, передающей |
|||
V = Ничего |
|
плохого |
|
параметр по ссылке. |
||
U = Проверка |
Ничего |
плохого не случится |
Попробуйте выполнить эту программу, поэкспери |
|||
V = Ничего |
|
плохого |
не случится |
|
||
V = Давайте |
надеяться на л |
|
ментируйте с ней. Не поддавайтесь желанию переда |
|||
Press any |
key to continue. |
|
вать объекты по значению, если в том нет абсолютной |
|||
|
необходимости. |
|||||
Рис. 11.11. |
|
|
|
Конечно, разочаровывает то, что изменить програм |
||
|
|
|
му можно с помошд^ю добавления или удаления всего |
|||
Результат |
|
программы из листпинга 11,3 |
одного символа в исходном коде программы (амперсан- |
|||
с операцией |
копкатпепации, |
передающей |
да). Обратите внимание, что обе версии синтаксически |
|||
параметр |
по |
ссылке |
|
|||
корректны. Компилятор не показывает, что могут воз никнуть проблемы.
Передавать параметры-объекты по значению — все равно, что ездить на танке. Вы попадете, куда хотите, но при этом натворите немало бед. Не спешите передавать объекты по значению, если это не является срочным решением.
О с т о р о ж н о ! Не передавайте функциям объекты по значению. Если объекты имеют внутренние указатели и могут работать с динамически распределяемой областью памяти, то не стоит даже и думать о передаче объектов
по значению. Отправляйте их по ссылке. Используйте модификатор const,
если функция не изменяет состояние объекта-параметра и целевого объекта.
Конструктор копирования
Поговорим о копировании объекта, элементы данных которого представляют собой указатели на динамически распределяемую память.
Предполагается, что каждый экземпляр объекта ссылается на специально вы деленную для него область памяти. Например, класс String содержит указатель, ссылаюпшйся на область динамически распределяемой памяти, которая имеет связанные с конкретным объектом String символы.
Когда элементы данных одного объекта копируются в элементы данных дру гого, соответствующие указатели обоих объектов будут иметь одно содержимое, т. е. ссылаться на одну область динамически распределяемой памяти. Эти объекты могут прекратить свое суш.ествование в разное время. Например, значение фор мального параметра функции в листинге 11.3 исчезает, когда функция завершает работу, а фактический аргумент продолжает суш,ествовать в пространстве клиен та, в функции main(). Когда объект исчезает, деструктор освобождает память, на которую ссылаются указатели объекта. Второй существующий объект также те ряет свои данные в динамически распределяемой области. Применение объекта с такими данными будет некорректным и даст "ошибку".
Если возвращенная динамически распределяемая память не будет занята немед ленно, то такой "фантомный" объект может вести себя совершенно нормально, как если бы его память существовала. Тестирование может показать програм мисту, что программа корректна.
