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

c++book1

.pdf
Скачиваний:
12
Добавлен:
17.02.2016
Размер:
2 Мб
Скачать
☆

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

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; // явное преобразование

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

191

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

Будь 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 (* ... *);

// не С++:

192

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

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) (*

// ...

*)

193

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

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

7.2.7 Поля Типа

Чтобы использовать производные классы не просто как удобную сокращенную запись в описаниях, надо разрешить следующую проблему: Если задан указатель типа base*, какому производному типу в действительности принадлежит указываемый объект? Есть три основных способа решения этой проблемы:

[1]Обеспечить, чтобы всегда указывались только объекты одного типа (#7.3.3),

[2]Поместить в базовый класс поле типа, которое смогут просматривать функции и

[3]Использовать виртуальные функции (#7.2.8).

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

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

enum empl_type (* M, E *);

struct employee (* empl_type type; employee* next; char* name; short department; // ...

*);

struct manager : employee (* employee* group; short level; // уровень

*);

Имея это, мы можем теперь написать функцию, которая печатает информацию о каждом служащем:

void print_employee(employee* e) (*

switch (e->type) (* case E:

cout << e->name << "\t" << e->department << "\n";

194

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

// ...

break; case M:

cout << e->name << "\t" << e->department << "\n"; // ...

manager* p = (manager*)e;

cout << " уровень " << p->level << "\n";

// ...

break;

*)

*)

и воспользоваться ею для того, чтобы напечатать список служащих:

void f() (*

for (; ll; ll=ll->next) print_employee(ll);

*)

Это прекрасно работает,особенно в небольшой программе, написанной одним человеком, но имеет тот коренной недостаток, что неконтролируемым компилятором образом зависит от того, как программист работает с типами. В больших программах это обычно приводит к ошибкам двух видов. Первый - это невыполнение проверки поля типа, второй - когда не все случаи case помещаются в переключатель switch, как в предыдущем примере. Оба избежать достаточно легко , когда программу сначала пишут на бумаге, но при модификации нетривиальной программы, особенно написанной другим человеком, очень трудно избежать как того, так и другого. Часто от этих сложностей становится труднее уберечься из-за того, что функции вроде print() часто бывают организованы так, чтобы пользоваться общностью классов, с которыми они работают. Например:

void print_employee(employee* e) (*

cout << e->name << "\t" << e->department << "\n"; // ...

if (e->type == M) (*

manager* p = (manager*)e;

cout << " уровень " << p->level << "\n";

// ...

*)

*)

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

7.2.8 Виртуальные Функции

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

195

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

правильное соответствие между объектами и применяемыми к ним функциями. Например:

struct employee (* employee* next; char* name; short department; // ...

virtual void print();

*);

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

void employee::print() (*

cout << e->name << "\t" << e->department << "\n"; // ...

*)

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

struct manager : employee (* employee* group; short level;

// ...

void print();

*);

void manager::print() (*

employee::print();

cout << "\tуровень" << level << "\n"; // ...

*)

Функция print_employee() теперь не нужна, поскольку ее место заняли функции члены print(), и теперь со списком служащих можно работать так:

void f(employee* ll) (*

for (; ll; ll=ll->next) ll->print();

*)

Каждый служащий будет печататься в соответствии с его типом. Например:

196

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

main()

(*

employee e;

e.name = "Дж.Браун"; e.department = 1234; e.next = 0;

manager m;

m.name = "Дж.Смит"; e.department = 1234; m.level = 2;

m.next = &e; f(&m);

*)

выдаст

Дж.Смит 1234 уровень 2 Дж.Браун 1234

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

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

7.3 Альтернативные Интерфейсы

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

197

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

7.3.1 Интерфейс

Рассмотрим такое написание класса slist для однократно связанного списка, с помощью которого можно создавать как однородные, так и неоднородные списки объектов тех типов, которые еще должны быть определены. Сначала мы определим тип ent:

typedef void* ent;

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

class slink (* friend class slist;

friend class slist_iterator; slink* next;

ent e;

slink(ent a, slink* p) (* e=a; next=p;*)

*);

В одном звене может храниться один ent, и с помощью него реализуется класс

slist:

class slist (*

friend class slist_iterator;

slink* last; // last->next - голова списка public:

int insert(ent a); // добавить в голову списка int append(ent a); // добавить в хвост списка ent get(); // вернуться и убрать голову списка void clear(); // убрать все звенья

slist() (* last=0; *)

slist(ent a) (* last=new slink(a,0); last->next=last; *) ~slist() (* clear(); *)

*);

Хотя список очевидным образом реализуется как связанный список, реализацию можно изменить так, чтобы использовался вектор из ent'ов, не повлияв при этом на пользователей. То есть, применение slink'ов никак не видно в описаниях открытых функций slist'ов, а видно только в закрытой части и определениях функций.

7.3.2 Реализация

Реализующие slist функции в основном просты. Единственная настоящая сложность - что делать в случае ошибки, если, например, пользователь попытается get() что-нибудь из пустого списка. Мы обсудим это в #7.3.4. Здесь приводятся определения членов slist. Обратите внимание, как хранение указателя на последний элемент кругового списка дает возможность просто реализовать оба действия append() и insert():

int slist::insert(ent a) (*

if (last)

last->next = new slink(a,last->next); else (*

198

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

last = new slink(a,0); last->next = last;

*) return 0;

*)

int slist::append(ent a) (*

if (last)

last = last->next = new slink(a,last->next); else (*

last = new slink(a,0); last->next = last;

*) return 0;

*)

ent slist::get() (*

if (last == 0) slist_handler("get fromempty list");

// взять из пустого списка slink* f = last->next;

ent r f->e; if (f == last)

last = 0;

else

last->next = f->next; delete f;

return f;

*)

Обратите внимание, как вызывается slist_handler (его описание можно найти в #7.3.4). Этот указатель на имя функции используется точно так же, как если бы он был именем функции. Это является краткой формой более явной записи вызова:

(*slist_handler)("get fromempty list");

И slist::clear(), наконец, удаляет из списка все элементы:

void slist::clear() (*

slink* l = last;

if (l == 0) return; do (*

slink* ll = l; l = l->next; delete ll;

*) while (l!=last);

*)

Класс slist не обеспечивает способа заглянуть в список, но только средства для вставления и удаления элементов. Однако оба класса, и slist, и slink, описывают класс slist_iterator как друга, поэтому мы можем описать подходящий итератор. Вот один, написанный в духе #6.8:

199

Учебник по C++. Скачан с сайта http://bizkit.land.ru . Самая полная коллекция в Сети.

class slist_iterator (* slink* ce; slist* cs;

public:

slist_iterator(slist& s) (* cs = &s; ce = cs->last; *)

ent operator()() (*

//для индикации конца итерации возвращает 0

//для всех типов не идеален, хорош для указателей ent ret = ce ? (ce=ce->next)->e : 0;

if (ce == cs->last) ce= 0; return ret;

*)

*);

7.3.3 Как Этим Пользоваться

Фактически класс slist в написанном виде бесполезен. В конечном счете, зачем можно использовать список указателей void*? Штука в том, чтобы вывести класс из slist и получить список тех объектов, которые представляют интерес в конкретной программе. Представим компилятор языка вроде С++. В нем широко будут использоваться списки имен; имя name - это нечто вроде

struct name (* char* string; // ...

*);

В список будут помещаться указатели на имена, а не сами объекты имена. Это позволяет использовать небольшое информационное поле e slist'а, и дает возможность имени находиться одновременно более чем в одном списке. Вот определение класса nlist, который очень просто выводится из класса slist:

#include "slist.h" #include "name.h"

struct nlist : slist (*

void insert(name* a) (* slist::insert(a); *) void append(name* a) (* slist::append(a); *) name* get() (**)

nlist(name* a) : (a) (**)

*);

Функции нового класса или наследуются от slist непосредственно, или ничего не делают кроме преобразования типа. Класс nlist - это ничто иное, как альтернативный интерфейс класса slist. Так как на самом деле тип ent есть void*, нет необходимости явно преобразовывать указатели name*, которые используются в качестве фактических параметров (#2.3.4).

Списки имен можно использовать в классе, который представляет определение класса:

200

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]