Эти типы не "абстрактны",
они столь же реальны, как int и float.
- Дуг Макилрой
В этой главе описываются возможности определения новых типов в С++, для которых доступ к данным ограничен заданным множеством функций доступа. Объясняются способы защиты структуры данных, ее инициализации, доступа к ней и, наконец, ее уничтожения. Примеры содержат простые классы для работы с таблицей имен, манипуляции стеком, работу с множеством и реализацию дискриминирующего (то есть, "надежного") объединения. Две следующие главы дополнят описание возможностей определения новых типов в С++ и познакомят читателя еще с некоторыми интересными примерами.
Предназначение понятия класса, которому посвящены эта и две последующие главы, состоит в том, чтобы предоставить программисту инструмент для создания новых типов, столь же удобных в обращении сколь и встроенные типы. В идеале тип определяемый пользователем, способом использования должен отличаться от встроенных типов, только способом создания.
Тип есть конкретное представление некоторой концепции (понятия). Например, имеющийся в С++ тип float с его операциями +, -, * и т.д. обеспечивает ограниченную, но конкретную версию математического понятия действительного числа. Новый тип создается для того, чтобы дать специальное и конкретное определение понятия, которому ничто прямо и очевидно среди встроенных типов не отвечает. Например, в программе, которая работает с телефоном, можно было бы создать тип trunk_module (элемент линии), а в программе обработки текстов - тип list_of_paragraphs (список параграфов). Как правило, программу, в которой создаются типы, хорошо отвечающие понятиям приложения, понять легче, чем программу, в которой это не делается. Хорошо выбранные типы, определяемые пользователем, делают программу более четкой и короткой. Это также позволяет компилятору обнаруживать недопустимые использования объектов, которые в противном случае останутся необнаруженными до тестирования программы.
В определении нового типа основная идея - отделить несущественные подробности реализации (например, формат данных, которые используются для хранения объекта типа) от тех качеств, которые существенны для его правильного использования (например, полный список функций, которые имеют доступ к данным). Такое разделение можно описать так, что работа со структурой данных и внутренними административными подпрограммами осуществляется через специальный интерфейс (канализируется).
Эта глава состоит из четырех практически отдельных частей:
#5.2 Классы и Члены. Этот раздел знакомит с основным понятием типа, определяемого пользователем, который называется класс (class). Доступ к объектам класса может ограничиваться набором функций, которые описаны как часть этого класса. Такие функции называются функциями членами. Объекты класса создаются и инициализируются функциями членами, специально для этой цели описанными. Эти функции называются конструкторами. Функция член может быть специальным образом описана для "очистки" каждого классового объекта при его уничтожении. Такая функция называется деструктором.
#5.3 Интерфейсы и Реализации. В этом разделе приводится два примера того, как класс проектируется, реализуется и используется.
#5.4 Друзья и Объединения. В этом разделе приводится много дополнительных подробностей, касающихся классов. В нем показано, как предоставить доступ к закрытой части класса функции, которая не является членом этого класса. Такая функция называется друг (friend). В этом разделе показано также, как определить дискриминирующее объединение.
#5.5 Конструкторы и Деструкторы. Объект может создаваться как автоматический, статический или как объект в свободной памяти. Объект может также быть членом некоторой совокупности (типа вектора или класса), которая в свою очередь может размещаться одним из этих трех способов. Довольно подробно объясняется использование конструкторов и деструкторов.
Класс - это определяемый пользователем тип. Этот раздел знакомит с основными средствами определения класса, создания объекта класса, работы с такими объектами и, наконец, уничтожения таких объектов после использования.
Рассмотрим реализацию понятия даты с использованием 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 ) { // делает сложную часть работы } }
В функции члене имена членов могут использоваться без явной ссылки на объект. В этом случае имя относится к члену того объекта, для которого функция была вызвана.
Описание 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) должна быть вызвана кодом ф
Защита закрытых данных связана с ограничением использования имен членов класса. Это можно обойти с помощью манипуляции адресами, но это уже, конечно, жульничество.
В функции члене на члены объекта, для которого она была вызвана, можно ссылаться непосредственно. Например:
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 (текущий, предыдущий и послед
Использование для обеспечения инициализации объекта класса функций вроде 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. Поскольку полный список функций членов находится в описании класса и как правило короткий, то нет никакой серьезной причины требовать
Размножение конструкторов в примере с 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 выбор нуля неоче
Объект класса без конструкторов можно инициализировать путем присваивания ему другого объекта этого класса. Это можно делать и тогда, когда конструкторы описаны. Например:
date d = today; // инициализация посредством присваивания
По существу, имеется конструктор по умолчанию, определенный как побитовая копия объекта того же класса. Если для класса X такой конструктор по умолчанию нежелателен, его можно переопределить конструктором с именем X(X&). Это будет обс
Определяемый пользователем тип чаще имеет, чем не имеет, конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие, деструктор, чтобы обеспечить соответствующую очистку объектов этого типа. Имя деструктора для класса 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() эти два вектора будут освобождены.
При программировании с использованием классов очень часто используется много маленьких функций. По сути, везде, где в программе традиционной структуры стояло бы просто какое-нибудь обычное использование структуры данных, дается функция. То, что было соглашением, стало стандартом, который распознает компилятор. Это может страшно понизить эффективность, потому что стоимость вызова функции (хотя и вовсе не высокая по сравнению с другими языками) все равно намного выше, чем пара ссылок по памяти, необходимая для тела функции.
Чтобы справиться с этой проблемой, был разработан аппарат inline-функций. Функция, определенная (а не просто описанная) в описании класса, считается inline. Это значит, например, что в функциях, которые используют приведенные выше char_stack, нет никаких вызовов функций кроме тех, которые используются для реализации операций вывода! Другими словами, нет никаких затрат времени выполнения, которые стоит принимать во внимание при разработке класса. Любое, даже самое маленькое действие, можно задать эффективно. Это утверждение снимает аргумент, который чаще всего приводят чаще всего в пользу открытых членов данных.
Функцию член можно также описать как inline вне описания класса. Например:
char char_stack { int size; char* top; char* s; public: char pop(); // ... }; inline char char_stack::pop() { return *--top; }
Что представляет собой хороший класс? Нечто, имеющее небольшое и хорошо определенное множество действий. Нечто, что можно рассматривать как "черный ящик", которым манипулируют только посредством этого множества действий. Нечто, чье фактическое представление можно любым мыслимым способом изменить, не повлияв на способ использования множества действий. Нечто, чего можно хотеть иметь больше одного.
Для всех видов контейнеров существуют очевидные примеры: таблицы, множества, списки, вектора, словари и т.д. Такой класс имеет операцию "вставить", обычно он также имеет операции для проверки того, был ли вставлен данный элемент. В нем могут быть действия для осуществления проверки всех элементов в определенном порядке, и кроме всего прочего, в нем может иметься операция для удаления элемента. Обычно контейнерные (то есть, вмещающие) классы имеют конструкторы и деструкторы.
Сокрытие данных и продуманный интерфейс может дать концепция модуля (см. например #4.4: файлы как модули). Класс, однако, является типом. Чтобы использовать его, необходимо создать объекты этого класса, и таких объектов можно создавать столько, сколько нужно. Модуль же сам является объектом. Чтобы использовать его, его надо только инициализировать, и таких объектов ровно один.
Пока описание открытой части класса и описание функций членов остаются неизменными, реализацию класса можно модифицировать не влияя на ее пользователей. Как пример этого рассмотрим таблицу имен, которая использовалась в настольном калькуляторе в Главе 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 <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, усовершенствованный таким образом, чтобы использовать хэшированный просмотр, как это делалось в примере с настольным калькулятором. Сделать это труднее из-за того ограничения, что уже написанные программы, в которы
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<sz; i++) tbl[i]="0;" } table::~table() { for (int i="0;" i<size; i++) for (name* n="tbl[i];" n; n="n-">next) { delete n->string; delete n; } delete tbl; }
Описав деструктор для класса name можно получить более простой и ясный вариант table::~table(). Функция просмотра практически идентична той, которая использовалась в примере настольного калькулятора (#3.1.3):
#include <string.h> 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 не так, как это будет делать С++ компилятор). Для С++ это было сочтено неприемлемым.
Программирование без сокрытия данных (с применением структур) требует меньшей продуманности, чем программирование со сокрытием данных (с использованием классов). Структуру можно определить не слишком задумываясь о том, как ее предполагается использовать. А когда определяется класс, все внимание сосредотачивается на обеспечении нового типа полным множеством операций; это важное смещение акцента. Время, потраченное на разработку нового типа, обычно многократно окупается при разработке и тестировании программы.
Вот пример законченного типа 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<cursize; } int next(int& i) { return x[i++]; } };
Чтобы протестировать этот класс, можно создать и распечатать множество случайных целых чисел. Такое множество могло бы быть результатом розыгрыша лотереи. Это простое множество можно также использовать для проверки последовательности целых на пов
#include <stream.h> 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<m) { int t = randint(n); 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<m) error("недопустимый размер intset"); 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 от пользователя скрыто. Множество внутренней упорядоченности не имеет, поэтому м
Дается три функции: iterate() для инициализации итерации, ok() для проверки, есть ли следующий элемент, и next() для того, чтобы взять следующий элемент:
class intset { // ... void iterate(int& i) { i = 0; } int ok(int& i) { return i<cursize; } int next(int& i) { return x[i++]; } };
Чтобы дать возможность этим трем операциям работать совместно и чтобы запомнить, куда дошел цикл, пользователь должен дать целый параметр. Поскольку элементы хранятся в отсортированном списке, их реализация тривиальна. Теперь можно определить
void print_in_order(intset* set) { int var; set->iterate(var); while (set->ok(var)) cout << set->next(var) << "\n"; }Другой способ задать итератор приводится в #6.8.
В это разделе описываются еще некоторые особенности, касающиеся классов. Показано, как предоставить функции не члену доступ к закрытым членам. Описывается, как разрешать конфликты имен членов, как можно делать вложенные описания классов, и как избежать нежелательной вложенности. Обсуждается также, как объекты класса могут совместно использовать члены данные, и как использовать указатели на члены. Наконец, приводится пример, показывающий, как построить дискриминирующее (экономное) объединение.
Предположим, вы определили два класса, 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() с ее помощью). Однако существует много задач, которые проще всего решаются, есл
Функция член одного класса может быть другом другого. Например:
class x { // ... void f(); }; class y { // ... friend void x::f(); };
Нет ничего необычного в том, что все функции члены одного класса являются друзьями другого. Для этого есть даже более краткая запись:
class x { friend class y; // ... };
Такое описание friend делает все функции члены класса y друзьями x.
Иногда полезно делать явное различие между именами членов класса и прочими именами. Для этого используется операция ::, "разрешения области видимости":
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) // ... } // ... }
* Иногда называется также квалификацией. (прим. перев.)
Описание класса может быть вложенным. Например:
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);} // ... };
Класс - это тип, а не объект данных, и в каждом объекте класса имеется своя собственная копия данных, членов этого класса. Однако некоторые типы наиболее элегантно реализуются, если все объекты этого типа могут совместно использовать (разделять) некоторые данные. Предпочтительно, чтобы такие разделяемые данные были описаны как часть класса. Например, для управления задачами в операционной системе или в ее модели часто бывает полезен список всех задач:
class task { // ... task* next; static task* task_chain; void shedule(int); void wait(event); // ... };
Описание члена task_chain (цепочка задач) как static обеспечивает, что он будет всего лишь один, а не по одной копии на каждый объект task. Он все равно остается в области видимости класса task, и "извне" доступ к нему можно получить
task::task_chain
В функции члене на него можно ссылаться просто task_chain. Использование статических членов класса может заметно снизить потребность в глобальных переменных.
Можно брать адрес члена класса. Получение адреса функции члена часто бывает полезно, поскольку те цели и причины, которые приводились в #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 <stream.h> 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.prin Во многих случаях можно воспользоваться виртуальными функциями (см. Главу 7) там, где иначе пришлось бы использовать указатели на функции.
По определению 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 != 0 }
Кроме того, объединение, определенное так, как это, нельзя инициализировать. Например:
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.0 }
Когда это невозможно (для таких типов, как 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() не пройдет }
Если у класса есть конструктор, то он вызывается всегда, когда создается объект класса. Если у класса есть деструктор, то он вызывается всегда, когда объект класса уничтожается. Объекты могут создаваться как:
[1] Автоматический объект: создается каждый раз, когда его описание встречается при выполнении программы, и уничтожается каждый раз при выходе из блока, в котором оно появилось;
[2] Статический объект: создается один раз, при запуске программы, и уничтожается один раз, при ее завершении;
[3] Объект в свободной памяти: создается с помощью операции new и уничтожается с помощью операции delete;
[4] Объект член: как объект другого класса или как элемент вектора.
Объект также может быть построен с помощью явного применения конструктора в выражении (см. #6.4), в этом случае он является автоматическим объектом. В следующих подразделах предполагается, что объекты принадлежат классу, имеющему конструктор и деструктор. Примером может служить класс table из #5.3.
Если 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! Кроме того, по ум
Рассмотрим следующее:
table tbl1(100); void f() { static table tbl2(200); } main() { f(); }
Здесь конструктор table::table(), определенный в #5.3.1, будет вызываться дважды: один раз для tbl1 и один раз для tbl2. Деструктор table::~table() также будет вызван дважды: для уничтожения tbl1 и tbl2 после выхода из main(). Конструкторы для глобальных статических объектов в файле выполняются в том порядке, в котором встречаются описания; деструкторы вызываются в обратном порядке. Неопределено, вызывается ли конструктор для локального статического объекта, если функция, в которой этот объект описан, не вызывается. Если конструктор для локального статического объекта вызывается, то он вызывается после того, как вызваны конструкторы для лексически предшествующих ему глобальных статических объектов.
Параметры конструкторов для статических объектов должны быть константными выражениями:
void g(int a) { static table t(a); // ошибка }
Традиционно выполнением программы считалось выполнение main(). Так никогда не было, даже в C, но только размещение статических объектов класса с конструктором и/или деструктором дают программисту простой и очевидный способ задания того,
Вызов конструкторов и деструкторов для статических объектов играет в С++ чрезвычайно важную роль. Это способ обеспечить надлежащую инициализацию и очистку структур данных в библиотеках. Рассмотрим <stream.h>. Откуда берутся cin, cout и cerr? Где они получают инициализацию? И, что самое главное, поскольку потоки вывода имеют внутренние буферы символов, как же эти буферы заполняются? Простой и очевидный ответ таков, что эта работа осуществляется соответствующими конструкторами и деструкторами до и после выполнения main(). Для инициализации и очистки библиотечных средств есть возможности, альтернативные использованию конструкторов и деструкторов. Все они или очень специальные, или очень уродливые.
Если программа завершается с помощью функции exit(), то деструкторы для статических объектов будут вызваны, а если она завершается с помощью abort(), то не будут. Заметьте, что это подразумевает, что exit() не завершает программу мгновенно. Вызов exit() в деструкторе может привести к бесконечной рекурсии.
Иногда, когда вы разрабатываете библиотеку, необходимо или просто удобно создать тип с конструктором и деструктором, предназначенными только для одного: инициализировать и очистить. Такой тип обычно используется только с одной целью, для размещения статического объекта так, чтобы вызывались конструктор и деструктор.
Рассмотрим:
main() { table* p = new table(100); table* q = new table(200); delete p; delete p; // возможно, ошибка }
Конструктор table::table() будет вызван дважды, как и деструктор table::~table(). То, что С++ не дает никаких гарантий, что для объекта, созданного с помощью new, когда-либо будет вызван деструктор, ничего не значит. В предыдущей программе q не у
Пользователь может определить новую реализацию операций new и delete (см. #3.2.6). Можно также определить способ взаимодействия конструктора или деструктора с операциями new и delete (см. #5.5.6)
Рассмотрим
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 указывают на отдельные объекты, что требует для каждого из них действие по выделению памяти и ее освобождению. Кроме того, указатель плюс объек
Чтобы описать вектор объектов класса, имеющего конструктор, этот класс должен иметь конструктор, который может вызываться без списка параметров. Нельзя использовать даже параметры по умолчанию. Например:
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; }
Но почему же компилятор не может найти число элементов вектора из объема выделенной памяти? Потому, что распределитель свободной памяти не является частью языка и может быть задан программистом.
Когда вы используете много небольших объектов, размещаемых в свободной памяти, то вы можете обнаружить, что ваша программа тратит много времени выделяя и освобождая память под эти объекты. Первое решение - это обеспечить более хороший распределитель памяти общего назначения, второе для разработчика классов состоит в том, чтобы взять под контроль управление свободной памятью для объектов некоторого класса с помощью подходящих конструкторов и деструкторов.
Рассмотрим класс name, который использовался в примерах table. Его можно было бы определить так:
struct name { char* string; name* next; double value; name(char*, double, name*); ~name(); };
Программист может воспользоваться тем, что размещение и освобождение объектов заранее известного размера можно обрабатывать гораздо эффективнее (и по памяти, и по времени), чем с помощью общей реализации new и delete. Общая идея состоит в том, чт
const NALL = 128; name* nfree;
Распределитель, используемый операцией new, хранит размер объекта вместе с объектом, чтобы обеспечить правильную работу операции delete. С помощью распределителя, специализированного для типа, можно избежать этих накладных расходов. Напри
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]; qnext = p-1; (p+1)->next = 0; } this = p; // затем инициализировать string = s; value = v; next = n; }
Присвоение указателю this информирует компилятор о том, что программист взял себе управление, и что не надо использовать стандартный механизм распределения памяти. Конструктор name::name() обрабатывает только тот случай, когда name размещается по
Заметьте, что просто как
name* q = new name[NALL];
память выделять нельзя, поскольку это приведет к бесконечной рекурсии, когда new вызовет name::name().
Освобождение памяти обычно тривиально:
name::~name() { next = nfree; nfree = this; this = 0; }
Присваивание указателю this 0 в деструкторе обеспечивает, что стандартный распределитель памяти не используется.
Когда в конструкторе производится указателю 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, или он вызван объектом, выходящим из области видимости
Если тот, кто реализует класс, является одновременно и его единственным пользователем, то имеет смысл упростить класс, исходя из предположений о его использовании. Когда класс разрабатывается для более широкого использования, таких допущений, как правило, лучше избегать.
Когда пользователь берет управление распределением и освобождением памяти, он может конструировать объекты размеры, которых во время компиляции недетерминирован. В предыдущих примерах вмещающие (или контейнерные - перев.) классы 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 без всякого содействия со стороны программиста.
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 два раза: один раз используя в качестве представления связанный список узлов, а другой раз - символьную строку. Поэкспериментируйте с разными способами печати выражения: с полностью расставленными скобками, в постфиксной записи
#include <stream.h> main() { cout << "Hello, world\n"; }модифицируйте ее, чтобы получить выдачу
Initialize Hello, world Clean up
Не делайте никаких изменений в main().
Copyright © by Alex Skums