Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf710 |
Часть IV * Расширенное использование С-^-^ |
Четвертый вид сообщения не требует преобразования. Даже если эти сообще ния направляются с использованием указателя базового класса, они интерпрети руются средой выполнения программ в соответствии с типом объекта, на который указывает указатель (динамическое связывание). Структура, использующая вир туальные функции, инкапсулирует алгоритмы. Они выполняются по-разному для разных видов объектов в функциях с теми же именами.
При использовании виртуальных функций увеличиваются затраты памяти и снижается производительность. Каждый объект того класса, который использует виртуальные функции, содержит скрытый элемент данных. Он определяет вид объектов или указатель, ссылающийся на таблицу с адресами доступных виртуаль ных функций. Каждый раз при вызове виртуальной функции этот указатель ис пользуется для поиска требуемого объектного кода. Время выполнения подобной операции увеличивается.
Рассматривалось множественное наследование. Это сложный вопрос. Реко мендуем реже использовать множественное наследование.
Виртуальные функции популярны в программировании на языке С+-Н. Помни те, однако, что механизм виртуальной функции является "хрупким". Необходимо применять общедоступное наследование. Обязательно использование тех же са мых имен во всех классах своей иерархии наследования. Требуется использовать тот же список параметров, возвращаемых значений и даже модификаторов кон стант. При самом незначительном несоответствии программа вызовет совершенно другую функцию просто потому, что у нее то же самое имя. Иначе говоря, исполь зуйте виртуальные функции там, где обработку разного вида связанных объектов можно приемлемо описать с помошд>ю одного и того же имени функции.
sr^^ei^16
у ^—^ асширенное использование перегрузки операций
Темы данной главы
•^ Перегрузка операций: краткий обзор •/ Унарные операции
•^ Операции, возвращающие компонент массива по индексу, и операции вызова функции
•^ Операции ввода/вывода •^ Итоги
Перегруженные операции языка C++ обсуждались в главе 10 (числовые классы) и главе 11 (нечисловые классы). В этой главе рассматриваются более экзотические варианты использования перегрузки операций в язы
ке C+ + . Для некоторых программистов любая перегрузка операции сама по себе является достаточно эксцентричной.
Расширенные перегруженные операции пишутся не часто. Однако расширен ные операции являются важным компонентом библиотеки C+ + , стандартным или нестандартным, и полезны для понимания того, как они работают. Это опреде ленно не самое важное при изучении C+ + , но эти операции интересны.
Перегрузка операций: краткий обзор
Перегруженная операция представляет собой функцию, переопределенную программистом, со специальным именем, составленным из зарезервированного слова operator и символа или символов операции. Кроме того, перегруженные операции известны под именами операторных функций, перегруженных оператор ных функций или просто операций. Они обеспечивают удобный синтаксис опера ций для манипулирования объектами классов, определенных программистами.
Включение перегруженных операций в язык C++ было вызвано желанием ин терпретировать переменные встроенного типа. Если можно добавить два числовых значения, добавьте два объекта Account. Если можно добавить три числовых зна чения, добавьте три объекта Account и т. д. Перегруженные операции позволяют это сделать.
Именно в языке C++ значение встроенных операций определяется по встро енным типам. Для перегруженных операций это делает программист. Значение не должно быть произвольным. Оно должно зависеть от характера добавляемых,
f~7^ |
.ширенное тспоАШОваите С'^^ |
умножаемых объектов. Но программист располагает достаточной свободой
вопределении смысла операции, и это может легко привести к неправильному использованию — к проектированию перегруженных операций, значение которых не очень понятно интуитивно. Хорошим примером такого неверного использова ния является унарная операция, к которой добавлена операция, спроектированная
вглаве 10, для отображения полей комплексных чисел (см. листинг 10.4). Если вы будете ее использовать в клиентской программе, то любой программист, осуидествляющий сопровождение, окажется в сложной ситуации. Немногие люди мо гут правильно угадать, что, например, +х означает отображение полей объекта х
вуказанном формате.
Вы не можете свободно выбирать имена перегруженных операций. Имя опера торной функции должно включать зарезервированное слово operator, за которым следует допустимая операция C + + (разрешается использование двухсимвольных операций типа == или +=).
Из этого правила есть пять исключений: ". ", ". *", ": ;", "?:" и "sizeof". Пере груженные операции могут быть определены либо как члены класса (следователь но, используемые как сообш,ения), либо как глобальные функции верхнего уровня ("друзья" класса, объекты которого применяются как операнды перегруженных операций). Если операция перегружается как член класса, она может иметь любые подходящие аргументы.
Цель сообш^ения будет использоваться как первый операнд. Если операция перегружается как глобальная функция, она должна содержать хотя бы один аргумент класса. Она не может иметь аргументы только встроенных типов. Это ограничение не применяется к операциям управления памятью (new, delete и delete []).
Операции, перегруженные в базовом классе, наследуются в производных клас сах. Очевидно, что эти операции не могут осундествлять доступ к членам, опреде ленным в производных классах, поскольку члены производного класса находятся вне области видимости базового класса. Следовательно, к ним невозможно осуще ствить доступ из методов базового класса. Перегруженные операции присваива ния являются аномалией — они не наследуются производными классами. Они могут осуществить доступ только к базовой части производного объекта, но не к его производной части. Значит, каждый класс в иерархии наследования должен определять свой собственный оператор присваивания.
Предшествование операций для перегруженных операций такое же, как и для их встроенных аналогов. Например, операция умножения всегда имеет более вы сокий приоритет, чем операция сложения, какое бы ни было значение для класса, определенного программистом. Синтаксис выражения для перегруженных опера ций аналогичен соответствующим встроенным операциям. Например, бинарные операции всегда появляются между их двумя аргументами независимо от того, встроены они или перегружены. (Однако в этой главе можно будет увидеть неко торые исключения из этого правила.)
Арность (количество операндов) для перегруженных операций и для соответст вующих встроенных операций одинакова. Бинарные операции остаются бинарны ми, для них требуются два операнда. Как глобальные функции-члены (например, "друзья") бинарные перегруженные операции должны иметь два параметра. Как функции-члены класса бинарные перегруженные операции должны содержать только один параметр, поскольку другой параметр становится целевым объектом сообщения.
Подобным образом, унарные встроенные операции остаются унарными, когда они перегружаются. Если унарная перегруженная операция реализована как глобальная унарная операция, не являющаяся членом класса (например, "друг"), то она будет содержать один параметр. Если эта перегруженная операция опреде ляется как функция-член (отправляемая как сообщение целевому объекту), она не будет иметь параметров.
Глава 16 • Расширенное использование перегрузки операций |
713 |
Вкачестве простого примера рассмотрим класс Account. Класс сохраняет ин формацию об имени владельца и текуидем балансе счета, а также поддерживает сервисы, которые позволяют клиентской программе осуществлять доступ к значе ниям элементов данных объекта, вносить вклады и снимать деньги.
Вдополнение к четырем расположенным внутри текста функциям-членам класс имеет общий конструктор. Для класса не требуется конструктор по умолча нию, поскольку объекты Account будут создаваться в динамически распределяемой области памяти, когда они нужны. Конструктор по умолчанию может быть поле зен, если объекты класса были созданы заранее, когда имя владельца и исходный баланс еще не были известны.
Поскольку класс динамически управляет памятью, хорошо было бы добавить
кнему копию конструктора и оператор присваивания или сделать закрытыми про тотипы этих функций-членов (см. главу 11). Здесь это не показано, поскольку объекты Account не передавались по значению. Один объект Account не инициали зировался из данных другого объекта Account и один объект Account не присваи вался другому объекту Account. В реальной жизни важно защитить объекты Account даже от случайного неверного использования.
class Account { |
|
|
|
|
|
/ / |
базовый класс |
иерархии |
||||
protected: |
|
|
|
|
|
|
|
|
|
|
||
double balance; |
|
|
|
|
|
/ / |
защищенные данные |
|||||
char |
*owner; |
|
|
|
|
|
|
|
|
|
|
|
public: |
|
|
|
|
|
|
|
|
|
|
|
|
Account(const char* |
name, |
double |
initBalance) |
/ / общий |
||||||||
{ |
owner = new char[strlen(name)+l]; |
/ / |
выделение памяти для динамически |
|||||||||
|
|
|
|
|
|
|
|
/ / |
распределяемой области памяти |
|||
|
i f |
(owner == 0) { |
cout |
« |
"\nOut |
of |
memory\n"; |
exit(O); |
} |
|||
|
strcpy(owner, |
name) ; |
|
|
|
/ / |
инициализация |
полей данных |
||||
|
balance = initBalance; |
} |
|
|
|
|
|
|
|
|||
double getBalO |
const |
|
|
|
/ / |
общий для обоих счетов |
||||||
{ |
return balance; |
} |
|
|
|
|
|
|
|
|
|
|
const |
char* getOwnerO const |
|
/ / |
защитить данные от изменений |
||||||||
{ |
return owner; } |
|
|
|
|
|
|
|
|
|
|
|
void withdraw(double amount) |
|
|
|
|
|
|
||||||
{ balance -= amount; |
} |
|
|
|
/ / |
извлечение |
обязанностей |
|||||
void |
deposit(double |
amount) |
|
|
|
|
|
|
||||
{ |
balance += amount; |
} } |
; |
|
|
/ / |
безусловное |
приращение |
Предполагалось создать массив указателей Account, динамически задать объ екты Account, инициализировать их, выполнить поиск по счетам, принадлежащим указанному владельцу, внести и снять некоторые денежные суммы. Для упроще ния примера снова будут использоваться "жестко заданные" данные, а не данные, загруженные из внешнего файла.
В листинге 16.1 приведена исходная программа д/ш данного примера. Функция createAccountO динамически создает объект Account, вызывает конструктор Account с двумя параметрами и возвращает указатель на новый созданный объект. Функция processRequestO устанавливает флаги ios для вывода на печать чисел с плавающей точкой в фиксированном формате и с нулевыми младшими разряда ми числа, выполняет поиск имени клиента в рамках объектов и выводит на печать сообщение, если имя не найдено. В противном случае функция запрашивает у пользователя код транзакции,'сумму транзакции и выполняет транзакцию на указанную сумму (вклад или снятие).
714 |
Часть IV » Расширенное использование C+'t^ |
||||||||
Листинг 16.1. Пример обработки класса Account методами, заданными программистами |
|||||||||
#include <iostream> |
|
|
|
|
|
|
|||
using namespace std; |
|
|
|
|
|
|
|||
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; } |
|
|
|
|
|
|||
double getBalO const |
|
|
// общее для обоих счетов |
||||||
{ |
return balance; } |
|
|
|
|
|
|
||
const char* getOwnerO const |
|
// защита данных от изменений |
|||||||
{ |
return owner; } |
|
|
|
|
|
|
||
void withdraw(double |
amount) |
|
// извлечение ответственности |
||||||
{ balance -=amount; } |
|
|
|||||||
void deposit(double |
amount) |
|
// безусловное |
приращение |
|||||
{ balance +=amount; } |
|
|
|||||||
} |
|
|
|
|
|
|
|
|
|
Account* createAccount(const |
char* name, double bal) |
|
|
||||||
{ Account* a = new Account (name, bal); |
|
// счет в динамически распределяемой области памяти |
|||||||
if (а == 0) {cout « |
"\nOut ofmemory\n"; exit(O); } |
|
|||||||
return a; } |
|
|
|
|
|
|
|
|
|
void processRequest(Account* a[] , const char name[]) |
|
|
|||||||
{ int i; int choice; double amount; |
|
|
|
|
|||||
cout. setf (ios:: fixed, ios: :floatfield); |
|
|
|||||||
cout.precision(2); |
i++) |
|
|
|
|
|
|||
for (i=0; a[i] != 0; |
|
|
|
|
// поиск имени |
||||
{ if (strcmp(a[i]->getOwner(),name)==0) |
|
() « |
|||||||
|
{ cout « |
"Account balance: " « |
a[i]->getBal |
endl; |
|||||
|
cout «"Enter |
1 todeposit, 2 to withdraw, 3 tocancel: |
|||||||
|
cin » |
choice; |
|
|
|
|
|
// тип транзакции |
|
|
if (choice != 1 &&choice !=2) break; |
|
// выход |
||||||
|
cout « |
"Enter amount: "; |
|
|
|
// сумма транзакции |
|||
|
cin » |
amount; |
|
|
|
|
|
||
|
switch |
(choice) { |
|
|
|
|
// выбрать следующий путь |
||
|
case 1: a[i]->deposit(amount); |
|
|
// безусловно |
|||||
|
|
|
break; |
|
|
|
|
// достаточно средств? |
|
|
case 2: if (amount <= a[i]->getBal()) |
|
|||||||
|
|
|
|
a[i]->withdraw(amount); |
|
|
|||
|
|
|
else |
|
|
|
|
// конец области switch |
|
|
|
|
|
cout « "Insufficient funds\n" ; |
|||||
|
cout « |
break; } |
|
|
|
endl; |
//OK |
||
|
"New balance: " « a[i] ->getBal() « |
||||||||
|
break; |
} } |
|
|
|
|
|
// конец цикла поиска |
|
if (a[i] - |
0) |
|
|
} |
} |
|
|
||
|
{ cout « |
"Customer isnot found\n"; |
|
|
716 |
Часть IV * Расширенное использование C-f^ |
||
|
case 2: if (amount <=a[i]-> |
getBalO) |
|
|
*a[i] -=amount; |
// а[1]->снятие(сумма); |
|
|
else |
|
|
|
cout |
< "Insufficient funcls\n"; |
|
|
break; |
} |
|
Обратите внимание, что цель сообщения — указатель Account. Он должен быть разыменован, когда используется в выражениях. Это неудобство, но не очень серьезное.
Реальный смысл синтаксиса выражения, конечно, вызов функции для отправ ления сообщения левому операнлу в таком выражении: a[i]->operator+=(amount) или a[i]->operator-=(amount).
Customer List: |
|
|
В листинге 16.2 представлена программа, |
|||||
|
|
использующая |
перегруженные |
операторные |
||||
1 Jones |
5000.00 |
|
функции |
вместо |
именованных |
программистом |
||
|
методов. Это просто для программы листин |
|||||||
Smith |
3000.00 |
|
||||||
Green |
1000.00 |
|
га 16.1. Прежде чем начать этап интерактивной |
|||||
Brown |
1000.00 |
|
обработки, функция main() вызывает функцию |
|||||
Enter customer name ( 'exit' to exit): Smith |
|
printListO, которая проходит по списку ука |
||||||
Recount balance: 3000 00 |
| |
зателей Account и выводит на печать содержа |
||||||
Enter 1 todeposit, 2 towithdraw, 3 tocancel: 1 |
ние объектов, на которые указывает указатель |
|||||||
Enter amount: |
1000 |
|
(см. рис. 16.2). Обратите |
внимание на то, что |
||||
New balance: 4000.00 |
| |
|||||||
Enter customer |
name ( exit' to exit): exit |
операторы форматируют имена, выводимые на |
||||||
Рис. 16.2. Вывод программы |
|
печать с |
выравниванием |
по |
левому |
краю. |
||
|
а остатки на счетах выводятся на печать с вы |
|||||||
из листинга 16.2 |
|
равниванием по правому краю. |
|
|
||||
|
|
|
|
|
||||
|
Подобно processRequestO, функция printListO итеративно выполняется по |
|||||||
|
списку до тех пор, пока в массиве не будет найден нулевой указатель (он играет |
|||||||
|
роль сигнальной метки). Обратите внимание, что заголовки циклов этих двух |
|||||||
|
функций различаются. В printListO индекс i является локальным для |
цикла, |
||||||
|
в processRequestO — глобальным для цикла. (Он является локальным для облас |
|||||||
|
ти видимости функций.) Причина различий состоит в том, что после завершения |
|||||||
|
цикла в printListO значение индекса больше не требуется. Итерации всегда |
|||||||
|
выполняются от начала списка до конца. В processRequestO итерацию можно |
|||||||
|
остановить до того, как |
будет достигнут |
конец |
списка (если имя найдено), |
а processRequestO должен знать об этом.
Листинг 16.2. Пример обработки класса Account методом перегрузки операции
#include <iostream> using namespace std;
class Account { protected:
double balance; char *owner;
public:
Account(const char* name, double initBalance) { owner = new char[strlen(name)+l];
if (owner ==0) {cout « |
"\nOut ofmemory\n" exit(O); } |
strcpy(owner, name); |
// инициализация полей данных |
balance = initBalance; } |
|
double getBalO const |
// общее для обоих счетов |
{ return balance; } |
|
|
|
Глава 16 • Расширенное использование перегрузки операций |
717 |
|||
|
const char* getOwnerO const |
// защита данных отизменений |
|
|||
|
{ return owner; } |
|
|
|||
|
void operator -= (double amount) |
// извлечение ответственности |
|
|||
|
{ balance -=amount; } |
|
||||
|
void operator += (double amount) |
// безусловное приращение |
|
|||
|
{ balance += amount; } |
|
||||
} ; |
|
|
|
|
|
|
Account* createAccount(const char* name, double bal) |
|
|||||
{ |
Account* a = new Account(name,bal); |
// счет вдинамически выделяемой области |
||||
|
if (а == 0) {cout < "\nOut of memory\n"; exit(O); } |
|
||||
|
return a; } |
|
|
|
|
|
void processRequest(Account* a[], const char name[]) |
|
|||||
{ |
int i; int choice; double amount; |
|
|
|||
|
cout.setf(ios::fixed,ios::floatfield); |
|
|
|||
|
cout.precision(2) ; |
|
|
|||
|
for (i=0; a[i] != 0; i++) |
// поиск имени |
|
|||
|
{ if (strcmp(a[i]->getOwner(),name)==0) |
|
||||
|
{ |
cout « |
"Account balance: " « a[i]->getBal() « endl; |
|
||
|
|
cout «"Enter 1 todeposit, 2 towithdraw, 3 tocancel: "; |
|
|||
|
|
cin » |
choice; |
// тип транзакции |
|
|
|
|
if (choice ! = 1 &&choice !=2) break; |
|
|||
|
|
cout « "Enter amount: "; |
// сумма транзакции |
|
||
|
|
cin » |
amount; |
|
||
|
|
switch |
(choice) { |
// a[i]->operator+==( сумма); |
|
|
|
|
case 1: *a[i] += amount; |
|
|||
|
|
|
|
break; |
|
|
|
|
case 2: if (amount <= a[i]->getBal()) |
|
|||
|
|
|
|
*a[i] -=amount; |
// a[i]->operator-=( сумма); |
|
|
|
|
|
else |
|
|
|
|
|
|
cout » "Insufficient funds\n"; |
|
|
|
|
cout « |
break; } |
// конец области действия switch |
|
|
|
|
"New balance: " « a[i]->getBal() « endl; |
|
|||
|
break; |
} } |
// конец цикла поиска |
|
||
|
if (a[i] - 0) |
|
} |
|
||
|
{ cout « |
"Customer isnot found\n"; } |
|
|||
void printList (Account* a[]) |
|
|
||||
{ |
cout « |
"Customer List:\n\n"; |
|
|
||
|
for (int i=0; a[i] != 0; i++) |
cout.width(30); |
|
|||
|
{ cout.setf(ios::left, ios::adjustfield); |
|
||||
|
cout « a[i]->getOwner(); |
cout.width(10); |
|
|||
|
cout.setf(ios::right, ios::adjustfield); |
|
||||
|
cout « |
a[i]->getBal() « endl; } |
|
|
||
|
cout « |
endl; } |
|
|
|
|
int mainO |
|
|
|
|
|
|
{ |
Account* accounts[100]; char name[80]; |
// данные программы |
|
|||
|
|
|||||
|
accounts[0] = createAccount("Jones", 5000); |
// создать объекты |
|
|||
|
accounts[1] = createAccount("Smith",3000); |
|
|
|||
|
accounts[2] = createAccount("Green", 1000); |
|
|
accounts[3] = createAccount("Brown",1000); accounts[4] = 0;
printList(accounts);
718 |
Часть IV • Расширенное использование С4-+ |
|
while (true) |
// запросы процесса |
|
{ cout « |
"Enter customer name ('exit' toexit): "; |
|
cin » |
name; |
// принять имя |
if (strcmp(name,"exit")==0) break; |
// выход? |
|
processRequest(accounts, name); |
//следующая транзакция |
|
} |
; |
|
return 0 |
|
|
} |
|
|
Реализовать перегруженные операции как глобальные функции просто. Цель сообщения становится первым параметром функции. Вместо элементов данных целевого объекта операции используют элементы данных первого параметра. Ниже приведены две операции, реализованные как глобальные функции.
void operator |
-= (Account |
|
&а, |
double |
amount) |
/ / |
глобальная функция |
{ а. balance |
-= amount; |
} |
|
|
|
/ / |
извлечение ответственности |
void operator |
+= (Account |
|
&a, |
double |
amount) |
|
|
{ a.balance |
+- amount; |
} |
|
|
|
/ / |
безусловное приращение |
Поскольку эти две функции осуществляют доступ к неоткрытым компонентам класса Account, они должны быть объявлены "друзьями" класса Account. У неко торых программистов это вызывает раздражение, поскольку требует дополнитель ной работы. Как уже упоминалось, современный подход к программированию не рассматривает дополнительные затраты на написание программы как недостаток. Дополнительные данные записываются только один раз, но читаются в ходе разра ботки, тестирования и сопровождения программы неоднократно.
В этом случае добавление описаний функций, "дружественных" для класса, четко показывает, что эти функции принадлежат заданному классу. Учтите, что они не могут использоваться без объектов класса Account. Функции-"друзья" принадлежат классу концептуально, т. е. являются частью операций, предоставля емых классом. Синтаксис функций-"друзей" отличается от синтаксиса функцийчленов. Частые обвинения против использования "дружественных" функций, на рушения инкапсуляции и создание дополнительных зависимостей между частями программы не являются результатом применения перегруженных операторных функций.
class Account |
{ |
|
|
|
|
/ / |
базовый класс иерархии |
|||
protected: |
|
|
|
|
|
|
|
|
||
|
|
double |
balance; |
|
|
|
/ / |
защищенные данные |
||
public: |
char *owner; |
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
||
Account(const char* name, |
double initBalance) |
/ / |
общие |
|||||||
{ |
owner = new char[strlen(name)+1]; |
/ / |
динамическое выделение |
|||||||
|
|
|
|
|
|
|
|
/ / |
распределяемой области |
|
|
i f |
(owner |
== 0) { cout |
« |
"\nOut |
of |
memory\n"; |
exit(O); } |
||
|
strcpy(owner, |
name); |
|
|
|
/ / |
инициализация полей данных |
|||
|
balance = initBalance; |
} |
|
|
|
|
|
|||
double getBalO |
const |
|
|
|
/ / |
общие для обоих счетов |
||||
{ |
return balance; } |
|
|
|
|
|
|
|||
const |
char* |
getOwnerO const |
|
/ / |
защита данных от изменений |
|||||
{ |
return owner; |
} |
|
|
|
|
|
|
||
friend void operator-= (Account &a, double amount); |
/ / операторы |
|||||||||
friend |
void operator+= (Account &a, |
double amount); |
|