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

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

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

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

 

 

 

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