
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfГлава 16 • Расширенное использование перегрузки операций |
731 |
Разрешение выполнять приведение произвольных указателей и ссылок не распространяется на объекты. Преобразование целых в объекты класса, опреде ленного программистом, не допускается. Невозможно выполнять преобразование объекта класса, заданного программистом, в целое, или в другое числовое значе ние, или в объект другого класса.
String |
s; Account а; |
int х; |
X = s; |
а = х; s = а; |
/ / это бессмыслица |
C++ допускает приведение указателей или ссылок классов, связанных насле дованием. Приведения такого типа могут быть неявными, если цель приведения (указатель или ссылка) является обндедоступным базовым классом и источником приведения (значением, указателем или ссылкой), а также если класс открыто порождается из цели приведения. Оно особенно полезно, когда указание объектов различных производных классов осуществляется массивом указателей базового класса. Вставка этих объектов в массив может выполняться без использования явного преобразования.
Приведение из базового указателя (или ссылки) к указателю производного класса (или ссылке) должно быть явным, подобно приведению несвязанных типов. Это особенно полезно, когда указание объекта производного класса осуществля ется указателем (или ссылкой) базового класса, но должно выполнять операции, определенные в производном классе, а не в базовом классе. Явное приведение указывает, к какому производному классу принадлежит запрашиваемая операция.
Приведение базового объекта к производному объекту не допускается. Это подобно интерпретации объектов несвязанных типов. Если необходимо, такое преобразование может предоставляться добавлением конструктора преобразова ния к производному классу. Этот конструктор содержит параметр базового класса. Примеры таких приведений и конструкторов были описаны в главе 15.
Для классов, связанных наследованием, C-f-+ допускает ослабление строгого контроля типов. Несмотря на то, что не допускается неявное приведение базового объекта к производному объекту, разрешаются неявные преобразования объектов производного класса в объекты базового класса. Если не используется явное при ведение, то дополнительные элементы данных (и операции) производных объектов отбрасываются.
СН-+ поддерживает строгий контроль за типами только для объектов классов, созданных программистом. Для остальных типов преобразование допускается. Некоторые преобразования могут выполняться только явно, в операции приведе ния (для указателей и ссылок любых типов). Другие преобразования могут осуще ствляться даже неявно, без явного использования приведения (между числовыми значениями или из объектов, указателей и ссылок производных типов в объекты, указатели и ссылки базового типа). Эти преобразования предоставляют про граммисту дополнительные возможности при реализации алгоритмов, которые обрабатывают значения разных типов. Поскольку из-за данных преобразований нарушается строгий контроль типов, они устраняют защиту, предоставляемую проверкой синтаксиса.
Однако этого недостаточно. C + + позволяет программистам реализовать до полнительное приведение для объектов классов той категории, для которой под держивается строгий контроль типов. Обратите внимание, что защита вследствие выполнения проверки синтаксиса обсуждается только для указанных классов. Программист с помощью конструкторов и операции преобразования показывает классы, для которых устанавливается защита.
Конструкторы преобразований описываются в главе 9. Конструктор преобра зования содержит один параметр типа, который должен преобразовываться в ука занный класс. Например, класс String содержит конструктор преобразования, который переводит значение символьного массива в значение String.
String (const char* s) |
/ / конструктор преобразования |
{ set(s); } |
|
732 |
Часть IV ^ Расширенное использование С-^^ |
При наличии конструктора массив символов может использоваться, когда ожидается объект String без возникновения синтаксической ошибки на этапе компиляции. Поскольку String является классом, определенным программистом, то ситуации, в которых появляется объект String, редки. Они ограничиваются определением объекта, передачей параметров по значению (не по указателю или по ссылке), присваиванием и отправкой сообщений объекту.
printStringC'Hi there"); |
/ / |
ошибка: передача по ссылке |
printString(String("Hi there")); |
/ / |
OK: объект создается |
int sz = StringC'Hi there"). getSizeO; |
/ / |
объект создается |
Приведение может быть неявным (без использования оператора приведения), если компилятор соответствует требуемому типу, как в операторе присваивания:
String |
s; |
/ / TO же, что и s = StringC'Hi there"; |
s = "Hi |
there" |
Bo всех этих случаях в стеке создается неименованный объект String и вызы вается конструктор преобразования. Затем объект удаляется. C++ не определяет точный момент для уничтожения объекта. Разработчик компилятора должен убе диться, что объект существует сразу же после его использования и исчезает до того, как закрывается текущая область действия.
Предполагается, что параметр конструктора преобразования должен иметь тип, принадлежащий одному из элементов данных класса. Например, у конструк тора преобразования для класса String есть параметр в виде массива символов (указателя символов), а у класса String — элемент данных — указатель симво лов. Однако совсем не обязательно должно быть именно так.
Проектировщик класса может применить преобразование из любого типа, ко торое он посчитает подходящим. Например, класс Account из листинга 16.1 (или листинга 16.2) может включать в себя конструктор преобразований с параметром типа String, даже если в классе Account отсутствует элемент данных String. Конструктор имеет следующий вид.
Account(String& |
s) |
/ / |
преобразование (изменяется String) |
{ char* р = s. resetO; |
/ / |
установить указатель на массив |
|
owner = new char[strlen(p)+l] |
/ / |
выделение памяти из динамически |
|
|
|
/ / |
распределяемой области |
i f (owner ==0) |
{ cout < "\nOut |
of meniory\n"; exit(O); } |
|
strcpy(owner, |
p); |
/ / |
инициализация полей данных |
balance = 0; } |
|
/ / |
значение по умолчанию для нового счета |
Теперь объект String может использоваться в любом месте, где ожидается объект Account. Несмотря на то, что объект Account с нулевым балансом совер шенно бесполезен, наиболее подходящим использованием этого конструктора яв ляется создание объектов Account, когда данные владельца представляются как объект String, а не как символьный массив.
String |
ownerC'Smith") |
|
|
Account |
a(owner); |
/ / |
создание и инициализация |
a += 500; |
/ / |
использование объекта Account |
Конструкторы преобразований позволяют проектировщику класса явно ука зать, какие типы могут использоваться там, где ожидаются значения указанного класса. Обратите внимание, что эти конструкторы реализуют приведение объек тов, а не указателей или ссылок, которые всегда допустимы в C + + . Приведение типов, реализованное конструкторами преобразования, может быть явным {earn компилятор не может определить из контекста, над каким классом выполнять пре образование) или даже неявным (если 1хель преобразования ясна из контекста).
734 |
Часть IV # Расширенное использование С4-4« |
функции перегруженных операций преобразования. Вот как выглядят для компи лятора обе версии конструктора.
Account(const String& |
s) |
|
|
|
|
{ int |
len = s.operator |
i n t ( ) ; |
/ / |
вызов оператора |
|
owner = new char[len+l]; |
|
/ / |
выделение памяти в динамически |
||
|
|
|
|
/ / |
распределяемой области |
i f |
(owner == 0) { cout « |
"\nOut of |
memory\n"; exit(O); } |
||
strcpy(owner, s.operator |
char*()); |
/ / |
вызов оператора |
||
balance =0; } |
|
|
|
|
В большинстве случаев операции преобразования используются для извлече ния значения одного из полей объекта. Но это не является собственным ограни чением. Операции преобразования используются проектировщиком класса для указания типа преобразований объектов класса. Все, что проектировщик опреде ляет как допустимое преобразование, выполняется. Например, класс Account мо жет поддерживать две операции преобразования в double и в String, даже если в классе Account не содержится элемент данных String.
Account::operator |
double |
() |
const |
/ / |
объект не изменяется |
{ return balance; |
} |
|
|
/ / |
возвращается значение двойной длины |
Account:[Operator |
String |
() |
const |
/ / |
создается объект String |
{ return owner; } |
|
|
|
/ / |
неявное преобразование |
Обратите внимание, что во второй операции преобразования выполняется не явное преобразование в класс String. Объект String создается и возвращается для использования в клиентской программе. Он автоматически уничтожается в области видимости клиента. Заметьте, что не сказано "когда клиентская часть прекращает выполнение", поскольку время уничтожения точно не определено. Использование ссылки в имени операции было бы синтаксически неправильно, поскольку все ссылки в C+-I- должны быть константами.
Account::operator String& () const |
/ / |
синтаксическая ошибка |
{ return owner; } |
/ / |
неявное преобразование |
Чтобы устранить эту проблему, можно определить имя операции как ссылку String типа константа.
Account::operator const String& () const |
/ / |
не |
синтаксическая ошибка |
{ return owner; } |
/ / |
не |
очень хорошая идея |
Однако возвращаемая ссылка является ссылкой на неименованный временный объект, который может быть уничтожен в любой момент, когда пожелает компи лятор. В результате клиентская программа может получить недействительную ссылку. Это плохая практика программирования.
При наличии таких преобразований клиентская программа может преобразо вать объект St ring в целое значение и в символьный указатель (с помощью опера ций преобразования), и в объект Account (используя конструктор преобразования Account). Она может преобразовать объект Account в значение двойной длины и в значение String (с помощью операций преобразования). Кроме того, массив символов можно преобразовать в объект String, используя конструктор преоб разования String.
В листинге 16.6 показаны эти преобразования. Объект String обраба тывается клиентской программой, как если бы он был символьным масси вом. Объект Account обрабатывается клиентской программой, как если бы он был значением двойной длины и значением String (см. рис. 16.6).
Если в указанном контексте может использоваться несколько типов, то компилятор должен знать, какой из них применять. В качестве подсказки может использоваться приведение типов. В операторах вывода значение
Глава 16 • Расширенное использование перегрузки операций |
735 |
любого типа может быть допустимым значением вывода. Явное приведение необ ходимо, если возможно преобразование более чем в один тип. Например, значение String может быть преобразовано в целое значение и в символьный массив. Компилятору требуется указать, что именно предполагал программист.
Листинг 16.6. Примеры использования конструкторов преобразования и операций преобразования
#inclucle <iostream> |
|
|
|
|
|
||
using |
namespace std; |
|
|
|
|
|
|
class |
String { |
|
|
// размер строки |
|
||
|
int |
size; |
|
|
|
||
|
char *str; |
|
|
// начало внутренней строки |
|||
|
void set (const char* s); |
|
// размещение закрытой строки |
||||
public |
|
'") |
// поумолчанию и преобразование |
||||
|
String (const char* s = |
||||||
|
{ set(s); } |
|
|
// конструктор копирования |
|||
|
String (const String &s) |
|
|||||
|
{ set(s.str); ) |
|
|
// деструктор |
|
||
|
"StringO |
|
|
|
|||
|
{ delete [ ]str; } |
|
String& s); |
//присваивание |
|||
|
String& operator = (const |
||||||
|
operator int() const; |
|
|
// длина текущей строки |
|||
} |
operator char* () const; |
|
// возвращение указателя вначало |
||||
|
|
|
|
|
|
|
|
void String::set(const char* |
s) |
// оценка размера |
|
||||
|
{ size = strlen(s); |
|
|
|
|||
|
str = new char[size + 1 ] ; |
|
//запрос памяти вдинамически распределяемой области |
||||
|
if (str ==0) {cout « |
"Out ofmemory\n"; exit(O); } |
|||||
|
strcpy(str, s); } |
//копирование клиентских данных вдинамически распределяемую область |
|||||
String& String::operator = (const String& s) |
|
||||||
{ |
if (this ==&s) return *this; |
// ничего не делается, если самоприсваивание |
|||||
|
delete [ ] str; |
|
|
// возвращение существующей памяти |
|||
|
set(s.str); |
|
|
// выделить/присвоить новую память |
|||
|
return *this; } |
|
|
// для поддержки цепочечного присваивания |
|||
String::operator int() const |
|
// изменения объекта String отсутствуют |
|||||
{ |
return size; } |
|
|
|
|
|
|
String::operator char* () const |
// объект не изменяется |
||||||
{ |
return str; ) |
|
|
// возвращение указателя вначало |
|||
class Account { |
|
|
// базовый класс иерархии |
||||
protected: |
|
|
|||||
|
double balance; |
|
|
// защищенные данные |
|||
|
char *owner; |
|
|
|
|
|
|
public: |
|
|
|
|
|
||
|
Account(const char* name, double initBalance) |
// общий |
|||||
|
{ owner = new char[strlen(name)+1]; |
//выделение памяти для динамически |
|||||
|
if (owner ==0) {cout « |
|
|
// распределяемой области |
|||
|
"\nOut ofmemory\n"; exit(O); } |
||||||
|
strcpy(owner, name); |
|
|
//инициализация памяти динамически распределяемой области |
|||
|
balance = initBalance; } |
|
|
|
|
||
|
Account(const String& s) |
|
// получение размера строки |
||||
|
{ int len = s; |
|
|
||||
|
owner = new char[len+l]; |
|
// выделение памяти для динамически распределяемой области |
Глава 16 • Расширенное использование перегрузки операций |
[ 737 щ |
используемым как правый операнд выражения. В операциях, возвращающих ком понент массива по индексу, и операциях вызова функции это не так.
Другое отличие состоит в том, что перегруженные операции могут быть реа лизованы только как компоненты класса. Невозможно реализовать их как гло бальные функции, не являющиеся членами. Таким образом, становится легче осуществить контекстный анализ компилятора.
Операции, возвращающие компонент массива по индексу
в идеальном случае вид выражения перегруженной операции индексирования (операции, возвращающей компонент массива по индексу) должен быть таким же, как и синтаксический вид встроенной операции индексирования. Имя переменной добавляется с индексом, заключенным в скобки. Например, s[i] должно интерп ретироваться как индекс i, применяемый к объекту (переменной) s.
Значение этой операции может быть произвольным. Большинство програм мистов и все библиотеки C++ интерпретируют такое выражение как извлечение значения i-oro компонента объекта s. Другой популярной интерпретацией являет ся присвоение значения i-ому компоненту объекта s.
Вобоих случаях предполагается, что объект s является контейнером, который содержит массив, или связанный список, или другой соответствующий набор ком понентов, а выражение s[i] ссылается на значение i-ro компонента в контейнере.
Вкачестве простого примера контейнерного класса рассмотрим упрощенный вариант класса Array. Этот контейнерный класс подобен классу из листинга 16.6. Компоненты контейнера имеют тип int, а не char.
Класс Array устраняет два недостатка встроенных массивов C+ + : переполне ние массива и неверные значения индекса. Первая проблема решается с помощью размещения компонентов в динамически распределяемой области памяти. Для защиты целостности программы класс Array должен обеспечить конструктор копирования, деструктор и перегруженную операцию присваивания.
Вторая проблема с неверным индексом решается посредством функций-членов getlnt() и setlnt(), осуществляющих доступ к внутренней памяти Array от имени клиентской программы.
Class Array { |
|
|
|
|
public: |
|
|
|
|
int |
size; |
/ / |
количество действительных |
компонентов |
int |
*ptr; |
/ / |
указатель на массив компонентов |
|
void |
set(const int* а, int n); |
/ / |
выделить/инициализировать |
память |
public: |
|
/ / |
динамически распределяемой |
области |
|
// общий конструктор |
|
||
Array (const int* a,int n); |
|
|||
Array (const Array &s); |
// конструктор копирования |
|
||
"ArrayO; |
// освобождение памяти динамически |
|||
|
|
// распределяемой области |
|
|
Аггау& operator = (const Array& а); |
// копирование массива в другой |
|||
int getSizeO const; |
// возвращение i-ro компонента |
|
||
int getlnt(int i) const; |
|
|||
void setlnt(int i, int x); |
// установка int x в позицию i |
|
} ;
Хотелось бы напомнить, что i-положение фактически означает (i+1) положе ние, т. е. первому компоненту соответствует индекс О, второму компоненту — индекс 1 и т. д.
Следовательно, функция-член getint О возвращает целое для индекса i масси ва внутренней динамически распределяемой области памяти. Поскольку getint ()
738 |
Часть IV • Расширенное использование C-f-*- |
||||
|
вызывается как функция, то дополнительно можно сделать некоторые полезные |
||||
|
веш,и. Рекомендуем проверить допустимость индекса относительно границ строки. |
||||
|
i nt |
Array::getlnt (int i ) const |
/ / |
объект не изменяется |
|
|
{ i f |
( i < 0 |
11 i >= size) |
/ / |
индекс выходит за границы |
|
|
return |
ptr[size - 1]; |
/ / |
возвращение к последнему компоненту |
|
return p t r [ i ] ; } |
/ / |
допустимый индекс: возвращаемое значение |
С помош,ью этой функции клиентская программа может реализовать итеративные алгоритмы, подобные алгоритмам, используемым для встроенных массивов C+ + . Однако они являются более безопасными, поскольку не могут осуш.ествлять до ступ к областям памяти за границами массива.
void printArray(const Array& а) |
/ / |
получение размера массива |
|||||
{ int size = a.getSizeO; |
|||||||
for (int |
i=0; |
i |
< size; i++) |
/ / |
просмотр каждого |
компонента |
|
{ cout |
« |
" " |
« |
a . g e t l n t ( i ) ; } |
/ / |
вывод следующего |
компонента |
cout « |
endl |
« |
endl; } |
|
|
|
Единственная проблема, связанная с этой дополнительной функциональной возможностью в getlntO, состоит в том, что она немного замедляет выполнение. Именно желание избежать этого замедления стало причиной того, что в С и C+ + первоначально не была введена проверка индекса. Однако большинство совре менных приложений не страдает от этого. Можно использовать более быструю версию функции, которая вообще не будет тратить время на проверку индексов
ибудет реализована в виде встроенного ассемблерного кода.
Вподобной структуре предполагается выполнение в клиентской программе проверки возвраш,аемого значения. Если такая проверка в клиентской части
необходима, то размер данных объекта Array, который может быть извлечен с использованием функции-члена getSizeO (как в функции printArrayO, выше или аналогичен выполняемому в листинге 16.6 для подобного контейнера, класса String. Это позволяет клиентской части явно выполнить проверку. Однако каждое решение fio проектированию представляет собой компромисс. Передача обязан ностей серверной программе и упрош,ение клиентской программы рассматривают ся в качестве надежной практики программной инженерии.
Возвраш^ение последнего значения в контейнер, когда не действителен индекс,— хорошее решение. Другая альтернатива — возврат специального сигнального значения, например нуля. Это позволит клиентской программе структурировать итерации вокруг объекта Array, прекраш^ая их при обнаружении нулевого кода. Но этот подход работает, только когда нулевое значение компонента является недействительным с точки зрения приложения.
Рассмотрим метод setlnt().
void Array::setlnt(int i , i n t x) |
/ / |
модификация объекта Array |
{ i f ( i < 0 I I i >= size) |
/ / |
проверка допустимости индекса |
return; |
/ / |
выход, если вне границ |
p t r [ i ] = х; } |
/ / действительный индекс: присвоить значение |
Можно доказать, что проверка границы для этой функции еш,е более важна, чем для метода getlntO. В getlntO суидествует риск внесения неверных данных в клиентскую программу, а это можно обнаружить во время отладки и тестирова ния. В setlnt() риск состоит в искажении информации в памяти. В этом случае вы не сможете заблаговременно обнаружить ошибки.
Переведем версии двух функций, позволяюш,их кJшeнтcкoй программе рабо тать с индексами, которые изменяются от 1 до границы массива.
int |
Array: |
: |
getint |
(int i ) const |
/ / |
объект не изменяется |
{ i f |
( i < 1 I |
I i > size) |
/ / |
индекс выходит за границы |
||
|
return |
ptr[size]; |
/ / |
возвращение к последнему компоненту |
||
return p t r [ i - 1 ] ; |
} |
/ / допустимый индекс: возвращаемое значение |