WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 | 2 || 4 |

«Б.Страуструп Язык программирования Си++ Москва 1991 Оглавление Предисловие Заметки для читателя Структура этой книги Замечания по реализации Упражнения Замечания по проекту языка ...»

-- [ Страница 3 ] --

Скорее всего, для всех остальных он будет непостижим. Кроме того, C препроцессор - очень простой макропроцессор. Когда вы попытаетесь сделать что-либо нетривиальное, вы, вероятно, обнаружите, что сде- лать это либо невозможно, либо чрезвычайно трудно (но см. #7.3.5).

4.8 Упражнения 1. (*1) Напишите следующие описания: функция, получающая параметр типа указатель на символ и ссылку на целое и не возвращающая значения;

указатель на такую функцию;

функция, получающая та- кой указатель в качестве параметра;

и функция, возвращающая такой указатель. Напишите определение функции, которая получа- ет такой указатель как параметр и возвращает свой параметр как возвращаемое значение. Подсказка: используйте typedef.

2. (*1) Что это значит? Для чего это может использоваться?

typedef int (rifii&) (int, int);

3. (*1.5) Напишите программу вроде "Hello, world", которая полу- чает имя как параметр командной строки и печатает "Hello, имя". Модифицируйте эту программу так, чтобы она получала по- лучала любое количество имен и говорила hello каждому из них.

4. (*1.5) Напишите программу, которая читает произвольное число файлов, имена которых задаются как аргументы командной стоки, и пишет их один за другим в cout. Поскольку эта программа при выдаче конкатинирует свои параметры, вы можете назвать ее cat (кошка).

5. (*2) Преобразуйте небольшую C программу в С++. Измените заго- ловочные файлы так, чтобы описывать все вызываемые функции и описывать тип каждого параметра. Замените, где возможно, ди- рективы #define на enum и const или inline. Уберите из.c фай- лов описания extern и преобразуйте определения функций к син- таксису С++. Замените вызовы malloc() и free() на new и delete. Уберите необязательные приведения типа.

6. (*2) Реализуйте sort() (#4.6.7) используя эффективный алгоритм сортировки.

7. (*2) Посмотрите на определение struct tnode в с.#8.5. Напишите функцию для введения новых слов в дерево узлов tnode. Напишите функцию для вывода дерева узлов tnode. Напишите функцию для вывода дерева узлов tnode со словами в алфавитном порядке. Мо- дифицируйте tnode так, чтобы в нем хранился (только) указатель на слово произвольной длины, помещенное с помощью new в сво- бодную память. Модифицируйте функции для использования нового определения tnode.

8. (*2) Напишите "модуль", реализующий стек. Файл.h должен описывать функции push(), pop() и любые другие удобные функции (только). Файл.c определяет функции и данные, необходимые для хранения стека.

9. (*2) Узнайте, какие у вас есть стандартные заголовочные файлы.

Составьте список файлов, находящихся в /usr/include и /usr/include/CC (или там, где хранятся стандартные заголовоч- ные файлы в вашей системе). Прочитайте все, что покажется ин- тересным.

10. (*2) Напишите функцию для обращения двумерного массива.

11. (*2) Напишите зашифровывающую программу, которая читает из cin и пишет в cout закодированные символы. Вы можете восполь- зоваться следующей простой схемой шифровки: Зашифрованная фор- ма символа c - это c^key[i], где key (ключ) - строка, которая передается как параметр командной строки. Программа использует символы из key циклически, пока не будет считан весь ввод. Пе- рекодирование зашифрованного текста с той же строкой key дает исходный текст. Если не передается никакого ключа (или переда- ется пустая строка), то никакого кодирования не делается.

12. (*3) Напишите программу, которая поможет расшифровывать тексты, зашифрованные описанным выше способом, не зная ключа.

Подсказка: David Kahn: The Code-Breakers, Macmillan, 1967, New York, pp 207-213.

13. (*3) Напишите функцию error, которая получает форматную стро- ку в стиле printf, которая содержит директивы %s, %c и %d, и произвольное количество параметров. Не используйте printf().

Если вы не знаете значения %s и т.д., посмотрите #8.2.4.

Используйте .

14. (*1) Как бы вы выбирали имени для типов указателя на функцию, определенных с помощью typedef?

15. (*2) Посмотрите какие-нибудь программы, чтобы создать представление о разнообразии стилей и имен, использующихся на практике. Как используются буквы в верхнем регистре? Как используется подчерк? Где используются короткие имена вроде x и y?

16. (*1) Что неправильно в следующих макроопределениях?

#define PI = 3. #define MAX(a,b) a>b?a:b #define fac(a) (a)*fac((a)-1) 17. (*3) Напишите макропроцессор, который определяет и расширяет простые макросы (как C препроцессор). Читайте из cin и пишите в cout. Сначала не пытайтесь обрабатывать макросы с параметра- ми. Подсказка: В настольном калькуляторе (#3.1) есть таблица имен и лексический анализатор, которые вы можете модифициро- вать.

Глава 5 Классы Эти типы не "абстрактны", они столь же реальны, как int и float.

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

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

Тип есть конкретное представление некоторой концепции (понятия).

Например, имеющийся в С++ тип float с его операциями +, -, * и т.д.

обеспечивает ограниченную, но конкретную версию математического по- нятия действительного числа. Новый тип создается для того, чтобы дать специальное и конкретное определение понятия, которому ничто прямо и очевидно среди встроенных типов не отвечает. Например, в программе, которая работает с телефоном, можно было бы создать тип trunk_module (элемент линии), а в программе обработки текстов - тип list_of_paragraphs (список параграфов). Как правило, программу, в которой создаются типы, хорошо отвечающие понятиям приложения, по- нять легче, чем программу, в которой это не делается. Хорошо выб- ранные типы, определяемые пользователем, делают программу более четкой и короткой. Это также позволяет компилятору обнаруживать не- допустимые использования объектов, которые в противном случае оста- нутся необнаруженными до тестирования программы.

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

Эта глава состоит из четырех практически отдельных частей:

#5.2 Классы и Члены. Этот раздел знакомит с основным понятием типа, определяемого пользователем, который называется класс (class). Доступ к объектам класса может ограничиваться набором функций, которые описаны как часть этого класса. Такие функции называются функциями членами. Объекты класса создаются и ини- циализируются функциями членами, специально для этой цели описанными. Эти функции называются конструкторами. Функция член может быть специальным образом описана для "очистки" каж- дого классового объекта при его уничтожении. Такая функция на- зывается деструктором.

#5.3 Интерфейсы и Реализации. В этом разделе приводится два примера того, как класс проектируется, реализуется и использу- ется.

#5.4 Друзья и Объединения. В этом разделе приводится много дополнительных подробностей, касающихся классов. В нем показа- но, как предоставить доступ к закрытой части класса функции, которая не является членом этого класса. Такая функция называ- ется друг (friend). В этом разделе показано также, как опреде- лить дискриминирующее объединение.

#5.5 Конструкторы и Деструкторы. Объект может создаваться как автоматический, статический или как объект в свободной памяти.

Объект может также быть членом некоторой совокупности (типа вектора или класса), которая в свою очередь может размещаться одним из этих трех способов. Довольно подробно объясняется использование конструкторов и деструкторов.

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

5.2.1 Функции члены Рассмотрим реализацию понятия даты с использованием struct для того, чтобы определить представление даты date и множества функций для работы с переменными этого типа:

struct date { int month, day, year;

};

// дата: месяц, день, год } date today;

void set_date(date*, int, int, int);

void next_date(date*);

void print_date(date*);

//...

Никакой явной связи между функциями и типом данных нет. Такую связь можно установить, описав функции как члены:

struct date { int month, day, year;

void set(int, int, int);

void get(int*, int*, int*);

void next();

void print();

};

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

date today;

// сегодня date my_burthday;

// мой день рождения void f() { my_burthday.set(30,12,1950);

today.set(18,1,1985);

my_burthday.print();

today.next();

} Поскольку разные структуры могут иметь функции члены с одинаковы- ми именами, при определении функции члена необходимо указывать имя структуры:

void date::next() { if ( ++day > 28 ) { // делает сложную часть работы } } В функции члене имена членов могут использоваться без явной ссыл- ки на объект. В этом случае имя относится к члену того объекта, для которого функция была вызвана.

5.2.2 Классы Описание date в предыдущем подразделе дает множество функций для работы с date, но не указывает, что эти функции должны быть единственными для доступа к объектам типа date. Это ограничение можно наложить используя вместо struct class:

class date { int month, day, year;

public:

void set(int, int, int);

void get(int*, int*, int*);

void next();

void print();

};

Метка public: делит тело класса на две части. Имена в первой, закрытой части, могут использоваться только функциями членами. Вто- рая, открытая часть, составляет интерфейс к объекту класса. Struct - это просто class, у которого все члены классы открытые, поэтому функции члены определяются и используются точно так же, как в пре- дыдущем случае. Например:

void date::ptinr() // печатает в записи, принятой в США { cout << month << "/" << day << "/" year;

} Однако функции не члены отгорожены от использования закрытых чле- нов класса date. Например:

void backdate() { today.day--;

// ошибка } В том, что доступ к структуре данных ограничен явно описанным списком функций, есть несколько преимуществ. Любая ошибка, которая приводит к тому, что дата принимает недопустимое значение (напри- мер, Декабрь 36, 1985) должна быть вызвана кодом функции члена, по- этому первая стадия отладки, локализация, выполняется еще до того, как программа будет запущена. Это частный случай общего утвержде- ния, что любое изменение в поведении типа date может и должно вызы- ваться изменениями в его членах. Другое преимущество - это то, что потенциальному пользователю такого типа нужно будет только узнать определение функций членов, чтобы научиться им пользоваться.

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

5.2.3 Ссылки на себя В функции члене на члены объекта, для которого она была вызвана, можно ссылаться непосредственно. Например:

class x { int m;

public:

int readm() { return m;

} };

x aa;

x bb;

void f() { int a = aa.readm();

int b = bb.readm();

//...

} В первом вызове члена member() m относится к aa.m, а во втором - к bb.m.

Указатель на объект, для которого вызвана функция член, является скрытым параметром функции. На этот неявный параметр можно ссы- латься явно как на this. В каждой функции класса x указатель this неявно описан как x* this;

и инициализирован так, что он указывает на объект, для которого бы- ла вызвана функция член. this не может быть описан явно, так как это ключевое слово. Класс x можно эквивалентным образом описать так:

class x { int m;

public:

int readm() { return this->m;

} };

При ссылке на члены использование this излишне. Главным образом this используется при написании функций членов, которые манипулиру- ют непосредственно указателями. Типичный пример этого - функция, вставляющая звено в дважды связанный список:

class dlink { dlink* pre;

// предшествующий dlink* suc;

// следу- ющий public:

void append(dlink*);

//...

};

void dlink::append(dlink* p) { p->suc = suc;

// то есть, p->suc = this->suc p->pre = this;

// явное использование this suc->pre = p;

// то есть, this->suc->pre = p suc = p;

// то есть, this->suc = p } dlink* list_head;

void f(dlink*a, dlink *b) { //...

list_head->append(a);

list_head->append(b);

} Цепочки такой общей природы являются основой для списковых классов, которые описываются в Главе 7. Чтобы присоединить звено к списку необходимо обновить объекты, на которые указывают указатели this, pre и suc (текущий, предыдущий и последующий). Все они типа dlink, поэтому функция член dlink::append() имеет к ним доступ.

Единицей защиты в С++ является class, а не отдельный объект класса.

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

class date { //...

date(int, int, int);

};

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

date today = date(23,6,1983);

date xmas(25,12,0);

// сокращенная форма // (xmas - рождество) date my_burthday;

// недопустимо, опущена инициализация Часто бывает хорошо обеспечить несколько способов инициализации объекта класса. Это можно сделать, задав несколько конструкторов.

Например:

class date { int month, day, year;

public:

//...

date(int, int, int);

// день месяц год date(char*);

// дата в строковом представлении date(int);

// день, месяц и год се- годняшние date();

// дата по умолчанию: сегодня };

Конструкторы подчиняются тем же правилам относительно типов пара- метров, что и перегруженные функции (#4.6.7). Если конструкторы су- щественно различаются по типам своих параметров, то компилятор при каждом использовании может выбрать правильный:

date today(4);

date july4("Июль 4, 1983");

date guy("5 Ноя");

date now;

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

Размножение конструкторов в примере с date типично. При разработ- ке класса всегда есть соблазн обеспечить "все", поскольку кажется проще обеспечить какое-нибудь средство просто на случай, что оно кому-то понадобится или потому, что оно изящно выглядит, чем ре- шить, что же нужно на самом деле. Последнее требует больших размыш- лений, но обычно приводит к программам, которые меньше по размеру и более понятны. Один из способов сократить число родственных функций - использовать параметры со значением по умолчанию, пример. В слу- чае date для каждого параметра можно задать значение по умолчанию, интерпретируемое как "по умолчанию принимать: today" (сегодня).

class date { int month, day, year;

public:

//...

date(int d =0, int m =0, int y =0);

date(char*);

// дата в строковом представлении };

date::date(int d, int m, int y) { day = d ? d : today.day;

month = m ? m : today.month;

year = y ? y : today.year;

// проверка, что дата допустимая //...

} Когда используется значение параметра, указывающее "брать по умолчанию", выбранное значение должно лежать вне множества возмож- ных значений параметра. Для дня day и месяца mounth ясно, что это так, но для года year выбор нуля неочевиден. К счастью, в евро- пейском календаре нет нулевого года. Сразу после 1 г. до н.э.

(year==-1) идет 1 г. н.э. (year==1), но для реальной программы это может оказаться слишком тонко.

Объект класса без конструкторов можно инициализировать путем присваивания ему другого объекта этого класса. Это можно делать и тогда, когда конструкторы описаны. Например:

date d = today;

// инициализация посредством присваивания По существу, имеется конструктор по умолчанию, определенный как побитовая копия объекта того же класса. Если для класса X такой конструктор по умолчанию нежелателен, его можно переопределить конструктором с именем X(X&). Это будет обсуждаться в #6.6.

5.2.5 Очистка Определяемый пользователем тип чаще имеет, чем не имеет, конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие, деструктор, чтобы обеспечить соответствующую очистку объектов этого типа. Имя дест- руктора для класса X есть ~X() ("дополнение конструктора"). В част- ности, многие типы используют некоторый объем памяти из свободной памяти (см. #3.2.6), который выделяется конструктором и освобожда- ется деструктором. Вот, например, традиционный стековый тип, из ко- торого для краткости полностью выброшена обработка ошибок:

class char_stack { int size;

char* top;

char* s;

public:

char_stack(int sz) { top=s=new char[size=sz];

} ~char_stack() { delete s;

} // деструктор void push(char c) { *top++ = c;

} char pop() { return *--top;

} } Когда char_stack выходит из области видимости, вызывается дест- руктор:

void f() { char_stack s1(100);

char_stack s2(200);

s1.push('a');

s2.push(s1.pop());

char ch = s2.pop();

cout << chr(ch) << "\n";

} Когда вызывается f(), конструктор char_stack вызывается для s1, чтобы выделить вектор из 100 символов, и для s2, чтобы выделить вектор из 200 символов. При возврате из f() эти два вектора будут освобождены.

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

Чтобы справиться с этой проблемой, был разработан аппарат inline-функций. Функция, определенная (а не просто описанная) в описании класса, считается inline. Это значит, например, что в функциях, которые используют приведенные выше char_stack, нет ника- ких вызовов функций кроме тех, которые используются для реализации операций вывода! Другими словами, нет никаких затрат времени выпол- нения, которые стоит принимать во внимание при разработке класса.

Любое, даже самое маленькое действие, можно задать эффективно. Это утверждение снимает аргумент, который чаще всего приводят чаще все- го в пользу открытых членов данных.

Функцию член можно также описать как inline вне описания класса.

Например:

char char_stack { int size;

char* top;

char* s;

public:

char pop();

//...

};

inline char char_stack::pop() { return *--top;

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

Для всех видов контейнеров существуют очевидные примеры:

таблицы, множества, списки, вектора, словари и т.д. Такой класс имеет операцию "вставить", обычно он также имеет операции для про- верки того, был ли вставлен данный элемент. В нем могут быть действия для осуществления проверки всех элементов в определенном порядке, и кроме всего прочего, в нем может иметься операция для удаления элемента. Обычно контейнерные (то есть, вмещающие) классы имеют конструкторы и деструкторы.

Сокрытие данных и продуманный интерфейс может дать концепция мо- дуля (см. например #4.4: файлы как модули). Класс, однако, является типом. Чтобы использовать его, необходимо создать объекты этого класса, и таких объектов можно создавать столько, сколько нужно.

Модуль же сам является объектом. Чтобы использовать его, его надо только инициализировать, и таких объектов ровно один.

5.3.1 Альтернативные реализации Пока описание открытой части класса и описание функций членов остаются неизменными, реализацию класса можно модифицировать не влияя на ее пользователей. Как пример этого рассмотрим таблицу имен, которая использовалась в настольном калькуляторе в Главе 3.

Это таблица имен:

struct name { char* string;

char* next;

double value;

};

Вот вариант класса table:

// файл table.h class table { name* tbl;

public:

table() { tbl = 0;

} name* look(char*, int = 0);

name* insert(char* s) { return look(s,1);

} };

Эта таблица отличается от той, которая определена в Главе 3 тем, что это настоящий тип. Можно описать более чем одну table, можно иметь указатель на table и т.д. Например:

#include "table.h" table globals;

table keywords;

table* locals;

main() { locals = new table;

//...

} Вот реализация table::look(), которая использует линейный поиск в связанном списке имен name в таблице:

#include name* table::look(char* p, int ins) { for (name* n = tbl;

n;

n=n->next) if (strcmp(p,n->string) == 0) return n;

if (ins == 0) error("имя не найдено");

name* nn = new name;

nn->string = new char[strlen(p)+1];

strcpy(nn->string,p);

nn->value = 1;

nn->next = tbl;

tbl = nn;

return nn;

} Теперь рассмотрим класс table, усовершенствованный таким образом, чтобы использовать хэшированный просмотр, как это делалось в приме- ре с настольным калькулятором. Сделать это труднее из-за того огра- ничения, что уже написанные программы, в которых использовалась только что определенная версия класса table, должны оставаться вер- ными без изменений:

class table { name** tbl;

int size;

public:

table(int sz = 15);

~table();

name* look(char*, int = 0);

name* insert(char* s) { return look(s,1);

} };

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

table::table(int sz) { if (sz < 0) error("отрицательный размер таблицы");

tbl = new name*[size=sz];

for (int i = 0;

i

i++) tbl[i] = 0;

} table::~table() { for (int i = 0;

i

i++) for (name* n = tbl[i];

n;

n=n->next) { delete n->string;

delete n;

} delete tbl;

} Описав деструктор для класса name можно получить более простой и ясный вариант table::~table(). Функция просмотра практически иден- тична той, которая использовалась в примере настольного калькулято- ра (#3.1.3):

#include name* table::look(char* p, int ins) { int ii = 0;

char* pp = p;

while (*pp) ii = ii<<1 ^ *pp++;

if (ii < 0) ii = -ii;

ii %= size;

for (name* n=tbl[ii];

n;

n=n->next) if (strcmp(p,n->string) == 0) return n;

if (ins == 0) error("имя не найдено");

name* nn = new name;

nn->string = new char[strlen(p)+1];

strcpy(nn->string,p);

nn->value = 1;

nn->next = tbl[ii];

tbl[ii] = nn;

return nn;

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

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

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

5.3.2 Законченный класс Программирование без сокрытия данных (с применением структур) требует меньшей продуманности, чем программирование со сокрытием данных (с использованием классов). Структуру можно определить не слишком задумываясь о том, как ее предполагается использовать. А когда определяется класс, все внимание сосредотачивается на обеспе- чении нового типа полным множеством операций;

это важное смещение акцента. Время, потраченное на разработку нового типа, обычно мно- гократно окупается при разработке и тестировании программы.

Вот пример законченного типа intset, который реализует понятие "множество целых":

class intset { int cursize, maxsize;

int *x;

public:

intset(int m, int n);

// самое большее, m int'ов в 1..n ~intset();

int member(int t);

// является ли t элементом? void insert(int t);

// добавить "t" в множество void iterate(int& i) { i = 0;

} int ok(int& i) { return i

} int next(int& i) { return x[i++];

} };

Чтобы протестировать этот класс, можно создать и распечатать мно- жество случайных целых чисел. Такое множество могло бы быть резуль- татом розыгрыша лотереи. Это простое множество можно также исполь- зовать для проверки последовательности целых на повторы. Но для большинства приложений тип множество должен быть немного более про- работанным. Как всегда, возможны ошибки:

#include void error(char* s) { cerr << "set: " << s << "\n";

exit(1);

} Класс intset используется в main(), которая предполагает два це- лых параметра. Первый параметр задает число случайных чисел, кото- рые нужно сгенерировать. Второй параметр указывает диапазон, в ко- тором должны лежать случайные целые:

main(int argc, char* argv[]) { if (argc != 3) error("ожидается два параметра");

int count = 0;

int m = atoi(argv[1]);

// число элементов множества int n = atoi(argv[2]);

// в диапазоне 1..n intset s(m,n);

while (count

if (s.member(t)==0) { s.insert(t);

count++;

} } print_in_order(&s);

} В программе, для которой требуется два параметра, счетчик числа параметров, argc, должен равняться трем, потому что имя программы всегда передается как argv[0]. Функция extern int atoi(char*);

функция atoi() это стандартная библиотечная функция для преобразо- вания представления целого в виде строки в его внутреннюю (двоич- ную) форму. Случайные числа генерируются с помощью стандартной функции rand():

extern int rand();

// Не очень случайные, будьте осторожны int randint(int u) // в диапазоне 1..u { int r = rand();

if (r < 0) r = -r;

return 1 + r%u ;

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

intset::intset(int m, int n) // самое большее, m int'ов в 1..n { if (m<1 || n

cursize = 0;

maxsize = m;

x = new int[maxsize];

} intset::~intset() { delete x;

} Целые числа вставляются, поэтому они хранятся в возрастающем по- рядке:

void intset::insert(int t) { if (++cursize > maxsize) error("слишком много элементов");

int i = cursize-1;

x[i] = t;

while (i>0 && x[i-1]>x[i]) { int t = x[i];

// переставить x[i] и [i-1] x[i] = x[i-1];

x[i-1] = t;

i--;

} } Для нахождения членов используется просто двоичный поиск:

int intset::member(int t) // двоичный поиск { int l = 0;

int u = cursize-1;

while (l <= u) { int m = (l+u)/2;

if (t < x[m]) u = m-1;

else if (t > x[m]) l = m+1;

else return 1;

// найдено } return 0;

// не найдено } И, наконец, нам нужно обеспечить множество операций, чтобы поль- зователь мог осуществлять цикл по множеству в некотором порядке, поскольку представление intset от пользователя скрыто. Множество внутренней упорядоченности не имеет, поэтому мы не можем просто дать возможность обращаться к вектору (завтра я, наверное, реализую intset по-другому, в виде связанного списка).

Дается три функции: iterate() для инициализации итерации, ok() для проверки, есть ли следующий элемент, и next() для того, чтобы взять следующий элемент:

class intset { //...

void iterate(int& i) { i = 0;

} int ok(int& i) { return i

} int next(int& i) { return x[i++];

} };

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

void print_in_order(intset* set) { int var;

set->iterate(var);

while (set->ok(var)) cout << set->next(var) << "\n";

} Другой способ задать итератор приводится в #6.8.

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

5.4.1 Друзья Предположим, вы определили два класса, vector и matrix (вектор и матрица). Каждый скрывает свое представление и предоставляет полный набор действий для манипуляции объектами его типа. Теперь определим функцию, умножающую матрицу на вектор. Для простоты допустим, что в векторе четыре элемента, которые индексируются 0...3, и что матрица состоит из четырех векторов, индексированных 0...3. Допустим также, что доступ к элементам вектора осуществляется через функцию elem(), которая осуществляет проверку индекса, и что в matrix имеется ана- логичная функция. Один подход состоит в определении глобальной функции multiply() (перемножить) примерно следующим образом:

vector multiply(matrix& m, vector& v);

{ vector r;

for (int i = 0;

i<3;

i++) { // r[i] = m[i] * v;

r.elem(i) = 0;

for (int j = 0;

j<3;

j++) r.elem(i) += m.elem(i,j) * v.elem(j);

} return r;

} Это своего рода "естественный" способ, но он очень неэффективен.

При каждом обращении к multiply() elem() будет вызываться 4*(1+4*3) раза.

Теперь, если мы сделаем multiply() членом класса vector, мы смо- жем обойтись без проверки индексов при обращении к элементу векто- ра, а если мы сделаем multiply() членом класса matrix, то мы сможем обойтись без проверки индексов при обращении к элементу матрицы.

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

class matrix;

class vector { float v[4];

//... friend vector multiply(matrix&, vector&);

};

class matrix { vector v[4];

//... friend vector multiply(matrix&, vector&);

};

Функция друг не имеет никаких особенностей, помимо права доступа к закрытой части класса. В частности, friend функция не имеет ука- зателя this (если только она не является полноправным членом функ- цией). Описание friend - настоящее описание. Оно вводит имя функции в самой внешней области видимости программы и сопоставляется с дру- гими описаниями этого имени. Описание друга может располагаться или в закрытой, или в открытой части описания класса. Где именно, зна- чения не имеет.

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

vector multiply(matrix& m, vector& v);

{ vector r;

for (int i = 0;

i<3;

i++) { // r[i] = m[i] * v;

r.v[i] = 0;

for (int j = 0;

j<3;

j++) r.v[i] += m.v[i][j] * v.v[j];

} return r;

} Есть способы преодолеть эту конкретную проблему эффективности не используя аппарат friend (можно было бы определить операцию вектор- ного умножения и определить multiply() с ее помощью). Однако су- ществует много задач, которые проще всего решаются, если есть воз- можность предоставить доступ к закрытой части класса функции, кото- рая не является членом этого класса. В Главе 6 есть много примеров применения friend. Достоинства функций друзей и членов будут обсуж- даться позже.

Функция член одного класса может быть другом другого. Например:

class x { //...

void f();

};

class y { //...

friend void x::f();

};

Нет ничего необычного в том, что все функции члены одного класса являются друзьями другого. Для этого есть даже более краткая за- пись:

class x { friend class y;

//...

};

Такое описание friend делает все функции члены класса y друзьями x.

5.4.2 Уточнение имени члена Иногда полезно делать явное различие между именами членов класса и прочими именами. Для этого используется операция ::, "разрешения области видимости":

class x { int m;

public:

int readm() { return x::m;

} void setm(int m) { x::m = m;

} };

В x::setm() имя параметра m прячет член m, поэтому единственный способ сослаться на член - это использовать его уточненное имя x::m. Операнд в левой части :: должен быть именем класса.

Имя с префиксом :: (просто) должно быть глобальным именем. Это особенно полезно для того, чтобы можно было использовать часто употребимые имена вроде read, put и open как имена функций членов, не теряя при этом возможности обращаться к той версии функции, ко- торая не является членом. Например:

class my_file { //...

public:

int open(char*, char*);

};

int my_file::open(char* name, char* spec) { //...

if (::open(name,flag)) { // использовать open() из UNIX(2) //...

} //...

} 5.4.3 Вложенные классы Описание класса может быть вложенным. Например:

class set { struct setmem { int mem;

setmem* next;

setmem(int m, setmem* n) { mem=m;

next=n;

} };

setmem* first;

public:

set() { first=0;

} insert(int m) { first = new setmem(m,first);

} //...

};

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

class set { struct setmem { int mem;

setmem* next;

setmem(int m, setmem* n) };

//...

};

setmem::setmem(int m, setmem* n) { mem=m, next=n} setmem m1(1,0);

Такая запись, как set::setmem::setmem(), не является ни необходи- мой, ни допустимой. Единственный способ скрыть имя класса - это сделать это с помощью метода файлы-как-модули (#4.4). Большую часть нетривиальных классов лучше описывать раздельно:

class setmem { friend class set;

// доступ только с помощью чле- нов set int mem;

setmem* next;

setmem(int m, setmem* n) { mem=m;

next=n;

} };

class set { setmem* first;

public:

set() { first=0;

} insert(int m) { first = new setmem(m,first);

} //...

};

5.4.4 Статические члены Класс - это тип, а не объект данных, и в каждом объекте класса имеется своя собственная копия данных, членов этого класса. Однако некоторые типы наиболее элегантно реализуются, если все объекты этого типа могут совместно использовать (разделять) некоторые дан- ные. Предпочтительно, чтобы такие разделяемые данные были описаны как часть класса. Например, для управления задачами в операционной системе или в ее модели часто бывает полезен список всех задач:

class task { //...

task* next;

static task* task_chain;

void shedule(int);

void wait(event);

//...

};

Описание члена task_chain (цепочка задач) как static обеспечива- ет, что он будет всего лишь один, а не по одной копии на каждый объект task. Он все равно остается в области видимости класса task, и "извне" доступ к нему можно получить, только если он был описан как public. В этом случае его имя должно уточняться именем его класса:

task::task_chain В функции члене на него можно ссылаться просто task_chain. Исполь- зование статических членов класса может заметно снизить потребность в глобальных переменных.

5.4.5 Указатели на члены Можно брать адрес члена класса. Получение адреса функции члена часто бывает полезно, поскольку те цели и причины, которые приводи- лись в #4.6.9 относительно указателей на функции, в равной степени применимы и к функциям членам. Однако, на настоящее время в языке имеется дефект: невозможно описать выражением тип указателя, кото- рый получается в результате этой операции. Поэтому в текущей реали- зации приходится жульничать, используя трюки. Что касается примера, который приводится ниже, то не гарантируется, что он будет рабо- тать. Используемый трюк надо локализовать, чтобы программу можно было преобразовать с использованием соответствующей языковой конструкции, когда появится такая возможность. Этот трюк использует тот факт, что в текущей реализации this реализуется как первый (скрытый) параметр функции члена*:

-------------------- * Более поздние версии С++ поддерживают понятие указатель на член:

cl::* означает "указатель на член класса cl". Например:

typedef void (cl::*PROC)(int);

PROC pf1 = &cl::print;

// приведе- ние к типу ненужно PROC pf2 = &cl::print;

Для вызовов через указатель на функцию член используются операции. и ->. Например:

(z1.*pf1)(2);

((&z2)->*pf2)(4);

(прим. автора) #include struct cl { char* val;

void print(int x) { cout << val << x << "\n";

};

cl(char* v) { val = v;

} };

// ``фальшивый'' тип для функций членов:

typedef void (*PROC)(void*, int);

main() { cl z1("z1 ");

cl z2("z2 ");

PROC pf1 = PROC(&z1.print);

PROC pf2 = PROC(&z2.print);

z1.print(1);

(*pf1)(&z1,2);

z2.print(3);

(*pf2)(&z2,4);

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

5.4.6 Структуры и объединения По определению struct - это просто класс, все члены которого отк- рытые, то есть struct s {...

есть просто сокращенная запись class s { public:...

Структуры используются в тех случаях, когда сокрытие данных неу- местно.

Именованное объединение определяется как struct, в которой все члены имеют один и тот же адрес (см. #с.8.5.13). Если известно, что в каждый момент времени нужно только одно значение из структуры, то объединение может сэкономить пространство. Например, можно опреде- лить объединение для хранения лексических символов C компилятора:

union tok_val { char* p;

// строка char v[8];

// идентификатор (максимум 8 char) long i;

// целые значения double d;

// зна- чения с плавающей точкой };

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

void strange(int i) { tok_val x;

if (i) x.p = "2";

else x.d = 2;

sqrt(x.d);

// ошибка если i != } Кроме того, объединение, определенное так, как это, нельзя инициа- лизировать. Например:

tok_val curr_val = 12;

// ошибка: int присваивается tok_val'у является недопустимым. Для того, чтобы это преодолеть, можно воспользоваться конструкторами:

union tok_val { char* p;

// строка char v[8];

// идентификатор (максимум 8 char) long i;

// целые значения double d;

// зна- чения с плавающей точкой tok_val(char*);

// должна выбрать между p и v tok_val(int ii) { i = ii;

} tok_val() { d = dd;

} };

Это позволяет справляться с теми ситуациями, когда типы членов могут быть разрешены по правилам для перегрузки имени функции (см.

#4.6.7 и #6.3.3). Например:

void f() { tok_val a = 10;

// a.i = 10 tok_val b = 10.0;

// b.d = 10. } Когда это невозможно (для таких типов, как char* и char[8], int и char, и т.п.), нужный член может быть найден только посредством анализа инициализатора в ходе выполнения или с помощью задания до- полнительного параметра. Например:

tok_val::tok_val(char* pp) { if (strlen(pp) <= 8) strncpy(v,pp,8);

// короткая строка else p = pp;

// длинная строка } Таких ситуаций вообще-то лучше избегать.

Использование конструкторов не предохраняет от такого случайного неправильного употребления tok_val, когда сначала присваивается значение одного типа, а потом рассматривается как другой тип. Эта проблема решается встраиванием объединения в класс, который отсле- живает, какого типа значение помещается:

class tok_val { char tag;

union { char* p;

char v[8];

long i;

double d;

};

int check(char t, char* s) { if (tag!=t) { error(s);

return 0;

} return 1;

} public:

tok_val(char* pp);

tok_val(long ii) { i=ii;

tag='I';

} tok_val(double dd) { d=dd;

tag='D';

} long& ival() { check('I',"ival");

return i;

} double& fval() { check('D',"fval");

return d;

} char*& sval() { check('S',"sval");

return p;

} char* id() { check('N',"id");

return v;

} };

Конструктор, получающий строковый параметр, использует для копи- рования коротких строк strncpy(). strncpy() похожа на strcpy(), но получает третий параметр, который указывает, сколько символов долж- но копироваться:

tok_val::tok_val(char* pp) { if (strlen(pp) <= 8) { // короткая строка tag = 'N' strncpy(v,pp,8);

// скопировать 8 символов } else { // длинная строка tag = 'S';

p = pp;

// просто сохра- нить указатель } } Тип tok_val можно использовать так:

void f() { tok_val t1("short");

// короткая, присвоить v tok_val t2("long string");

// длинная строка, присвоить p char s[8];

strncpy(s,t1.id(),8);

// ok strncpy(s,t2.id(),8);

// проверка check() не пройдет } 5.5 Конструкторы и деструкторы Если у класса есть конструктор, то он вызывается всегда, когда создается объект класса. Если у класса есть деструктор, то он вызы- вается всегда, когда объект класса уничтожается. Объекты могут соз- даваться как:

[1] Автоматический объект: создается каждый раз, когда его описа- ние встречается при выполнении программы, и уничтожается каж- дый раз при выходе из блока, в котором оно появилось;

[2] Статический объект: создается один раз, при запуске програм- мы, и уничтожается один раз, при ее завершении;

[3] Объект в свободной памяти: создается с помощью операции new и уничтожается с помощью операции delete;

[4] Объект член: как объект другого класса или как элемент векто- ра.

Объект также может быть построен с помощью явного применения конструктора в выражении (см. #6.4), в этом случае он является ав- томатическим объектом. В следующих подразделах предполагается, что объекты принадлежат классу, имеющему конструктор и деструктор. При- мером может служить класс table из #5.3.

5.5.1 Предостережение Если x и y - объекты класса cl, то x=y в стандартном случае озна- чает побитовое копирование y в x (см. #2.3.8). Такая интерпретация присваивания может привести к изумляющему (и обычно нежелательному) результату, если оно применяется к объектам класса, для которого определены конструктор и деструктор. Например:

class char_stack { int size;

char* top;

char* s;

public:

char_stack(int sz) { top=s=new char[size=sz];

} ~char_stack() { delete s;

} // деструктор void push(char c) { *top++ = c;

} char pop() { return *--top;

} };

void h() { char_stack s1(100);

char_stack s2 = s1;

// неприятность char_stack s3(99);

s3 = s2;

// неприятность } Здесь char_stack::char_stack() вызывается дважды: для s1 и для s3. Для s2 он не вызывается, поскольку эта переменная инициализиру- ется присваиванием. Однако деструктор char_stack::~char_stack() вы- зывается трижды: для s1, s2 и s3! Кроме того, по умолчанию действу- ет интерпретация присваивания как побитовое копирование, поэтому в конце h() каждый из s1, s2 и s3 будет содержать указатель на вектор символов, размещенный в свободной памяти при создании s1. Не оста- нется никакого указателя на вектор символов, выделенный при созда- нии s3. Таких отклонений можно избежать: см. Главу 6.

5.5.2 Статическая память Рассмотрим следующее:

table tbl1(100);

void f() { static table tbl2(200);

} main() { f();

} Здесь конструктор table::table(), определенный в #5.3.1, будет вызываться дважды: один раз для tbl1 и один раз для tbl2. Деструк- тор table::~table() также будет вызван дважды: для уничтожения tbl и tbl2 после выхода из main(). Конструкторы для глобальных стати- ческих объектов в файле выполняются в том порядке, в котором встре- чаются описания;

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

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

Параметры конструкторов для статических объектов должны быть константными выражениями:

void g(int a) { static table t(a);

// ошибка } Традиционно выполнением программы считалось выполнение main().

Так никогда не было, даже в C, но только размещение статических объектов класса с конструктором и/или деструктором дают програм- мисту простой и очевидный способ задания того, что будет выпол- няться до и/или после вызова main().

Вызов конструкторов и деструкторов для статических объектов игра- ет в С++ чрезвычайно важную роль. Это способ обеспечить надлежащую инициализацию и очистку структур данных в библиотеках. Рассмотрим . Откуда берутся cin, cout и cerr? Где они получают ини- циализацию? И, что самое главное, поскольку потоки вывода имеют внутренние буферы символов, как же эти буферы заполняются? Простой и очевидный ответ таков, что эта работа осуществляется соответству- ющими конструкторами и деструкторами до и после выполнения main().

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

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

Иногда, когда вы разрабатываете библиотеку, необходимо или просто удобно создать тип с конструктором и деструктором, предназначенными только для одного: инициализировать и очистить. Такой тип обычно используется только с одной целью, для размещения статического объ- екта так, чтобы вызывались конструктор и деструктор.

5.5.3 Свободная память Рассмотрим:

main() { table* p = new table(100);

table* q = new table(200);

delete p;

delete p;

// возможно, ошибка } Конструктор table::table() будет вызван дважды, как и деструктор table::~table(). То, что С++ не дает никаких гарантий, что для объ- екта, созданного с помощью new, когда-либо будет вызван деструктор, ничего не значит. В предыдущей программе q не уничтожается, а p уничтожается дважды! Программист может счесть это ошибкой, а может и не счесть, в зависимости от типа p и q. Обычно то, что объект не уничтожается, является не ошибкой, а просто лишней тратой памяти.

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

Пользователь может определить новую реализацию операций new и delete (см. #3.2.6). Можно также определить способ взаимодействия конструктора или деструктора с операциями new и delete (см. #5.5.6) 5.5.4 Объекты класса как члены Рассмотрим class classdef { table members;

int no_of_members;

//...

classdef(int size);

~classdef();

};

Очевидное намерение состоит в том, что classdef должен содержать таблицу длиной size из членов members, а сложность - в том, как сделать так, чтобы конструктор table::table() вызывался с парамет- ром size. Это делается так:

classdef::classdef(int size) : members(size) { no_of_members = size;

//...

} Параметры для конструктора члена (здесь это table::table()) поме- щаются в определение (не в описание) конструктора класса, вмещающе- го его (здесь это classdef::classdef()). После этого конструктор члена вызывается перед телом конструктора, задающего его список па- раметров.

Если есть еще члены, которым нужны списки параметров для конструкторов, их можно задать аналогично. Например:

class classdef { table members;

table friends;

int no_of_members;

//... classdef(int size);

~classdef();

};

Список параметров для членов разделяется запятыми (а не двоеточи- ями), и список инициализаторов для членов может представляться в произвольном порядке:

classdef::classdef(int size) : friends(size), members(size) { no_of_members = size;

//...

} Порядок, в котором вызываются конструкторы, неопределен, поэтому не рекомендуется делать списки параметров с побочными эффектами:

classdef::classdef(int size) : friends(size=size/2), members(size);

// дурной стиль { no_of_members = size;

//...

} Если конструктору для члена не нужно ни одного параметра, то ни- какого списка параметров задавать не надо. Например, поскольку table::table был определен с параметром по умолчанию 15, следующая запись является правильной:

classdef::classdef(int size) : members(size) { no_of_members = size;

//...

} и размер size таблицы friends будет равен 15.

Когда объект класса, содержащий объект класса, (например, classdef) уничтожается, первым выполняется тело собственного дест- руктора объекта, а затем выполняются деструкторы членов.

Рассмотрим традиционную альтернативу тому, чтобы иметь объекты класса как члены, - иметь члены указатели и инициализировать их в конструкторе:

class classdef { table* members;

table* friends;

int no_of_members;

//... classdef(int size);

~classdef();

};

classdef::classdef(int size) { members = new table(size);

friends = new table;

// размер таблицы по умолчанию no_of_members = size;

//...

} Так как таблицы создавались с помощью new, они должны уничто- жаться с помощью delete:

classdef::~classdef() { //...

delete members;

delete friends;

} Раздельно создаваемые объекты вроде этих могут оказаться полезны- ми, но учтите, что members и friends указывают на отдельные объек- ты, что требует для каждого из них действие по выделению памяти и ее освобождению. Кроме того, указатель плюс объект в свободной па- мяти занимают больше места, чем объект член.

5.5.5 Вектора объектов класса Чтобы описать вектор объектов класса, имеющего конструктор, этот класс должен иметь конструктор, который может вызываться без списка параметров. Нельзя использовать даже параметры по умолчанию. Напри- мер:

table tblvec[10];

будет ошибкой, так как для table::table() требуется целый параметр.

Нет способа задать параметры конструктора в описании вектора. Чтобы можно было описывать вектор таблиц table, можно модифицировать описание table (#5.3.1), например, так:

class table { //...

void init(int sz);

// как старый конструктор public:

table(int sz) // как раньше, но без по умолчанию { init(sz);

} table() // по умолчанию { init(15);

} } Когда вектор уничтожается, деструктор должен вызываться для каж- дого элемента этого вектора. Для векторов, которые не были размеще- ны с помощью new, это делается неявно. Однако для векторов в сво- бодной памяти это не может быть сделано неявно, поскольку компиля- тор не может отличить указатель на один объект от указателя на пер- вый элемент вектора объектов. Например:

void f() { table* t1 = new table;

table* t2 = new table[10];

delete t1;

// одна таблица delete t2;

// неприятность: 10 таблиц } В этом случае длину вектора должен задавать программист:

void g(int sz) { table* t1 = new table;

table* t2 = new table[sz];

delete t1;

delete[] t2;

} Но почему же компилятор не может найти число элементов вектора из объема выделенной памяти? Потому, что распределитель свободной па- мяти не является частью языка и может быть задан программистом.

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

Рассмотрим класс name, который использовался в примерах table.

Его можно было бы определить так:

struct name { char* string;

name* next;

double value;

name(char*, double, name*);

~name();

};

Программист может воспользоваться тем, что размещение и освобож- дение объектов заранее известного размера можно обрабатывать гораз- до эффективнее (и по памяти, и по времени), чем с помощью общей ре- ализации new и delete. Общая идея состоит в том, чтобы предвари- тельно разместить "куски" из объектов name, а затем сцеплять их, чтобы свести выделение и освобождение к простым операциям над свя- занным списком. Переменная nfree является вершиной списка неисполь- зованных name:

const NALL = 128;

name* nfree;

Распределитель, используемый операцией new, хранит размер объекта вместе с объектом, чтобы обеспечить правильную работу операции delete. С помощью распределителя, специализированного для типа, можно избежать этих накладных расходов. Например, на моей машине следующий распределитель использует для хранения name 16 байт, тог- да как для стандартного распределителя свободной памяти нужно байт. Вот как это можно сделать:

name::name(char* s, double v, name* n) { register name* p = nfree;

// сначала выделить if (p) nfree = p->next;

else { // выделить и сцепить name* q = (name*)new char[ NALL*sizeof(name) ];

for (p=nfree=&q[NALL-1];

q

p--) p->next = p-1;

(p+1)->next = 0;

} this = p;

// затем инициализировать string = s;

value = v;

next = n;

} Присвоение указателю this информирует компилятор о том, что прог- раммист взял себе управление, и что не надо использовать стандарт- ный механизм распределения памяти. Конструктор name::name() обраба- тывает только тот случай, когда name размещается посредством new, но для большей части типов это всегда так. В #5.5.8 объясняется, как написать конструктор для обработки как размещения в свободной памяти, так и других видов размещения.

Заметьте, что просто как name* q = new name[NALL];

память выделять нельзя, поскольку это приведет к бесконечной ре- курсии, когда new вызовет name::name().

Освобождение памяти обычно тривиально:

name::~name() { next = nfree;

nfree = this;

this = 0;

} Присваивание указателю this 0 в деструкторе обеспечивает, что стандартный распределитель памяти не используется.

5.5.7 Предостережение Когда в конструкторе производится указателю this, значение this до этого присваивания неопределено. Таким образом, ссылка на член до этого присваивания неопределена и скорее всего приведет к ка- тастрофе. Имеющийся компилятор не пытается убедиться в том, что присваивание указателю this происходит на всех траекториях выполне- ния:

mytype::mytype(int i) { if (i) this = mytype_alloc();

// присваивание членам };

откомпилируется, и при i==0 никакой объект размещен не будет.

Конструктор может определить, был ли он вызван операцией new, или нет. Если он вызван new, то указатель this на входе имеет нуле- вое значение, в противном случае this указывает на пространство, уже выделенное для объекта (например, на стек). Поэтому можно просто написать конструктор, который выделяет память, если (и толь- ко если) он был вызван через new. Например:

mytype::mytype(int i) { if (this == 0) this = mytype_alloc();

// присваивание членам };

Эквивалентного средства, которое позволяет деструктору решить вопрос, был ли его объект создан с помощью new, не имеется, как нет и средства, позволяющего ему узнать, вызвала ли его delete, или он вызван объектом, выходящим из области видимости. Если для пользова- теля это существенно, то он может сохранить где-то соответствующую информацию для деструктора. Другой способ - когда пользователь обеспечивает, что объекты этого класса размещаются только соот- ветствующим образом. Если удается справиться с первой проблемой, то второй способ интереса не представляет.

Если тот, кто реализует класс, является одновременно и его единственным пользователем, то имеет смысл упростить класс, исходя из предположений о его использовании. Когда класс разрабатывается для более широкого использования, таких допущений, как правило, лучше избегать.

5.5.8 Объекты переменного размера Когда пользователь берет управление распределением и освобождени- ем памяти, он может конструировать объекты размеры, которых во вре- мя компиляции недетерминирован. В предыдущих примерах вмещающие (или контейнерные - перев.) классы vector, stack, intset и table реализовывались как структуры доступа фиксированного размера, со- держащие указатели на реальную память. Это подразумевает, что для создания таких объектов в свободной памяти необходимо две операции по выделению памяти, и что любое обращение к хранимой информации будет содержать дополнительную косвенную адресацию. Например:

class char_stack { int size;

char* top;

char* s;

public:

char_stack(int sz) { top=s=new char[size=sz];

} ~char_stack() { delete s;

} // деструктор void push(char c) { *top++ = c;

} char pop() { return *--top;

} };

Если каждый объект класса размещается в свободной памяти, это де- лать не нужно. Вот другой вариант:

class char_stack { int size;

char* top;

char s[1];

public:

char_stack(int sz);

void push(char c) { *top++ = c;

} char pop() { return *--top;

} };

char_stack::char_stack(int sz) { if (this) error("стек не в свободной памяти");

if (sz < 1) error("размер стека < 1");

this = (char_stack*) new char[sizeof(char_stack)+sz-1];

size = sz;

top = s;

} Заметьте, что деструктор больше не нужен, поскольку память, кото- рую использует char_stack, может освободить delete без всякого со- действия со стороны программиста.

5.6 Упражнения 1. (*1) Модифицируйте настольный калькулятор из Главы 3, чтобы использовать класс table.

2. (*1) Разработайте tnode (#с.8.5) как класс с конструкторами, деструкторами и т.п. Определите дерево из tnode'ов как класс с конструкторами, деструкторами и т.п.

3. (*1) Преобразуйте класс intset (#5.3.2) в множество строк.

4. (*1) Преобразуйте класс intset в множество узлов node, где node - определяемая вами структура.

5. (*3) Определите класс для анализа, хранения, вычисления и пе- чати простых арифметических выражений, состоящих из целых констант и операций +, -, * и /. Открытый интерфейс должен выглядеть примерно так:

class expr { //...

public:

expr(char*);

int eval();

void print();

} Параметр строка конструктора expr::expr() является выражением.

Функция expr::eval() возвращает значение выражения, а expr::print() печатает представление выражения в cout. Прог- рамма может выглядеть, например, так:

expr x("123/4+123*4-3");

cout << "x = " << x.eval() << "\n";

x.print();

Определите класс expr два раза: один раз используя в качестве представления связанный список узлов, а другой раз - символь- ную строку. Поэкспериментируйте с разными способами печати вы- ражения: с полностью расставленными скобками, в постфиксной записи, в ассемблерном коде и т.д.

6. (*1) Определите класс char_queue (символьная очередь) таким образом, чтобы открытый интерфейс не зависел от представления.

Реализуйте char_queue как (1) связанный список и как (2) век- тор. О согласованности не заботьтесь.

7. (*2) Определите класс histogram (гистограмма), в котором ве- дется подсчет чисел в определенных интервалах, которые зада- ются как параметры конструктора histogram. Обеспечьте функцию вывода гистограммы на печать. Сделайте обработку значений, вы- ходящих за границы. Подсказка: .

8. (*2) Определите несколько классов, предоставляющих случайные числа с определенными распределениями. Каждый класс имеет конструктор, задающий параметры распределения, и функцию draw, которая возвращает "следующее" значение. Подсказка: .

Посмотрите также класс intset.

9. (*2) Перепишите пример date (#5.8.2), пример char_stack (#5.2.5) и пример intset (#5.3.2) не используя функций членов (даже конструкторов и деструкторов). Используйте только class и friend. Сравните с версиями, в которых использовались функ- ции члены.

10. (*3) Для какого-нибудь языка спроектируйте класс таблица имен и класс вхождение в таблицу имен. Чтобы посмотреть, как на са- мом деле выглядит таблица имен, посмотрите на компилятор этого языка.

11. (*2) Модифицируйте класс выражение из Упражнения 5 так, чтобы обрабатывать переменные и операцию присваивания =. Используйте класс таблица имен из Упражнения 10.

12. (*1) Дана программа:

#include main() { cout << "Hello, world\n";

} модифицируйте ее, чтобы получить выдачу Initialize Hello, world Clean up Не делайте никаких изменений в main().

Глава 6 Перегрузка операций Здесь водятся Драконы!

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

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

6.1 Введение Часто программы работают с объектами, которые являются конкретны- ми представлениями абстрактных понятий. Например, тип данных int в С++ вместе с операциями +, -, *, / и т.д. предоставляет реализацию (ограниченную) математического понятия целых чисел. Такие понятия обычно включают в себя множество операций, которые кратко, удобно и привычно представляют основные действия над объектами. К сожалению, язык программирования может непосредственно поддерживать лишь очень малое число таких понятий. Например, такие понятия, как комплексная арифметика, матричная алгебра, логические сигналы и строки не полу- чили прямой поддержки в С++. Классы дают средство спецификации в С++ представления неэлементарных объектов вместе с множеством действий, которые могут над этими объектами выполняться. Иногда оп- ределение того, как действуют операции на объекты классов, позволя- ет программисту обеспечить более общепринятую и удобную запись для манипуляции объектами классов, чем та, которую можно достичь используя лишь основную функциональную запись. Например:

class complex { double re, im;

public:

complex(double r, double i) { re=r;

im=i;

} friend complex operator+(complex, complex);

friend complex operator*(complex, complex);

};

определяет простую реализацию понятия комплексного числа, в которой число представляется парой чисел с плавающей точкой двойной точ- ности, работа с которыми осуществляется посредством операций + и * (и только). Программист задает смысл операций + и * с помощью опре- деления функций с именами operator+ и operator*. Если, например, даны b и c типа complex, то b+c означает (по определению) operator+(b,c). Теперь есть возможность приблизить общепринятую ин- терпретацию комплексных выражений. Например:

void f() { complex a = complex(1, 3.1);

complex b = complex(1.2, 2);

complex c = b;

a = b+c;

b = b+c*a;

c = a*b+complex(1,2);

} Выполняются обычные правила приоритетов, поэтому второй оператор означает b=b+(c*a), а не b=(b+c)*a.

6.2 Функции операции Можно описывать функции, определяющие значения следующих опера- ций:

+ - * / % ^ & | ~ !

= < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- [] () new delete Последние четыре - это индексирование (#6.7), вызов функции (#6.8), выделение свободной памяти и освобождение свободной памяти (#3.2.6). Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений. Нельзя, например, определить унарную операцию % или бинарную !. Невозможно определить новые лексические символы операций, но в тех случаях, когда мно- жество операций недостаточно, вы можете использовать запись вызова функции. Используйте например, не **, а pow(). Эти ограничения мо- гут показаться драконовскими, но более гибкие правила могут очень легко привести к неоднозначностям. Например, на первый взгляд опре- деление операции **, означающей возведение в степень, может пока- заться очевидной и простой задачей, но подумайте еще раз. Должна ли ** связываться влево (как в Фортране) или вправо (как в Алголе)?

Выражение a**p должно интерпретироваться как a*(*p) или как (a)**(p)?

Имя функции операции есть ключевое слово operator (то есть, опе- рация), за которым следует сама операция, например, operator<<.

Функция операция описывается и может вызываться так же, как любая другая функция. Использование операции - это лишь сокращенная за- пись явного вызова функции операции. Например:

void f(complex a, complex b) { complex c = a + b;

// сокращенная запись complex d = operator+(a,b);

// явный вызов } При наличии предыдущего описания complex оба инициализатора явля- ются синонимами.

6.2.1 Бинарные и унарные операции Бинарная операция может быть определена или как функция член, по- лучающая один параметр, или как функция друг, получающая два пара- метра. Таким образом, для любой бинарной операции @ aa@bb может ин- терпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой.

Унарная операция, префиксная или постфиксная, может быть определена или как функция член, не получающая параметров, или как функция друг, получающая один параметр. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определено и то, и дру- гое, то и aa@, и @aa являются ошибками. Рассмотрим следующие приме- ры:

class X { // друзья friend X operator-(X);

// унарный минус friend X operator-(X,X);

// бинарный минус friend X operator-();

// ошибка: нет операндов friend X operator-(X,X,X);

// ошибка:

тернарная // члены (с неявным первым параметром: this) X* operator&();

// унарное & (взятие адреса) X operator&(X);

// бинарное & (операция И) X operator&(X,X);

// ошибка: тер- нарное };

Когда операции ++ и -- перегружены, префиксное использование и постфиксное различить невозможно.

6.2.2 Предопределенный смысл операций Относительно смысла операций, определяемых пользователем, не де- лается никаких предположений. В частности, поскольку не предполага- ется, что перегруженное = реализует присваивание ее первому операн- ду, не делается никакой проверки, чтобы удостовериться, является ли этот операнд lvalue (#с.6).

Значения некоторых встроенных операций определены как равносиль- ные определенным комбинациям других операций над теми же аргумента- ми. Например, если a является int, то ++a означает a+=1, что в свою очередь означает a=a+1. Такие соотношения для определяемых пользо- вателем операций не выполняются, если только не случилось так, что пользователь сам определил их таким образом. Например, определение operator+= () для типа complex не может быть выведено из определе- ний complex::operator+() и complex::operator=().

По историческому совпадению операции = и & имеют определенный смысл для объектов классов. Никакого элегантного способа "не опре- делить" эти две операции не существует. Их можно, однако, сделать недееспособными для класса X. Можно, например, описать X::operator&(), не задав ее определения. Если где-либо будет браться адрес объекта класса X, то компоновщик обнаружит отсутствие определения*. Или, другой способ, можно определить X::operator&() так, чтобы она вызывала ошибку во время выполнения.

* В некоторых системах компоновщик настолько "умен", что ругается, даже если неопределена неиспользуемая функция. В таких системах этим методом воспользоваться нельзя. (прим автора) 6.2.3 Операции и определяемые пользователем типы Функция операция должна или быть членом, или получать в качестве параметра по меньшей мере один объект класса (функциям, которые пе- реопределяют операции new и delete, это делать необязательно). Это правило гарантирует, что пользователь не может изменить смысл ника- кого выражения, не включающего в себя определенного пользователем типа. В частности, невозможно определить функцию, которая действует исключительно на указатели.

Функция операция, первым параметром которой предполагается основ- ной встроенный тип, не может быть функцией членом. Рассмотрим, нап- ример, сложение комплексной переменной aa с целым 2: aa+2, при под- ходящим образом описанной функции члене, может быть проинтерпрети- ровано как aa.operator+(2), но с 2+aa это не может быть сделано, потому что нет такого класса int, для которого можно было бы опре- делить + так, чтобы это означало 2.operator+(aa). Даже если бы та- кой тип был, то для того, чтобы обработать и 2+aa и aa+2, понадоби- лось бы две различных функции члена. Так как компилятор не знает смысла +, определяемого пользователем, то не может предполагать, что он коммутативен, и интерпретировать 2+aa как aa+2. С этим при- мером могут легко справиться функции друзья.

Все функции операции по определению перегружены. Функция операция задает новый смысл операции в дополнение к встроенному определению, и может существовать несколько функций операций с одним и тем же именем, если в типах их параметров имеются отличия, различимые для компилятора, чтобы он мог различать их при обращении (см. #4.6.7).

6.3 Определяемое преобразование типа Приведенная во введении реализация комплексных чисел слишком ог- раничена, чтобы она могла устроить кого-либо, поэтому ее нужно расширить. Это будет в основном повторением описанных выше методов.

Например:

class complex { double re, im;

public:

complex(double r, double i) { re=r;

im=i;

} friend complex operator+(complex, complex);

friend complex operator+(complex, double);

friend complex operator+(double, complex);

friend complex operator-(complex, complex);

friend complex operator-(complex, double);

friend complex operator-(double, complex);

complex operator-() // унарный - friend complex operator*(complex, complex);

friend complex operator*(complex, double);

friend complex operator*(double, complex);

//...

};

Теперь, имея описание complex, мы можем написать:

void f() { complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5);

a = -b-c;

b = c*2.0*c;

c = (d+e)*a;

} Но писать функцию для каждого сочетания complex и double, как это делалось выше для operator+(), невыносимо нудно. Кроме того, близ- кие к реальности средства комплексной арифметики должны предостав- лять по меньшей мере дюжину таких функций. Посмотрите, например, на тип complex, описанный в .

6.3.1 Конструкторы Альтернативу использованию нескольких функций (перегруженных) составляет описание конструктора, который по заданному double соз- дает complex. Например:

class complex { //...

complex(double r) { re=r;

im=0;

} };

Конструктор, требующий только один параметр, необязательно вызы- вать явно:

complex z1 = complex(23);

complex z2 = 23;

И z1, и z2 будут инициализированы вызовом complex(23).

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

class complex { double re, im;

public:

complex(double r, double i = 0) { re=r;

im=i;

} friend complex operator+(complex, complex);

friend complex operator*(complex, complex);

};

и действия, в которые будут входить переменные complex и целые константы, стали бы допустимы. Целая константа будет интерпретиро- ваться как complex с нулевой мнимой частью. Например, a=b*2 означа- ет:

a=operator*( b, complex( double(2), double(0) ) ) Определенное пользователем преобразование типа применяется неявно только тогда, когда оно является единственным.

Объект, сконструированный с помощью явного или неявного вызова конструктора, является автоматическим и будет уничтожен при первой возможности, обычно сразу же после оператора, в котором он был соз- дан.

6.3.2 Операции преобразования Использование конструктора для задания преобразования типа явля- ется удобным, но имеет следствия, которые могут оказаться нежела- тельными:

[1] Не может быть неявного преобразования из определенного поль- зователем типа в основной тип (поскольку основные типы не яв- ляются классами) [2] Невозможно задать преобразование из нового типа в старый, не изменяя описание старого [3] Невозможно иметь конструктор с одним параметром, не имея при этом преобразования.

Последнее не является серьезной проблемой, а с первыми двумя мож- но справиться, определив для исходного типа операцию преобразова- ния. Функция член X::operator T(), где T - имя типа, определяет преобразование из X в T. Например, можно определить тип tiny (кро- шечный), который может иметь значение только в диапазоне 0...63, но все равно может свободно сочетаться в целыми в арифметических опе- рациях:

class tiny { char v;

int assign(int i) { return v = (i&~63) ?

(error("ошибка диапазона"),0) : i;

} public:

tiny(int i) { assign(i);

} tiny(tiny& i) { v = t.v;

} int operator=(tiny& i) { return v = t.v;

} int operator=(int i) { return assign(i);

} operator int() { return v;

} } Диапазон значения проверяется всегда, когда tiny инициализируется int, и всегда, когда ему присваивается int. Одно tiny может присва- иваться другому без проверки диапазона. Чтобы разрешить выполнять над переменными tiny обычные целые операции, определяется tiny::operator int(), неявное преобразование из tiny в int. Всегда, когда в том месте, где требуется int, появляется tiny, используется соответствующее ему int. Например:

void main() { tiny c1 = 2;

tiny c2 = 62;

tiny c3 = c2 - c1;

// c3 = 60 tiny c4 = c3;

// нет проверки диапазона (необязательна) int i = c + c2;

// i = 64 c1 = c2 + 2 * c1;

// ошибка диапазона: c1 = (а не 66) c2 = c1 -i;

// ошибка диапазона: c2 = 0 c3 = c2;

// нет проверки диапазона (необязательна) } Тип вектор из tiny может оказаться более полезным, поскольку он экономит пространство. Чтобы сделать этот тип более удобным в обра- щении, можно использовать операцию индексирования.

Другое применение определяемых операций преобразования - это ти- пы, которые предоставляют нестандартные представления чисел (ариф- метика по основанию 100, арифметика, арифметика с фиксированной точкой, двоично-десятичное представление и т.п.). При этом обычно переопределяются такие операции, как + и *.

Функции преобразования оказываются особенно полезными для работы со структурами данных, когда чтение (реализованное посредством опе- рации преобразования) тривиально, в то время как присваивание и инициализация заметно более сложны.

Типы istream и ostream опираются на функцию преобразования, чтобы сделать возможными такие операторы, как while (cin>>x) cout<

Действие ввода cin>>x выше возвращает istream&. Это значение не- явно преобразуется к значению, которое указывает состояние cin, а уже это значение может проверяться оператором while (см. #8.4.2).

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

6.3.3 Неоднозначности Присваивание объекту (или инициализация объекта) класса X явля- ется допустимым, если или присваиваемое значение является X, или существует единственное преобразование присваиваемого значения в тип X.

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

допустим только один уровень неяв- ных преобразований, определенных пользователем. Иногда значение нужного типа может быть построено более чем одним способом. Такие случаи являются недопустимыми. Например:

class x { /*... */ x(int);

x(char*);

};

class y { /*... */ y(int);

};

class z { /*... */ z(x);

};

overload f;

x f(x);

y f(y);

z g(z);

f(1);

// недопустимо: неоднозначность f(x(1)) или f(y(1)) f(x(1));

f(y(1));

g("asdf");

// недопустимо: g(z(x("asdf"))) не пробуется g(z("asdf"));

Определяемые пользователем преобразования рассматриваются только в том случае, если без них вызов разрешить нельзя. Например:

class x { /*... */ x(int);

} overload h(double), h(x);

h(1);

Вызов мог бы быть проинтерпретирован или как h(double(1)), или как h(x(1)), и был бы недопустим по правилу единственности. Но пер- вая интерпретация использует только стандартное преобразование и она будет выбрана по правилам, приведенным в #4.6.7.

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

Более общий подход разрешил бы компилятору применять любое преобра- зование, которое он сможет найти;

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

Самый общий подход учитывал бы всю имеющуюся информацию о типах и рассматривал бы все возможные преобразования. Например, если использовать предыдущее описание, то можно было бы обработать aa=f(1), так как тип aa определяет единственность толкования. Если aa является x, то единственное, дающее в результате x, который тре- буется присваиванием, - это f(x(1)), а если aa - это y, то вместо этого будет использоваться f(y(1)). Самый общий подход справился бы и с g("asdf"), поскольку единственной интерпретацией этого может быть g(z(x("asdf"))). Сложность этого подхода в том, что он требует расширенного анализа всего выражения для того, чтобы определить ин- терпретацию каждой операции и вызова функции. Это приведет к замед- лению компиляции, а также к вызывающим удивление интерпретациям и сообщениям об ошибках, если компилятор рассмотрит преобразования, определенные в библиотеках и т.п. При таком подходе компилятор бу- дет принимать во внимание больше, чем, как можно ожидать, знает пи- шущий программу программист!

6.4 Константы Константы классового типа определить невозможно в том смысле, в каком 1.2 и 12e3 являются константами типа double. Вместо них, од- нако, часто можно использовать константы основных типов, если их реализация обеспечивается с помощью функций членов. Общий аппарат для этого дают конструкторы, получающие один параметр. Когда конструкторы просты и подставляются inline, имеет смысл рассмотреть в качестве константы вызов конструктора. Если, например, в есть описание класса comlpex, то выражение zz1*3+zz2*comlpex(1,2) даст два вызова функций, а не пять. К двум вызовам функций приведут две операции *, а операция + и конструк- тор, к которому обращаются для создания comlpex(3) и comlpex(1,2), будут расширены inline.

6.5 Большие объекты При каждом применении для comlpex бинарных операций, описанных выше, в функцию, которая реализует операцию, как параметр переда- ется копия каждого операнда. Расходы на копирование каждого double заметны, но с ними вполне можно примириться. К сожалению, не все классы имеют небольшое и удобное представление. Чтобы избежать не- нужного копирования, можно описать функции таким образом, чтобы они получали ссылочные параметры. Например:

class matrix { double m[4][4];

public:

matrix();

friend matrix operator+(matrix&, matrix&);

friend matrix operator*(matrix&, matrix&);

};

Ссылки позволяют использовать выражения, содержащие обычные ариф- метические операции над большими объектами, без ненужного копирова- ния. Указатели применять нельзя, потому что невозможно для примене- ния к указателю смысл операции переопределить невозможно. Операцию плюс можно определить так:

matrix operator+(matrix&, matrix&);

{ matrix sum;

for (int i=0;

i<4;

i++) for (int j=0;

j<4;

j++) sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j];

return sum;

} Эта operator+() обращается к операндам + через ссылки, но возвра- щает значение объекта. Возврат ссылки может оказаться более эффек- тивным:

class matrix { //...

friend matrix& operator+(matrix&, matrix&);

friend matrix& operator*(matrix&, matrix&);

};

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

6.6 Присваивание и инициализация Рассмотрим очень простой класс строк string:

struct string { char* p;

int size;

// размер вектора, на который указывает p string(int sz) { p = new char[size=sz];

} ~string() { delete p;

} };

Строка - это структура данных, состоящая из вектора символов и длины этого вектора. Вектор создается конструктором и уничтожается деструктором. Однако, как показано в #5.10, это может привести к неприятностям. Например:

void f() { string s1(10);

string s2(20);

s1 = s2;

} будет размещать два вектора символов, а присваивание s1=s2 будет портить указатель на один из них и дублировать другой. На выходе из f() для s1 и s2 будет вызываться деструктор и уничтожать один и тот же вектор с непредсказуемо разрушительными последствиями. Решение этой проблемы состоит в том, чтобы соответствующим образом опреде- лить присваивание объектов типа string:

struct string { char* p;

int size;

// размер вектора, на который указывает p string(int sz) { p = new char[size=sz];

} ~string() { delete p;

} void operator=(string&) };

void string::operator=(string& a) { if (this == &a) return;

// остерегаться s=s;

delete p;

p=new char[size=a.size];

strcpy(p,a.p);

} Это определение string гарантирует,и что предыдущий пример будет работать как предполагалось. Однако небольшое изменение f() приве- дет к появлению той же проблемы в новом облике:

void f() { string s1(10);

s2 = s1;

} Теперь создается только одна строка, а уничтожается две. К неини- циализированному объекту определяемая пользователем операция присваивания не применяется. Беглый взгляд на string::operator=() объясняет, почему было бы неразумно так делать: указатель p будет содержать неопределенное и совершенно случайное значение. Часто операция присваивания полагается на то, что ее аргументы инициали- зированы. Для такой инициализации, как здесь, это не так по опреде- лению. Следовательно, нужно определить похожую, но другую, функцию, чтобы обрабатывать инициализацию:

struct string { char* p;

int size;

// размер вектора, на который указывает p string(int sz) { p = new char[size=sz];

} ~string() { delete p;

} void operator=(string&);

string(string&);

};

void string::string(string& a) { p=new char[size=a.size];

strcpy(p,a.p);

} Для типа X инициализацию тем же типом X обрабатывает конструктор X(X&). Нельзя не подчеркнуть еще раз, что присваивание и инициали- зация - разные действия. Это особенно существенно при описании деструктора. Если класс X имеет конструктор X(X&), выполняющий нет- ривиальную работу вроде освобождения памяти, то скорее всего потре- буется полный комплект функций, чтобы полностью избежать побитового копирования объектов:

class X { //...

X(something);

// конструктор: создает объект X(&X);

// конструктор: копирует в инициализации operator=(X&);

// присваивание: чистит и копирует ~X();

// деструктор: чистит };

Есть еще два случая, когда объект копируется: как параметр функ- ции и как возвращаемое значение. Когда передается параметр, инициа- лизируется неинициализированная до этого переменная - формальный параметр. Семантика идентична семантике инициализации. То же самое происходит при возврате из функции, хотя это менее очевидно. В обо- их случаях будет применен X(X&), если он определен:

string g(string arg) { return arg;

} main() { string s = "asdf";

s = g(s);

} Ясно, что после вызова g() значение s обязано быть "asdf". Копи- рование значения s в параметр arg сложности не представляет: для этого надо взывать string(string&). Для взятия копии этого значения из g() требуется еще один вызов string(string&);

на этот раз иници- ализируемой является временная переменная, которая затем присваива- ется s. Такие переменные, естественно, уничтожаются как положено с помощью string::~string() при первой возможности.

6.7 Индексирование Чтобы задать смысл индексов для объектов класса, используется функция operator[]. Второй параметр (индекс) функции operator[] мо- жет быть любого типа. Это позволяет определять ассоциативные масси- вы и т.п. В качестве примера давайте перепишем пример из #2.3.10, где при написании небольшой программы для подсчета числа вхождений слов в файле применялся ассоциативный массив. Там использовалась функция. Здесь определяется надлежащий тип ассоциативного массива:

struct pair { char* name;

int val;

};

class assoc { pair* vec;

int max;

int free;

public:

assoc(int);

int& operator[](char*);

void print_all();

};

В assoc хранится вектор пар pair длины max. Индекс первого не- использованного элемента вектора находится в free. Конструктор выг- лядит так:

assoc::assoc(int s) { max = (s<16) ? s : 16;

free = 0;

vec = new pair[max];

} При реализации применяется все тот же простой и неэффективный ме- тод поиска, что использовался в #2.3.10. Однако при переполнении assoc увеличивается:

#include int assoc::operator[](char* p) /* работа с множеством пар "pair":

поиск p, возврат ссылки на целую часть его "pair" делает но- вую "pair", если p не встречалось */ { register pair* pp;

for (pp=&vec[free-1];

vec<=pp;

pp--) if (strcmp(p,pp->name)==0) return pp->val;

if (free==max) { // переполнение: вектор увеличивается pair* nvec = new pair[max*2];

for ( int i=0;

i

i++) nvec[i] = vec[i];

delete vec;

vec = nvec;

max = 2*max;

} pp = &vec[free++];

pp->name = new char[strlen(p)+1];

strcpy(pp->name,p);

pp->val = 0;

// начальное значение: return pp->val;

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

vouid assoc::print_all() { for (int i = 0;

i

i++) cout << vec[i].name << ": " << vec[i].val << "\n";

} Мы можем, наконец, написать простую главную программу:

main() // считает вхождения каждого слова во вводе { const MAX = 256;

// больше самого большого слова char buf[MAX];

assoc vec(512);

while (cin>>buf) vec[buf]++;

vec.print_all();

} 6.8 Вызов функции Вызов функции, то есть запись выражение(список_выражений), можно проинтерпретировать как бинарную операцию, и операцию вызова можно перегружать так же, как и другие операции. Список параметров функ- ции operator() вычисляется и проверяется в соответствие с обычными правилами передачи параметров. Перегружающая функция может ока- заться полезной главным образом для определения типов с единствен- ной операцией и для типов, у которых одна операция настолько преоб- ладает, что другие в большинстве ситуаций можно не принимать во внимание.

Для типа ассоциативного массива assoc мы не определили итератор.

Это можно сделать, определив класс assoc_iterator, работа которого состоит в том, чтобы в определенном порядке поставлять элементы из assoc. Итератору нужен доступ к данным, которые хранятся в assoc, поэтому он сделан другом:

class assoc { friend class assoc_iterator;

pair* vec;

int max;

int free;

public:

assoc(int);

int& operator[](char*);

};

Итератор определяется как class assoc_iterator{ assoc* cs;

// текущий массив assoc int i;

// текущий индекс public:

assoc_iterator(assoc& s) { cs = &s;

i = 0;

} pair* operator()() { return (ifree)? &cs->vec[i++] : 0;

} };

Надо инициализировать assoc_iterator для массива assoc, после че- го он будет возвращать указатель на новую pair из этого массива всякий раз, когда его будут активизировать операцией (). По дости- жении конца массива он возвращает 0:

main() // считает вхождения каждого слова во вводе { const MAX = 256;

// больше самого большого слова char buf[MAX];

assoc vec(512);

while (cin>>buf) vec[buf]++;

assoc_iterator next(vec);

pair* p;

while ( p = next() ) cout << p->name << ": " << p->val << "\n";

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

Конечно, такое применение объектов для представления итераторов никак особенно с перегрузкой операций не связано. Многие любят использовать итераторы с такими операциями, как first(), next() и last() (первый, следующий и последний).

6.9 Класс String Вот довольно реалистичный пример класса строк string. В нем про- изводится учет ссылок на строку с целью минимизировать копирование и в качестве констант применяются стандартные символьные строки С++.

#include #include class string { struct srep { char* s;

// указатель на данные int n;

// счетчик ссылок };

srep *p;

public:

string(char *);

// string x = "abc" string();

// string x;

string(string &);

// string x = string... string& operator=(char *);

string& operator=(string &);

~string();

char& operator[](int i);

friend ostream& operator<<(ostream&, string&);

friend istream& operator>>(istream&, string&);

friend int operator==(string& x, char* s) {return strcmp(x.p->s, s) == 0;

} friend int operator==(string& x, string& y) {return strcmp(x.p->s, y.p->s) == 0;

} friend int operator!=(string& x, char* s) {return strcmp(x.p->s, s) != 0;

} friend int operator!=(string& x, string& y) {return strcmp(x.p->s, y.p->s) != 0;

} };

Конструкторы и деструкторы просты (как обычно):

string::string() { p = new srep;

p->s = 0;

p->n = 1;

} string::string(char* s) { p = new srep;

p->s = new char[ strlen(s)+1 ];

strcpy(p->s, s);

p->n = 1;

} string::string(string& x) { x.p->n++;

p = x.p;

} string::~string() { if (--p->n == 0) { delete p->s;

delete p;

} } Как обычно, операции присваивания очень похожи на конструкторы.

Они должны обрабатывать очистку своего первого (левого) операнда:

string& string::operator=(char* s) { if (p->n > 1) { // разъединить себя p->n--;

p = new srep;

} else if (p->n == 1) delete p->s;

p->s = new char[ strlen(s)+1 ];

strcpy(p->s, s);

p->n = 1;

return *this;

} Благоразумно обеспечить, чтобы присваивание объекта самому себе работало правильно:

string& string::operator=(string& x) { x.p->n++;

if (--p->n == 0) { delete p->s;

delete p;

} p = x.p;

return *this;

} Операция вывода задумана так, чтобы продемонстрировать применение учета ссылок. Она повторяет каждую вводимую строку (с помощью опе- рации <<, которая определяется позднее):

ostream& operator<<(ostream& s, string& x) { return s << x.p->s << " [" << x.p->n << "]\n";

} Операция ввода использует стандартную функцию ввода символьной строки (#8.4.1).

istream& operator>>(istream& s, string& x) { char buf[256];

s >> buf;

x = buf;

cout << "echo: " << x << "\n";

return s;

} Для доступа к отдельным символам предоставлена операция индекси- рования. Осуществляется проверка индекса:

void error(char* p) { cerr << p << "\n";

exit(1);

} char& string::operator[](int i) { if (i<0 || strlen(p->s)

return p->s[i];

} Головная программа просто немного опробует действия над строками.

Она читает слова со ввода в строки, а потом эти строки печатает.

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

main() { string x[100];

int n;

cout << "отсюда начнем\n";

for (n = 0;

cin>>x[n];

n++) { string y;

if (n==100) error("слишком много строк");

cout << (y = x[n]);

if (y=="done") break;

} cout << "отсюда мы пройдем обратно\n";

for (int i=n-1;

0<=i;

i--) cout << x[i];

} 6.10 Друзья и члены Теперь, наконец, можно обсудить, в каких случаях для доступа к закрытой части определяемого пользователем типа использовать члены, а в каких - друзей. Некоторые операции должны быть членами:

конструкторы, деструкторы и виртуальные функции (см. следующую гла- ву), но обычно это зависит от выбора.

Рассмотрим простой класс X:

class X { //...

X(int);

int m();

friend int f(X&);

};

Внешне не видно никаких причин делать f(X&) другом дополнительно к члену X::m() (или наоборот), чтобы реализовать действия над классом X. Однако член X::m() можно вызывать только для "настоящего объекта", в то время как друг f() может вызываться для объекта, созданного с помощью неявного преобразования типа. Например:

void g() { 1.m();

// ошибка f(1);

// f(x(1));

} Поэтому операция, изменяющая состояние объекта, должна быть чле- ном, а не другом. Для определяемых пользователем типов операции, требующие в случае фундаментальных типов операнд lvalue (=, *=, ++, *= и т.д.), наиболее естественно определяются как члены.

И наоборот, если нужно иметь неявное преобразование для всех опе- рандов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют опера- ции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, || и т.д.).

Если никакие преобразования типа не определены, то оказывается, что нет никаких существенных оснований в пользу члена, если есть друг, который получает ссылочный параметр, и наоборот. В некоторых случаях программист может предпочитать один синтаксис вызова друго- му. Например, оказывается, что большинство предпочитает для обраще- ния матрицы m запись m.inv(). Конечно, если inv() действительно об- ращает матрицу m, а не просто возвращает новую матрицу, обратную m, ей следует быть членом.

При прочих равных условиях выбирайте, чтобы функция была членом:

никто не знает, вдруг когда-нибудь кто-то определит операцию преоб- разования. Невозможно предсказать, потребуют ли будущие изменения изменять состояние объекта. Синтаксис вызова функции члена ясно указывает пользователю, что объект можно изменить;

ссылочный пара- метр является далеко не столь очевидным. Кроме того, выражения в члене могут быть заметно короче выражений в друге. В функции друге надо использовать явный параметр, тогда как в члене можно использо- вать неявный this. Если только не применяется перегрузка, имена членов обычно короче имен друзей.

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

Изложенный аппарат должен уберечь программиста/читателя от худших крайностей применения перегрузки, потому что программист предохра- нен от изменения значения операций для основных типов данных вроде int, а также потому, что синтаксис выражений и приоритеты операций сохраняются.

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

6.12 Упражнения 1. (*2) Определите итератор для класса string. Определите опера- цию конкатенации + и операцию "добавить в конец" +=. Какие еще операции над string вы хотели бы иметь возможность осущест- влять?

2. (*1.5) Задайте с помощью перегрузки () операцию выделения подстроки для класса строк.

3. (*3) Постройте класс string так, чтобы операция выделения подстроки могла использоваться в левой части присваивания. На- пишите сначала версию, в которой строка может присваиваться подстроке той же длины, а потом версию, где эти длины могут быть разными.

4. (*2) Постройте класс string так, чтобы для присваивания, пере- дачи параметров и т.п. он имел семантику по значению, то есть, когда копируется строковое представление, а не просто управля- ющая структура данных класса sring.

5. (*3) Модифицируйте класс string из предыдущего примера таким образом, чтобы строка копировалась только когда это необходи- мо. То есть, храните совместно используемое представление двух строк, пока одна из этих строк не будет изменена. Не пытайтесь одновременно с этим иметь операцию выделения подстроки, кото- рая может использоваться в левой части.

6. (*4) Разработайте класс string с семантикой по значению, копи- рованием с задержкой и операцией подстроки, которая может сто- ять в левой части.

7. (*2) Какие преобразования используются в каждом выражении сле- дующей программы:

struct X { int i;

X(int);

operator+(int);

};

struct Y { int i;

Y(X);

operator+(X);

operator int();

};

X operator* (X,Y);

int f(X);

X x = 1;

Y y = x;

int i = 2;

main() { i + 10;

y + 10;

y + 10 * y;

x + y + i;

x * x + i;

f(7);

f(y);

y + y;

106 + y;

} Определите X и Y так, чтобы они оба были целыми типами. Изме- ните программу так, чтобы она работала и печатала значения всех допустимых выражений.

8. (*2) Определите класс INT, который ведет себя в точности как int. Подсказка: определите INT::operator int().

9. (*1) Определите класс RINT, который ведет себя в точности как int за исключением того, что единственные возможные операции - это + (унарный и бинарный), - (унарный и бинарный), *, /, %.

Подсказка: не определяйте INT::operator int().

10. (*3) Определите класс LINT, ведущий себя как RINT, за исклю- чением того, что имеет точность не менее 64 бит.

11. (*4) Определите класс, который реализует арифметику с произ- вольной точностью. Подсказка: вам надо управлять памятью ана- логично тому, как это делалось для класса string.

12. (*2) Напишите программу, доведенную до нечитаемого состояния с помощью макросов и перегрузки операций. Вот идея: определите для INT + так, чтобы он означал -, и наоборот, а потом с по- мощью макроопределения определите int как INT. Переопределение часто употребляемых функций, использование параметров ссылоч- ного типа и несколько вводящих в заблуждение комментариев по- могут устроить полную неразбериху.

13. (*3) Поменяйтесь со своим другом программами, которые у вас получились в предыдущем упражнении. Не запуская ее попытайтесь понять, что делает программа вашего друга. После выполнения этого упражнения вы будете знать, чего следует избегать.

14. (*2) Перепишите примеры с comlpex (#6.3.1), tiny (#6.3.2) и string (#6.9) не используя friend функций. Используйте только функции члены. Протестируйте каждую из новых версий. Сравните их с версиями, в которых используются функции друзья. Еще раз посмотрите Упражнение 5.3.

15. (*2) Определите тип vec4 как вектор их четырех float. Опреде- лите operator[] для vec4. Определите операции +, -, *, /, =, +=, -=, *=, /= для сочетаний векторов и чисел с плавающей точ- кой.

16. (*3) Определите класс mat4 как вектор из четырех vec4. Опре- делите для mat4 operator[], возвращающий vec4. Определите для этого типа обычные операции над матрицами. Определите функцию, выполняющие для mat4 исключение Гаусса.

17. (*2) Определите класс vector, аналогичный vec4, но с длиной, которая задается как параметр конструктора vector::vector(int).

18. (*3) Определите класс matrix, аналогичный mat4, но с размер- ностью, задаваемой параметрами конструктора matrix::matrix(int,int).

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

7.1 Введение Представим себе процесс написания некоторого универсального средства (например, тип связанный список, таблица имен или плани- ровщик для системы моделирования), которое предназначается для использования многими разными людьми в различных обстоятельствах.

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

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

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

7.2 Производные классы Чтобы разделить задачи понимания аппарата языка и методов его применения, знакомство с понятием производных классов делается в три этапа. Вначале с помощью небольших примеров, которые не надо воспринимать как реалистичные, будут описаны сами средства языка (запись и семантика). После этого демонстрируются некоторые неоче- видные применения производных классов, и, наконец, приводится за- конченная программа.

7.2.1 Построение производного класса Рассмотрим построение программы, которая имеет дело с людьми, служащими в некоторой фирме. Структура данных в этой программе мо- жет быть например такой:

struct employee { // служащий char* name;

// имя short age;

// возраст short department;

// подразделение int salary;

// жа- лование employee* next;

//...

};

Список аналогичных служащих будет связываться через поле next.

Теперь давайте определим менеджера:

struct manager { // менеджер employee emp;

// запись о менеджере как о служащем employee* group;

// подчиненные люди //...

};

Менеджер также является служащим;

относящиеся к служащему employee данные хранятся в члене emp объекта manager. Для читающего это человека это, может быть, очевидно, но нет ничего выделяющего член emp для компилятора. Указатель на менеджера (manager*) не яв- ляется указателем на служащего (employee*), поэтому просто исполь- зовать один там, где требуется другой, нельзя. В частности, нельзя поместить менеджера в список служащих, не написав для этого специ- альный код. Можно либо применить к manager* явное преобразование типа, либо поместить в список служащих адрес члена emp, но и то и другое мало элегантно и довольно неясно. Корректный подход состоит в том, чтобы установить, что менеджер является служащим с некоторой добавочной информацией:

struct manager : employee { employee* group;

//...

};

manager является производным от employee и, обратно, employee есть базовый класс для manager. Класс manager дополнительно к члену group имеет члены класса employee (name, age и т.д.).

Имея определения employee и manager мы можем теперь создать список служащих, некоторые из которых являются менеджерами. Напри- мер:

void f() { manager m1, m2;

employee e1, e2;

employee* elist;

elist = &m1;

// поместить m1, e1, m2 и e2 в elist m1.next = &e1;

e1.next = &m2;

m2.next = &e2;

e2.next = 0;

} Поскольку менеджер является служащим, manager* может использо- ваться как employee*. Однако служащий необязательно является менед- жером, поэтому использовать employee* как manager* нельзя.

7.2.2 Функции члены Просто структуры данных вроде employee и manager на самом деле не столь интересны и часто не особенно полезны, поэтому рассмотрим, как добавить в них функции. Например:

class employee { char* name;

//...

public:

employee* next;

void print();

//...

};

class manager : public employee { //...

public:

void print();

//...

};

Надо ответить на некоторые вопросы. Как может функция член произ- водного класса manager использовать члены его базового класса employee? Как члены базового класса employee могут использовать функции члены производного класса manager? Какие члены базового класса employee может использовать функция не член на объекте типа manager? Каким образом программист может повлиять на ответы на эти вопросы, чтобы удовлетворить требованиям приложения?

Рассмотрим:

void manager::print() { cout << " имя " << name << "\n";

//...

} Член производного класса может использовать открытое имя из свое- го базового класса так же, как это могут делать другие члены последнего, то есть без указания объекта. Предполагается, что на объект указывает this, поэтому (корректной) ссылкой на имя name яв- ляется this->name. Однако функция manager::print компилироваться не будет, член производного класса не имеет никакого особого права доступа к закрытым членам его базового класса, поэтому для нее name недоступно.

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

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

С другой стороны, можно ведь использовать механизм friend, чтобы предоставить такой доступ или отдельным функциям, или всем функциям отдельного класса (как описывается в #5.3). Например:

class employee { friend void manager::print();

//...

};

решило бы проблему с manager::print(), и class employee { friend class manager;

//...

};

сделало бы доступным каждый член employee для всех функций класса manager. В частности, это сделает name доступным для manager::print().

Другое, иногда более прозрачное решение для производного класса - использовать только открытые члены его базового класса. Например:

void manager::print() { employee::print();

// печатает информацию о служащем //... // печатает информацию о менеджере } Заметьте, что надо использовать ::, потому что print() была переоп- ределена в manager. Такое повторное использование имен типично. Не- осторожный мог бы написать так:

void manager::print() { print();

// печатает информацию о служащем //... // печатает информацию о менеджере } и обнаружить, что программа после вызова manager::print() неожидан- но попадает в последовательность рекурсивных вызовов.

7.2.3 Видимость Класс employee стал открытым (public) базовым классом класса manager в результате описания:

class manager : public employee { //...

};

Это означает, что открытый член класса employee является также и открытым членом класса manager. Например:

void clear(manager* p) { p->next = 0;

} будет компилироваться, так как next - открытый член и employee и manager'а. Альтернатива - можно определить закрытый (private) класс, просто опустив в описании класса слово public:

class manager : employee { //...

};

Это означает, что открытый член класса employee является закрытым членом класса manager. То есть, функции члены класса manager могут как и раньше использовать открытые члены класса employee, но для пользователей класса manager эти члены недоступны. В частности, при таком описании класса manager функция clear() компилироваться не будет. Друзья производного класса имеют к членам базового класса такой же доступ, как и функции члены.

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

Когда описывается производная struct, ее базовый класс по умолча- нию является public базовым классом. То есть, struct D : B {...

означает class D : public B { public:...

Отсюда следует, что если вы не сочли полезным то сокрытие данных, которое дают class, public и friend, вы можете просто не использо- вать эти ключевые слова и придерживаться struct. Такие средства языка, как функции члены, конструкторы и перегрузка операций, не зависят от механизма сокрытия данных.

Можно также объявить некоторые, но не все, открытые члены базово- го класса открытыми членами производного класса. Например:

class manager : employee { //...

public:

//...

employee::name;

employee::department;

};

Запись имя_класса :: имя_члена ;

не вводит новый член, а просто делает открытый член базового класса открытым для производного класса. Теперь name и department могут использоваться для manager'а, а salary и age - нет. Естественно, сделать закрытый член базового класса открытым членом производного класса невозможно. Невозможно с помощью этой записи также сделать открытыми перегруженные имена.

Подытоживая, можно сказать, что вместе с предоставлением средств дополнительно к имеющимся в базовом классе, производный класс можно использовать для того, чтобы сделать средства (имена) недоступными для пользователя. Другими словами, с помощью производного класса можно обеспечивать прозрачный, полупрозрачный и непрозрачный доступ к его базовому классу.

7.2.4 Указатели Если производный класс derived имеет открытый базовый класс base, то указатель на derived можно присваивать переменной типа указатель на base не используя явное преобразование типа. Обратное преобразо- вание, указателя на base в указатель на derived, должно быть явным.

Например:

class base { /*... */ };

class derived : public base { /*... */ };

derived m;

base* pb = &m;

// неявное преобразование derived* pd = pb;

// ошибка: base* не является derived* pd = (derived*)pb;

// явное преобразование Иначе говоря, объект производного класса при работе с ним через указатель и можно рассматривать как объект его базового класса. Об- ратное неверно.

Будь base закрытым базовым классом класса derived, неявное преоб- разование derived* в base* не делалось бы. Неявное преобразование не может в этом случае быть выполнено, потому что к открытому члену класса base можно обращаться через указатель на base, но нельзя че- рез указатель на derived:

class base { int m1;

public:

int m2;

// m2 - открытый член base };

class derived : base { // m2 - НЕ открытый член derived };

derived d;

d.m2 = 2;

// ошибка: m2 из закрытой части класса base* pb = &d;

// ошибка: (закрытый base) pb->m2 = 2;

// ok pb = (base*)&d;

// ok: явное преобразование pb->m2 = 2;

// ok Помимо всего прочего, этот пример показывает, что используя явное приведение к типу можно сломать правила защиты. Ясно, делать это не рекомендуется, и это приносит программисту заслуженную "награду". К несчастью, недисциплинированное использование явного преобразова- ния может создать адские условия для невинных жертв, эксплуатирую- щих программу, в которой это делается. Но, к счастью, нет способа воспользоваться приведением для получения доступа к закрытому имени m1. Закрытый член класса может использоваться только членами и друзьями этого класса.

7.2.5 Иерархия типов Производный класс сам может быть базовым классом. Например:

class employee {... };

class secretary : employee {... };

class manager : employee {... };

class temporary : employee {... };

class consultant : temporary {... };

class director : manager {... };

class vice_president : manager {... };

class president :

vice_president {... };

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

class temporary {... };

class employee {... };

class secretary : employee {... };

// не С++:

class temporary_secretary : temporary : secretary {... };

class consultant : temporary : employee {... };

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

class temporary {... };

class employee {... };

class secretary : employee {... };

// Альтернатива:

class temporary_secretary : secretary { temporary temp;

... };

class consultant : employee { temporary temp;

... };

Это выглядит неэлегантно и страдает как раз от тех проблем, для преодоления которых были изобретены производные классы. Например, поскольку consultant не является производным от temporary, consultant'а нельзя помещать с список временных служащих (temporary employee), не написав специальный код. Однако во многих полезных программах этот метод успешно используется.

7.2.6 Конструкторы и деструкторы Для некоторых производных классов нужны конструкторы. Если у ба- зового класса есть конструктор, он должен вызываться, и если для этого конструктора нужны параметры, их надо предоставить. Например:

class base { //...

public:

base(char* n, short t);

~base();

};

class derived : public base { base m;

public:

derived(char* n);

~derived();

};

Параметры конструктора базового класса специфицируются в опреде- лении конструктора производного класса. В этом смысле базовый класс работает точно также, как неименованный член производного класса (см. #5.5.4). Например:

derived::derived(char* n) : (n,10), m("member",123) { //...

} Объекты класса конструируются снизу вверх: сначала базовый, потом члены, а потом сам производный класс. Уничтожаются они в обратном порядке: сначала сам производный класс, потом члены а потом базо- вый.

7.2.7 Поля типа Чтобы использовать производные классы не просто как удобную сок- ращенную запись в описаниях, надо разрешить следующую проблему:

Pages:     | 1 | 2 || 4 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.