Министерство образования и науки Республики Казахстан

1 наследование и композиция

1.1 Статические члены класса

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

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

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

1.2 Формы наследования

1.2.1 Одиночное наследование

Порожденные класс

Для порождения нового класса от существующего может использоваться следующая форма записи:

class имя класса : (public | protected | private) имя базового класса {

объявления членов

};

Одна из особенностей порожденного класса - видимость унаследованных членов. Для определения доступности членов основного класса порожденному классу используются ключевые слова public, protected и private. Рассмотрим пример:

class student {

protected:

int id;

char name[30];

public:

sudent(int i,char *nm);

void print();

};

class grad_student : public student {

protected:

char thesis[30];

public:

grad_student(int i,char *nm, char *t);

void print();

};

В этом примере grad_student - порожденный класс, а student - базовый класс. Использование ключевого слова public в заголовке описания порожденного класса означает, что protected и public члены класса student должны быть унаследованы как protected и public члены grad-student соответственно. Члены private недоступны. Общее наследование также означает, что полученный класс grad-student - подтип класса student. Следовательно, студент окончивший институт - студент.

Порожденный класс представляет собой модификацию основного класса, которая наследует общие и защищенные члены базового класса. Таким образом, в примере grad_student члены student - id,name, print унаследованы. Функция-член print перекрывается. Это означает, что порожденный класс имеет реализацию функции-члена, отличающуюся от базового класса. Не следует путать с перегрузкой, где то же самое имя функции может иметь различные значения для каждой уникальной сигнатуры.

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

Код используется многократно. Тип grad_student использует существующий проверенный код из student.

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

1.2.2 Типизированные преобразования и видимость

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

Рассмотрим пример конструктора базового и порожденного класса:

student::student(int i,char *nm):id(i) { name=new char [strlen(nm)+1]; strcpy(name,nm); }

grad_student::grad_student(int i,char *nm,char *t): student(i,nm) {

thesis=new char[strlen(t)+1]; strcpy(thesis,t); }

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

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

grad_student gs(1,”NNN”,”Tn”);

student &rs=&gs;

Переменная rs - ссылка на student. Базовый класс для grad_student - student. Следовательно, такое преобразование ссылки допустимо.

Функция-член print - перегружена.

void student::print() { cout << id << name; }

void grad_student::print() { student::print(); cout << thesis; }

Для того, чтобы grad_student::print вызвала функцию student::print, должен быть использован идентификатор с разрешенной областью student::print. Иначе произойдет бесконечный цикл. Рассмотрим пример отношения между базовым и порожденным классом.

void main(void) {

student s(1,”Ma”), *ps=&s;

gard_student gs(2,”Jh”,”Pet”), *pgs;

ps->print(); // student::print()

ps=pgs=&gs;

ps->print(); // student::print()

pgs->print(); // gard_student::print()

}

Правило заключается в том, что указатель на порожденный класс может быть неявно преобразован в указатель на базовый класс. Переменная ps может указывать на объекты обоих классов, но переменная указатель pgs может указывать только на объекты типа grad_student.

1.2.3 Множественное наследование

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

Для описания иерархии множественного наследования можно использовать прямой ациклический граф (ПАГ) (Ориентированный граф наследования без петель). Подкласс может унаследовать протокол одного или более родительских классов. При этом помимо спецификаторов public и private-производных классов, используется дополнительная опция virtual.

Первый пример

Класс Derived является производным от обоих базовых классов Base1 и Base2.

class Base1 {

int id;

public:

Base1(void) { cout << “ Constructor Base1”; id=0; }

Base1(int anid) { cout << “ Constructor Base1”; id=anid; }

void assignid(int anid) { id=anid; }

int accessid(void) { return id; }

};

class Base2 {

char name[20];

public:

Base2(void) { cout << “ Constructor Base2”; strcpy(name,”void”); }

Base2(char *str) { cout << “ Constructor Base2”; strcpy(name,str); }

void assignName(char *str) { strcpy(name,str); }

char *accessName(void) { return name; }

};

class Derived: public Base1,public Base2 {

char ch;

public:

Derived(void) { cout << “ Construct Derived”; ch=‘a’; }

Derived(char c,int anid,char *str):Base1(anid),Base2(str) {

cout << “ Construct Derrived”; ch=c; }

void assign(char c) { ch=c; }

friend ostream& operator<<(osream& o,Derived& d);

};

ostream& operator<<(ostream&o,Derived& d) {

o<<“id”<<d.accessid()<<“name”<<d.accessName() <<“ ch”<<c; return o;

}

main() {

Derived object1; cout << object;

Derived object2(‘e’,26,”Robert:); cout << object2;

}

Результаты работы программы

Constructor Base1 Constructor Base2 Construct Derived id 0 name void ch a

Constructor Base1 Constructor Base2 Construct Derived id 26 name Robert ch e

Когда object1 объявлен при помощи выражения Derived object1, вызываются void-конструкторы двух базовых классов в той последовательности, в которой определено наследование ( Base1-конструктор первый, Base2-конструктор второй).

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

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

class d : public a,public b, public c { };

d D1;

pd = &D1; // #define db sizeof(a)

pa = pd; // #define dc sizeof(a)+sizeof(b)

pb = pd; // pb = (char*)pd + db

pc = pd; // pc = (char*)pd + dc

Преобразование ссылки на объект производного класса к ссылке на объект базового класса требует добавления к указателю текущего объекта this соответствующего смещения (db,dc), обратное преобразование - вычитание этого же смещения. Такое действие выполняется компилятором, когда в объекте производного класса наследуется функция из второго и т.д. базового класса, например при определении в классе "b" функции "f()" и ее наследовании в классе "d" вызов D1.f() будет реализован следующим образом:

this = &D1; // Адрес объекта производного класса

this = (char*)this + db // Адрес объекта класса b в нем

b::f(this); // Вызов функции в классе b со своим объектом

Рассмотрим особенности механизма виртуальных функций при множественном наследовании. Во-первых, на каждый базовый класс в производном классе создается своя таблица виртуальных функций (в нашем случае - для "a" в "d", для "b" в "d" и для "c" в "d").

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

class a {

public: virtual void f();

virtual void g();

};

class b {

public: virtual void h();

virtual void t();

};

class c : public a, public b { // f(),t() наследуются

public: void g(); // g() переопределяется

void h(); // h() переопределяется

}

a A1;

b B1;

c C1;

pa = &A1;

pb = &B1;

pa->f(); // Вызов a::f()

pb->h(); // Вызов b::h()

pa = &C1;

pb = &C1;

pa->f(); // Вызов a::f()

pa->g(); // Вызов c::g()

pb->h(); // Вызов c::h()

pb->t(); // Вызов b::t()