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

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

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

I 390 I

Часть ii « Объектно-ориентировонное прогротттроваиив на С+-^

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

Статические функции-члены

Вот почти и все, но прежде чем завершить данную главу, нужно познакомиться с пятым смыслом ключевого слова static в С+Н-. Это ключевое слово может применяться как модификатор в функции-члене класса, не обращающейся к не­ статическим элементам данных. Другими словами, такая статическая функциячлен может обращаться только к своим параметрам, статическим элементам данных и (о, ужас!) к глобальным переменным.

Хорошим кандидатом на превращение функции-члена в статическую является функция quantityO. Она не имеет никаких параметров, обращается к элементу данных count и не получает доступа ни к каким нестатическим элементам данных.

class Point {

// закрытые координаты

int X, у;

static int count;

// закрытый счетчик объектов

public:

// многосторонний конструктор

Point (int а=0, intb=0)

{ X = а; у = b; count++; }

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

static int quantityO

{ return count; }

// это не может быть const

. . . } ;

 

Статическая функция не может объявляться как const, даже если она не изме­ няет тех значений, к которым обращается. Статическая функция-член обращается к параметрам, статическим элементам данных и глобальным переменным. Данные значения не являются частью состояния объекта. В применении к функции-члену ключевое слово const утверждает, что она не изменяет элементы данных целевого объекта, к которым эта функция обращается. Поскольку статическая функция не обращается к нестатическим элементам данных, ничего обещать нельзя.

Понятно, что это звучит не очень убедительно, но за всем сказанным стоит весьма жесткая логика. Приведенная выше серия примеров начиналась с функции quantity(), реализованной как глобальная функция, обращающаяся к глобальной переменной count. Затем глобальная переменная count была превращена в ста­ тические компонентные данные. Наконец, функция quantityO в соответствии с переменной count стала функцией-членом класса Point. Как нестатическая функция-член, она была определена как const, показывая тем самым, что она не изменяет нестатические элементы данных класса. Наконец, эта функция была пре­ вращена в статическую функцию-член, и модификатор const стал нерелевантным.

Подобно статическим элементам данных и аналогично вызову нестатической функции, статическая функция-член может вызываться через целевой объект класса (или указатель на объект класса). Ее можно вызывать непосредственно-с помошд^ю операции области действия класса, даже если объект класса создан не был.

int mainO

Point::quantityO;

// выводит О

{ cout «

"\пЧисло точек " «

Point р1(20,40);

р1::quantityO;

// выводит 1

cout «

"\пЧисло точек " «

cout «

"\пЧисло точек " «

Point::quantityO;

//выводит О

. . . }

 

 

 

Глава 9 Классы Снн+ как единицы модульности программы

| 391

В листинге 9.6 приведен пример класса Point со статическим элементом дан­ ных count. Он инициализируется вне определения класса, хотя является закры­ тым. Функция quantityO определяется как static, и обращаться к ней можно с помощью операции области действия (первый вызов) и целевого объекта (второй вызов).

Листинг 9.6. Использование статических элементов данных и функций-членов

#inclucle <iostream> using namespace std;

class Point

{

 

 

i nt X,

y;

/ /

закрытые координаты

static

int count;

 

 

public:

 

 

 

Point

(int a=0, int b=0)

/ /

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

{X = a; у = b; count++;

count « " Создана точка: x=" « x « " у=" у « endl; }

static

int quantityO

// const не разрешается

{

return

count; }

void set (int a, int b)

// функция-модификатор

{ X = a;

у = b;

}

void get

(int&

a, int& b)

const

{ X = a;

у = b;

}

// функция-селектор

void move (int a, int b)

// функция-модификатор

{ X += a;

у += b; }

"PointO

 

 

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

{ count-;

 

 

cout «

" Удалена точка: x=" « x «

"

 

} ;

 

 

у="« у « endl; }

int

Point::cont

= 0;

 

 

int mainO

 

Point::quantity() « endl;

{

cout « "Число точек: " «

Point p1, p2(30), p3(50,70);

// начало координат, ось x. общая точка

cout « "Число точек: " «

р1. quantityO

« endl;

return 0;

 

 

 

}

 

 

 

Результат выполнения программы показан на рис. 9.7. Рассмотрим еще одну версию класса Point, содержащего функцию,

сравнивающую координаты своих двух параметров Point ивозвраща­ ю щ у ю true, если координаты совпадают.

class Point {

// закрытые

координаты

int X, у;

static int count;

// закрытый

счетчик объектов

public:

 

 

Point (int a=0, int b=0) // многосторонний конструктор

{ X = a; у = b; count++

}

 

static int quantityO

// const не разрешается

{ return count; }

bool samePoints (const Point &p1, const Point &p2) { return p1.x ==p2.x &&p1.y == p2.y }

. . . } ;

Создана точка: х=640 у=0 Число точек: 1 Создана точка: х=0 у=0 Создана точка: х=30 у=0

Создана точка: х=50 у=70 Число точек: 4 Удалена точка: х=50 у=70

Удалена точка: х=30 у=0 Удалена точка: х=0 у=0

Рис . 9 . 7 . Результат

программы из лист,инга 9.7

392

Чость II« Объектно-ориентировонное профатттроваитв на С+^

 

Переданы ли здесь идеи разработчика сопровождающему приложение про­

 

граммисту? Нет, Например, очевидным промахом является то, что в данной

 

версии не отражается тот факт, что функция samePoints() не изменяет состояние

 

целевого объекта. Следовательно, ее нужно определить как const. Но это еще

 

не все. Нужно указать также, что эта функция работает только со своими пара­

 

метрами. Она может быть глобальной, так как не нуждается в элементах данных

 

целевого объекта Point и работает только с элементами данных, переданными ей

 

в параметрах. Между тем она включена в класс Point. Это показывает, что она

 

логически принадлежит классу Point и имеет дело с объектами данного класса,

 

а не с объектами класса Rectangle, Circle или др. Вот почему ее следует опреде­

 

лить как static.

 

 

 

 

 

Для иллюстрации посмотрим, как можно вызывать эту функцию. Вот несколь­

 

ко способов:

 

 

 

 

 

 

Point р1, р2(30), рЗ(50,70);

 

 

 

 

 

i f (p1.samePoints(p2,p3)==true) cout

«

"Точки совпадают\п";

 

 

Но какое отношение объект р1 имеет к сравнению р2 и рЗ? Помните байку про

 

крокодила и обезьяну? Некрасивый вариант. Еще один способ — использовать

 

объект р2 дважды:

 

 

 

 

 

Point р1,

 

р2(30). рЗ(50.70);

 

 

 

 

 

i f (p2.samePoints(p2,p3)==true) cout

«

"Точки совпадают\п";

 

 

Все равно некрасиво. Объект должен использоваться только один раз. Давайте

 

определим эту функцию как статическую:

 

 

 

 

class Point {

 

 

/ /

частные координаты

 

int X,

у;

 

 

 

static

int

count;

 

/ /

частный счетчик

объектов

 

public:

 

 

 

 

 

Point

 

(int

a=0, int b=0)

 

/ /

многосторонний

конструктор

 

{

X = a;

у = b; count++;}

 

 

static

int

quantityO

 

/ /

const не разрешается

 

{ return count; }

 

 

static

bool

samePoints (const Point &p1, const Point &p2)

 

 

{

return

p1.x == p2.x && p1.y

== p2.y;

}

 

 

. . . }

;

 

 

 

 

 

 

Теперь можно вызывать функцию с помощью операции области действия:

Point р1. р2(30), рЗ(50,70);

i f (Point::samePoints(p2,p3)==true) cout << "Точки совпадают\п";

Отлично!

Так когда же использовать статические элементы данных и статические функ­ ции? Если элементы данных определяются как статические, это показывает, что такие глобальные данные логически принадлежат классу (пример — count). Когда в качестве статических определяются функции-члены, это свидетельствует о том, что глобальные функции логически принадлежат классу и работают с его статиче­ скими данными, глобальными данными или параметрами, но не с нестатическими данными (примеры — quantityO, samePointO).

Применение статических данных и функций не является при изучении C-f-f главной задачей, однако нужно понимать эти вопросы и использовать ключевое слово static для лучшей передачи своих намерений программистам, отвечающим за клиентскую часть и сопровождение приложения.

Глава 9 • Классы 0+4- как единицы модульности програллмы

393

Итоги

в этой главе рассматривалось использование классов С+Н- как инструментов для создания программ. Применение классов устраняет недостатки, свойственные глобальным функциям как инструменту объектно-ориентированного программи­ рования.

Первый недостаток глобальных функций состоит в следующем: они не всегда сообщают, что имел в виду разработчик и что функции, обращающиеся к одной и той же структуре данных, на самом деле связаны логически.

Например, если программа использует структуры данных Point и Rectangle, а разработчик помещает все функции доступа к Point в одно место, а все функции доступа к Rectange — в другое, то все замечательно. Если же функции доступа разделены, компилятор жаловаться не будет, но при сопровождении в программе сложно разобраться. Важна дисциплина программирования.

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

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

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

Классы несут в себе большой потенциал для улучшения качества ПО.

о

 

^ ^ ^ ^ ^

 

 

 

ператорные функции

Темы данной главы

 

*^ Перегрузка операций

 

«^ Ограничение на перегрузку операций

•^ Перегруженные операции как компоненты класса

1^ Учебный пример: рациональные числа

•^ Смешанные типы как параметры

ь^ Дружественные функции

 

*^ Итоги

 

]

^ ^

предыдущей главе рассказывалось о синтаксисе и семантике классов

Ш

^5к -языка C+-I-. Язык С+Н

не первый язык программирования, под-

^

4l!l!l-^^ держивающий концепцию классов, но именно в нем это впервые

успешно реализовано в "индустриальном масштабе".

 

Первоначально C + + внедрялся достаточно медленно, так как в индустрии ПО

с сомнением воспринималась его эффективность и надежность. Сомнения в основ­ ном были безосновательны. Большинство программ C + + занимают столько же памяти, сколько эквивалентные программы на С, не более, и почти все выполня­ ются не медленнее, чем эквивалентные программы на языке С. Безусловно, есть некоторые исключения, связанные с использованием библиотеки lostream, вир­ туальных функций и шаблонов. (О них подробнее рассказывается в следующих главах.) Между тем значительный прогресс в области создания аппаратных средств привел к увеличению объемов памяти и быстродействия компьютеров. Это осла­ било требования к памяти и к скорости выполнения программ. Опыт применения языка C + + ясно демонстрирует, что программирование с применением классов может быть весьма эффективным. Ожидается, что все новые языки программиро­ вания будут поддерживать классы.

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

Связывать данные и операции объекта

Управлять внешним доступом к элементам класса

Глава 10 • Операторные функции

395

Вводить дополнительные области действия для предотвращения конфликтов имен

Переносить обязанности с клиентов на серверы

В предыдущей главе показано, что разработчик СН-+ Бьерн Страуструп вклю­ чил в классы С-Ы- намного больше, чем предусматривает приведенный короткий список. Конструкторы и деструкторы помогают объектам класса управлять своими ресурсами (в основном динамически распределяемой памятью). Доступность этих функций возлагает дополнительное бремя на разработчика классов (программис­ та, отвечающего за серверную часть приложения). Он обязан предусмотреть раз­ нообразные конструкторы для поддержки клиента в разнообразных контекстах. Кроме того, программист должен обеспечить инициализацию данных объекта, но это уже незначительный побочный эффект.

Использование составных объектов еще более усложняет дело. Разработчику класса-контейнера надо позаботиться об инициализации компонентов объекта. Новый синтаксис для этого реализует список инициализации компонентов. Идея составного класса требует использования таких дополнительных деталей, как ком­ поненты-константы, компоненты-указатели и рекурсивные компоненты. Принцип атрибутов класса ведет к другим расширениям этой идеи, таким, как статические элементы данных и статические функции, характеризующие класс в целом, а не отдельные экземпляры объектов класса.

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

Обычно C++ позволяет делать это несколькими способами. Мы обсудим раз­ ные методы реализации перегруженных операторных функций. Данные методы помогают эффективнее использовать C+ + , а также позволяют лучше понять внутренние механизмы программы C+ + .

Перегрузка операций

В C++ концепция определяемых программистом типов (классов) является расширением концепции встроенных числовых типов. Здесь можно определять переменные новых типов, применяя тот же синтаксис, что и для простых числовых переменных. Подобно встроенным типам, разрешается использовать экземпляры объектов определяемых программистом типов как элементы массива или как элементы данных еще более сложных типов, передавать объекты таких вновь образованных типов, как параметры и, возвращать их из функций, присваивать указателям и ссылкам определяемые программистом значения, применяя такой же синтаксис, как для встроенных значений. С помощью того же синтаксиса, что и для встроенных типов, можно также определять указатели и ссылки на значе­ ния-константы.

Такое подобие не случайно. Одна из важных целей разработки C++ заклю­ чалась в интерпретации определяемых программистом типов таким же образом, как встроенных типов. Данная цель не имеет ничегЧ) общего с объектно-ориенти­ рованным программированием, улучшающим продуктивность разработки, повы­ шающим эффективность сопровождения ПО или положительно отражающимся на других аспектах разработки ПО. Это чисто эстетическая цель. И вполне закон­ ная. Программирование, как и любая творческая деятельность, имеет значитель­ ную эстетическую составляющую. Хотя в книгах по программированию данная тема обсуждается редко, создаваемые программы должны быть настолько же элегантны, насколько удобны в чтении и сопровождении.

396

Часть II * Объектно-ориентированное програтттроваите на С+4

 

Конечно, многие программы, особенно крупные, трудно назвать элегантными.

 

Как, впрочем, удобными в сопровождении, легко читаемыми и переносимыми.

 

Между тем язык спроектирован так, чтобы программист имел возможность до­

 

стичь этих целей.

 

 

В то же время существует значительный разрыв между одинаковой интерпре­

 

тацией классов и числовых типов. Определяемые программистами типы С+ +

 

не являются в точности такими же, как "естественные" числовые типы. Самая

 

большая разница в том, что операции С+Н

сложение, вычитание, сравнение

на равенство и т. д.— нельзя применять к объектам определяемых программистом типов. Конечно, можно написать функции для реализации подобных операций, но запись выражений при этом будет весьма сложной.

Рассмотрим простой пример: комплексные числа, характеризуемые значения­ ми действительной и мнимой части. Те, кто не знаком с мнимыми числами, могут рассматривать их как точки на плоскости, где действительные компоненты соот­ ветствуют координате х, а мнимые — координате у. При с/южении или вычитании сложных чисел результатом также будет мнимое число. Его действительная часть есть результат сложения (или вычитания) действительных компонентов двух опе­ рандов, а мнимая — результат сложения (или вычитания) мнимых компонентов. Умножение и деление выполняется сложнее, но тоже реализуются как операции с компонентами комплексного числа.

Представим комплексные числа как класс с двумя элементами данных — real и imag. Для простоты обсуждения объявим их public (в private они превратятся в следующей версии класса).

struct Complex {

double real, imag; } ; / / общедоступные элементы данных

В листинге 10.1 дан пример программы, определяющей экземпляры типа Complex, инициализирующие и выполняющие некоторые арифметические опера­ ции с объектами.

••••••••*•••••«•••••"•••••••• мшиммжшммшшшшшммшмм mm т ( i — м ш — — i ii HI ттшттшштттттт

Внимание Данный пример нельзя считать хорошим примером программы C++. Авторы большинства книг по C++ избегают демонстрировать отрицательные примеры. В результате читателю не удается научиться отличать хорошие программы C++ от плохих. Это все равно, что пытаться научиться живописи, только посещая музеи, в которых представлены шедевры. Программирование на C++ — всегда попытка найти лучшее решение, превосходящее конкурирующий или альтернативный вариант.

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

А затем привожу лучшее решение и рассказываю о том, чем именно оно лучше.

В примере (см. листинг 10.1) клиент выполняет вычисления с комплексными числами, используя прямой доступ к общедоступным компонентам объекта. В кли­ енте вместо функций-членов определяются имена и элементы данных real и imag. В результате клиентская программа представляет собой сочетание доступа к по­ лям данных и вычисления с полями данных. Смысл этих вычислений не выража­ ется в вызовах функций, его трудно уяснить из анализа деталей вычислений на нижнем уровне. Операции низкого уровня не перенесены на функции-серверы, и программисту придется держать в уме одновременно несколько уровней алго­ ритма: вычисления высокого уровня и детальные операции. Обязанности не разде­ лены, и изменения в конструкции класса Complex повлияют на всю программу. Применять ключевое слово struct вместо class здесь не стоит, так как все эле­ менты данных общедоступны.

Глава 10 • Операторные функции

397

Листинг 10.1. Пример операций с объектами класса Complex

#inclucle <iostream> using namespace std;

struct

Complex

{

 

 

 

 

 

 

 

 

 

// тип, определяемый программистом

 

double real,

imag;

}

;

 

 

 

 

 

 

 

 

int

mainO

 

 

 

 

 

 

 

 

 

 

 

// объекты типа Complex

{

Complex X, y,

z1, z2;

 

 

 

 

 

 

 

X. real

= 20;

x.imag

= 40;

 

 

 

 

 

// инициализация

 

y. real

= 30;

y.imag

= 50;

 

 

 

 

 

 

 

 

 

cout

«

"Первое значение:

";

 

 

 

)" «

endl;

 

cout

«

" ( "

«

x.real «

",

"

«

x.imag

«

 

cout

«

"Второе значение:

";

 

 

 

 

") " «

endl;

 

cout

«

" ( "

«

у. real «

",

"

«

у. imag

«

 

z1. real

= X. real

+ y. real;

 

 

 

 

// сложить действительные компоненты в z1

 

zl.imag

= x.imag

+ y.imag;

 

 

 

 

 

// сложить мнимые компоненты

 

cout

«

"Сумма двух значений: ";

 

 

« " ) "

«

endl;

 

cout

«

" ( "

«

z l . r e a l «

",

 

" «

zl.imag

 

z2. real

= x. real

+ y. real;

 

 

 

 

 

// сложить действительные компоненты в z2

 

z2.imag

= x.imag

+ y.imag;

 

 

 

 

 

// сложить мнимые компоненты

 

z1. real

= z. real

+ x. real;

 

 

 

 

 

// сложить действительные компоненты в z1

 

zl.imag

= z.imag

+ x.imag;

 

 

 

 

 

// сложить мнимые компоненты

 

cout

«

"Сложение первого значения с суммой:

 

 

 

cout

«

" ( " «

z l . r e a l «

",

 

" «

zl.imag

« ' ) " « endl;

 

z2. real

+= 30.0;

 

 

 

 

 

 

 

 

// сложение с действительной частью z2

 

cout «

"Сложение 30 с суммой: ";

z2.imag

« ' ) "

«

endl;

 

cout «

"(" «

z2.real «

", " «

 

return 0;

 

 

 

 

 

 

 

 

 

 

 

 

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Первое

значение:

<20,

40>

 

Результат выполнения данной программы пока­

 

зан на рис. 10.1. Хотя этот пример нельзя считать

Второе

значение:

<30,

50>

 

хорошим, но мы получили неплохую отправную точ­

Сумма двух значений:

 

<50. 90>

Сложение первого

значения

с суммой: <70,

130>

ку для обсуждения перегрузки операторных функ­

Сложение 30 с суммой:

 

<80, 90>

ций. Кроме того, хочется воспользоваться случаем,

 

 

 

 

 

 

чтобы повторить список случаев плохого применения

Рис. ]0.] ш Результат

выполнения

 

C++. Это очень важный список: повторяйте его при

 

анализе своей программы. Вы сможете оценить, на­

 

программы

 

из листинга

ЮЛ

 

 

сколько корректно используете C + + , и улучшить

 

 

 

 

 

 

 

 

 

 

 

 

качество создаваемого ПО.

 

 

 

 

Чтобы инкапсулировать детали реализации и изолировать от них клиент,

 

 

нужно написать функции доступа, манипулируюш,ие объектами типа Complex для

 

 

обслуживания клиента. Например, если требуется добавить переменные данного

 

 

типа, нужно написать функцию с двумя параметрами типа Complex, которая вы­

 

 

полняет необходимые операции с компонентами объекта и возвраидает результат

 

 

того же типа. Это означает, что интерфейс функции, например addComplex(),

 

 

может выглядеть следуюш^им образом:

 

 

Complex addComplex(const

Complex &а, const Complex &b);

 

 

 

Как уже упоминалось выше, сложение двух комплексных чисел требует отдель­

 

 

ного сложения их веш[.ественных и мнимых компонентов.

Complex addCompex(const Complex &а,

const

Complex &b)

{ Complex c;

/ /

локальный объект

с.real = a.real + b.real;

/ /

сложение вещественных компонентов

398

Часть II • Объектно-ориентированное программирование на С^-Ф

 

c.imag

= а. imag + b.imag;

/ / сложение мнимых компонентов

 

return

с; }

 

Чтобы использовать данную функцию, в клиенте определяются и инициализи­ руются переменные типы Complex. Клиент передает их этой функции как парамет­ ры и использует возвращаемое значение типа Complex.

Complex X, у,

z1, z2;

/ /

объекты типа Complex

X.real = 20;

x.imag

= 40;

/ /

инициализация

у. real = 30;

у.imag

= 50;

/ /

использование в вызове функции

z1 = acldComplex(x,y);

Все это очень здорово (и весьма тривиально). Большинство программистов применяют подобный функциональный стиль программирования, не чувствуя, что использование таких имен функций, как addComplexO, делает их программы трудночитаемыми. Настояш,ий программист (во всяком случае, считающий себя таковым) напишет код клиента так:

Complex X, у,

z1, z2;

/ /

объекты типа Complex

X. real

= 20;

х.imag

= 40;

/ /

инициализация

у.real

= 30;

у.imag

= 50;

 

 

z2 = X + у;

 

 

/ /

использование в выражении

Если это сделать, то компилятор сообщит, что операция сложения не определена, несмотря на все претензии С+4- на универсальный подход и попытки одинаково интерпретировать разные типы. Поскольку использовать встроенные операции для определяемых программистом типов нельзя, придется придумать новые функции вида addComplexO и реализовать с их помощью необходимые операции. Неравноправие между определяемыми типами и встроенными типами C++ бо­ лезненно воспринимается каждым настоящим программистом.

В качестве лекарства C++ предлагает следующее. Программист может огра­ ничиться специальными именами функций с ключевым словом operator и знаком операции, которую нужно использовать в исходном коде, например +. Он разра­ батывает и реализует такую функцию operator+() точно так же, как реализуется любая другая функция, в частности addComplexO. Как язык, поддерживающий программиста, C++ позволяет вызывать данную функцию с помощью знака опе­ рации, соответствующего знаку операции в имени функции. Если функция назы­ вается operator+0, то можно вызвать ее с помощью такой же записи, как для встроенных числовых типов:

Z = X + у;

/ / на самом деле это z = operator+(x,y);

Ну не замечательно ли? Предлагаемый функцией сервис ассоциируется со встроенной операцией C+ + . Великолепно!

Но это не столь уж уникальное средство. В C++ одно и то же имя функции

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

вэтой области действия и определяет, какое из них нужно использовать для реа­ лизации вызова функции.

Сказанное относится к любому имени функции в C+ + . Что касается арифме­ тических операций, то перегрузка операций применяется в любом языке програм­ мирования, а не только в C++. Она означает множественную интерпретацию одного и того же символа. Рассмотрим, например, операцию сложения:

int a.b.c; float d,e,f;

a = 20, b = 30; d = 40.0; e = 50.0;

с = a + b; f = d + e;

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

Глава 10 • Операторные функции

399

В C++ (и в других языках) операция + используется для сложения целых чи­ сел или чисел с плавающей точкой, а это совершенно разные действия. Для целых переменных каждый бит одного операнда складывается с каждым битом другого и с битом переноса из бита нижнего порядка.

Для значений с плавающей точкой двоичное представление состоит из мантис­ сы и экспоненты. Чтобы уйти от сложности двоичных (или шестнадцатеричных) значений, проиллюстрируем данный вопрос с помощью десятичной системы. В представлении с мантиссой и экспонентой число 3000.0, например, будет выгля­ деть как 3*10"3, а 300.0 — как 3*10"2. (Здесь используется знак ", хотя в C+ + нет операции экспоненты.) При сложении чисел с плавающей точкой мантисса меньшего операнда сдвигается вправо, так что экспоненты двух операндов стано­ вятся одинаковыми (при сложении 3000 и 300 число 300 сдвигается на три десятич­ ных позиции вправо и представляется как 3.3*10"3). После этого складываются биты мантиссы (а не все биты, как для целых). При сложении 3000 и 300 резуль­ татом будет 3.3*10"3.

Каковы бы ни были детали сложения чисел с плавающей точкой, ясно, что они отличаются от целочисленного сложения. На уровне языка ассемблера эти опера­ ции представлены разными кодами операций. Языки высокого уровня не вынужда­ ют программистов заучивать разные обозначения для целочисленного сложения и сложения с плавающей точкой.

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

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

лючениями) к любому определяемому программистом типу!

 

Здесь операторная функция operator+()

реализована ддя параметров типа

Complex.

 

 

 

Complex operator+(const Complex &a, const

Complex &b)

/ / волшебное имя

{ Complex с;

/ /

локальный объект

 

с. real = a. real + b. real;

/ /

сложение вещественных компонентов

c.imag = a. imag + b.imag;

/ /

сложение мнимых компонентов

return c; }

 

 

 

Как написана эта функция? Просто скопирована приведенная выше функция ас1с1Сотр1ех(), сохранено тело функции и список параметров, удалено имя функции addComplex, замененное на имя operator+. Дело сделано! C++ сделает и осталь­ ное. Он будет воспринимать операцию сложения с операндами типа Complex и не выведет сообщение о синтаксической ошибке, в которой говорится, что операция сложения для типа Complex не определена. Теперь эта операция определена.

Complex X, у,

z;

 

/ /

объекты типа Complex

X.real

= 20; x.imag

= 40;

/ /

инициализация

у. real

= 30;

у.imag

= 50;

 

 

z = X + у;

 

 

/ /

использование в выражении

Что фактически означает фраза "компилятор воспринимает операцию сложе­ ния с операндами типа Complex"? Какой код он будет генерировать? Компилятор вызовет только что написанную перегруженную функцию operator+() и использует

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