
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf370 |
Чос.,., Объектно-ориентированное протрашшшрова^ыв по С-^-^ |
системе. Программист, отвечающий за клиента, должен обеспечить целостность программы. Для объектов, управляемых правилами области действия, такого тре бования нет. Например, объект п1 автоматически удаляется, когда при выполне нии программы достигается закрывающая фигурная скобка функции Client(). Программисту не нужно прилагать никаких усилий, чтобы это произошло. Все это необходимо для управления памятью в том случае, если разработчик серверной части включает в класс конструктор Name.
Время вызова конструктора и деструктора
Термин "конструктор" предполагает, что эта функция создает объект. Термин "деструктор" предполагает, что данная функция объект уничтожает. Не попадай тесь в эту ловушку. Термины описывают конструкторы и деструкторы некорректно.
В предыдущем разделе было отмечено, что конструктор вызывается после со здания объекта, а деструктор — перед его уничтожением. Часто в книгах по С+ + не обращают внимание на подобное различие и утверждают, что конструкторы и деструкторы вызываются при создании и уничтожении объектов. И напрасно, поскольку программисты думают, что конструкторы создают объекты, а деструкто ры — уничтожают их.
Это не так. За создание и уничтожение объектов отвечают правила области действия (для именованных объектов) или операции new и delete (для объектов неименованных). Конструкторы только инициализируют поля объекта после того, как они уже созданы, и распределены дополнительные ресурсы, например, дина мическая память. Деструкторы лишь возвращают ресурсы, необходимые объектам во время их существования, например динамическую память, выделенную в конст рукторах и других функциях.
Область действия класса и подмена имен во вложенных областях
Реальное время вызова конструктора/деструктора зависит от области действия и класса памяти экземпляров объектов.
Область действия определяет доступность переменных и объектов в разных частях программы. Класс памяти определяет срок жизни переменных и объектов от создания до уничтожения. В данном разделе мы продолжим обсуждение области действия и классов памяти, начатое в главе 6. Если вы почувствуете, что эта тема слишком сложна, пропустите ее при первом чтении (остается надеяться, что будет еще и второе чтение). Этот материал важен, но может подождать, пока вы наберетесь опыта написания и чтения исходных кодов C++.
Поскольку глобальные переменные можно определять в любом месте файла, даже после каких-либо определений функций, они недоступны в функциях, опре деляемых в файле раньше, до объявления переменной.
Cylinder Cglobal; |
/ / доступна |
в любом месте файла |
int mainO |
// область действия ограничена main() |
|
{ Cylinder с; |
||
. . . . } |
|
|
int у; |
// невидима в main(), видима в foo() |
|
void foo() |
// неможет вызываться изmain(), если нет прототипа |
|
{ у = 0; |
// доступ к глобальной переменной |
|
Cylinder Clobal; |
|
// компоненты public видимы |
Clodal.setCylinder(10,30) |
||
Cglobal.setCylinder(5,20); } |
// компоненты public видимы |
|
.... |
|
// Cglobal, у, foo() здесь видимы |
В C + + все это законно, ноне является хорошим стилем программирования.
Глава 9 • Классь! C++ как единицы модульности программы |
373 i |
2)критикой приведенного примера за несоблюдение принципов объектно-ориентированного программирования.
Отметим, что конструктор вызывается только после вызова объекта класса, созданного по правилам области действия или операцией new. Он не вызывается после malloc(). Аналогично деструктор вызывается только перед уничтожением объекта согласно правилам области действия или операции delete. Вызов функ ции free О не активизирует деструктор.
Если используются функции malloc() или f гее(), то программист, отвечаюш^ий за клиентскую часть, должен обеспечить возврат памяти в динамически распреде ляемую область, когда объекты станут не нужны. В клиенте нужно распределять динамическую память и позднее освобождать ее, возвращая в динамическую об ласть. Нарушение этой обязанности приводит к порче содержимого памяти и ее "утечкам". Важно различать динамическое управление объектами и динамическое управление выделяемой для объектов памятью, когда элементы данных класса представляют собой указатель на динамическую память.
В листинге 9.4 показан пример, аналогичный приведенному в листинге 9.3, но вместо new для выделения памяти объекту используется функция malloc(). Она выделяется динамически через указатель р. Очевидно, управление памятью здесь будет более сложным. Клиент распределяет в динамической области память для неименованного объекта и затем использует ее для содержимого объекта (строка "Смит"). Результат выполнения данного примера будет тем же, что и про граммы из листинга 9.3. (Здесь выключены операторы отладки в конструкторе и деструкторе.)
Листинг 9.4. Управление памятью в клиенте, а не в сервере
#inclucle <iostream> using namespace std;
struct Name { |
|
|
// указатель public надинамическую память |
|
|
char ^contents; |
|
||
|
Name (char* name); |
|
// или Name (char name[]); |
|
|
void show_name(); |
|
// деструктор исключает утечки памяти |
|
|
~Name(); } |
; |
|
|
Name::Name(char* name) |
// конструктор преобразования |
|||
{ |
int len = strlen(name); |
// число символов |
||
|
contents = new char[len+1]; |
// выделение динамической памяти для аргумента |
||
|
if(contents == NULL) |
// неудачное выполнение 'new' ' |
||
|
{ cout « |
"Her памяти\п"; exit(1); } |
// отказ |
|
|
strcpy(contents, name); } |
// стандартные действия |
||
void Name::show_name() |
|
|||
{ cout « contents « |
"\n"; } |
|
||
Name::-Name() |
|
|
// деструктор освобождает динамическую |
|
{ delete contents; } • |
|
// память, а неудаляет указатель contents |
||
void ClientO |
|
|
// вызывается конструктор преобразования |
|
{ |
Name n1("Джонс"); |
|
||
|
Name *p =(Name)malloc(sizeof(Name)); |
// невызывается конструктор преобразования |
||
|
p->contents = new char[strlen("CMMT")+l]; |
// распределение памяти |
||
|
if (p->contents == NULL) |
// неудачное выполнение 'new' |
||
|
{ cout « |
"Нет памяти\п"; exit(1); } |
// отказ |
|
|
strcpy(p->contents, |
"Смит"); |
// стандартные действия |
|
|
n1.show_name(); p->show_name(); |
// использование объектов |
||
|
delete p->contents; |
|
// для избежания утечек памяти |
374 |
Часть 11 • Объектно-ориентированное програ1ммировани0 на C-^-t- |
||
|
free (р); |
/ / |
обратите внимание на последовательность действий |
} |
|
/ / |
р удаляется, вызывается деструктор для объекта п1 |
int |
mainO |
/ / |
перенос обязанностей на функции-серверы |
{ |
ClientO; |
|
|
|
return 0; |
|
|
} |
|
|
|
|
В данном примере объект п1 создается согласно правилам области действия, |
||
|
конструктор соответственно инициализирует для него динамическую память. |
||
|
Неименованный объект, на который ссылается указатель р, распределяется с по |
||
|
мощью функции mallocO, и конструктор не вызывается. При вызове функции |
||
|
mallocO выделяется только память для объекта (указатель p->contents). До |
||
|
полнительная память из динамически распределяемой области, необходимая для |
||
|
хранения информации (фамилии), не выделяется. Следовательно, клиент распре |
||
|
деляет и инициализирует память в динамической распределяемой области, на |
||
|
которую ссылается p->contents. |
||
|
Когда функция ClientO завершает работу, об удалении объекта п1 и возврате |
||
|
динамически распределяемой памяти можно не беспокоиться. Он уничтожается |
||
|
по правилам области действия, и память возвращается деструктором. Для неиме |
||
|
нованного объекта, на который указывает р, все иначе. В клиенте его не только |
||
|
нужно уничтожать, но и возвращать используемую объектом динамически распре |
||
|
деляемую память. |
|
|
|
В этом небольшом примере применяются классы, объекты, сообщения, дина |
||
|
мическое управление памятью, конструкторы и деструкторы — весь впечатляю |
||
|
щий арсенал программирования на C-f-Ь. Одновременно здесь нарушаются все |
||
|
принципы объектно-ориентированного программирования. Единственное оправ |
||
|
дание в том, что это сделано намеренно. Однако часто программисты делают это, |
||
|
сами того не замечая. Давайте снова разберем весь перечень грехов. |
||
|
В примере нарушен принцип инкапсуляции: клиент использует имена полей |
||
|
объекта contents, создавая тем самым дополнительную зависимость. Если имя |
||
|
поля класса Name изменится, то придется изменять и функцию ClientO. |
||
|
Нарушен также принцип сокрытия информации (как это обсу>кдалось в гла |
||
|
ве 8). Клиент знает, что класс с именем Name использует динамически распределя |
||
|
емую память, а не символьный массив фиксированного размера. Если изменится |
||
|
архитектура класса Name, это повлияет и на функцию ClientO. |
||
|
Такие зависимости вынуждают координировать действия разработчиков и про |
||
|
граммистов. У разработчиков придется выяснить все детали по классу Name, такие, |
||
|
как имена полей, управление динамической памятью и еще бог знает что. И это |
||
|
вместо простого изучения интерфейса функций, которым можно обойтись при |
||
|
использовании переменной п1. |
||
|
Код функции ClientO |
не выражается в терминах вызовов функций-членов |
класса Name. Вместо этого он содержит многочисленные операции доступа к дан ным и манипуляции с данными, поэтому при сопровождении программы придется потратить дополнительное время, пытаясь уяснить, какие именно цели в ней преследуются.
Хуже всего то, что обязанности не переносятся на класс сервера, хотя весь не обходимый для этого сервис имеется. Распределение и освобождение памяти осу ществляется в клиенте, а не с помощью объекта сервера.
Результат весьма плачевный. Клиент получился намного более сложным, чем следовало. Кроме того, здесь легко сделать ошибку — достаточно внести неболь шие изменения в функцию ClientO. В данной версии клиента сначала освобожда ется память, на которую указывает объект р, а затем делается попытка освободить динамически распределяемую память. При выполнении программы операционная система обвиняет ее в нарушении доступа к памяти и принудительно завершает.
Щ 376^ I |
Част II ^ Объектно-ориентерованное орс-^-; --. ..мирование на C-t-^- |
Использование в коде клиента возвращаемых объектов
Функции C + + могут возвращать встроенные значения, указатели, ссылки
иобъекты, но не могут возвраидать массивы (возвращаемые массивы можно имитировать возвратом указателей). Встроенные значения разрешается исполь зовать только как г-значения. Другие возвращаемые значения (указатели, ссылки
иобъекты) допускается использовать как 1-значения. Это открывает интересные возможности, делает исходный код C++ более выразительным, но в чем-то усложняет его понимание.
Материал данной главы можно легко пропустить при первом чтении, хотя обсуждаемые здесь принципы программирования весьма распространены.
Возврат указателей и ссылок
Начнем обсуждение с простых (несоставных) встроенных типов. Значения этих элементарных типов применяются как г-значения, а указатели и ссылки можно использовать как г- и 1-значения.
Рассмотрим следующую версию класса Point. Его функция setPointO изме няет состояние целевого объекта Point, а функции getX() и getY() возвращают целочисленное значение. Функция getPt г() возвращает указатель на элемент дан ных X, а getRef О — ссылку на элемент данных х. Здесь не пре;]усмотрено функ ций, возвращающих указатель и ссылку на элемент данных у, так как функций getPt г() и getRef () достаточно для иллюстрации разных вопросов, включая моди фикацию состояния объекта.
class |
Point |
|
|
|
{ int |
X, |
у; |
/ / |
закрытые данные |
public: |
|
|
|
|
void |
setPoint(int |
а, int b) |
|
|
{ |
X = a; у = b; } |
|
|
|
int |
getXO |
/ / |
возвращает значение |
{return x; } int getYO
{return y; }
int* |
getPtrO |
|
/ / |
возвращает указатель на значение |
{ |
return &х; |
} |
|
|
int& |
getRefО |
|
/ / |
возвращает ссылку на значение |
{ |
return х; |
} } ; |
/ / |
нет операции получения адреса для ссылки |
Чтобы решить, следует ли использовать операцию получения адреса, нужно применять ту же логику, что и при присваивании или передаче параметра. Функ ция getPtrO возвращает указатель. Следовательно, при возврате значениях было бы несоответствие типов — синтаксическая ошибка. Функция getRef () воз вращает ссылку, и эта ссылка может (и должна) инициализироваться значением, на которое она будет указывать до конца своей жизни. Следовательно, использо вание &х дало бы несоответствие типов (синтаксическая ошибка). Это адрес, а не значение int.
Когда функция возвращает значение, ее можно использовать только как г-значение в правой части присваивания или сравнения (или как входной параметр в вызове функции). В данном примере клиент манипулирует с возвращаемым функцией значением. Значение изменяется, но объект, чье значение возвращает функция, не модифицируется, поскольку при возврате по значению создается копия значения-оригинала (подобно передаче параметров по значению):
Глава 9 * Классы С-^^- кок единицы модульности программы |
377 |
||
Point pt; pt.setPoint(20,40); |
|
|
|
int a = pt.getXO, b = 2* pt.getYO + 4; |
/ / |
OK, используется |
как г-значение |
a += 10; |
/ / |
'a' изменяется, |
но pt.x нет |
Возвращаемые функцией указатель или ссылку можно использовать как г-зна чение и как 1-значение в левой части присваивания либо как входной параметр при вызове функции. В следующем примере первая строка интерпретирует возвраща емые функциями getPtrO и getRef() результаты как г-значения. Ничего необыч ного. Вторая строка модифицирует значения, на которые ссылаются указатель ptr и ссылка ref. Обратите внимание, что оба они указывают на данные переменной pt (т. е. pt. х). Эти данные — закрытые, но клиент может изменять их без помощи функций доступа. Третья строка использует вызов функции как 1-значение и изме няет состояние переменной pt. Скобки в выражении *pt.getPtr(); не нужны. Операция-селектор имеет более высокий приоритет, чем операция разыменова ния, применяемая здесь для разыменования возвращаемого методом значения, а не указателя на целевой объект (pt — не указатель, это имя объекта Point).
int |
*ptr = pt.getPtrO; |
int &ref |
= pt.getRefO; |
|
|
|
|
/ / OK, |
используется как r-значение |
*ptr |
+= 10; ref += 10; |
/ / |
закрытые данные изменяются через псевдонимы |
|
*pt.getPtr()=50; pt.getRef()=100; |
/ / закрытые данные изменены |
Во-первых, данный синтаксис использования функции как 1-значения необычен. Во-вторых, такая практика, как некоторые могут сказать, есть нарушение ин капсуляции и сокрытия информации: изменяются закрытые данные, которые не должны быть доступны в клиенте. Но кто сказал, что под сокрытием информации подразумевается неизменение закрытых данных? Вызов функции setPointO не изменяет закрытые данные и не нарушает сокрытия информации, как и вызов getRefO. Инкапсуляция и сокрытие информации — это вопрос предотвращения зависимости между классами, а не предотвращение изменения закрытых данных.
С точки зрения разработки ПО основная проблема этого примера в том, что здесь используются псевдонимы. Псевдонимы также ссылаются на элемент данныхх, но используют для этого другие имена: ptr, ref, getPtr() и getRef(). Между тем эти имена, особенно getPtr() и getRefO, никак не показывают, что они ссылаются на х. Следовательно, такой подход вынуждает тратить дополни тельное время на понимание программы при ее сопровождении. Используйте его осторожно, если это вообще стоит делать. В С+Н- данный метод допустим, но опасен. Он может принести еще больше вреда, чем применение глобальных переменных.
Возврат указателей и ссылок требует, чтобы передаваемый в вызывающую программу адрес после завершения функции был допустимым. В предыдущем при мере функции getPt г() и getRef () возвращают указатель на pt. х, а pt. х остается в области действия после завершения функции. Иногда это не так. В следующем примере функции getDistPtr() и getDistRef() вычисляют расстояние между це левым объектом Point и началом координат. А возвращают указатель и ссылку на вычисленное значение расстояния. Какой досадный просчет!
class |
Point |
|
{ int |
X, у; |
|
public: |
|
|
. |
. . |
/ / s e t P o i n t O , getXO, getY(), getPtrO, getRefO |
int* |
getDistPtrO |
{int dist = (int)sqrt(x*x + y*y);
return &clist; } |
// нет копирования, ноdist исчезла |
int& getDistRefO |
|
{int dist = (int)sqrt(x*x + y*y);
return dist; } } ; |
// другой синтаксис, некоторые проблемы |