Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf750 |
Часть IV * Расширенное использование С-4-4- |
||
printMatrix(ml); |
/ / |
вывод на печать состояния матрицы |
|
for (i=0; i < n; i++) |
/ / |
занесение нулей в элементы главной диагонали |
|
m1(i,i) = 0; |
/ / |
m 1 [ i ] [ i ] = О |
|
printMatrix(ml); |
/ / |
вывод на печать нового состояния |
|
cout « |
"m[10][10] = " « m1(10,10) « |
endl; |
/ / выход за границы |
return |
0; |
|
|
} |
|
|
|
Эта структура не поддерживает сложение, умножение, сравнение и другие по лезные операции с матрицами. Ее назначение состоит только в том, чтобы проде монстрировать использование операции вызова функции.
Операции ввода/вывода
Стандартная библиотека С+4- перегружает операцию ввода » и операцию вывода « дая всех встроенных классов. Очевидно, операции ничего не знают о классах, определенных программистом. Именно поэтому, когда требуется вы полнить ввод или вывод данных объекта, необходимо делать это дая каждого элемента данных объекта в отдельности.
Было бы прекрасно перегружать операции ввода/вывода также и дая классов, определенных программистом. Инкапсулирование этих операций в перегружен ные операции способствовало бы упрощению клиентской программы, исключе ние деталей нижнего уровня из клиентской программы и позволило бы передать некоторые обязанности из клиентской программы в серверные классы.
Перегрузка операции >>
Рассмотрим класс String из листинга 16.6, осундествляющий динамическое управление своей памятью и поддержку доступа клиентской программы кее внут
ренним данным.
class String { |
// размер строки |
int size; |
|
char *str; |
// начало внутренней строки |
void set(const char* s); |
// выделение закрытой строки |
public: |
// noумолчанию и преобразование |
String (const char* s = "") |
|
{ set(s); } |
// конструктор копирования |
String (const String &s) |
|
{ set(s.str); } |
// деструктор |
"St ring0 |
|
{ delete [ ]str; } |
// присваивание |
String& operator = (const String& s); |
|
operator int() const; |
// длина текущей строки |
operator char* () const; |
// возвращение указателя в начало |
} ; |
|
Было бы чудесно выполнить перегрузку операций ввода/вывода для этого клас са так, чтобы клиентская программа могла использовать что-то подобное:
int main |
() |
|
|
|
{ String |
s; |
|
|
|
cout |
« |
"Enter customer name: "; |
|
|
cin |
» |
s; |
/ / |
принять имя |
cout |
« |
"The customer name i s : |
"; |
|
cout |
« |
s « endl; |
/ / |
вывести на экран имя |
return |
0; } |
|
|
|
Глава 16 • Расширенное использование перегрузки операций |
| 751 |
Интерфейс перегруженной операции ввода — это бинарный оператор, кото рый воздействует на потоковый объект типа i (который поддерживает ввод для библиотечного объекта cin и дисковых файлов) и на объект типа String. Давайте выполним перегрузку операции » для класса String.
void String::operator »(istream& |
in) |
|
||
{ char name[80]; |
/ / |
локальная память для данных |
||
in » |
name; |
/ / |
принять данные |
|
delete |
[ ] |
str; |
/ / |
возвращение существующей памяти |
set(name); |
} |
/ / |
выделение/инициализация новой памяти |
|
Обратите внимание на то, что параметр функции передается по ссылке, а не по значению, поскольку он управляет своей памятью динамически. Заметьте, что это не ссылка на объект const,— вводимый объект изменяется в зависимости от операции ввода. Учитывайте, что функция не помечена как const, поскольку она изменяет состояние элементов данных объекта, освобождает суш,ествующую память динамически распределяемой области и устанавливает указатель на другую область памяти динамически распределяемой области памяти.
Это работает. Однако подобная операция имеет громоздкий интерфейс. В со
ответствии с правилами |
C + + для преобразования |
синтаксиса вызова функции |
|
в синтаксис |
выражения |
необходимо вызвать ее с объектом String как целью, |
|
а объектом istream как параметром. |
|
||
S.operator » |
(cin); |
/ / эквивалентно |
s » cin; |
Желательно иметь специальное разрешение, подобное приведенному выше для операции индексирования и операции вызова функции. Но в данном случае оно отсутствует, и ни один программист не сможет использовать операцию ввода, если объект cin не будет операндом в левой части.
Если это не срабатывает, давайте спроектируем эту операцию как член класса istream. Класс istream является библиотечным классом.
Итак, эту операцию невозможно перегрузить как член класса istream, но можно (хотя и нет желания) перегрузить ее как член класса String. Что нам
следует яелать? В отсутствие |
всех других альтернатив нам не остается ничего |
||||
другого, кроме перегрузки этой операции как глобальной функции. |
|||||
void operator » (String& s, |
istream& in) |
/ / |
глобальная функция |
||
{ char |
name[80]; |
/ / |
локальная |
память для данных |
|
i n » |
name; |
/ / |
принять данные |
||
String temp(name); |
/ / |
создать/инициализировать новый объект |
|||
s - temp; } |
/ / |
копировать его в аргумент |
|||
Это не безупречно и довольно медленно. Во-первых, создается временный стро ковый объект, а затем он копируется в аргумент. Но это является частью внеш него ввода/вывода, поэтому никоим образом не влияет на производительность программы.
Вот как вызывается эта функция с использованием синтаксиса вызова функ ции: имя функции, первый параметр и второй параметр.
operator » (s, cin); |
/ / то же, что и s >> cin |
Как можно видеть, приобретено не так уж и много. Проблема состоит в том, что объект String является первым операндом, а не вторым. Давайте изменим порядок параметров функции.
void operator » (istream& in, |
String& |
s) |
/ / |
глобальная функция |
|
{ char |
name[80]; |
/ / |
локальная |
память для данных |
|
in » |
name; |
/ / |
принять данные |
||
String temp(name); |
/ / |
создать/инициализировать новый объект |
|||
s = temp; } |
/ / |
копировать |
его в аргумент |
||
7 5 2 |
'iQOb |
tV ^ riiCuibD^-^^T "^C v;^ . ,- .;-'"-•-, .-: - C-b-t" |
Вот это намного лучше — с объектом String как вторым параметром синтаксис выражения такой, как нам хотелось видеть.
operator » (cin. s); |
/ / то же, что и cin » s; |
На следующем этапе эту функцию нужно сделать "другом" класса String, чтобы не возиться с объектом цели, а перейти непосредственно к использованию его не общедоступной функции set() и элементов данных.
class |
String |
{ |
|
|
|
int |
size; |
|
|
/ / |
размер строки |
char |
*str; |
|
|
/ / |
начало внутренней строки |
void |
set(const char* |
s); |
/ / |
выделение закрытой строки |
|
public: |
|
|
|
|
|
friend |
void |
operator » |
(istream& in, |
String& s); |
|
. . . |
} ; |
|
|
/ / |
остальная часть класса String |
Это почти безукоризненно. Однако с точки зрения интерпретации классов, определенных программистом, подобно встроенным классам, такая функция не оправдывает надежд. Для встроенных типов библиотека lost ream поддерживает цепочечные операции.
double |
X, |
у; |
|
cin » |
X » |
у; |
/ / то же, что и cin » х; cin » у; |
Для нашего примера эта клиентская программа не работает — она генерирует синтаксическую ошибку.
String |
s; |
int |
qty; |
|
GOut « |
"Enter |
customer |
name and quantity: "; |
|
cin » |
s » |
qty; |
/ / ошибка: не цепочечные вызовы |
|
Какой смысл последней строки в последнем фрагменте программы? Если вы рассмотрите определение функции operator » (перегруженной для всех возможных типов), то увидите, что возвращаемый ей тип является ссылкой на тип istream. Данный цепочечный синтаксис возможен — никакое специальное разрешение неодела^юбы этого.
Эта функция была вызвана с помощью объектов cin и s как параметров. Когда операция возвращает ссылку на объект istream, ему отправляется другая версия операции из библиотечного класса istream с переменной qty в качестве аргумента.
(operator»(cin,s)) . operator»(qty); / / то же, что и cin » s » qty;
Но определенная здесь функция возвращает void, а не istream&. Это не слишком хорошо для отправки ей какого-либо сообщения. Рекомендуем вам переопреде лить возвращаемый тип в istream&.
|
В листинге 16.12 представлена программа, |
Enter custoroer name and quantity: Simons 25 |
реализующая класс String и выполняющая пе |
The customer name is: Simons |
регрузку operator» как "друга" класса. Воз |
Quantity ordered is: 25 |
вращение ссылки на istream поддерживает |
Рис. 16.9. Вывод программы |
цепочные операции. Вывод программы показан |
на рис. 16.9. |
|
из листинга 16.12 |
|
754 Часть IV * Расширенное использование С+4"
Перегруженная операция <<
Подобно operator » , операция вывода operator « |
может быть перегружена |
в типы, определенные программистом. |
|
Подобно перегруженной операции ввода operator |
>>, реализация операции |
вывода как функции-члена типа, определенного пользователем, например String, не слишком удачная мысль. Вам потребуется использовать громоздкий синтаксис, где объект String располагается слева от оператора, а объект вывода cout — справа.
String s; |
|
s « cout; |
/ / то же, что и s. operator « (cout); |
Идея реализации операции вывода как функции-члена библиотеки потокового класса ostream является также не очень удачной. Единственная возможность — реализовать ее как глобальную функцию. Убедитесь в том, что объект ostream является первым параметром, а не вторым. Иначе придется столкнуться с синтак сисом, в котором объект String должен быть слева от оператора.
void |
operator « |
(ostream& out, const String& s) |
{ out |
« s.getO; |
} |
Эту функцию не требуется делать "дружественной" для типа, определенного про граммистом, поскольку у нее имеется доступ ко всей необходимой информации. Независимо от того, имеется или отсутствует у этой функции доступ к данным, большинство программистов превратило бы ее в "друга".
Данная функция хорошо подходит для вывода компонентов по отдельности, но не для цепочечных операций.
cout |
« |
"The customer name i s : "; |
cout |
« |
s; |
cout |
« |
endl; |
Это неудобно. Подобно перегруженной операции ввода, используйте возращение ссылки на объект (здесь на объект класса вывода ostream).
ostream& operator « (ostream& out, const String& s) { return out « S.getO; }
Теперь становится возможным объединение в цепочку операции вывода для типов, определенных пользователем, таким же образом, как и для встроенных типов:
|
|
|
cout « |
"The customer name i s : « s « endl; |
/ / красивый синтаксис |
||
|
|
|
В листинге 16.13 показана программа, которая реализует класс String и пере |
||||
|
|
|
гружает operator |
» |
как "друга" класса. Возврат ссылки istream поддерживает |
||
|
|
|
|
|
|
цегючечные операции. Вывод программы пред |
|
|
Enter customer |
name and quantity: Smith |
42 |
ставлен на рис. 16.10. |
|||
|
The customer name is: Smith |
|
|
Несмотря на то, что этой же цели можно |
|||
|
Quantity ordered is: |
42 |
|
|
|||
|
|
|
|
|
|
достичь, написав специализированные функции- |
|
n |
1 z <m |
r^ |
-^ |
|
|
члены для ввода |
и вывода данных объекта, |
гИС. lo . lU . Вывод |
программы |
|
|
|
|||
|
из листинга |
16.13 |
|
перегруженные операции придают программам |
|||
|
|
|
|
|
|
на СТ-+ элегантный вид. |
|
Глава 16 • Расширенное использование перегрузки операций |
755 g |
Листинг 16.13. Перегрузка операций ввода и вывода для типа, определенного программистом
#inclucle <iostream> using namespace std;
class String |
{ |
|
|
|
|
// размер строки |
|
|
|
||||
|
int |
size; |
|
|
|
|
|
|
|
|
|||
|
char |
*str; |
|
|
|
|
|
// начало внутренней строки |
|
||||
|
void set(const char* s); |
|
|
// выделение закрытой строки |
|||||||||
public: |
|
|
|
|
(istream& in, |
String& s); |
|
|
|
||||
friend |
istream& operator » |
|
|
|
|||||||||
friend ostream& operator « |
(ostream& out, |
const String& s); |
|
|
|
||||||||
|
String |
(const char* s = "") |
|
// noумолчанию и преобразование |
|||||||||
|
{ set(s); } |
|
|
|
|
// конструктор копирования |
|
|
|||||
|
String |
(const String &s) |
|
|
|
|
|||||||
|
{ set(s.str); } |
|
|
// деструктор |
|
|
|
||||||
|
"StringO |
|
|
|
|
|
|
|
|
||||
|
{ delete [ ]str; } |
|
|
// присваивание |
|
|
|
||||||
|
String& operator = (const String& s); |
|
|
|
|||||||||
|
char* get() const |
|
|
|
// возвращение указателя в начало |
||||||||
|
{ return str; } |
|
|
|
|
|
|
|
|
|
|||
) ; |
|
|
|
|
|
|
|
|
|
|
|
|
|
void String::set(const char* s) |
|
// оценить размер |
|
|
|
||||||||
|
{ size = strlen(s); |
|
|
|
|
|
|||||||
|
str = new char[size + 1]; |
|
// запрос динамически распределяемой |
||||||||||
|
if (str ==0) {cout « |
|
|
// области памяти |
|
|
|
||||||
|
"Out of memory\n"; exit(O); } |
|
|
|
|||||||||
|
strcpy(str,s); |
} |
|
|
// копирование клиентских данных в динамически |
||||||||
|
|
|
|
|
|
|
|
|
// распределяемую |
область памяти |
|||
String& String::operator = (const String& s) |
|
|
|
|
|||||||||
{ |
if (this ==&s) return *this; |
|
// ничего, если самоприсваивание |
||||||||||
|
delete [ ] |
str; |
|
|
|
|
// возвращение существующей |
|
памяти |
||||
|
set(s.str); |
|
|
|
|
|
// выделение/установка новой |
памяти |
|||||
|
return *this; } |
|
|
|
|
// для поддержки цепочечного |
присваивания |
||||||
istream& operator » |
(istream& in, String& s) |
|
|
|
|
||||||||
{ |
char name[80]; |
|
|
|
|
// локальная память для данных |
|||||||
|
in » |
name; |
|
|
|
|
|
// принять данные |
|
памяти |
|||
|
delete [] s.str; |
|
|
|
|
// возвращение существующей |
|||||||
|
s.set(name); |
|
|
|
|
// выделение/инициализация |
новой памяти |
||||||
|
return cin; } |
|
|
|
|
|
|
|
|
|
|||
ostream& operator « (ostream& out, const String& s) |
|
|
|
|
|||||||||
|
{ return out « |
s.str; } |
|
|
// допускается для |
друзей |
|
|
|||||
int main () |
|
endl; |
|
|
|
|
|
|
|
||||
{ |
cout « |
endl « |
|
|
// локальные данные |
|
|
||||||
|
String s; int gty; |
|
|
|
|
|
|||||||
|
cout « |
"Enter customer name and quantity: "; |
|
|
|
|
|||||||
|
cin » s » |
qty; |
|
|
|
s « |
// принять имя и количество |
|
|
||||
|
cout « |
"The customer name is: " « |
endl; |
// очень красиво |
|
|
|||||||
|
cout « |
"Quantity ordered |
is: " « |
qty « |
endl; |
|
|
|
|
||||
return 0;
STi/k^ea УГ
их,'аблоны как еще одно средство проектирования
Темы данной главы
•^ Простой пример повторного использования структуры класса «/ Синтаксис определения шаблонного класса
^Шаблонные классы с несколькими параметрами
^Связи между реализациями шаблонных классов
^Специализации шаблонов
ь^ Шаблонные функции
^Итоги
2 ^ ^ ^ оставшихся двух главах этой книги рассматриваются вопросы совре- Ш "ЦИ^-менного программирования на C++: программирование с шаблонами
^ ^^"Z^^ и программирование с исключительными ситуациями.
Обычно контейнерные классы и алгоритмы обработки (сортировка, поиск и т. п.) проектируют для компонентов конкретного типа. Если контейнер содержит набор целых величин, его нельзя использовать для хранения, например, объектов-счетов. Если функция выполняет сортировку массива целых значений, ее нельзя приме нять для сортировки учетных записей товарно-материальных ценностей. Часто ее невозможно использовать даже для сортировки значений двойной длины с плаваюш,ей точкой. Шаблоны C++ позволяют программисту пренебречь этим ограни чением. С шаблонами можно проектировать родовые классы и алгоритмы, а затем определять, компонент какого типа следует обрабатывать конкретным объектомконтейнером или вызовом конкретной функции.
Программирование с исключительными ситуациями используется для упрош.е- ния программы, реализующей сложную логику. Обычно алгоритмы обработки применяют в C++ операторы if или switch для отделения обработки данных от обработки ошибочных или неправильно используемых данных. Для пошаговых алгоритмов сегменты исходного текста программы для основного алгоритма и д/1я исключительных состояний записываются в альтернативных ветвях одной исход ной программы. Это затрудняет ее чтение. Исключительные ситуации в C+ + позволяют программисту изолировать исключительные случаи в других частях ис ходной программы и упростить основную обработку, что бы ее л^гче было понять.
Эти возможности языка, шаблоны и исключительные ситуации имеют обш,ие характеристики. Они сложны, и на их выполнение потребуется затратить допол нительное время.
758 |
Часть IV • Расширенное использование Снь-^ |
Накладные расходы памяти и времени являются непосредственным результа том сложности этих методов программирования. Если пишутся прикладные систе мы реального времени при жестких ограничениях на память и время выполнения, не следует использовать шаблоны и исключительные ситуации. Если приложения предположительно будут выполняться на компьютерах с достаточным количеством памяти и быстрыми процессорами, то ограничения на память и по времени не столь важны.
Рекомендуем вам постепенно вводить в свои программы такие возможности языка. Убедитесь в том, что погоня за интересными и многообеш,аюш,ими возмож ностями языка не слишком затруднит жизнь сопровождаюш^его программиста.
Вэтой главе обсуждается программирование с использованием шаблонов C+-f.
Вследующей главе рассматриваются исключительные ситуации в C4--h и другие расширенные возможности языка.
Простой пример повторного использования структуры класса
Подход со строгим контролем типов в C-f-1- позволяет компилятору выявлять ошибки программирования, когда программист использует один тип вместо дру гого. В языке С+4- имеется большое количество исключений из этого правила. Числовые значения могут заменять друг друга. Типы, определенные программис том, могут использоваться вместо других типов, предусматривая, что имеются в наличии конструкторы преобразования и операторы преобразования. Классы, связанные наследованием, также позволяют использовать ограниченную подста новку.
Имейте в виду, что суш,ествует много ограничений на использование типизиро ванных значений. Многие алгоритмы по существу те же самые, независимо от типа значений, которыми они оперируют. Например, для поиска указанного номе ра счета в массиве объектов Account требуется просмотреть каждый компонент массива и сравнить имя владельца с указанным именем. Подобным же образом, для поиска указанной позиции в списке материально-технических ценностей в массиве позиций требуется просмотреть каждый компонент массива и сравнить идентификатор позиции с указанным идентификатором. Это одинаковые действия, но невозможно передать массив позиций учета как параметр в функцию, реали зующую поиск в массиве счетов. Советуем написать другую функцию. Она будет почти идентична функции поиска счета. Отличаться будет только операция срав нения: одна функция сравнивает указанное имя с именем владельца в объекте Account, а вторая сравнивает указанный id с id в объекте item.
Контейнерные классы — стеки, запросы, списки, деревья и т.д.— могут со держать различные виды компонентов. Часто в компонентных классах компоненты обрабатываются одинаковым способом, независимо от самих компонентов. На пример, операции со стеком — внесение нового компонента в верхнюю часть стека, извлечение компонента из вершины стека и проверка стека не зависят от характера компонентов. Они выполняются одинаково, независимо от того, явля ются компоненты символами, счетами или записями материальных ценностей. Было бы неплохо иметь возможность спроектировать общий стек и использовать его для компонентов любого типа, которые требуются приложению. Это невоз можно из-за строгого контроля типов в C-h-f. Стек символов содержит символы и не может включать объекты счетов или позиции учета материальных ценностей.
Рассмотрим стек символов. Это широко распространенная структура данных. Она используется в компиляторах, калькуляторах, диспетчерах экрана, а также в других приложениях, где наборы элементов должны поддерживать протокол LIFO (last in, first out — последний вошел, первый вышел). В примере проверки скобок в выражении из главы 8 использовался стек, названный областью памятк
