Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
267
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

370

Чос.,., Объектно-ориентированное протрашшшрова^ыв по С-^-^

системе. Программист, отвечающий за клиента, должен обеспечить целостность программы. Для объектов, управляемых правилами области действия, такого тре­ бования нет. Например, объект п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++ как единицы модульности программы

371 J

Поскольку локальная переменная может определяться в любом месте блока (блока функции или неименованного блока), она недоступна в этом блоке до точки определения. Если имя локальной переменной совпадает с именем глобальной, то локальное имя используется в том блоке, где оно определяется, а глобальное — вне локального блока.

В дополнение к этим двум областям действия (о которых подробно рассказыва­ лось в главе 6) С4-+ добавляет еще одну — область действия класса. Каждое имя, определенное в области действия класса (элементы данных, функции-члены, закрытый или общедоступный компонент класса), известно во всей области дей­ ствия класса. Правила однопроходной компиляции, действующие для глобальных и локальных областей, к области действия класса не применяются, поэтому во всех примерах с классом Cylinder элементы данных доступны в функциях-членах этого класса независимо от порядка определения его компонентов.

Если определяемое в классе имя совпадает с глобальным именем, то все ссыл­ ки на него в области действия класса относятся к имени, определенному в области действия класса. Вне области действия класса (т. е. вне его функций-членов) ссылки на данное имя относятся к глобальному имени.

Когда имя, определяемое в области действия класса, совпадает с локальным именем, определенным в одной из его функций-членов, в данной функции приме­ няются локальное имя, а имя, определенное в области действия всего класса, используется в других функциях-членах.

Короче, локальное имя может скрывать имя из области действия класса и глобальное имя, а имя, определенное в области действия класса — скрывать глобальное имя. Эти правила сокрытия имен могут переопределяться операцией глобальной области действия :: (для глобальных имен) и операцией области действия класса (для локальных имен).

В предлагаемом ниже примере имя radius используется для глобальной пере­ менной, для элементов данных Cylinder и для локального имени в функции-члене setCylinderO класса Cylinder.

double

radius

= 100;

 

/ /

глобальное имя

 

struct

Cylinder {

 

 

/ /

начало области действия

класса

 

double radius,

heigth;

 

/ /

компонент

radius скрывает

 

 

 

 

 

 

 

/ /

глобальное

имя radius

 

void setCylinder(double r,

double

h)

 

 

 

{

double

radius;

 

 

 

 

 

 

 

radius

= r;

height

= h;

 

/ /

локальное

имя radius скрывает

 

 

 

 

 

 

 

/ /

элемент данных radius

 

 

Cylinder::radius = radius; }

/ /

область действия класса

 

 

 

 

 

 

 

 

/ /

переопределяет это правило

void scaleCylinder(double

factor)

 

 

 

 

{

radius

= ::radius;

 

 

/ /

глобальная операция области

 

height

*= factor;

}

 

/ /

действия переопределяет

правило

 

 

 

 

 

 

 

. . .

}

;

 

 

 

/ /

конец области действия

 

Когда параметру г в setCylinder() присваивается значение radius, то происхо­ дит присваивание локальной переменной, а не элементу данных Cylinder. Чтобы присвоить значение элементу данных radius, нужно использовать операцию обла­ сти действия класса. В функции setCylinder() radius означает элемент данных. Для получения значения глобальной переменной radius следует использовать операцию глобальной области действия.

I 372 2

Часть II ^ Объектно-ориентированное прогрс::,/-г:'^*"-^аан140 на C-i-^

 

Иногда программисты применяют для параметра метода и для элемента данных

 

одно и то же имя. Например, в такой версии setCylinderO функция будет некор­

 

ректна:

 

 

 

void Cylinder::setCylinder(double

radius, double h)

 

{ radius = radius; height = h; }

/ /

некорректная функция

 

/ /

локальный параметр

 

 

/ /

скрывает элемент данных

Эта функция компилируется и выполняется без проблем, однако разработчик и компилятор понимают присваивание radius = radius; по-разному. Для разра­ ботчика radius слева означает элемент данных, а radius справа — параметр. Для компилятора radius с обеих сторон — параметр. Присваивать параметр самому себе не особенно полезно, но это один из примеров, когда компилятор отказыва­ ется думать за программиста и строить догадки. Хотите присваивать параметр самому себе? Пожалуйста, C++ не запрендает.

Класс памяти относится к сроку существования переменных (автоматических, внешних, статических): когда они создаются и уничтожаются.

Для локальных автоматических переменных выделяется память в стеке, когда при выполнении программы достигается их определение (размер памяти может зависеть от области действия). Если одно и то же имя используется в разных областях действия, то оно будет ссылаться на разные области памяти. При отсут­ ствии инициализации содержимое памяти будет не определено. Для объекта конструктор вызывается сразу после выделения памяти.

Автоматические переменные уничтожаются, когда выполнение достигает конца блока, где они определены. Для объекта деструктор вызывается непосредственно перед возвращением памяти системе.

Память для внешних или статических (локальных или глобальных) переменных выделяется и инициализируется в фиксированной области перед началом выпол­ нения программы. Если явное начальное значение^ после выделения памяти не указано, память инициализируется нулями. Для объекта перед началом выполне­ ния main() вызывается конструктор (и все функции, которые он может вызывать). Порядок вызова конструкторов для разных объектов не определен.

Внешние и статические (локальные или глобальные) переменные уничтожают­ ся, когда завершается выполнение функции main(), т. е. достигается закрывающая фигурная скобка, или программа прерывается другими путями. Для объектов деструктор вызывается непосредственно перед уничтожением объекта.

Хороший шанс продолжить обсуждение классов памяти, начатое в главе 6. Если программа не использует глобальных переменных или определяемых про­ граммистом классов, порядок выполнения ее кода будет четко известен. Выполне­ ние начинается с первой строки main() и заканчивается ее последней'строкой.

Память для динамических переменных выделяется и освобождается явно.- Обычно операции new или delete либо функции malloc() и f гее() не вызываются в той же функции (области действия). Часто выделение памяти для динамической переменной происходит в одной функции, а ее освобождение — совсем в другой. (Очевидно, эти функции должны принадлежать одному классу клиента.)

Управление памятью

спомощью операций и вызовов функций

вданном разделе сравнивается использование операций new и delete с при­ менением функций mallocO и f гее(). Как и предыдущий раздел, вы можете про­ пустить его, если он покажется слишком технически сложным, но не забудьте вернуться к нему позднее и познакомиться с двумя аспектами:

1) рекомендацией по использованию операций new и delete в сравнении с mallocO и freeO;

Глава 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. В данной версии клиента сначала освобожда­ ется память, на которую указывает объект р, а затем делается попытка освободить динамически распределяемую память. При выполнении программы операционная система обвиняет ее в нарушении доступа к памяти и принудительно завершает.

Глава 9 • Классы С-^-^ как единицы г^одудьности nporpoi^f^bi

| 375 |

Это разумно, так как объект, указываемый р, исчез, а значит, исчез и указатель p->contents. Не каждая ОС может позволить себе роскошь проверки всех обра- ш,ений к памяти за счет скорости работы, и на многих платформах такая ошибка останется незамеченной.

void

ClientO

 

 

 

 

 

 

 

 

 

{ Name n1("Джонс");

/ /

вызывается

конструктор

преобразования

Name

*р=(Name*)malloc(unsignecl(sizeof(Name)));

 

 

 

 

 

 

 

/ /

конструктор

преобразования

не вызывается

p->contents

= new char[strlen("CMHT")+1];

 

 

 

 

 

 

(p->contents == NULL)

/ /

распределение динамической

памяти

i f

 

 

 

/ /

вызов

'new'

был неуспешным

 

{

cout «

"Нет памяти\п";

exit(1);

}

/ /

отказ

 

 

 

strcpy(p->contents, "Смит");

 

 

 

/ /

вызов

'new'

был успешным

n1.show_name(); p->show_name();

 

 

/ /

использование объектов

free

(р);

 

 

/ /

неверная

последовательность действий!

delete p->contents;

 

 

 

/ /

удалять здесь нечего!

}

 

 

 

/ / р удален,

вызван деструктор для объекта п1

Кроме того, если память для объекта распределяется с помош,ью new, то ее сле­ дует освобождать по delete. Использование функции f гее() — семантическая ошибка. Семантической ошибкой является и применение операции delete для возврата памяти, выделенной по функции malloc().

Обратите внимание, что семантическая ошибка отлична от синтаксической. Она может выражаться в принудительном завершении программы при ее выпол­ нении или в некорректных результатах (выявляемых с помош,ью тестирования). Принцип "семантически некорректной программы" — неудачный вклад С + -1- в программную инженерию. Результаты некорректной последовательности вызо­ вов "не определены", и программисту придется самому обеспечить отсутствие в программе бомбы замедленного действия, способной взорваться в любой момент.

Две характеристики функций mallocO и f гее() — отсутствие вызовов кон­ структоров/деструкторов и опасность получения некорректной программы при совмеш^ении с операциями new и delete — могут привести к проблемам, поэтому применение функций malloc() и f гее() для динамического распределения памяти в С+4- непопулярно. Однако оно очень популярно в языке С (где нет операций new и delete). Эти функции часто используются в унаследованных системах, а также в приложениях, динамически обрабатывающих множество операций с памятью для повышения производительности. Функции mallocO и free() можно применять для создания в избранных классах специализированных операций new и delete. О таком "продвинутом" использовании операций рассказывается далее.

Версия, представленная в листинге 9.3, лучше, чем версия из листинга 9.4: она не жертвует инкапсуляцией, не нарушает принцип сокрытия информации и не создает необходимости дополнительной координации работы программистов. Между тем она обременяет клиента обязанностями выделения и освобождения памяти для объекта Name, на который ссылается указатель р. Программисты часто используют динамическое управление памятью, когда оно не особенно полезно. Это как раз тот самый случай. Память для данного объекта можно выделять и освобождать с помош,ью правил области действия, а не явного управления.

void ClientO

 

 

{ Name n1("Джонс");

/ /

вызывается конструктор преобразования

Name п2("Смит");

/ /

нет динамического выделения/освобождения памяти

п1.show_name(); п2.show_name();

}

/ /

вызывается деструктор для объектов п1 и п2

Убедитесь, что ваши программы на C + + оказались не сложнее, чем могли бы быть.

Щ 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; } } ;

// другой синтаксис, некоторые проблемы

378

Часть II • Объектно-ориентировоннс)

Локальная переменная dist исчезает после завершения функций getDistPtr() и getDistRef (). Использование ее адреса может дать корректный результат, если занимаемая переменной память не применяется для чего-то еш,е, в противном случае результаты вычислений окажутся некорректными. Некоторые компилято­ ры могут при этом давать предупреждение, другие — нет. Как бы то ни было, данная версия класса Point (приведенная выше) и программы-клиента (ниже) синтаксически корректны.

Point pt; pt.setPoint(20,40);

 

 

// недопустимый указатель

int * ptr = pt.getDistPtrO;

 

 

cout «

" Указатель на расстояние

: " << *ptr « endl;

// OK

int &ref =pt.getDistRef0;

 

ref « endl;

// недопустимая ссылка

cout «

" Ссылка на расстояние: " «

 

// OK

cout «

" Указатель на расстояние

: " « *ptr « endl;

// плохо

cout «

" Ссылка на расстояние: " «

ref << endl;

 

// плохо

Указатель нарасстояние: 44 Ссылка нарасстояние: 44 Указатель нарасстояние: 4198928 Ссылка на расстояние: 4198928

Рис. 9 . 5 .

оррекшные

и некорректные

Результаты выполнения данного примера на моей машине пред­ ставлены на рис. 9.5. С первым случаем использования недопусти­ мого указателя и ссылки все обошлось: оба значения корректны, хотя ни ссылка, ни указатель допустимыми не являются. Попытка снова вывести значения дает некорректные результаты. Это говорит

отом, что любое другое их использование некорректно. Такие ошиб-

^^могут оставаться незамеченными. Разве можно, протестировав

-'

-> v

v

результаты возврата

значения ref и *ptr и увидев корректные результаты, ожидать,

указателя и ссылки

что они могут измениться? Бдительность программиста, очевидно,

 

будет направлена на другие вопросы. А ваша?

 

Все мы воспринимаем корректные результаты выполнения программы как

свидетельство ее правильности. Конечно, может возникнуть желание протести­

ровать программу на другом наборе данных и охватить дополнительные пути ее

алгоритма, но повторять тесты для одних и тех же входных данных непродуктивно,

да и в голову никому не придет. Ведь результаты должны быть такими же. Но это

в других языках программирования. Конечно, и в C + + тоже, но только если вы

 

знаете, что делаете.

 

О с т о р о ж н о ! Убедитесь, что при возврате указателя или ссылки из функции

W

они не указывают на память, уже ставшую недействительной по правилам

области действия C++. Нарушение данной рекомендации не приведет

к синтаксической ошибке и может не проявиться в виде некорректных

результатов. Не всегда следует интерпретировать правильные результаты

 

как свидетельство корректности программы.

В обш,ем случае полезно ограничиться использованием в качестве возвраш.аемых значений булевых флагов, которые говорят об успешном или неуспешном выполнении той или иной функции. Однако эстетическая привлекательность таких функций, как getXO и getY(), велика, и программисты всегда будут с ними рабо­ тать. Не увлекайтесь моидными возможностями C + + и не возвраш,айте указате­ лей или ссылок, особенно на значения, которые могут стать недействительными. Компилятор не защитит вас от такой ошибки.

Возврат объектов

в предлагаемом далее примере к классу Point добавлены еще три функции: closestPointValO, closestPointPtrO и closestPointRef(). Каждая такая функ­ ция воспринимает в качестве параметра ссылку на объект Point и вычисляет для параметра и для адресата расстояние до начала координат. Если объект-параметр

Глово 9 • Классы C++ как единиць! модульности программы

379

находится ближе к началу координат, то функция возвращает в этот объект, а если к начальной точке ближе адресат сообщения, то функция возвращает целевой объект (как разыменованный указатель this, ссылающейся на него).

Первая функция возвращает сам объект, вторая — указатель на ближайший объект, а третья — ссылку на ближайший объект. Преимущество данного интер­ фейса в том, что он позволяет создавать цепочку сообщений, когда значение, возвращаемое одной функцией, используется как цель для вызова другой. Все три вида возвращаемых значений (объекты, указатели и ссылки) можно использовать как г- и 1-значения.

class

Point

{ i n t

X,

у;

public:

 

. . .

/ / s e t P o i n t O , getXO. getY(). getPtrO, getRefO

. . .

/ / g e t D i s t P t r O . getDistRefO

Point

closestPointVal(Point& pt)

{

i f

(x*x + y*y

< pt.x *pt.x + pt.y * pt.y)

 

 

 

 

 

return

*this;

/ /

значение объекта: копирование

в объект temp

 

else

 

 

 

 

 

 

 

 

 

 

return

pt;

}

/ /

значение объекта: копирование

в объект temp

Point*

closestPointPtr(Point&

р)

 

 

 

 

 

 

 

 

 

/ /

возвращает указатель: нет

копирования

{

return (х*х + у*у

< р.х*р.х

+ р.у* р.у) ? this

: &р;

}

 

Point& closestPointRef(Point&

р)

 

 

 

 

 

 

 

 

 

/ /

возвращает ссылку: нет

копирования

{

return (х*х + у*у

< р.х*р.х

+ р.у* р.у) ? *this

:

р;

} }

;

Здесь this — ключевое слово, обозначающее указатель на целевой объект сообщения. В следующем примере это объект р1. Первая функция использует обыкновенную запись (два оператора return), а последние две — сокращенную (условный оператор).

Обратите внимание, как режимы адресации отражаются на возвращаемых объектах. Функция closestPointValO возвращает объект Point (по значению). Когда возвращается целевой объект, указатель this (ссылающийся на цель) нужно разыменовать, а поля целевого объекта р1 — скопировать в поля прини­ мающего (объекта pt). Эта ссылка — синоним объекта, на который она указы­ вает (объект р2), а поля данного объекта копируются в принимаюш>1Й объект (объект pt).

Point

р1,р2; p1.setPoint(20.40);

p2.setPoint(30,50);

 

 

/ /

присваивание для объектов Point

Point

pt = p1.GlosestPointVal(p2);

/ /

копируются поля ближайшего объекта

Функция closestPointPtrO возвращает указатель на ближайший объект Point. Если целевой объект ближе, чем объект, заданный параметром, возвращается указатель this (ссылающийся на цель, например р1). В следующем примере зна­ чение указателя копируется в принимающий указатель (р). Если ближе объект, заданный параметром, то используется его ссылка р. Так как данная ссылка — синоним указываемого ею объекта (а не адреса этого объекта), в принимающий указатель копируется значение &р. Этот указатель можно использовать для до­ ступа к компонентам ближайшего объекта (р1 или р2).

Point *р = p1.closestPointPtr(p2);

 

 

/ /

возвращается указатель: быстрый способ

p->setPoint(0,0);

/ /

перемещение р1 или р2 в начало координат

Соседние файлы в предмете Программирование на C++