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

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

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

730

Часть IV # Расш-

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

i nt

х;

double у;

X = int

('А');

у =

(clouble)x;

double

*р = &у;

int

*q

- (int*)p;

/ /

функционально-подобный

синтаксис;

х содержит 65

/ /

традиционный синтаксис,

у содержит

65.0

/ /

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

/ /

int q указывает на double у: тревога

Единственное различие между этими двумя формами приведения типа состоит в том, что традиционный синтаксис может использоваться с любым допустимым именем типа, а функционально-подобный синтаксис требует идентификатор для имени типа. Поэтому в последнем примере и представлено два примера приведе­ ния числовых типов и только один пример приведения типов указателей. Имя int* является допустимым именем типа, но не допустимым идентификатором.

C + + позволяет приведение любых числовых типов без каких-либо ограниче­ ний. Приведения могут быть явными (с использованием операции приведения) или неявными (без применения операции).

int х; double у;

/ / явное приведение: в C++ без проблем

X = ' А'; у = х;

C + + также допускает приведение произвольных указателей (и ссылок), включая любые типы, определенные пользователем. Эти преобразования могут выпол­ няться только при использовании явного приведения. Неявное приведение указа­ телей не допускается. Используя такие приведения, вы создадите себе проблемы. В данной.части программы указатель String ссылается на целое значение. Указа­ тель String может законно отвечать на любое сообщение String. Выполняя это, он интерпретирует память, как если бы она принадлежала объекту String. Поскольку первым элементом данных String является целое size, при запуске объекта String сообидение getSizeO извлечет это значение. Фактически значе­ ние находится в переменной z целого типа. Если первый элемент данных в классе String не был бы целого типа, то программа отобразила бы "мусор".

int

Z = 42; String *ptr = (String*) &z;

/ /

создаем проблемы

cout

«"Size: " «ptr->getSize() « e n d l ;

/ /

выводит 42

В этой части программы указатель целого типа ссылается на объект String. Указатель интерпретирует память объекта String, как если бы это было целое. Значение, извлеченное указателем, может использоваться в любом выражении, которое требует целое значение. В данном примере объект String при запуске содержит элемент данных size целого типа и такое значение извлекается целым указателем. Если бы класс String начинался не с элемента данных целого типа, то данная часть программы выводила бы на печать чепуху.

int

*г;

String s("Hello, World!");

 

г =

( i n t * ) &s;

 

cout

«

"String: " « 2 + *r « endl;

/ / выводит 15

Hello WorldI

How Is the w

Size: 42

String; 15

Рис. 16.5.

Вывод для программы из листинга 16.5 с добавленными двумя

частями программы

На рис. 16.5 показан вывод программы из листинга 16.5, в конец ко­ торой добавлены эти две части. Эти операции интерпретируют распреде­ ление памяти структуры String правильно и извлекают первое значение объекта String, которое является элементом данных целого типа. Это допускается в C + + , но с точки зрения программной инженерии представ­ ляет собой кошмар для программиста, осуществляющего сопровождение. Если изменить порядок элементов данных в определении класса, не внося никаких изменений в программу, вывод будет совсем другим.

Глава 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хель преобразования ясна из контекста).

Глова 16 * Расширенное использование перегрузки операций

733

Конструкторы преобразований ослабляют строгий контроль типов. Они устра­ няют заш,иту компилятора от небрежного выполнения преобразования. Однако они очень популярны в C-I-+, потому что допускают большую гибкость при напи­ сании программы.

Второй механизм определения возможных преобразований объектов — ис­ пользование операций преобразования. Операция преобразования представляет собой перегруженную операцию, имя которой является именем типа цели. Операция преобразования подчиняется обш.им правилам для перегруженных операций. Однако ее синтаксис необычен.

Подобно конструкторам и деструкторам, она не должна иметь возврандаемый тип. Подобно деструкторам, у нее не должно быть параметров. В отличие от кон­ структоров и деструкторов операция преобразования должна возвраш,ать значе­ ние. Тип этого значения будет соответствовать типу, в котором осуш.ествляется преобразование (т. е. типу, используемому в имени операции). Операция преобра­ зования в целый тип для класса String:

String::operator i n t ( ) const

/ /

отсутствуют изменения в объекте String

{ return size; }

/ /

возвращается только значение, не тип

Обычно возвраидаемым значением является значение соответствуюш.его типа одного из полей данных класса. Если у класса имеется более одного элемента данных этого типа, то проектирови;ик класса должен решить, какое из них лучше всего использовать в преобразовании. Если вы не знаете, какое поле выбрать, не стоит беспокоиться. Вспомните, что все операции используются для удобства синтаксиса клиентской программы.

В зависимости от обстоятельств, класс может содержать более одной операции преобразования. Так выглядит операция преобразования символьного указателя для класса String, который может заменить метод reset().

String::operator char* () const

/ /

объект не изменяется

{ return St г; }

/ /

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

Упростите клиентскую программу (конструктор преобразований Account), ис­ пользуя две операции преобразования String.

Account(const String& s)

 

 

 

{ int

len = (int)s;

/ /

получить размер

строки

owner = new char[len+l];

/ /

выделить память в динамически

 

 

 

/ /

распределяемой области

i f

(owner == 0) { cout «

"\nOut of memory\n"; exit(O);

}

strcpy(owner,

(char*)s);

/ /

инициализировать

поля данных

balance = 0;

}

 

 

 

Здесь явное приведение помогает программисту, осуш,ествляюш,ему сопровожде­ ние, понять поток значений в функции. Однако операции явного приведения не являются обязательными. Если компилятор без труда определяет требуемый тип, то явное приведение можно опустить, будет осуществляться неявное приведение.

Account(const String& s)

 

 

 

{ int

len = s;

 

/ /

неявное приведение к целому типу

owner = new char[len+l];

/ /

выделение памяти в динамически

 

 

 

/ /

распределяемой области

i f

(owner == 0) { cout < "\nOut

of

memory\n"; exit(O);

}

strcpy(owner,

s);

/ /

неявное приведение

к символьному массиву

balance = 0;

}

 

 

 

Объект String может использоваться в любом месте, где ожидается значение целого типа или символьного массива. Не забывайте, что под прикрытием про­ граммы C+-h явное или неявное приведение является сообш.ением: вызовом

Name: Smith
Balance: 700 Credit limit: 1400
Рис, 16.6.
Вывод для программы из листинга 16.6

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];

 

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

736

Чость IV ^ Расширенное использование С-^-^

 

if (owner == 0) {cout «

"\nOut of memory\n"; exit(O); }

 

strcpy(owner, s);

// инициализация полей данных

 

balance = 0; }

// объект не изменяется

operator double () const

{

return balance; )

 

operator String () const { return owner; }

void operator -= (double amount) { balance -=amount; }

void operator += (double amount) { balance +=amount; }

}

int mainO

{

String ownerC'Smith");

Account a(owner);

a += 500; a -=200; a += 400; String s = a;

double limit = 2 * a;

cout « "Name: " « (char *)s « endl; cout « "Balance: " «(double)a «endl; cout « "Credit limit: " « limit « endl; return 0;

//создать объект String

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

//выбор изстека ответственности

//безусловное приращение

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

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

//перегруженные операции

//обработать как значение String

//обработать как значение двойной длины

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

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

Если указанное преобразование не находится, то компилятор ищет встроенное

преобразование (среди числовых типов), чтобы сделать возможным разрешение вызова. Рассмотрим оператор:

cout « "Balance: " «(float)a «endl;

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

Класс Account не обеспечивает операцию преобразования в тип с плавающей точкой (float). Однако это не означает, что приведенная выше строка содержит ошибку. Поскольку преобразование значений double типу float доступно как встроенное преобразование, компилятор переводит значение Account к типу double, а затем значение double к типу float. Компилятор не может добавить более одного встроенного преобразования float к преобразованию, определенно­ му программистом. Компилятор не может присоединить в цепочке к преобразова­ нию, определенному программистом, более одного встроенного преобразования. Однако в цепочке можно соединить встроенное и определенное программистом преобразование.

Операции, возвращающие компонент массива по индексу, и операции вызова функции

Эти две операции являются бинарными. Однако они отличаются от других би­ нарных операций С+Н- способом преобразования вызова операции клиентском, части в бинарное выражение. Для других перегруженных операций цель сообиь ния используется как левый операнд в выражении с операцией (из имени мето;:; вставленной между правым и левым операндом и параметром вызова функп

Глава 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 ] ;

}

/ / допустимый индекс: возвращаемое значение

Глава 16 • Расширенное использование перегрузки операций

|

739 |

void

Array::setlnt(int i . i n t x)

/ /

модификация объекта Array

 

{ i f

( i < 1

I I i

> size)

/ /

проверка допустимости

индекса

 

return;

 

 

/ /

выход, если вне границ

 

 

p t r [ i - 1 ]

= х;

}

/ / действительный индекс: присвоить

значение

Приведем вариант printArrayO, который использует преимущество этой схе­ мы. Теперь удобнее применять обычные преобразования и не требуется писать программы, подобные следующей:

void printArray(const Array& а)

/ /

получение размера массива

{ int size = a.getSizeO;

for (int

i=1; i

<= size; i++)

/ /

выполнение от 1 до size

{ cout

«

"

" «

a. getint ( i ) ; }

/ /

вывод на печать следующего компонента

cout «

endl

«

endl; }

 

 

Влистинге 16.7 представлена реализация класса Array с клиентской программой драйвера. Вывод программы показан на рис. 16.7.

Вданном примере функции set(), конструкторы, деструктор и опера­ тор присваивания подобны функциям-членам класса String из листин­ га 16.6. Основное отличие состоит в том, что функции String используют

всвоих циклах завершающий нуль, а функции Array — некоторое коли­ чество компонентов в контейнере.

1

3

5

7

11

13

17

19

2

6

10

 

14

22

26

34

38

Рис. 16.7.

Вывод программы из листинга 16.7

Листинг 16.7. Использование класса Array как контейнера для целых компонентов

#inclucle <iostream>

 

using

namespace std;

 

class

Array {

 

public:

// количество допустимых компонентов

int

size;

int

*ptr;

// указатель намассив компонентов

void set(const int* a, int n);

// выделение/инициализация памяти

public:

// динамически распределяемой области

// общий конструктор

Array (const int* a,int n);

Array (const Array&s);

// конструктор копирования

"ArrayO;

// возврат памяти в "кучу"

Array& operator = (const Array& a);

// копирование массива вдругой массив

int getSizeO const;

// возвращение i-ro компонента

int getlnt(int i) const;

void setlnt(int i, int x);

// установка int х в позицию i

} ;

 

 

void Array::set(const int* a, int n)

// определение размера массива

{ size = n;

ptr = new int[size];

// запрос памяти динамически распределяемой области

if (ptr ==0) {cout « "Out ofmemory\n" exit(O); }

for (int i=0; i < size; i++)

// копирование клиентских данных в "кучу"

 

ptr[i] = a[i]; }

Array::Аггау (const int* а. int n)

// общее

{ set(a,n); }

 

Array::Array (const Array &a)

// копирование конструктора

{ set(a.ptr,a.size); }

 

Array::~Array()

// деструктор

{ delete [ ]ptr; }

 

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