Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ООП (С++) Лекции Бобин.doc
Скачиваний:
57
Добавлен:
08.02.2015
Размер:
625.66 Кб
Скачать

Глава 8. Классы ресурсоемких объектов.

п.8.1. Понятие ресурсоемкого объекта

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

п.8.2. Автоматически создаваемые компоненты класса

При отсутствии указаний со стороны программиста компилятор для каждого класса автоматически создает:

  1. конструктор умолчаний;

  2. конструктор копирования;

  3. операторная функция присваивания;

  4. деструктор.

Автоматически создаваемый конструктор умолчаний последовательно вызывает конструктор умолчания для всех компонентов класса.

Это не всегда соответсвует требованиям к классам ресурсоемких объектов.

Пример:

class Smth

{

int *p;

...

};

...

Smth s;

s.p = ?

Автоматически создаваемый конструктор копирования последовательно вызывает конструкторы копирования для всех компонентов класса. При это копия внешнего ресурса не создается.

Пример:

Smth t = s;

У вновь созданного объекта указатель «смотрит» на тот же самый внешний ресурс. Автоматически создаваемый оператор присваивания последовательно вызывает операторы присваивания для всех компонентов класса.

Автоматически создаваемый деструктор последовательно вызывает деструкторы для всех компонентов класса.

п.8.3. Особенности классов ресурсоемких объектов

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

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

NULL

  1. Конструктор копирования.

Предположим, есть объект и нужно снять с него копию.

2.

1.

объект

копия

  1. Выделяем новый внешний ресурс.

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

  1. Оператор присваивания нашего класса должен работать по такому алгоритму:

Предположим, есть «левый» и «правый» объекты. Мы хотим левому объекту присвоить правый.

  1. Уничтожаем внешний ресурс «левого» объекта.

  2. Для «левого» объекта выделяем новый внешний ресурс, аналогичный ресурсу «правого».

  3. Копируем внешний ресурс из «правого» объекта в «левый».

Оператор присваивания должен в результате выдать «левый» объект.

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

Пример:

левый

правый

=

?

На первом пункте удалится внешний ресурс нашего объекта. А на третьем пункте программа упадет, так как внешний ресурс уже не доступен. Необходима проверка «внутри» оператора присваивания:

тип & тип::operator = (const тип & other)

{

if(this != other)

{

... // выполняются пункты 1-3

}

return *this;

}

  1. Деструктор класса ресурсоемкого объекта должен освобождать внешний ресурс

Деструктор должен обеспечивать возможность многократного вызова.

{

...

free (p);

p = NULL; // обеспечивает возможность многократного вызова.

...

}

Выводы:

  1. Автоматически создаваемые компоненты класса не всегда пригодны для работы с классами ресурсоемких объектов.

  2. При создании конструктора, деструктора и оператора присваивания нужно учитывать «особые моменты».

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

п.8.4. Умные слова

В языке С++ конструктор копирования, аналогичный рассмотренному в п.8.3, называется конструктором глубокого копирования(deepcopyconstructor). Оператор присваивания, рассмотренный в пункте 8.3, называетсяоператором копирующего присваивания.

п.8.5. Создание и уничтожение объектов в С++

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

  1. Выделение участка памяти для создаваемого объекта (в автоматической (стек), в статической (сегмент данных) или в динамической памяти).

  2. Инициализация выделенного участка конструктором.

Уничтожение объекта происходит также в 2 этапа:

  1. Освобождение принадлежащих объекту внешних ресурсов с помощью деструктора.

  2. Освобождение выделенного для объекта участка памяти.

При работе с объектами в автоматической и статической памяти всё, описанное выше, автоматически контролируется компилятором, а работа с динамической памятью – отдельный случай, так как компилятор не контролирует создание и удаление объектов.

Пример:

class Point

{

double x, y;

public:

Point();

void Print();

};

Point::Point()

{

x = 0.0;

y = 0.0;

}

Point::~Point()

{

}

void Point::Print()

{

cout << x << “ “ << y << endl;

}

int main()

{

Point p;

p.Print()

return 0;

}

В Си использовали функции malloc() иfree()для работы с динамической памятью. В С++ они непригодны для использования.

Пример:

class Point

{

double x;

double y;

public:

void Init();

void Destroy();

void Print();

};

void Print::Init()

{

x = 0.0;

y = 0.0;

}

void Print::Destroy()

{

}

void Point::Print()

{

cout << “Point = “ << x << “ “ << y << endl;

}

int main()

{

Point *p;

p = (Point *)malloc(sizeof(Point));

if (p == NULL)

return -1;

p->Init();

p->Print();

...

p->Destroy();

free(p);

return 0;

}

Функция malloc()не вызывает конструктор, следовательно, функцияInit()обязательна. Функцияfree()не вызывает деструктор, следовательно, функцияDestroy()обязательна.

Задача создания и удаления внешних ресурсов в языке С++ ложится на программиста.

В отличие от Си, в С++ средства управления динамической памятью встроены в сам язык. Поэтому их работа контролируется компилятором. Для выделения и освобождения памяти в С++ используются два оператора: newиdeleteсоответственно.

При использовании newсначала выделяется область динамической памяти, достаточно для хранения объекта. Затем для этой области автоматически выделяется конструктор.

Общий синтаксис вызова оператора new:

имя_класса * имя_указателя = new имя класса;

имя_класса * имя_указателя = new имя_класса(список_параметров);

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

delete имя_указателя;

Пример:

class Point

{

double x;

double y;

public:

Point();

~Point();

void Print();

};

Point::Point()

{

x = 0.0;

y = 0.0;

}

Point::~Point()

{

}

void Point::Print()

{

cout << “Point = “ << x << “ “ << y << endl;

}

int main()

{

Point *p;

p = new Point;

p->Print();

...

delete p;

return 0;

}

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

Пример:

class Object

{

char Name[25];

void *Data;

public:

Object(int Size, const char *NName);

~Object();

};

Object::Object(int Size, const char *NName)

{

Data = new char[Size];

strcpy(Name,NName);

cout << “Creating object ” << Name << “ with “ << Size << “ bytes “<< endl;

}

Object::~Object()

{

cout << “Deleting object “ << Name << endl;

}

int main()

{

Object *Typed = new Object(100, “Typed”);

delete Typed;

void *Untyped = new Object(100, “Untyped”);

delete Untyped; // Вызова деструктора не произойдет

return 0;

}

п.8.6. Динамические массивы

При создании динамического массива при помощи оператора newсначала выделяется память под весь массив. Затем для каждого элемента массива автоматически выделяется конструктор умолчания. В классе должен существовать открытый конструктор умолчания. Общий синтаксис:

имя_класса * имя_указателя = new имя_класса[количество_элементов];

Для уничтожения массива используется особый вариант оператора delete. Этот оператор вычисляет количество элементов в массиве и для каждого элемента вызывает деструктор.

delete [] имя_указателя;

Пример:

class Point

{

double x;

double y;

public:

Point();

~Point();

void Print();

};

Point::Point()

{

x = 0.0;

y = 0.0;

}

Point::~Point()

{

}

void Point::Print()

{

cout << “Point = “ << x << “ “ << y << endl;

}

int main()

{

Point * const arr = new Point[100]; // Создание массива

delete [] arr; // Уничтожение массива

return 0;

}

п.8.7. Перегрузка операторов new и delete

При вызове оператора newсначала вызывается функцияoperator new, которая выделяет память, а затем конструктор класса. При вызове оператораdeleteсначала вызывается деструктор класса, а затем функцияoperatordelete. Вызовы конструкторов и деструкторов не подвластны программисту, однако есть возможность указать свои функцииoperator newиoperator delete.

п.8.7.1. Перегрузка глобальных операторов

Если стандартные версии операторов newиdeleteне подходят в рамках данной конкретной программы, то используется их глобальная перегрузка. В этом случае стандартные версии операторов становятся полностью недоступными, даже внутри перегруженных.

Синтаксис:

void * operator new(unsigned);

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

Синтаксис перегрузки оператора delete:

void operator delete(void *);

Параметр – адрес участка памяти, который выделяется с помощью оператора new.

Пример:

void * operator new(unsigned Size)

{

cout << “Allocating ” << Size << “ bytes “ << endl;

void *Result = malloc(Size);

if(Result == NULL)

cout << “Failed” << endl;

else

cout << “OK” << endl;

return Result;

}

void operator delete(void *Address)

{

cout << “Freeing memory” << endl;

free(Address);

}

Использование перегруженных операторов:

class Point

{

double x;

double y;

public:

Point();

~Point();

};

Point::Point()

{

x = 0.0;

y = 0.0;

}

Point::~Point()

{

}

int main()

{

int *pint = new int [5];

Point *pPoint = new Point [5];

Point pPointi = newPoint;

delete [] pPoint;

delete [] pint;

delete pPointi;

return 0;

}

Важное замечание: Перегружать надо либо оба оператора –newиdelete, либо ни одного.

п.8.7.2. Перегрузка операторов для классов

Операторы newиdeleteможно перегружать только в рамках одного класса как его компонентные функции. В этом случае при создании и уничтожении объектов данного класса будут использоваться локальные версии операторов, а при работе с остальными – глобальные. Локальные версии этих операторов перегружаются как статически функции класса, хотя ключевое словоstaticможно не указывать.

Пример:

class Point

{

double x;

double y;

public:

Point();

~Point();

void * operator new(unsigned size);

void operator delete(void * Address);

};

int TotalNew = 0;

int TotalDelete = 0;

Point::Point()

{

x = 0.0;

y = 0.0;

}

Point::~Point()

{

}

void *Point operator new(unsigned size)

{

void * Result = malloc(size);

if(Result == NULL)

return NULL;

TotalNew++;

return Result;

}

void Point::operator delete(void * Address)

{

free(Address);

if(Address != NULL)

TotalDelete++;

}

int main()

{

int *pint = new int;

Point *pPoint = new Point;

Point *pPointi = new Point;

delete pint;

delete pPoint;

delete pPointi;

cout << “TotalNew: “ << TotalNew << endl << “TotalDelete: “ << TotalDelete;

return 0;

}

п.8.7.3. Перегрузка операторов для массива

Аналогично можно перегружать операторы new[] иdelete[]. Синтаксис:

void * operator new [] (unsigned);

void operator delete [] (void *);

Глава 9. Наследование классов в С++

п.9.1. Техника повторного использования программного кода

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

п.9.1.1. Композиция

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

неотъемлемая часть

целое

окружность не может существовать без центра

Point

Circle

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

часть

целое

Часть не является неотъемлемой для целого.

Пример:

class Point

{

double x, y;

public:

Point(double x = 0.0, double y = 0.0);

Point(const Point &p);

void Print() const;

}

class Circle

{

Point Centre;

double Radius;

public:

Circle(double x = 0.0, double y = 0.0, double r = 1.0);

Circle(const double &c);

void Print() const;

};

Point::Point(double x, double y):x(x),y(y)

{

}

Point::Point(const Point &p):x(p.x),y(p.y)

{

}

Circle::Circle(double x, double y, double r):Centre(Point(x,y)),Radius(r)

{

}

Circle::Circle(const Circle &c):Centre(c.Centre),Radius(c.Radius)

{

}

void Circle::Print() const

{

cout << “Centre in ”;

Point Print();

cout << “Radius is ” << Radius << endl;

}

int main()

{

Circle C1(1.0, 1.0, 3.0);

Circle C2;

C1.Print();

C2.Print();

return 0;

}

Пример (агрегация):

class Person

{

char Name[100];

int Age;

public:

Person(const char *Name, int Age);

Person(const Person &other);

void Print() const;

};

class Book

{

char Title[100];

const Person * pAuthor;

public:

Book(const char *Title, const Person &Author);

Book(const Book &other);

void Print() const;

};

Person::Person(const char *Name, int Age): Age(Age)

{

if(Name == NULL)

this->Name[0] = ‘\0’;

else

strcpy(this->Name, Name);

}

Person::Person(const Person &other): Age(other.Age)

{

strcpy(this->Name, other.name);

}

void Person::Print()

{

cout << “Person ” << Name << “is ” << Age << “ years old.”<< endl;

}

Book::Book(const char *Title, const Person &Author)

{

if(Title == NULL)

this->Title[0] = ‘\0’;

else

strcpy(this->Title, Title);

pAuthor = &Author;

}

Book::Book(const Book &other):pAuthor(other.pAuthor)

{

strcpy(this->Title, other.Title);

}

void Book::Print() const

{

cout << “Book titled ” << Title << endl;

cout << “Aurhor info:” << endl;

pAuthor->Print();

}

int main()

{

Person Somebody(“Ivanov I. I.”, 37);

Book B1(“Helloworld”, Somebody);

Book B2(“Byebyeworld”, Somebody);

B1.Print();

B2.Print();

return 0;

}

п.9.1.2 Наследование

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

Синтаксис:

ключ_класса имя_производного_класса: спецификатор_доступа имя_базового_класса

В качестве спецификатора доступа могут выступать слова private,protected,public. В качестве ключа класса используются ключевые слова либоclassлибоstruct.Unionв наследованиинеучаствует! Ключи у базового и производного класса должны совпадать: классы от классов и структуры от структур.

Пример:

class Point

{

double x,y;

public:

Point & operator = (const Point &);

};

class Circle: Point

{

double r;

public:

Circle &operator = (const Circle &c);

};

Point & Point::operator = (const Point &p)

{

if(this != &p)

{

x = p.x;

y = p.y;

}

return *this;

}

Circle & Circle::operator = (const Circle &c, const Point &p)

{

Point::operator = (p); // Присваивание компонентов базового класса

if(this != &c)

r = c.r; // Присваивание компонентов производного класса

return *this;

}

Базовый класс, находящийся в составе производного выступает как его компонент. Соответственно, на него распространяется механизм ограничения доступа к компонентам класса. Спецификатор доступа позволяет ограничить доступ к компонентам базового класса, которые наследуются производным классом. Если спецификатор доступа отсутствует, то при наследовании классов предполагается private. А при наследовании структурpublic.

Виды наследования: открытое, защищённое, закрытое. Вид наследования определяется спецификатором доступа.

п.9.1.3. Открытое наследование

Открытое наследование задается спецификатором доступа public. При открытом наследовании все открытые и защищённые компоненты базового класса становятся соответственно открытыми и защищёнными компонентами производного класса. Закрытые компоненты базового класса становятся недоступными в производном класса.

class Base

{

// Компоненты базового класса

};

class Derived: public Base

{

// Компоненты производного класса

};

п.9.1.4. Защищенное наследование

Защищенное наследование задается спецификатором доступа protected. При защищенном наследовании открытые и защищенные компоненты базового класса становятся защищенными компонентами производного класса. Закрытые компоненты базового класса становятся недоступными.

class Base

{

// Компоненты базового класса

};

class Derived: protected Base

{

// Компоненты производного класса

};

п.9.1.5. Закрытое наследование

Закрытое наследование задается спецификатором доступа private. При закрытом наследовании все открытые и защищенные компоненты базового класса становятся закрытыми компонентами производного класса. Все закрытые компоненты базового класса становятся недоступными.

class Base

{

// Компоненты базового класса

};

class Derived: private Base

{

// Компоненты производного класса

};

Спецификаторы доступа в теле класса: все компоненты класса, которые находятся в теле "private", доступны только в теле этого класса. Компоненты, расположенные в секции "protected" доступны в теле класса и его наследниках. Компоненты, расположенные в секции “public” – в любом месте программы. При этом компоненты базового класса всегда становятся компонентам производного класса.

п.9.2. Смягчение ограничений доступа на наследование

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

уточн.имя:имя_базового_класса::имя_компонента;

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

Пример:

// Базовый класс:

class Base

{

...// компоненты базового класса

public:

void Func(void);

};

// Производный класс:

{

...// компоненты производного класса

public:

using Base::Func;

};

При любом виде наследования, закрытые компоненты базового класса всегда становятся недоступными.

п.9.3. Создание и уничтожение объектов производных классов

Создание объекта производного класса осуществляется в 3 этапа:

  1. Сначала выделяется память для всего класса. Поскольку базовый класс является компонентом производного класса, то требуемая для него память в нем присутствует.

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

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

базовый класс

производный класс

Уничтожение объекта также производится в 3 этапа:

  1. Вызывается деструктор производного класса.

  2. Вызывается деструктор базового класса.

  3. Освобождается память.

п.9.4. Правила подстановки и повышающее приведение типа

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

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

Пример:

Derived d; // объект производного класса

Base &b = d; // ссылка на объект базового класса

b

d

Base *b = new Derived;

...

delete b;

При выполнении оператора deleteвызовется деструктор базового класса, так как это указатель на классBase(базовый). После этого освободится область памяти. Однако деструктор производного класса не вызовется.

Опасность: Если в производном классе выделены какие-то внешние ресурсы, то они не будут удалены.

Если речь идет о самом объекте, то объект усекается до объекта базового класса (его интерфейс усекается):

Derived d;

Base b = d;

п.9.5. Список инициализации конструктора

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

п.9.6. Скрытие имен и переопределение компонентов класса

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

Пример:

struct Top

{

int x;

void func();

{

}

};

struct Right:Top

{

int z;

};

...

struct Bottom: Left, Right

{

};

int main()

{

Bottom b;

b.func(); // Вызов функции func() и обращение к переменной x некорректны,

b.x = 5; // поскольку компилятор не знает, в каком базовом классе

// их искать (Left или Right)

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

b.Left::func(); // правильный вызов.

b.Right::x = 5; // правильное обращение.

Top &t = b; // компилятор не знает, какой из двух подобъектов типа

// Top нужно использовать при повышающем приведении типа

return 0;

}

п.9.7. Наследование и перегрузка операторов

Перегруженный оператор – это компонентная функция.

Компонентные функции – операторы базового класса, как и любые другие компоненты.

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

Пример(использования перекрытия компонентов):

struct Base

{

double Element1;

void Element2(double);

static void Element3(void);

};

struct Derived:Base

{

static double Element1(void); // 1)

static double Element2; // 2)

void Element3(double); // 3)

};

  1. Статическая функция закрывает не статическую переменную.

  2. Статическая переменная закрывает не статическую функцию.

  3. Не статическая функция закрывает статическую.

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

п.9.8. Наследование и автоматически создаваемые методы класса

Если в производном классе программист явно не определил конструкторы умолчания и копирования, деструктор или оператор присваивания, то компилятор сделает это автоматически.

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

Пример:

class Base

{

...

public:

Base();

Base(const Base &);

Base & operator = (const Base &);

~Base();

};

class Derived: public Base

{

public:

Base();

Base(const Base &);

Base & operator = (const Base &);

~Base();

};

Derived::Derived(): Base()

{

...

}

Derived::Derived(const Derived & d): Base(d)

{

...

}

...

п.9.9. Наследование и статические компоненты класса

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

Спецификатор при наследовании

public

protected

private

Спецификатор в базовом классе

public

public

protected

private

protected

protected

protected

private

private

п.9.10 Множественное наследование

Figure2d

Circle

Material

MaterialCircle

Множественное наследование – способ порождения нового класса от нескольких базовых. При множественном наследовании производный класс наследует все компоненты всех своих базовых.

ключ_класса имя_класса:

спецификатор1 имя_базового_класса1,

спецификатор2 имя_базового_класса2,

...

{

...

};

class MaterialCircle: public Circle, public Material

{

...

};

Спецификатор доступа нужно указывать у каждого базового класса.

Множественно наследование может быть причиной ряда серьезных проблем.

п.9.11. Виртуальные базовые классы

x

A

C

B

D

B::x

C::x

Класс содержит в своем составе два экземпляра поля «x», наследованного от одного и того же класса А двумя разыми способами. По своей физической сути эти поля одинаковы. Их следовало бы заменить одним полем. Для предотвращения таких ситуаций используются виртуальные базовые классы.

Виртуальность базовому классу обеспечивается путем добавления ключевого слова virtualв списке наследования:

class B: virtual public A

{

...

};

Виртуальный базовый класс, встретившийся на каком-то уровне иерархии наследования всегда ведет себя как непосредственный базовый для самого последнего производного.

A

C

B

D

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

п.9.11.1 Реализация технологии виртуального базового класса

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

A

объект класса В

подобъект класса А

В

(вирт. баз.)

указатель на подобъект класса А

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

п.9.12. Наследование и друзья классов

Дружба не наследуется.

Глава 10. Виртуальные функции в С++

п.10.1. Недостаток повышающего приведения типа

Animal

voice()

Dog

voice()

Cat

voice()

class Animal

{

public:

void voice() const

{

cout << “Mmm... “ << endl;

}

};

class Dog: public Animal

{

public voice() const // 2 метода voice() содержатся

{ // в классе Dog

cout << “Гав! “ << endl;

}

};

class Cat: public Animal

{

public voice() const

{

cout << “Мяу! “ << endl;

}

};

void PullTheTail(const Animal &a)

{

a.voice();

}

int main()

{

Dog Sharik;

Cat Matroskin;

PullTheTail(Sharik);

PullTheTail(Matroskin);

return 0;

}

Ожидается реакция: Сначала фраза «Гав!», потом «Мяу!». На самом деле обе фразу будут «Mmm...». ФункцияPullTheTail()принимает на вход ссылку на объект базового класса, следовательно, при ее вызове объектSharikклассаDogбудет «преобразован» к классуAnimal. Поэтому при вызове этой функции будет вызван методvoice()для базового класса.

Замечание: Если производные классы получены в результате защищенного или закрытого наследования, то функцияPullTheTail()не будет компилироваться.

Причина такого результата:

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

п.10.2. Раннее и позднее связывание вызовов функций

Связыванием(linking) называется сопоставление вызовов функции с ее кодом. Связывание, которое выполняется в процессе трансляции программы компилятором или компоновщиком, называетсяраннимилистатическим.

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

При генерации машинного кода для вызова метода voice()переменнойa(a.voice()) в теле функцииPullTheTail()компилятор видит, что с переменнойaсвязан тип «Animal». Поэтому у компилятора нет другого выхода, кроме как генерировать вызов методаvoice()для классаAnimal(информация о реальном типе объекта утеряна в ходе повышающего приведения типа; реальный тип объекта будет известен только во время выполнения программы).

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

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

п.10.3 Виртуальные функции

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

class Animal

{

public:

virtual void voice() const

{

cout << “Mmm...” << endl;

}

};

class Dog: public Animal

{

public:

virtual void voice() const

{

cout << “Гав!” << endl;

}

};

class Cat: public Animal

{

public:

virtual void voice() const

{

cout << “Мяу!” << endl;

}

};

Результат: При вызове функцииPullTheTail()применительно к объекту «Sharik» будет произведено повышающее приведение типа ссылки на этот объект до ссылки на объект базового класса. Таким образом, внутри функцииPullTheTail()переданная на вход ссылка будет рассматриваться как ссылка на объект классаAnimal, однако, благодаря технологии позднего связывания, во время выполнения этой функции будет проанализирован реальный тип объекта, на который смотрит ссылка-параметр. В результате анализа будет установлено, что ссылка смотрит на объект классаDog. Поэтому в теле функцииPullTheTail()будет вызыван методvoice()классаDog.

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

п.10.4 Реализация технологии позднего связывания

void PullTheTail(const Animal &a)

{

a.voice();

}

...

Dog Sharik;

PullTheTail(Sharik);

Заявленный тип часто называют статическим, а фактический – динамическим.

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

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

Animal

Dog

Cat

ТВФ Animal

Адрес

Sharik

vptr

Matroskin

vptr

адр. Animal::voice()

ТВФ Dog

Адрес

адр. Dog::voice()

ТВФ Cat

Адрес

адр. Cat::voice()

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

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

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

Становится очевидным, что в С++ компилятор может вставлять в программу скрытый код (и скрытые поля), который оказался недоступным для прграммиста.

class Animal

{

public:

virtual void voice();

};

class Dog: public Animal

{

public:

virtual void voice();

virtual void scare();

};

class Bulldog: public Dog

{

public:

virtual scare();

{

cout << “Откусилногу!” << endl;

}

};

class Terrier: public Dog

{

public:

virtual void scare()

{

cout << “Убежал в кусты” << endl;

}

};

int main()

{

Bulldog Vasja;

Terrier Asja;

Dog *dogs[2];

dogs[0] = &Vasja;

dogs[1] = &Asja;

dogs[0]->scare();

dogs[1]->scare();

return 0;

}

ТВФ Animal

Адрес

адр. Animal::voice()

ТВФ Bulldog

Адрес

адр. Dog::voice()

ТВФ Terrier

Адрес

адр. Dog::voice()

адр. Terrier::scare()

адр. Bulldog::scare()

ТВФ Dog

Адрес

адр. Dog::voice()

адр. Dog::scare()

Vasja

vptr

Asja

vptr

п.10.5. Чисто виртуальные функции и абстрактные классы.

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

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

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

Выводы:

  1. Поскольку в таблице виртуальных функций есть пустые ячейки, создавать объекты абстрактных классов запрещено.

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

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

Назначение чисто виртуальной функции – формирование требований к интерфейсу производных классов. Для создания чисто виртуальной функции необходимо объявитьее в теле класса как виртуальную с дописыванием конструкции «= 0».

Пример:

class Animal

{

public:

virtual void voice() = 0;

};

class Dog: public Animal

{

public:

virtual void voice();

};

void Dog::voice()

{

cout << “Гав!” << endl;

}

ТВФ Animal

Адрес

NULL

ТВФ Dog

Адрес

Dog::voice()

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

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

Множественное наследовании от интерфейсных классов не вызывает тех проблем, которые обсуждались в разделе «множественное наследование».

п.10.6. Виртуальные функции и конструкторы

Чтобы была возможность пользоваться виртуальными функциями, в полностью созданном объекте полиморфного класса должен быть правильно настроенный указатель на ТВФ (vptr). В С++ создание объекта производится в два этапа: выделение памяти и инициализация. Следовательно, настройка указателя должна производиться в конструкторе. При трансляции программы компилятор скрытно добавляет специальный код инициализации указателя на ТВФ. Любая виртуальная функция, вызванная из теля конструктора, ведет себя как не виртуальная.

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

п.10.7. Виртуальные функции и деструктор

Деструктор можно сделать виртуальным

Пример(будет работать неправильно):

class Dog: public Animal

{

char *name;

public:

Dog()

{

name = new char [100];

...

}

~Dog()

{

delete[] name;

name = NULL;

}

};

Animal *a = new Dog;

...

delete a;

Вызывается деструктор для а(указатель наAnimal(), который не осуществляет освобождение внешних ресурсов, выделенных в производном классе). Результат – утечка памяти. Решение: вызвать деструктор для производного класса (то есть нужно создать виртуальный деструктор в базовом классе):

virtual ~Animal()

{

}

virtual ~Dog()

{

delete[] name;

name = NULL;

}

Будем всегда стараться делать все деструкторы виртуальными. Деструкторы могут быть чисто виртуальными. Но для них всегда нужно представить тело.

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