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

Бьярн Страуструп - Язык программирования C++

.pdf
Скачиваний:
1042
Добавлен:
17.03.2018
Размер:
3.01 Mб
Скачать

Бьерн Страуструп.

Язык программирования С++

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

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

#define PI = 3.141593; #define MAX(a,b) a>b?a:b #define fac(a) (a)*fac((a)-1)

17. (*3) Напишите макропроцессор с простыми возможностями, как у препроцессора С. Текст читайте из cin, а результат записывайте в cout. Вначале реализуйте макроопределения без параметров. Подсказка: в программе калькулятора есть таблица имен и синтаксический анализатор, которыми можно воспользоваться.

18. (*2) Напишите программу, извлекающую квадратный корень из двух (2) с помощью стандартной функции sqrt(), но не включайте в программу <math.h>. Сделайте это упражнение с помощью функции sqrt() на Фортране.

19. (*2) Реализуйте функцию print() из $$4.6.7.

121

Бьерн Страуструп.

Язык программирования С++

 

5. КЛАССЫ

 

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

 

- Даг Макилрой

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

5.1 Введение и краткий обзор

Понятие класса, которому посвящена эта и три следующих главы, служит в С++ для того, чтобы дать программисту инструмент построения новых типов. Ими пользоваться не менее удобно, чем встроенными. В идеале использование определенного пользователем типа не должно отличаться от использования встроенных типов. Различия возможны только в способе построения.

Тип есть вполне конкретное представление некоторого понятия. Например, в С++ тип float с операциями +, -, * и т.д. является хотя и ограниченным, но конкретным представлением математического понятия вещественного числа. Новый тип создается для того, чтобы стать специальным и конкретным представлением понятия, которое не находит прямого и естественного отражения среди встроенных типов. Например, в программе из области телефонной связи можно ввести тип trunk_module (линиясвязи), в видеоигре - тип explosion (взрыв), а в программе, обрабатывающей текст, - тип list_of_paragraphs (список-параграфов). Обычно проще понимать и изменять программу, в которой типы хорошо представляют используемые в задаче понятия. Удачно подобранное множество пользовательских типов делает программу более ясной. Оно позволяет транслятору обнаруживать недопустимое использование объектов, которое в противном случае останется невыявленным до отладки программы.

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

Глава состоит из четырех частей:

$$5.2 Классы и члены. Здесь вводится основное понятие пользовательского типа, называемого классом. Доступ к объектам класса может ограничиваться множеством функций, описания которых входят в описание класса. Эти функции называются функциями-членами и друзьями. Для создания объектов класса используются специальные функции-члены, называемые конструкторами. Можно описать специальную функцию-член для удаления объектов класса при его уничтожении. Такая функция называется деструктором.

$$5.3 Интерфейсы и реализации. Здесь приводятся два примера разработки, реализации и использования классов.

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

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

122

Бьерн Страуструп.

Язык программирования С++

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

5.2 Классы и члены

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

5.2.1 Функции-члены

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

struct date { int month, day, year; }; date today;

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

void print_date(const 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_birthday; void f()

{

my_birthday.set(30,12,1950); today.set(18,1,1991); my_birthday.print(); today.next();

}

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

void date::next()

{

if (++day > 28 ) {

// здесь сложный вариант

}

}

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

5.2.2 Классы

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

123

Бьерн Страуструп.

Язык программирования С++

такое ограничение, описав класс вместо структуры:

class date {

int month, day, year; public:

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

void print()

};

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

void date::print()

// печать даты в принятом в США виде

{

 

cout << month << '/' << day << '/' << year ;

}

Однако от функций не членов частные члены класса date уже ограждены:

void backdate()

 

{

// ошибка

today.day--;

}

 

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

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

5.2.3Ссылка на себя

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

class X { int m; public:

int readm() { return m; }

};

void f(X aa, X bb)

{

int a = aa.readm(); int b = bb.readm();

// ...

}

При первом вызове readm() m обозначает aa.m, а при втором - bb.m.

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

X *const this;

124

Бьерн Страуструп.

Язык программирования С++

и инициализируется, чтобы указывать на объект, для которого функция-член вызывалась. Этот указатель нельзя изменять, поскольку он постоянный (*const). Явно описать его тоже нельзя, т.к. 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 = this->suc

p->suc = 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);

}

Списки с такой общей структурой служат фундаментом списочных классов, описываемых в главе 8. Чтобы присоединить звено к списку, нужно изменить объекты, на которые настроены указатели this, pre и suc. Все они имеют тип dlink, поэтому функция-член dlink::append() имеет к ним доступ. Защищаемой единицей в С++ является класс, а не отдельный объект класса.

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

class X { int m; public:

readme() const { return m; } writeme(int i) { m = i; }

};

Функцию-член со спецификацией const можно вызывать для постоянных объектов, а функцию-член без такой спецификации - нельзя:

void f(X& mutable, const X& constant)

{

mutable.readme();

// нормально

mutable.writeme(7);

// нормально

constant.readme();

// нормально

constant.writeme(7);

// ошибка

}

125

Бьерн Страуструп.

Язык программирования С++

В этом примере разумный транслятор смог бы обнаружить, что функция X::writeme() пытается изменить постоянный объект. Однако, это непростая задача для транслятора. Из-за раздельной трансляции он в общем случае не может гарантировать "постоянство" объекта, если нет соответствующего описания со спецификацией const. Например, определения readme() и writeme() могли быть в другом файле:

class X { int m; public:

readme() const; writeme(int i);

};

В таком случае описание readme() со спецификацией const существенно.

Тип указателя this в постоянной функции-члене класса X есть const X *const. Это значит, что без явного приведения с помощью this нельзя изменить значение объекта:

class X { int m; public:

// ...

void implicit_cheat() const { m++; } // ошибка

void explicit_cheat() const { ((X*)this)->m++; }

// нормально

};

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

Логического постоянства можно достигнуть приведением, удаляющим спецификацию const:

class calculator1 { int cache_val; int cache_arg;

// ...

public:

int compute(int i) const; // ...

};

int calculator1::compute(int i) const

{

if (i == cache_arg) return cache_val;

// нелучший способ

((calculator1*)this)->cache_arg = i;

((calculator1*)this)->cache_val = val; return val;

}

Этого же результата можно достичь, используя указатель на данные без const:

struct cache { int val; int arg;

};

class calculator2 {

126

Бьерн Страуструп.

Язык программирования С++

cache* p; // ...

public:

int compute(int i) const; // ...

};

int calculator2::compute(int i) const

{

if (i == p->arg) return p->val;

// нелучший способ p->arg = i; p->val = val;

return val;

}

Отметим, что const нужно указывать как в описании, так и в определении постоянной функции-члена. Физическое постоянство обеспечивается помещением объекта в защищенную по записи память, только если в классе нет конструктора ($$7.1.6).

5.2.4 Инициализация

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

class date {

// ...

date(int, int, int);

};

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

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

//

краткая форма

date

xmas(25,12,0);

date

my_birthday;

//

неправильно, нужен инициализатор

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

class date {

 

int month, day, year;

 

public:

 

// ...

// день, месяц, год

date(int, int, int);

date(int, int);

// день, месяц и текущий год

date(int);

// день и текущие год и месяц

date();

// стандартное значение: текущая дата

date(const char*);

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

};

 

Параметры конструкторов подчиняются тем же правилам о типах параметров, что и все остальные функции ($$4.6.6). Пока конструкторы достаточно различаются потипам своих параметров, транслятор способен правильно выбрать конструктор:

date today(4);

date july4("July 4, 1983"); date guy("5 Nov");

127

Бьерн Страуструп.

Язык программирования С++

date now;

// инициализация стандартным значением

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

class date {

int month, day, year; public:

// ...

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

};

date::date(int d, int m, int y)

{

day = d ? d : today.day; month = m ? m : today.month; year = y ? y : today.year;

//проверка правильности даты

//...

}

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

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

date d = today; // инициализация присваиванием

На самом деле, имеется стандартный конструктор копирования, определенный как поэлементное копирование объектов одного класса. Если такой конструктор для класса X не нужен, можно переопределить его как конструктор копирования X::X(const X&). Подробнее поговорим об этом в $$7.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;

}

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

};

Когда объект типа char_stack выходит из текущей области видимости, вызывается деструктор:

128

Бьерн Страуструп.

Язык программирования С++

void f()

{

char_stack s1(100); char_stack s2(200); s1.push('a'); s2.push(s1.pop()); char ch = s2.pop(); cout << ch << '\n';

}

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

5.2.6 Подстановка

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

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

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

class char_stack { int size;

char* top; char* s; public:

char pop(); // ...

};

inline char char_stack::pop()

{

return *--top;

}

Отметим, что недопустимо описывать разные определения функции-члена, являющейся подстановкой, в различных исходных файлах ($$R.7.1.2). Это нарушило бы понятие о классе как о цельном типе.

5.3 Интерфейсы и реализации

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

Очевидные примеры хороших классов дают контейнеры разных видов: таблицы, множества, списки, вектора, словари и т.д. Такой класс имеет операцию занесения в контейнер. Обычно имеется и операция проверки: был ли данный член занесен в контейнер? Могут быть операции упорядочивания

129

Бьерн Страуструп.

Язык программирования С++

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

5.3.1 Альтернативные реализации

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

struct name { char* string; name* 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 тем, что это настоящий тип. Можно описать несколько таблиц, завести указатель на таблицу и т.д. Например:

#include "table.h" table globals; table keywords; table* locals;

main()

{

locals = new table; // ...

}

Приведем реализацию функции table::look(), в которой используется линейный поиск в списке имен таблицы:

#include <string.h>

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, надо было

130

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