
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf464 |
Часть il * Объектно-opi |
:^ирование |
Семантика копирования и семантика значений
Суш,ествуют два обндих подхода в программировании, соответствующих двум разным концепциям вычислительной техники — семантика значения и семан тика ссылок. (Под семантикой здесь понимается смысл копирования данных.)
В самом распространенном подходе в программировании используется семан тика значений. Каждый вычислительный объект (например, переменная встроен ного типа или объект определяемого программистом типа) имеет собственную отдельную область памяти. Если один вычислительный объект приравнять к дру гому, повторится битовая последовательность одного объекта в памяти другого объекта. В C + + семантика значений используется как для встроенных перемен ных, так и для объектов определяемого программистом типа.
i nt V = 10; int t = v; t = 20; |
/ / семантика значений, v = 10 |
Такая интуиция более распространена по следующей причине: с ее точки зрения, когда объекты имеют одно значение, они используют две разные битовые последовательности, и изменение одного объекта не влияет на последователь ность битов, уже существующую в другом объекте.
В другой, менее распространенной интуиции программирования используется семантика ссылок. Когда вычислительному объекту присваивается значение, он получает ссылку (или указатель) на это значение. Приравнивание вычислительных объектов означает присваивание их ссылкам одной и той же области памяти. Когда изменяется массив символов, на который указывает один из этих объектов, меняется другой объект, так как оба объекта ссылаются на одну область. В C+ + такая семантика ссылок используется для указателей и ссылок, при передаче па раметров по ссылке или по указателю, для массивов и связанных структур данных с указателями.
int V = 10; int& t = v; t = 20; |
/ / согласно семантике ссылок, v = 20 |
Семантика ссылок распространена меньнле. Она используется в основном из соображений производительности (например, исключает копирование объектов при передаче параметров). Иногда она действует неявно, как в данном примере. Что касается C+ + , то программист всегда должен помнить о разнице между семантикой значений и семантикой ссылок.
Вы узнали еще не обо всех неприятностях с программой из листинга 11.4. Когда при ее выполнении достигается закрывающая фигурная скобка вложенного цикла, объект t должен исчезнуть, так как он определен в этом вложенном цикле. Объект V определен во внешней области действия функции main() и должен быть доступен для дальнейшего использования. В листинге 11.4 показана попытка вы вода значения v в конце функции main(). Обратите внимание, что этот оператор отделен от предшествующего оператора вывода только закрывающей фигурной скобкой вложенной области действия. На первый взгляд, между двумя оператора ми в клиенте не происходит никаких событий. Следовательно, они должны давать один и тот же результат. Но результат разный. В этом случае рекомендуется выра ботать вам собственный подход, который поможет читать подобные фрагменты исходного кода.
Первый оператор дает вполне нормальный результат (см. рис. 11.14). Это не совсем то, что можно было бы ожидать, но, по крайней мере, он есть. Второй результат — просто "мусор". Что произошло между двумя операторами? Когда достигается закрывающая фигурная скобка области действия, для локального объекта t в этой вложенной области вызывается деструктор класса String. Как видно из листинга 11.4 и из рис. 11.14, данный деструктор освобождает динами чески распределяемую память, принадлежащую объекту Ь, но система этого не помнит. Она запоминает лишь то, что память, на которую ссылается str, следует освободить согласно деструктору класса String. У объекта v уже нет динамиче ской памяти, но никто об этом не знает. Формально он находится в области дей ствия. Но это только на первый взгляд.
Глава 11 * Конструкторы и деструкторы: потенциальные проблемы |
465 |
Ситуация аналогична передаче параметра по значению, но это енде не конец. Когда программа достигает закрывающей фигурной скобки, объект v должен ис чезнуть согласно правилам области действия. Перед этим вызывается деструктор и пытается освободить уже освобожденную динамически распределяемую область памяти. Программа некорректна. Она вышла из-под контроля.
Конструктор копирования, определяемый программистом
Конструктор копирования должен распределять динамическую память для це левого объекта в соответствии с операцией конкатенации, о которой рассказыва лось в предыдущем разделе. Вот его алгоритм:
1.Скопировать длину символьного массива параметра в поле 1еп целевого объекта.
2. Выделить динамически распределяемую память: установить на нее указатель str целевого объекта.
3. Проверить, успешно ли выделена память. Отказать, если в системе нет памяти.
4. Скопировать символы из целевого объекта в только что выделенную память.
Конструктор копирования, определяемый программистом и позволяющий ре шить проблему:
String::String(const |
String& s) |
/ / |
определяемый |
программистом |
|
|
|
|
/ / |
конструктор |
копирования |
{ 1еп = s.len; |
|
/ / |
длина исходного текста |
||
str |
= new char[len+1]; |
/ / |
запрос отдельной динамической памяти |
||
i f |
(str == NULL) exit(1); |
/ / |
проверка на успех |
||
strcpy(str, s . s t r ) ; |
} |
/ / |
копирование |
исходного текста |
Обратите внимание, что параметр s передается по ссылке. Это ссылка на фак тический аргумент-объект. При передаче параметра не происходит копирования элементов данных аргумента. Динамическая память фактического аргументаобъекта копируется в динамическую память целевого объекта.
Это менее эффективно, чем элементное копирование, показанное в листин ге 11.4. Семантика значений работает медленнее, чем семантика ссылок. Здесь вы имеете дело со значениями, а не со ссылками или указателями. Между тем, семантика значений надежна. Вспомните вариант клиента, который привел ко всем этим проблемам.
String t = v; |
/ / нет проблем, если используется конструктор копирования |
После выполнения данной строки указатели str в объектах v и t ссылаются на разные области динамически распределяемой памяти. Проблема целостности решена.
Внимание Если среди элементов данных класса встречаются указатели и объекты этого класса работают с динамически распределяемой памятью,
то разработчик класса должен решить, что именно нужно в нем использовать — семантику ссылок или семантику значений. Если необходима семантика значений и требуется инициализировать один объект значением другого объекта, убедитесь, что этот класс имеет определяемый программистом конструктор копирования.
|
|
Глава 11 • Конструкторы и деструкторы: потенциальные проблемы |
467 |
|||||||||||
const |
char* |
String::show() |
const |
|
// защита данных отизменений |
|||||||||
{ |
return str; } |
|
|
|
|
|
|
|
|
|
||||
void |
String: :moclify(const |
char |
a[]) |
// передача по значению |
||||||||||
{ |
strncpy(str,a,len-1); |
|
|
|
|
// защита отпереполнения |
||||||||
|
str[len - 1] = 0; |
} |
|
|
|
|
// правильное завершение строки |
|||||||
int |
mainO |
endl « |
|
endl; |
|
|
|
|
|
|
|
|||
{ |
cout « |
|
|
|
|
|
|
|
|
|||||
|
String иС'Проверка"); |
|
|
|
|
|
|
|
||||||
|
String v("Ничего плохого неслучится"); |
// результат ОК |
|
|||||||||||
|
cout « |
'' |
u = |
« |
u.showO |
« |
endl; |
|
||||||
|
cout |
« |
'' |
V - |
« |
v.showO |
« |
endl; |
// результат ОК |
|
||||
|
u += v; |
|
|
« |
U.showO |
« |
endl; |
// u.operator+=(v); |
||||||
|
cout |
« |
'' |
u = |
/ / |
результат ОК |
|
|||||||
|
cout |
« |
'' |
V = |
« |
V.showO « |
endl; |
/ / |
ОК - передача по ссылке |
|||||
|
v.modify("Давайте надеяться налучшее."); |
/ / |
порча содержимого памяти |
|||||||||||
|
{ |
String t = v; |
|
|
|
|
|
|
|
|
||||
|
|
t.modify("Ничего плохого не случится") |
/ / |
меняем только |
t |
|||||||||
|
|
cout « |
" t = " « t.showO |
« |
endl; |
/ / |
ОК, корректный |
результат |
||||||
|
|
cout « |
" V = " «V . showO |
« |
endl; } |
/ / |
V также изменился |
|||||||
|
cout « |
" V = " « |
V.showO « |
endl; |
/ / |
t больше нет, v теряет память |
||||||||
|
return 0; |
|
|
|
|
|
|
|
|
|
|
|||
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
ВЛистинге 11.5 класс String имеет три конструктора. Они выделяют память
вдинамически распределяемой области и инициализируют ее содержимое. В пер вом конструкторе преобразования инициализирующие данные — это пустая стро ка, завершаемая нулем. Во втором конструкторе преобразования это символьный массив, подставляемый клиентом в фактическом аргументе. В конструкторе копи рования инициализирующими данными является символьный массив внутри объ екта, поставляемого клиентом. Поскольку этот массив находится в динамической памяти, он не имеет имени, а ссылка на него осуществляется через указатель str. Так как параметр-объект s принадлежит к тому же классу String, что и целевой инициализируемый объект, конструктор копирования имеет право доступа к этому закрытому указателю str с помощью уточненного имени s. str.
Вполне естественно, что разные конструкторы используют аналогичные алго ритмы, так как вид полученного в результате объекта не должен зависеть от вызванного при его создании конструктора. Если класс содержит один или два конструктора, повторите код. Если алгоритм используется очень часто, то про граммисты обычно инкапсулируют его в частную функцию и вызывают ее из разных компонентных функций. Данная функция должна быть закрытой, так как клиент не заинтересован в прямой работе с памятью объекта. Это детали нижнего уровня, которые не должны запутывать алгоритм клиента и занимающегося им программиста. Подобная закрытая функция представлена в листинге 11.5. Когда она копирует свой параметр в выделенную динамическую память, то использует имя указателя р.
char* |
allocate(const char* s) |
// закрытая функция |
{ char |
*p = new char[len+1]; |
// выделение памяти для объекта |
if (p==NULL) exitd); |
// проверка на успех; выход, если неповезло |
|
strcpy(p,s); |
// копирование текста в динамическую память |
|
return p; } |
// возврат указателя надинамическую память |
468 |
Часть II о Объвкто-ортешировамиое |
програтмтрошттв на С4-4- |
||||
|
Листинг 11.5 показывает, что первый конструктор преобразования передает |
|||||
|
функции allocateO пустую строку, второй отправляет свой собственный пара |
|||||
|
метр— символьный массив, а конструктор копирования передает |
allocateO |
||||
|
символьный массив своего параметра, т. е. s. str. |
|
|
|||
|
Когда один объект инициализирует другой объект, вызывается конструктор |
|||||
|
копирования. Это неизбежно. Вопрос в том, какой именно конструктор вызывает |
|||||
|
ся. Если класс не предусматривает свой собственный конструктор копирования, |
|||||
|
то компилятор генерирует вызов системного конструктора, который копирует |
|||||
|
элементы данных объекта. Если для объектов этого класса не выделяется динами |
|||||
|
чески распределяемая память, то все замечательно. Если объекты используют |
|||||
|
индивидуальные сегменты динамически распределяемой памяти (семантику зна |
|||||
|
чений), то применение предусмотренного системой конструктора копирования |
|||||
|
подрывает целостность приложения. Чтобы сохранить целостность программы, |
|||||
|
в классе следует использовать собственный конструктор копирования, который |
|||||
|
выделяет целевому объекту свою динамически распределяемую память. |
|||||
|
В предыдущем предложении "следует предусмотреть" подчеркивает взаимо |
|||||
|
связь "клиент-сервер" между разными сегментами программы C + + и ме>кду раз |
|||||
|
ными уровнями понимания. С помощью клиентской программы обрабатываются |
|||||
|
объекты. Сервер поддерживает клиента, реализуя вызываемые клиентом функции- |
|||||
|
члены. Конструкторы вызываются неявно, но при этом не изменяется соотноше |
|||||
|
ние клиент-сервер. |
|
|
|
|
|
|
Если в приложении необходима семантика копирования, в классах с динамиче |
|||||
|
ским управлением памятью используются конструкторы копирования для других |
|||||
|
контекстов, когда один объект инициализирует другой. Одним из таких контекстов |
|||||
|
является передача параметров-объектов по значению. При наличии соответствую |
|||||
|
щего конструктора копирования будет замечательно работать первая версия пере |
|||||
|
груженной операции конкатенации из листинга 11.3. |
|
|
|||
|
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) exit(1); |
/ / |
проверка на успех |
|
|
|
strcpy(p,str); |
/ / |
копирование первой части |
результата |
||
|
strcat (p . s . str); |
/ / |
добавление |
второй части результата |
||
|
delete str; |
/ / |
важный шаг |
|
|
|
|
str |
= р; } |
/ / |
str указывает на новую память |
При вызове данной функции и создании фактического аргумента вызывается определенный программистом конструктор копирования. Он выделяет динамиче скую память для формального параметра s. Когда эта функция завершает работу и для формального параметра вызывается деструктор, освобождается его собст венная динамическая память, а не динамическая память, принадлежащая факти ческому аргументу. Проблема, связанная с целостностью, исчезает. Однако сохраняется проблема производительности. Когда параметр передается по значе нию, вызов операции конкатенации предусматривает создание объекта, вызов конструкторов копирования, выделение динамически распределяемой памяти, копирование символов из одного объекта в другой, вызов деструктора и осво бождение динамической памяти. Для вызова по ссылке не требуется проведение подобных операций. Семантика ссылок решает проблему производительности, поскольку устраняется излишнее копирование.
О с т о р о ж н о ! Не передавайте объекты функциям по значению. Если объекты имеют внутренние указатели и работают с динамически распределяемой памятью, не передавайте эти объекты по значению. Если же необходимо отправить такие объекты по значению, определите конструктор копирования.
Глава 11 • Конструкторы и деструкторы: потенциальные проблемы |
469 |
Возврат по значению
При возврате объекта из функции по значению применяйте семантику значе ний. В главе 10 уже обсуждались классы, не работающие с динамически распреде ляемой памятью. Поскольку ситуация с возвратом объекта из функции в точности такая же, как и при инициализации одного объекта другим, рассмотрим ее.
Исходный |
|
|
Листинг 11.6 представляет еще одну версию |
||
|
|
класса String. В каждый конструктор включены |
|||
Исходный: |
|
отладочные операторы и добавлена перегруженная |
|||
Исходный; |
|
операция сравнения, реализованная как функция- |
|||
Исходный; |
|
||||
Создан: |
Атланта' |
|
член. Кроме того, здесь добавлена функция клиента |
||
Создан: |
Бостон' |
|
enterDataO |
и обновлена функция main(). Програм |
|
Создан: 'Чикаго' |
|
||||
Создан: |
Денвер' |
|
ма просит |
пользователя ввести название города |
|
Введите |
название города для поиска: Бостон |
и ищет это имя в базе данных. Для простоты база |
|||
Создан: |
'Бостон' |
|
данных определена в функции main() как массив |
||
Город |
Бостон найден |
|
|||
|
|
|
|
символов, и используется простой последователь |
|
Рис. |
11,16. Результат |
выполнения |
ный поиск. Результаты выполнения показаны на |
||
\\ \а |
|
||||
|
|
программы |
из листпинга 11.6 |
Р^^- 11-1Ь. |
|
Листинг 11.6. Использование конструктора копирования для возврата объекта из функции
#include <iostream> using namespace std;
class |
String { |
|
|
|
// динамически |
распределяемый символьный массив |
|||||
|
char |
*str; |
|
|
|
||||||
|
int |
len; |
|
|
|
// закрытая функция |
|
|
|||
|
char* |
allocate(const char* s) |
|
|
|
||||||
|
{ |
char |
*p = new char[len+1]; |
|
// выделение динамической памяти для объекта |
||||||
|
i f |
(p==NULL) e x i t d ) ; |
|
|
// проверка науспех; выход в случае неудачи |
||||||
|
strcpy(p,s); |
|
|
// копирование текста вдинамическую |
память |
||||||
|
|
return p; |
} |
|
|
// возврат указателя надинамическую |
память |
||||
public- |
|
|
|
|
|
// конструктор преобразования/по умолчанию |
|||||
|
String |
(int |
length=0); |
|
|
||||||
|
String(const |
char*); |
|
|
// конструктор |
преобразования |
|
||||
|
String(const |
String& s); |
|
|
// конструктор |
копирования |
|
||||
|
"String |
0 ; |
|
|
|
// освобождение динамической памяти |
|
||||
|
void operator += (const String&); |
// конкатенация с другим объектом |
|
||||||||
|
void modify(const char*); |
|
|
// изменение содержимого массива |
|
||||||
|
bool operator == (const String&) const; |
// сравнение содержимого |
|
||||||||
|
const char* showO const; |
|
|
// возврат указателя |
массива |
|
|||||
} ; |
|
|
|
|
|
|
|
|
|
|
|
String::String(int length) |
|
|
|
|
|
|
|||||
{ |
len = length; |
|
|
// копирование пустой строки вдинамическую память |
|||||||
|
str = allocateC'"); |
str « |
|
||||||||
|
cout « |
" Исходный: '" « |
'"\n"; } |
|
|
|
|||||
String::String(const char* s) |
|
// определение длины |
исходного текста |
|
|||||||
{ |
len = strlen(s); |
|
|
|
|||||||
|
str = allocate(s); |
str |
|
// выделение памяти, копирование текста |
|||||||
|
cout « |
" Созданный: '" « |
« '\п"; } |
|
|
|
|||||
String::String(const String& s) |
|
// конструктор |
копирования |
|
|||||||
{ |
len = s.len; |
|
|
|
// определение длины |
исходного текста |
|
||||
|
str = allocate(s.str); |
« |
|
// выделен1де памяти, |
копирование текста |
||||||
|
cout « |
" Скопированный: |
str « "'\п"; } |
|
|
|