410Часть !1 • Объектно-ориентированное программирование но C^-f
Листинг 10.3. Перегруженные операторные функции как члены класса
#include <iostream> using namespace std;
class Complex { |
// тип, определяемый программистом |
double real, imag; |
// закрытые данные |
public: |
// общедоступные функции-члены |
Complex(double r, double i) |
// общий |
конструктор |
{ real =r; imag = i; } |
|
|
Complex operator+(const Complex &b) |
// только один параметр |
return Complex (real = b.real. imag + b.imag); } |
// быстро и изящно |
void operator += (const Complex &b) { real = real +b.real;
imag = imag + b.imag; }
void operator += (double b) { real += b; }
//изменяется ли целевой объект?
//сложение вещественных компонентов
//сложение мнимых компонентов
//другой интерфейс
void operator + |
() |
|
« |
imag « |
// используется |
как showComplex(const Complex &х) |
{ cout « |
"(" « |
real « ", |
« endl; } |
|
|
|
|
|
|
|
// конец класса Complex |
int mainO |
|
|
|
|
z1(0,0), |
z2(0,0); |
|
// созданные объекты |
{ Complex х(20,40), y(30,50), |
|
cout « |
"Значение x: |
+x; |
|
|
//тоже, |
что x.operator+(); |
cout « |
"Значение у: |
y.operator+(); |
// и так можно |
ввызове функции |
z1 = x.operator+(y); |
|
|
|
// использование |
cout « |
"z1 = X +y: "; |
|
|
|
// вывод z1 |
|
+z1; |
|
|
|
|
|
|
z2 = у + y; |
|
|
|
|
//тоже, |
что z2=x.operator+(y); |
cout « |
"z2 = X +y: "; |
|
|
|
// использование |
как операции |
+z2; |
|
|
|
|
|
// вывод z2 |
|
z1 += x; |
|
|
|
+z1; |
//тоже, |
что z1.operator+=(x); |
cout « |
"Сложение x с z1: |
|
//тоже, |
что z2.operator+=(30.0); |
z2 += 30.0; |
|
|
; +z2; |
cout « |
"Сложение 30 с z2 |
|
|
|
return 0; |
|
|
|
|
|
|
|
Применение ключевого слова const
Использование ключевого слова const с параметрами функции здесь ничем не отличается от листинга 10.2. Нет никаких причин что-либо менять. Между тем некоторые параметры из листинга 10.2 в листинге 10.3 отсутствуют. Вот как вы глядят глобальные функции-серверы из листинга 10.2: *
Complex operator+(const Complex &а, const |
Complex &b) |
/ / |
волшебное имя |
{ |
Complex с; |
/ / локальный объект |
|
|
с. real |
= а. real + b. real; |
/ / |
сложение |
вещественных |
компонентов |
|
с. imag = а. imag + b.imag; |
/ / |
сложение мнимых компонентов |
|
return |
с; } |
|
|
|
|
|
void operator += (Complex &а, const |
Complex &b) |
/ / |
еще одно волшебное имя |
{ |
а. real |
= а.real + b. real; |
/ / |
сложение |
вещественных |
компонентов |
|
а.imag |
= а. imag + b.imag;} |
/ / |
сложение мнимых компонентов |
|
|
Глава 10 « Опероторнью функции |
411 |
void operator += (Complex &а, |
double &b) |
|
|
/ / другой |
интерфейс |
{ а.real += b; |
} |
|
|
/ / |
сложение вещественных компонентов |
void showComplex(const |
Complex &х) |
/ / |
это |
operator+() в листинге 10.3 |
{ cout « " ( " « |
x.real |
« ", |
" « x.imag « |
" ) |
" « endl; } |
|
В листинге 10.2 разработчик выразил свое знание о первом параметре функ ции operator+() с помощью ключевого слова const. Аналогично первые параметры обеих функций operator+=() выражают это знание в форме отсутствия ключевого слова const. В листинге 10.3 данных параметров нет. Как можно отразить отсут ствие или наличие ключевого слова const для таких объектов? Ведь исчезли они лишь из интерфейса функции, но не из приложения, что особенно ясно из синтак сиса операции в клиенте.
Complex х(20,40), у(30.50), |
z1(0,0), z2(0,0); //определение, инициализация |
z2 |
= X + у; |
/ / |
X и у здесь не изменяются |
z1 |
+= х; |
/ / |
z1 |
изменяется |
в результате операций |
z2 |
+= 30.0; |
/ / |
z2 |
изменяется |
в результате операций |
Как бы ни реализовывались эти операции — как автономные функции или как функции-члены, операнды в правой части не изменяются. В результате операции изменяются операнды в левой части. Из анализа синтаксиса вызова функции это не следует. (Не забывайте, что синтаксис оператора — просто альтернативная форма, допустимая при следовании ограничениям языка.)
Complex х(20,40), у(30,50), z1(0,0), |
z2(0,0); //определение, инициализация |
z2 = X.operator + (у); |
/ / |
х при вызове не изменяется |
z1. operator += (х); |
/ / |
z1 изменяется в результате операций |
z2.operator += (30.0); |
/ / |
z2 изменяется в результате операций |
Итак, как же показать, что элементы данных объекта (адресат сообщения) при выполнении метода не изменяются? С помощью ключевого слова const. Но где оно должно находиться? Между закрывающей круглой скобкой списка пара метров и открывающей фигурной скобкой тела функции. В прототипе функции оно включается между закрывающей скобкой списка параметров и точкой с запятой. Мы уже говорили, что следует всегда думать об использовании ключевого слова const. Так оно и есть.
В листинге 10.4 приведена та же программа, что в листинге 10.3, но, там где нужно, добавлено ключевое с/юво const. Кроме того, комплексные функции-члены реализованы вне границ класса (фигурных скобок). При этом пришлось исполь зовать операцию области действия класса. Она применяется к перегруженным операторным функциям точно так же, как к любым другим функциям-членам. Ко нечно, ключевое слово const повторяется в прототипе функции и в ее реализации. Любое различие помечается как синтаксическая ошибка (возможно, в сообщении будет говориться совсем о другом). Результат программы соответствует рис. 10.3.
Листинг 10.4. Перегруженные операторные функции, реализованные вне спецификации класса
#inclucle <iostream> using namespace std;
class'Complex { |
|
/ / |
тип, определяемый программистом |
. double real, |
imag; |
/ / |
закрытые данные |
public: |
|
/ / |
общедоступные функции-члены |
Complex(double r, double i ) |
/ / |
общий конструктор |
Complex operator+(const Complex &b) const |
/ / |
целевой объект не изменяется |
void operator |
+= (const Complex &b) |
/ / |
целевой объект изменяется |
412 |
Часть II • Объвктно-оривштрованное програтмтроваитв на С+4- |
|
void operator += (double b) |
|
|
// целевой объект изменяется |
|
void operator + () const |
|
|
// целевой объект не изменяется |
}; |
|
|
|
|
|
|
// конец класса Complex |
Complex::Complex(double |
r, double i) |
|
// общий |
конструктор |
|
{ real =r; imag = i; } |
|
|
|
|
|
|
|
|
Complex:.'Complex operator+(const Complex &b) const |
|
|
|
|
{ |
return Complex |
(real = b.real, imag + b.real); } |
|
|
|
|
void Complex::operator += (const Complex &b) |
// изменяется целевой объект |
{ |
real = real + b. real; |
|
|
// сложение с вещественными |
компонентами целевого объекта |
|
imag = imag + b.imag; } |
|
// сложение с мнимыми компонентами целевого объекта |
void Complex::operator += (double b) |
|
// целевой объект изменяется |
{ |
real += b; } |
|
|
|
// сложение вещественного компонента с целевым объектом |
void Complex::operator + () const |
" ) " « |
/ / |
целевой объект не изменяется |
{ cout « |
"(" « |
real « |
", " « |
imag « |
endl; } |
|
|
int mainO |
|
|
|
|
z1(0,0), |
z2(0,0); |
|
|
|
|
{ |
Complex x(20,40), y(30,50), |
/ / |
определение, инициализация |
|
cout « |
"Значение x: "; |
+x; |
|
|
/ / т о ж е , |
что |
x.operator+(); |
|
cout « |
"Значение у: |
; |
y.operator+(); |
/ / |
и так можно |
|
z1 = x.operator+(y); |
+z1; |
|
|
/ / |
использование в вызове функции |
|
cout « |
"z1 = X + y: |
|
|
/ / |
то же, |
что |
z2=x.operator+(y); |
|
z2 = у + y; |
|
|
+z2 |
|
|
|
cout « |
"z2 = X + y; "; |
|
|
|
|
|
|
|
z1 += x; |
|
|
|
+z1; |
|
/ / т о ж е , |
что |
z1.operator+(y); |
|
cout « |
"Сложение x с z1: "; |
|
|
|
|
|
|
z2 += 30.0; |
|
|
|
|
|
/ / т о ж е , |
что |
z2.operator+=(30.0); |
cout « "Сложение 30 с z2: "; +z2; return 0;
}
С о в е т у е м Используйте ключевое слово const для параметров перегруженных
операторных функций, не изменяющих значений этих параметров.
При реализации перегруженных операций как функций-членов не забывайте применять ключевое слово const к целевому объекту. Если целевой объект не изменяется, функция должна быть помечена как const.
Отсутствие ключевого слова const должно говорить о том,
что целевой объект изменяется.
Учебный пример: рациональные числа
в данном разделе обсуждается еще один популярный пример перегруженных операторных функций — класс, инкапсулирующий реализацию рациональных чисел (точных дробей). Он реализует арифметические операции и сравнения, не поддерживаемые для целых чисел.
Рациональные числа можно представить в виде двух компонентов: числителя и знаменателя. Они позволяют выполнять операции с дробями без ошибок округ ления, например 1/4 + 3/2 = 14/8 = 7/4.
В реализации класса числитель и знаменатель можно сделать закрытыми элементами данных. Если приложение планируется переносить на 16-разрядную машину, то элементы данных следует определить как long. Если же оно будет выполняться только на 32-разрядной машине, то элементы данных могут иметь тип int или long (в этом случае они представляют одинаковый диапазон).
Глава 10 • Операторные функции |
413 |
class Rational { |
|
|
long nmr; |
// закрытые данные |
|
long dnm; |
|
public: |
// конструктор noумолчанию: нулевые значения |
Rationale) |
{ nmr = 0; dnm = 0; } |
// плохая идея |
|
Rational(long n, long d) |
// общий конструктор: дробь как n/d |
|
nmr = n; dnm = d; |
/ / ОСТАЛЬНАЯ ЧАСТЬ КЛАССА Rational |
|
|
|
Общий конструктор инициализирует поля объекта значениями, заданными клиентом. Конструктор по умолчанию может создавать для дальнейшего присваи вания значений неинициализированные объекты. Большинство программистов не любят оставлять поля объекта неинициализированными, поэтому используют те или иные значения по умолчанию. В данном примере объект инициализируется ну левым значением.
Rational а(1,4), |
Ь(3,2). с, d; |
|
|
с = а + b; |
/ / |
1/4+3/2 = |
(1*2+4*3)/(4*2)=14/8=7/4; |
|
/ / |
с.nmr есть |
7, с.dnm есть 4 |
Может ли конструктор по умолчанию инициализировать оба элемента данных нулем? Если объект используется не как г-значение, а только как I-значение, то подобно объекту с в приведенном примере, никакого вреда это не принесет. Однако если работаюидий с клиентом программист предполагает, что неинициали зированный объект всегда инициализируется нулем, и использует такой объект в вычислениях (например для подсчета суммы), то могут возникнуть проблемы.
Rational а(1,4), |
Ь(3,2), с. d; |
с = а + b; |
// с. nmr есть 7, с. dnm есть 4 |
d += b; |
// О/О + 3/2 = (0*2+3*0)/(0*2); d.nmr=0, d.dnm=0 |
Вот почему лучше присваивать знаменателю ненулевое значение, например 1:
Rational::Rational() |
|
{ nmr = 0; dnm = 1; } |
/ / нулевое значение в виде 0/1 |
Благодаря этому конструктору по умолчанию объекты Rational можно исполь зовать как г-значения и как 1-значения. Когда он используется как 1-значение (объект "с" ниже), вызов конструктора работает "вхолостую".
Rational а(1,4), |
Ь(3,2), с, d; |
с = а + b; |
// с. nmr есть 7, с. dnm есть 4 |
d += b; |
// 0/1 + 3/2 = (0*2+3*0)/(0*2); d.nmr=3, d.dnm=2 |
Арифметические операции могут реализовываться как перегруженные опера торные функции, которые следуют правилам операций с дробями. Ниже приведе на операторная функция operator+(), поддерживаюи;ая сложение двух объектов Rational. Компонент на первой строке функции описывает следуюш,ий алгоритм: числитель результата есть сумма произведений числителя одной дроби на знаме натель другой, а знаменатель — результат произведения знаменателей операндов.
Rational Rational::operator |
+ (const |
Rational &x) |
const |
{ Rational |
temp; |
/ / |
n1/d1+n2/d2 = |
((n1*d2)+(n2*d.1))/(d1*d2) |
temp.nmr |
= (nmr * x.dnm) |
+ (x.nmr |
* dnm); |
|
temp.dnm = dnm *x.dnm; |
// например, 1/4 + 3/2 = 14/8 |
return temp; } |
|
414 |
Часть II * Объектно-ориентированное орогра1^м1^рование но C-f-i: |
Проблема данной реализации в том, что она не нормализует результат. Во-пер вых, это неудобно для пользователя. Во-вторых, знаменатели при вычислениях увеличиваются, а значит, возможно переполнение. Чтобы избежать этого, класс Rational должен поддерживать алгоритм нормализации, вызываемый в конце каждой арифметической операции (включая создание объекта).
class Rational {
|
long nmr; dnm; |
|
/ / |
закрытые данные |
|
public: |
|
/ / |
конструктор |
по умолчанию: нулевые значения |
{ |
Rationale) |
|
nmr = 0; dnm = 1; } |
|
/ / |
общий конструктор: дробь |
как n/d |
{ |
Rational(long n, long d) |
nmr = n; dnm = d; |
|
|
|
|
|
|
|
|
normalizeO; } |
|
|
|
|
|
// важное ключевое слово |
Rational operator + (const Rational &x) const |
{ Rational temp; |
|
// nVcl1+n2/d2 = ((n1*d2)+(n2*d1))/(d1*d2) |
temp.nmr = (nmr *x.dnm) + (x.nmr * dnm); |
|
|
|
temp.dnm = dnm * x.dnm; |
// например, 1/4 + 3/2 = 14/8 |
temp. normalizeO; |
|
|
|
|
|
|
|
return temp; } |
|
|
|
|
|
|
|
void normalizeO |
|
/ / |
найти наибольший общий делитель |
{ if (nmr ==0) { dnm = 1 return; |
} |
|
/ / |
если ноль, |
ничего не делается |
int sign = 1; |
|
/ / если число отрицательное, |
превратить в -1 |
if (nmr < 0) {sign = -1; nmr = -nmr; } |
/ / |
сделать оба числа положительными |
if (dnm < 0) {sign |
sign; |
dnm = -dnm; } |
|
|
|
long gcd = nmr, value = dnm; |
|
|
/ / |
поиск наибольшего общего делителя |
while (value != gcd) { |
/ / остановить, |
когда найден наименьший общий делитель |
|
if (gcd > value) |
|
|
|
|
|
|
|
|
gcd = gcd - value:; |
|
|
|
/ / |
вычесть меньшее из большего |
|
else value = value - gcd; |
} |
|
|
|
|
|
nmr = sign * (nmr/gcd); |
dnm = dnm/gcd; |
} |
|
/ / делитель положительный |
|
|
|
|
|
/ / ОСТАЛЬНАЯ ЧАСТЬ КЛАССА Rational |
} |
|
|
|
|
|
|
|
|
Можно проанализировать математическую часть алгоритма нормализации. Но нас интересует программирование, а не математика, и потому займемся вопросами программирования.
|
пппг |
dnm gcd |
value |
Начало алгоритма |
14 |
8 |
|
Перед циклом |
|
14 |
8 |
После первого прохода |
|
6 |
8 |
После второго прохода |
|
6 |
2 |
После третьего прохода |
|
4 |
2 |
После четвертого прохода |
|
2 |
2 |
После цикла |
|
2 |
|
Перед завершением |
|
|
|
программы |
|
|
|
value != gcd |
gcd > value |
<-(начальные значения полей) |
true: итерация |
true: уменьшение gcd |
true: итерация |
false: уменьшение value |
true: итерация |
true: уменьшение gcd |
true: итерация |
true: уменьшение gcd |
false: завершение |
итерации |
<-(итоговое значение GCD)
<-(итоговые значения полей)
Рис. 10.4. Точная трассировка выполнения функции-члена normalize()
Здесь можно видеть два вызова функции normalizeO. Один из них в функции operator+() применяет эту операцию к локальной переменной temp. В примере сложения 1/4 и 3/2 результатом будет temp, nmr = 14, temp, dnm = 8. Перед пер вым проходом цикла while gcd = 14, value = 8. На первом проходе (14 > 8)
Глава 10 « Операторные функции |
415 |
god = 14-8 = 6, value = 8. На втором проходе (6 <= 8) god = 6, |
value = 8-6 = 2. |
После третьего прохода god = 4, value = 2. После четвертого прохода god = 2, value = 2, и цикл завершает работу. Итак, мы проследили работу алгоритма (см. рис. 10.4).
Второй вызов функции-члена normalizeO — это вызов в обш^ем конструкторе. Он необходим в том случае, если клиент создает объект вида:
Rational х(14,8); / / допустимо, но некрасиво
Какова цель такого вызова функции-члена normalizeO? В главе 9 достаточно много рассказывалось о том, что вид функций-членов значительно отличается от обычных глобальных функций, и для их вызова применяется разный синтаксис. (То, что является параметром в глобальной функции, становится получателем со общения.) Как видно из примера, здесь нет целевого объекта (получателя сообще ния), и функция вызывается подобно любой глобальной функции.
Если объект, который следует использовать в качестве адресата сообщения, явно не задан, то предполагается, что это — вызывающий функцию объект (если она не является глобальной). В данном примере адресатом сообщения будет объект X типа Rational, и функция normalizeO будет использовать в своих алго ритмах поля nmr и dnm этого объекта.
Некоторым программистам не очень нравится, что вызовы глобальных функ ций и функций-членов синтаксически могут выглядеть похоже (без адресата со общения). Вызывая глобальные функции, они применяют операцию глобальной области действия ::, а для вызова функций-членов того же класса используют указатель объекта this.
Почему они невзлюбили одно и то же обозначение функций-членов и глобаль ных функций? Ведь синтаксически это вполне корректно. Компилятор просматри вает список функций-членов, определенных в классе. Если он находит соответствие, то проверяет интерфейс и генерирует вызов функции. Если соответствие не най дено, то компилятор повторяет поиск глобальных функций, доступных в данной области действия.
Все дело в удобстве этой записи для сопровождающего приложение програм миста. Чтобы программа была понятнее (уменьшение сложности исходного кода означает повышение качества ПО), разработчику класса лучше указать, какие именно функции используются в классе — функции-члены или глобальные.
Внимание Нужно всегда стремиться найти способы сообщить максимум информации о разрабатываемом классе. Когда в вызове функции отсутствует адресат сообщения, укажите, что это вызов функции-члена класса (с помощью указателя this) или глобальной функции
(через операцию ::).
В следующей версии класса Rational эти методы применяются для вызова функции normalizeO в общем конструкторе и вызова глобальной функции labs, возвращающей в функции normalizeO абсолютное значение типа long. Кроме того, функция normalizeO перенесена из раздела public класса Rational в раздел private.
Если оставить данную функцию-член в разделе public, пришлось бы сообщить программисту, работающему над кодом клиента, что можно писать алгоритмы, приводящие к ненормализованным состояниям объектов Rational, а следователь но, нужно использовать данную функцию-член в коде клиента. Между тем эта функция добавлена в класс как раз с обратной целью — освободить клиент от обязанностей нормализации и перенести их на серверный класс. Если включить функцию в раздел public, это могло бы побудить программистов, создающих кли ентскую часть, использовать ее и создать тем самым лишние зависимости. Такое решение столь же плохо, как включение в раздел public элементов данных класса.
416 |
Часть II • Объектно-ориентированное программирование но C^-t- |
Таким образом, перед создателем класса стоит важная задача изучения потреб ностей потенциальных клиентов и предоставления им максимально возможного сервиса, но не более того. Набор сервисов, предлагаемых классом, называется общедоступным интерфейсом класса. Данный интерфейс должен быть максималь но узким, но при этом не лишать клиентский код сервисов, которые позволяют сделать программу понятной и независимой от внутренней архитектуры класса сервера.
class Rational { |
|
|
|
|
long nmr; dnm; |
|
/ / |
закрытые данные |
void |
normalizeO |
|
/ / |
закрытая функция-член |
{ if (nmr == 0) {dnm = 1; return; } |
|
|
int sign = 1; |
-1; nmr = ::labs(nmr); } |
// для иллюстрации |
if (nmr <0) {sign |
if (dnm <0) {sign = -sign; dnm = ::labs(dnm); } |
|
long gcd = nmr, value =dnm; |
//поиск наибольшего общего делителя (НОД) |
while (value != gcd) { |
// остановить, когда найден НОД |
|
if (gcd > value) |
|
//вычесть наименьшее значение изнаибольшего |
|
gcd =gcd - value; |
|
else value =value - gcd; } |
|
// делитель положительный |
nmr = sign * (nmr/gcd); dnm =dnm/gcd; } |
public: |
|
//конструктор noумолчанию: нулевые значения |
RationalO |
|
{ |
nmr = 0; dnm = 1; } |
|
// общий конструктор: дробь как n/d |
Rational(long n, long d) |
{ |
nmr = n; dnm = d; |
|
|
|
|
this->normalize(); } |
|
|
|
Rational operator + (const Rational &x) const |
|
{return Rational(nmr*x.dnm +X.nmr*dnm, dnm*x.dnm); }
//ОСТАЛЬНАЯ ЧАСТЬ КЛАССА Rational
};
Ещ е одно важное изменение, относящееся кклассу Rational, касается функции operator+(). В данной версии исключен вызов operatorO для передачи резуль татов вычислений конструктору Rational как аргументов. Применяемая в пре
дыдущей версии класса Rational операторная функция обходится достаточно
"дорого". Воспроизведем ее исходный код:
Rational Rational::operator + (const Rational &x) const
{ Rational temp; |
//n1/d1+n2/d2 = ((n1*d2)+(n2*d1))/(d1*d2) |
temp.nmr = (nmr *x.dnm) + (x.nmr * dnm); |
temp.dnm =dnm * x.dnm; |
//например, 1/4 + 3/2 = 14/8 |
temp. normalizeO; return temp; }
Сколько вызовов функций содержится во второй строке приведенного ниже примера для данной версии перегруженной операторной функции?
Rational а(1,4), |
Ь(3,2), с, d; |
с = а + b; |
/ / с.nmr равно 7, с.dnm равно 4 |
Самый простой ответ: "Ни одного. Здесь только сложение двух дробей". Но это не так. Здесь нет операции сложения — используется перегруженная оператор ная функция (именно функция). Клиент можно переписать так, чтобы данная функция вызывалась явно:
Rational а(1,4), Ь(3,2), с, d; с = а.operator + (b);
/ / с.nmr равно 7, с.dnm равно 4
|
|
Глава 10 • Операторные функции |
417 |
|
Теперь каждому ясно, что здесь есть, как минимум, один вызов функции. Од |
|
нако если посмотреть на ее тело, то можно увидеть вызов конструктора Rational |
|
при создании экземпляра объекта temp. А вызов функции normalize()? Так что на |
|
самом деле здесь три вызова функции. |
|
|
|
Когда функция возвращает значение, тип которого соответствует определяе |
|
мому программистом классу, создается новый неименованный объект данного |
|
класса, вызывается конструктор копирования, инициализирующий поля данного |
|
нового объекта значениями полей существующего объекта (в данном случае temp). |
|
Наконец, выполняется операция присваивания в выражении с = а. operator+(b). |
|
Это также эквивалентно вызову функции. Таким образом, можно насчитать уже |
|
пять вызовов. |
|
|
|
Но и это еще не все. При достижении закрывающей фигурной скобки и завер |
|
шении функции все локальные переменные (здесь — переменная temp) должны быть |
|
уничтожены. Что происходит при уничтожении экземпляра объекта? Правильно, |
|
вызывается деструктор. Кстати, после присваивания в клиенте неименованный |
|
объект, использовавшийся для возврата значения из функции, тоже должен быть |
|
уничтожен. При этом также вызывается деструктор. Итого — семь вызовов. |
|
Хотелось бы сказать, что в новой версии операторной функции число вызовов |
|
сократится до двух, но увы... Можно устранить вызовы конструктора и деструктора |
|
для normalizeO и конструктора копирования для возвращаемого значения. Вмес |
|
то этого вводится вызов обобщенного конструктора и (внутри конструктора) функ |
|
ции normalize(). Таким образом, общее число вызовов будет равно пяти. Не очень |
|
большое достижение, но такие вещи обычно накапливаются. |
|
|
При написании программы на C-h+ нужно научиться видеть скрытые вызовы |
|
функций. Здесь вызывается две функции, там — три, и программа будет выпол |
|
нять достаточно много операций, реализуя при этом совсем немного полезных |
|
действий. Недаром программисты не любят возвращать из функций объекты, хотя |
|
в С+4- это допустимо. |
|
|
|
О с т о р о ж н о ! |
Научитесь распознавать в программе C++ вызовы конструкторов |
Щт |
и деструкторов. Следует избегать лишних вызовов функций. Нужно делать это |
только для поддержки необходимого синтаксиса в клиенте. |
|
|
Какие еще сервисы должен предоставлять класс Rational своим клиентам? |
|
Кроме operator+O нужно реализовать перегруженные операторные функции для |
|
трех других арифметических операций: operate г-(), operator*(), |
operator/(). |
|
Подобно обсуждавшемуся в этой главе классу Complex, каждая арифметическая |
|
операторная функция должна возвращать значение типа класса. Результат можно |
|
присваивать другой переменной данного типа или использовать в качестве адреса |
|
та сообщения в цепочке вызовов. |
|
|
|
Численные алгоритмы часто требуют сравнения значений, и класс Rational — |
|
не исключение. Перегруженные операции сравнения должны возвращать true при |
|
совпадении значений и false (или 0) в противном случае. |
|
|
bool |
Rational::operator == (const Rational &other) |
const |
|
|
{ return (nmr * other.dnm == dnm * other.nmr); } |
|
|
|
bool |
Rational::operator < (const Rational &other) |
const |
|
|
{ return (nmr * other.dnm < dnm * other.nmr); } |
|
|
|
bool |
Rational::operator > (const Rational &other) |
const |
|
|
{ return (nmr * other.dnm > dnm * other.nmr); } |
|
|
Аналогично перегружаются другие операции сравнения. Обратите внимание, что функции не изменяют своих параметров и целевых объектов, что и указывает ся с помощью const. Остается надеяться, что читатели это видят.
418Часть II • Объектно-ориентированное орогра1^1М1ирование на С-^^-
Влистинге 10.5 показана реализация класса Rational и теста, демонстрирую- ш,его некоторые поддерживаемые им операции. Это хороший пример программы на С+4- и использования перегруженных операторных функций, отражаюш,ий способы применения числовых типов данных и операций.
Ли с т и нг 10.5. Класс Rational и его тестирование
#include <iostream> using namespace std;
class Rational { long nmr, dnm; void normalizeO;
public:
Rationale)
{nmr =0; dnm = 1; } Rational(long n, long d)
{nmr = n; dnm = d;
this->normalize(); }
Rational operator + (const Rational &x) const;
Rational operator - (const Rational &x) const;
Rational operator * (const Rational &x) const;
Rational operator / (const Rational &x) const; void operator += (const Rational &x);
void operator -= (const Rational&); void operator *= (const Rational&); void operator /= (const Rational&);
//закрытые данные
//закрытая функция-член
//конструктор поумолчанию: нулевые значения
//общий конструктор: дробь как n/d
//цель - const
//цель изменяется
bool operator == (const Rational &other) const; |
// цель - const |
bool operator < (const Rational &other) const; |
|
bool operator > (const Rational &other) const; |
|
void showO const; |
|
// конец спецификации класса |
|
|
|
Rational Rational::operator |
+ (const Rational &x) const |
{ |
return Rational(nmr*x.dnm |
+X. nmr*dnm, dnm*x.dnm); } |
Rational Rational::operator |
- (const Rational &x) const |
{ return Rational(nmr*x.dnm - x.nmr*dnm, dnm*x.dnm); } |
Rational Rational::operator |
* (const Rational &x) const |
{ |
return Rational(nmr * x.nmr, dnm *x.dnm); } |
|
Rational Rational::operator |
/ (const Rational &x) const |
{ |
return Rational(nmr * x.dnm, dnm *x.nmr); } |
|
void Rational::operator += (const Rational &x) |
// 3/8+3/2=(6+24)/16=15/8 |
{ |
nmr = nmr * x.dnm +x.nmr |
* dnm; |
|
dnm =dnm * x.dnm; |
|
// n1/d1+n2/d2 = (n1*d2+n2*d1)/(d1*d2) |
|
this->normalize(); } |
|
|
void Rational::operator -= (const Rational &x) |
// 3/8+3/2=(6+24)/16=15/8 |
{ |
nmr = nmr *x.dnm - x.nmr |
*dnm; |
|
dnm =dnm * x.dnm; |
|
// n1/d1+n2/d2 = (n1*d2-n2*d1)/(d1*d2) |
|
this->normalize(); } |
|
|
void Rational::operator *= (const Rational &x) |
|
{ |
nmr = nmr *x.nmr: dnm =dnm * x.dnm; |
|
|
this->normalize(); } |
|
|
void Rational::operator /= (const Rational &x) |
|
{ |
nmr =nmr *x.dnm; dnm =dnm * x.nmr; |
|
|
this->normalize(); } |
|
|
|
|
|
|
Глава 10 • Операторные функции |
Г 419 | |
bool Rational:ioperator == (const Rational &other) const |
|
{ return (nmr * other.dnm == dnm * other.nmr)'; } |
|
bool Rational::operator < (const Rational &other) const |
|
{ return (nmr * other.dnm < dnm *other.nmr); } |
|
|
bool Rational::operator > (const Rational &other) const |
|
{ return (nmr * other.dnm > dnm * other.nmr); } |
|
|
void Rational::normalize() |
|
|
// закрытая функция-член |
|
{ if (nmr == 0) { dnm = 1; return; } |
|
|
|
int sign = 1; |
|
|
|
// для иллюстрации |
|
if (nmr <0) { sign = -1; nmr = -nmr; } |
|
|
if (dnm <0) {sign = -sign; dnm = -dnm; } |
// поиск наибольшего общего делителя |
long gcd = nmr, value =dnm; |
|
|
while (value !=gcd) { |
|
|
// остановить, когда найден НОД |
|
if (gcd >value) |
|
|
|
// вычитание наименьшего числа из наибольшего |
gcd = gcd - value; |
|
|
else value =value -gcd; } |
|
|
// делитель положительный |
|
nmr = sign * (nmr/gcd); dnm =dnm/gcd; } |
|
void Rational::show() const |
dnm; } |
|
|
|
{ cout « |
•' " « |
nmr « V " « |
|
|
|
int mainO |
|
d; |
|
|
|
{ Rational a(1.4), b(3,2). c, |
|
// с.nmr равно 7, с.dnm равно 4 |
|
с = a + b; |
" +"; b.showO; cout |
« |
|
a.showOi |
cout « |
|
|
c.showO; |
cout « |
endl; |
|
|
|
|
d = b - а |
cout « |
" -"; a.showO; cout |
« |
|
|
b.showO |
|
|
d.showO |
cout « |
endl; |
|
|
// c.nmr равно 3, с.dnm равно 8 |
|
с = a * b |
cout « |
" *"; b.showO; cout |
« |
|
a.showO; |
|
|
C.showO |
cout « |
endl; |
|
|
|
|
d = b / a |
cout « |
" /"; a.showO; cout |
« |
|
|
b.showO |
|
|
d.showO; |
cout « |
endl; |
|
|
|
|
C.showO с += b;
cout « " b.showO; cout « "="; c.showO; cout « endl; d.showO;
d *= b;
cout « " b.showO; cout « " ="; d.showO; cout « endl; if (b < c)
{ b.showO; cout « " <"; c.showO; cout « endl; } return 0;
}
1/4 + 3/2 = 7/4 3/2 - 1/4 = 5/4 1/4 * 3/2 = 3/8 3/2 / 1/4 = 6/1 3/8 += 3/2 = 15/8 6/1 *= 3/2 = 9/1 3/2 < 15/8
Рис. 10.5.
Результат выполнения программы из лист^инга 10.5
Многие разработчики чувствуют, что такой составной класс как Rational должен предоставлять клиентам строго определенный доступ к своим компонентам и предусматривать соответствующие функции set() и get().
long Rational::getNumer |
() |
const |
/ / обратите внимание на const |
{ return nmr; } |
|
|
|
long Rational::getDenom |
() |
const |
|
{ return dnm; } |
|
|
|