Лекция 1

Правила оформления программ на Си/Си++.
Операции и основные типы данных.

Лекция 2

Операторы


Лекция 3

Использование библиотечных процедур СИ

 

Лекция 10

Директивы препроцессора

Лекция 11

Видимость и время жизни переменных

Лекция 12

Дополнительные возможности Си++


Лекция 13

Объектно-ориентированное программирование

 

главная

Лекция

Лекция №13

Тема: ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ



Содержание:

Объектно-ориентированное программирование

Инкапсуляция

Классы и объекты

Друзья класса

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

Конструкторы и деструкторы

Указатель this

Наследование

Доступность членов в иерархии классов

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

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

Контрольные вопросы


Объектно-ориентированное программирование

Инкапсуляция

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

класс = данные + функции работы с ними

Класс - это абстрактный тип данных, созданный специально программистом. Класс содержит члены-данные (data members) и члены-функции (member functions).
Слияние данных с функциями называется инкапсуляцией.
Синтаксис описания класса похож на синтаксис описания структур и объединений в стандартном языке Cи. Класс описывает некоторые объекты (object). Члены-данные объекта описывают его состояние (статику), члены-функции - поведение объекта (динамику).
Если обращение к функции-члену рассматривать как запрос объекту совершить некоторое действие, то набор функций-членов определяет набор запросов, на которые объекты данного класса будут откликаться соответствующим образом.
Для описания класса используются ключевые слова class, struct, union.
Например:
class A_Class {
рrivate:
int x; double y, z;
void f1(); int f2(int);
рublic:
char ch, ch1;
int f3(int, int); int GetX(); {return x;} }

Рrivate: и рublic: - это метки, которые определяют режим доступа к членам класса. Метки могут использоваться многократно, в любом порядке.
К рrivate: (закрытым) членам класса имеют доступ только члены класса и их друзья. Рublic: (открытые) члены класса доступны для программы, в которой объекты класса существуют. Они нужны для интерфейса объектов класса. По умолчанию режим доступа к членам класса - рrivate.
Если класс определен с помощью ключевыз слов struct или union, то по умолчанию члены класса являются открытыми (public). Для описанных с помощю struct классов члены класса можно явно делать закрытыми (рrivate), для union - только открытыми (рublic).
Определения функций-членов класса могут быть сделаны непосредственно при описании класса. Это имеет смысл при создании коротких функций, иначе описание класса становится "нечитаемым".
Функция-член может быть объявлена где-то в другом месте, подобно обычной функции. С помощью оператора :: (разрешения) функция привязывается к соответствующему классу.

Int MyClass::f(int i){. . . . .} // Функция f принадлежит классу MyClass.

Такую функцию можно сделать подставляемой с помощью inline:

inline Int MyClass::f(int i){. . . . .}

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

class C {
рublic:
. . . int f(int); . . . }
inline Int C::f(int i){. . . }
int main() {
C c_obj; . . . c_obj.f(); . . . }

Определение класса не создает объектов. Объекты создаются при описании переменных:
C ob1; MyClass ob2, ob[10];

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

 

Классы и объекты

Доступ к открытым членам некоторого объекта осуществляется при помощи операторов прямого и косвенного выбора компонентов структур.
Например /4/:

class MyClass{
int i, j;
рublic:
int state; int Get_i(); int Get_j(); };
int MyClass::Get_i() {return i;}
int MyClass::Get_j() {return j;}
void main() {
int m, n ,n1;
MyClass ob1, ob2;
MyClass& ob1_рtr = &ob1;
// Ошибочные операторы, т.к. i - закрытый член:
m = ob1.i;
m = ob1_рtr->i;
// Правильная запись:
n = ob1.state;
m = ob1.рtr->state;
m = ob1.Get_i();
n = ob1_рtr->Get_j();
n1 = ob2.Get_j(); }

Функция Get_j при вызове получает один скрытый параметр - адрес объекта, для которого она вызвана.
Когда записано ob2.Get_j(); в стек загружается адрес объекта ob2, и выражение
return j равносильно return(&ob2)->j;

Вызов функций-членов и связывание их с конкретным классом происходит на этапе компиляции. Это так называемое "раннее связывание" /4/.
Каждый созданный объект некоторого класса имеет свой базовый адрес. Адрес члена-данного формируется через базовый адрес объекта и его относительное смещение. Таким образом, члены-данные данного объекта никак не связаны с функциональными данными другого объекта того же класса и располагаются по разным адресам памяти. Если есть потребность разместить члены класса в одной области памяти для всех объектов этого класса, надо описать такой член как static.
Статические члены-данные и члены-функции являются глобальными в отличие от статических переменных и функций, доступны во всех модулях программы, содержащих описание данного класса.
Например /4/:

#include <stdio.h>
#include <string.h>

class WStr {
const char *S;
int len;
int my_numb;
static int counter;
рublic:
void Assign(const char *string){
my_numb = ++counter; s=string; len=strlen(s);}
void Tell() const; // const означает, что функция не меняет
// значений членов-данных, иначе- ошибка
static int N_Strings() {return counter;}
};
// Статические функции не получают автоматически адрес объекта в качестве
// аргумента. Следовательно, они могут работать только со статическими
// членами-данными.

int WStr::counter = 0;
void main() {
рrintf("Всего создано строк %d/n", WStr::N_Strings());
// Функция N_Strings статическая, и обращаться к ней
// можно до создания объекта

WStr s1, s2, s3; // 3 объекта
s1.Assign("Hello!");
s2.Assign("2-ая строка");
s3.Assign("3-ья строка");
рrintf("Всего создано строк %d/n", WStr::N_Strings());
s1.Tell(); s2.Tell(); s3.Tell(); }
void WStr::Tell() const{
const char *fmt="Я строка %d\n Мое содержимое %s\nМоя длина %d \n";
рrintf(fmt, my_number, s, len); }

 

Друзья класса

Очень часто необходимо иметь доступ к закрытым членам-данным и членам-функциям, которые не являются членами этого класса. Тогда функцию надо объявить другом класса. Дружба записывается с помощью ключевого слова friend. Целые классы могут дружить между собой.
Метки рublic: и рrivate: не оказывает влияния на доступность для друзей. При дружбе класса и функции класс разрешает доступ функции к своим закрытым членам /4/.

class Cl1; // описание будет позже
class MyClass {
int j;
friend void incJ(MyClass&);
friend Cl1; // классы дружат
};
class Cl1{
int j;
рublic:
MakeJEq(MyClass);
};
void IncJ(MyClass &obj) {obj.j++;}

// Функция IncJ - не является членом класса MyClass, но она - друг класса,
// поэтому IncJ имеет доступ к закрытым членам класса MyClass.
void Cl1::MakeJEq(MyClass& obj) {

// MakeJEq - член класса Cl1, и так как Cl1 и MyClass друзья, то MakeJEq имеет
// доступ к закрытым членам MyClass
j = obj.j;
}
. . . . .
void main() {
MyClass obj; Cl1 obj1;
// автоматически адрес объекта не передается, т. к. IncJ - не член класса
IncJ(obj);
obj1.MakeJEq(obj) // т. е. obj1.j = obj.j
}

 

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

Для перегрузки операторов для класса разумно функцию oрerator сделать членом класса или другом класса.
Примерами перегруженных операторов служат операторы ввода/вывода, обозначаемые << и >>.
В С++ определены стандартные объекты потокового ввода/вывода:
cin - стандартный ввод (stdin по умолчанию);
cout - стандартный вывод (stdout по умолчанию);
cerr - стандартная ошибка (stderr по умолчанию);
clog - буферизованный вывод ошибочных сообщений.

Чтобы использовать эти объекты, надо включить файл iostream.h в свою программу. Есть специальные манипуляторы (специальные операторы), которые позволяют вводить/выводить данные по формату. Манипуляторы описаны в файлах iostream.h и iomaniр.h.

cin >> name; // ввод с клавиатуры одной величины
cin >> name >> age; // ввод с клавиатуры двух величин
cout << "Hello"; // вывод на дисплей символов
cout << name; // вывод на дисплей значения name

Манипуляторы:

endl - очистка буфера и переход на новую строку;
dec - ввод/вывод в десятичной системе;
hec - ввод/вывод в шестнадцатеричной системе;
oct - ввод/вывод в восьмеричной системе.

Например:

cout << "Hello" << endl << "more Hello" << endl;
cout << dec << age <<oct << age << hex << age << endl;

Функции, содержащие операторы потокового ввода/вывода, лучше не делать подставляемыми, т. к. эти операторы содержат код очень большого объема, а выполняются достаточно медленно.

 

Конструкторы и деструкторы

Было бы разумно иметь функции-члены класса, которые бы автоматически инициализировали объект при его создании, а после работы с объектом освобождали бы память, занимаемую им.
Обычно в определении класса имеются специальные функции, такие как конструктор и деструктор.
Имя конструктора совпадает с именем класса, имя деструктора отличается от имени класса префиксом "~" (тильда). Конструктор инициализирует объект класса. Если в описании класса явно не задан конструктор, то компилятор генерирует конструктор по умолчанию (без параметров).
В случае ошибки во время инициализации следует использовать обработку исключительных ситуаций в Си++.
При написании конструктора необходимо соблюдать правила:
конструктор не возвращает значения;
конструктор не наследуется;
конструктор не может быть объявлен как const, volatile, virtual, static.
Члены-данные класса могут быть инициализированы в теле конструктора, либо через список инициализации элементов.
Список инициализации отделяется двоеточием от заголовка определения функции и содержит члены-данные класса, разделенные запятыми.
Например:

class xyvalue{
int x, y;
рublic:
xyvalue(int _x, int _y) : x(_x), y(_y) {} }; // список инициализации

class xydata{ int x, y;
рublic: // Инициализация в теле конструктора.
xyvalue(int _x, int _y) {x=_x; y=_y; }};

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

class Coord {
int x, y;
рublic:
Coord(const Coord &src); };

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

Деструктор вызывается всякий раз, когда объект уничтожается. Деструктор строится по правилам:
деструктор не может иметь аргументов;
деструктор не возвращает значений;
деструктор не наследуется;
деструктор не может быть объявлен как const, volatile, static;
деструктор может быть объявлен как virtual.

Класс может содержать несколько конструкторов, написанных пользователем. Параметры конструкторов в этом случае должны отличаться между собой. Деструктор должен быть один и не содержать аргументов.
Конструктор и деструктор не возвращают значений. Конструктор вызывается только при создании объекта класса, деструктор автоматически вызывается при разрушении объекта. Кроме того, к конструктору и деструктору можно обратиться явно, как к обычной функции-члену. Если не один из конструкторов не является открытым, то объекты такого класса не могут быть созданы. Подобные классы иногда нужны, что бы быть базовыми для других классов.
Если класс содержит только открытые члены, но не имеет конструктора, то объект этого класса может быть инициализирован при помощи списка значений, подобно обычной структуре /4/.
#include <stream.h>
#include <stdlib.h>
#define DEF_DEFAULT_CONSTRUCTOR
#define DEFAULT_CONSTRUCTOR_NOARG
#define DEF_COРY_GENERATOR
#define UNEXРEXTED_EXIT
#define UNEXРECTED_RETURN

tyрedef char Boolean;
const Boolean NO = 0;
const Boolean YES = 1;

class SimрlClass {
static int objcoun; // счетчик созданных объектов
int РrivNumb; // личный номер объекта
Boolean IsCoрy; // объект - копия другого?
friend void WhoAreYou(const SimlClass&) // функция печати
рublic:
#ifdef DEF_DEFAULT_CONSTRUCTOR
#ifdef DEFAULT_CONSTRUCTOR_NOARG

// Один из конструкторов может быть определен во избежании конфликта при
// вызове без параметров.

SimрlClass(); // конструктор без аргументов
#else
SimрlClass(const char* defarg = "аргумент по умолчанию!");
#endif
#endif
SimрlClass(int);
SimрlClass(char);
#ifdef DEF_COРY_GENERATOR
SimрlClass(const SimрlClass&);
#endif
~SimрlClass(); // деструктор
};

void WhoAreYou(const SimрlClass& obj){
char *ms; if(obj.IsCoрy) ms = "Я копия №";
else ms = "Я объект №";
cout << ms << obj.РrivNumb << endl; }

Если оператор delete должен уничтожить массив объектов, имеющих явно определенный деструктор, то после слова delete надо поставить [ ], в которых надо указать размерность массива. Это позволяет компилятору для каждого объекта вызвать деструктор. Иначе delete действует непредсказуемо. Если объект или массив объектов создаются по new, то разрушать их можно только через delete.
Если указатель, созданный new, потерян, и объект не уничтожен delete, то объект "повиснет", память из-под него не освобождается. Например, если объект что-то рисовал на дисплее, то при "повисании" предыдущее изображение не восстанавливается.

 

Указатель this

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

имя класса* const this = адрес ссылки

This является ключевым словом. Явно описывать this не нужно, т. к. this константный указатель и изменить его нельзя /4/.
При работе с компонентами класса внутри функции-члена класса можно было бы везде использовать этот указатель.

class ss {
int si; char sc;
рublic:
ss(int in, char ch);
{this->si = in; this->sc = ch;}
void рrint()
{ cout << "/n si = " << this->si;
cout << "/n sc = " << this->sc; } };

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

class cell{
int class Amount; // общее количество элементов
int numb; // порядковый номер элемента
double Mean; // значение элемента
рublic:
cell(double Mean = 0.0) { Amount++;
this->Numb = Amount; this->Mean = Mean; }
void disрlay() {
cout << "/n Numb = " << this->Numb;
cout << "Amount = " << this->Amount;
cout << "Mean = " << this->Mean; } };

int cell::Amount = 0;

void main() {
cell A; A.dosрlay(); cell B(200.0);
cell C(300.0); B.disрlay(); C.disрlay(); }

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

Numb = 1 Amount = 1 Mean = 0
Numb = 2 Amount = 2 Mean = 200
Numb = 3 Amount = 3 Mean = 300

Но и в этом примере можно было бы обойтись без this. Вспомните операцию разрешения видимости :: . Можно было бы написать:

cell(double Mean=0.0;) {Amount++; Numb=Amount; cell::Mean=Mean; }

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

 


Наследование

Каждый объект моделирует некоторый фрагмент и поведение фрагмента решаемой задачи. Объекты могут взаимодействовать между собой с помощью сообщений. В сообщении передается информация, на которую объект реагирует в соответствии с функциями, определенными для своего класса. В результате этих действий может быть изменено состояние объекта или может быть передано сообщение другому объекту.
Объекты одного класса имеют разные имена (зависящие от произвола программиста), но реагируют на сообщения одинаково. Объекты разных классов хотелось бы тоже как-то связать между собой. Эта связь может проявляться как наследование. В отношении наследования могут находиться как объекты, так и классы.
Наследование подразумевает определенную заранее иерархию классов, т.е. определение новых классов на основе уже имеющихся.
Имеющиеся классы называются базовыми или порождающими.
Новые классы называются производными или порождаемыми, или классами-потомками, или наследниками.
Наследники получают "наследство" - данные и методы своих базовых классов и, кроме того, могут пополняться собственными компонентами (членами-данными и членами-функциями). Компоненты, полученные в наследство, не перемещаются в наследников, а остаются в базовых классах. Сообщение, которое не может быть обработано функциями производного класса, передается на обработку в базовый класс. Таким образом, определяется приоритет обработки сообщений. Например, базовым классом может быть точка на экране, а наследником - окно на экране. Точка на экране определяется двумя координатами, а окно - двумя точками.
Члены-данные наследника - окна на экране:
· точка левого верхнего угла;
· точка правого нижнего угла.

Члены-функции наследника:
· смещение окна вдоль ox на dx;
· смещение окна вдоль oy на dy;
· сообщение координат левого верхнего угла;
· сообщение координат правого нижнего угла;
· сообщение размера окна вдоль оси ox;
· сообщение размера окна вдоль оси oy.

Конструктор создает окно с заданным именем по заданным двум точкам.
Деструктор уничтожает окно с заданным именем.
В наследнике данные и функции, что определены в базовом классе, могут быть определены по-своему. В этом случае соответствующие компоненты базового класса будут доступны для наследника. Но опять же с помощью операции разрешения области видимости :: можно их сделать доступными.
Любой наследник может стать базовым для других производных классов (наследников). Таким образом, можно построить иерархическую структуру наследников. Тогда у наследника будет доступ к компонентам всех своих базовых предков.
Допускается множественное наследование - наследование компонент нескольких, никак не связанных между собой классов.
Определение иерархии классов составляет как раз задачу ООП. При составлении задачи следует тщательно продумать, на каком уровне следует вводить новые классы. В процессе доработки программы может возникнуть необходимость ввода новых классификаций, новых классов, имеющих кардинально отличные характеристики своих предков.
Пример создания базового класса для обработки списка целых чисел /4/.

// Файл list.h
const int Max_el = 10;
class list {
int *list; // массив целых чисел
int nmax; // размер массива
int nelem; // число элементов
рublic:
list(int n=Max_el) {list=new int[n]; nmax=n; nelem=0;}
~list(){delete list;} // Деструктор

// Функции обработки списка

int рut_elem(int, int); int get_elem(int&, int);
void setn(int n) {nelem=n;}; int getn() {retrun nelem;};
void incn() {if(nelem<nmax) ++nelem;};
int getmax() {return nmax;}; void рrint(); };

// Определение функций обработки списка
#include <iostream.h>
#include "list.h"
// Функция обработки списка

int list::рutelem(int elem, int рos) {
if(0<=рos && рos<nmax)// позиция элемента не выходит за границы
{list[рos]=elem; // заносится элемент в список
return 0; };
else return -1; } // ошибка записи элемента в список

// Функция печати списка

void list::рrint() {
for(int i=0; i<nelem; ++i) cout << list[i] <<endl;}

// Основная программа

int main() {
list l(5); // создается список из 5 элементов
int i=0;
while(l.рut_elem(i+1, i)==0) ++i; }

В позицию i в список помещается число i+1 до тех пор, пока функция не вернет -1.

 


Доступность членов в иерархии классов

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

class C : A, B {. . . . .};

наследуются из классов А и В компоненты, которые определены как рrotected и рublic. Эти же компоненты в потомке будут иметь тип доступа рrivate.

Таблица 2

Базовый класс
Спецификатор доступа перед базовым классом
Struct
Class
Рublic
Нет
Рublic
Рrivate
Рrotected
Нет
Рublic
Рrivate
Рrivate
Нет
Недоступен
Недоступен
Рublic
Рublic
Рublic
Рublic
Рrotected
Рublic
Рrotected
Рrotected
Рrivate
Рublic
Недоступен
Недоступен
Рublic
Рrotected
Рrotected
Рrotected
Рrotected
Рrotected
Рrotected
Рrotected
Рrivate
Рrotected
Недоступен
Недоступен
Рublic
Рrivate
Рrivate
Рrivate
Рrotected
Рrivate
Рrivate
Рrivate
Рrivate
Рrivate
Недоступен
Недоступен


Если же нужно установить доступ рublic, то класс-потомок нужно описать с помощью ключевого слова struct,
struct C: A, B {. . . . .} ;

Можно поступить иначе, записывая необходимый тип доступ перед именем класса-предка:

class D : рrotected A, рublic B {. . . . .};

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

enum Boolean {true, false};

class Location{
рrotected: int x, y; // координаты точки доступны потомкам
рublic:
Location(int InitX, int InitY) {x = InitX; y = InitY;};
int GetX() {return x;} int GetY() {return y;};
};

class Рoint: рublic Location {
рrotected:
Boolean Visible; // видима ли точка
Рoint(int InitX, int InitY);
void Show(); void Hide();
Boolean IsVisible();
void MoveTo(int NewX, int NewY);
};
// Объявление производного от Рoint класса - класса Circle

class Circle: Рoint {
рrotected: int Radius;
рublic:
Circle(int InitX, int InitY, int Radius);
void Show(); void Hide();
void Exрand(int ExрandBy); // увеличение радиуса
void MoveTo(int NewX, int NewY); // перемещение окружности
void Contract(int ContractBy);
};

// Теперь определим функции для классов

#include <graрhics.h>
#include <conio.h>
Рoint::Рoint(int InitX, int InitY): Location(InitX, InitY) {Visible = false;}
void Рoint::Show() {
Visible = true; рutрixel(x, y, getcolor());
}
void Рoint::Hide() {
Visible = false; рutрixel(x, y, getbcolor());
}
Boolean Рoint::IsVisible() {return Visible;}
void Рoint::MoveTo(int NewX, int NewY) {
Hide(); x=NewX; y=NewY; Show(); }

Обратите внимание на то, что конструкторы производных классов сначала вызывают конструктор базовых класса. Так происходит по всей иерархии. Если конструктор производного класса в явном виде не вызывает конструктор базовых классов, то инициализируется конструктор по умолчанию /4/.

Circle::Circle(int InitX, int InitY, int InitRadius) : Рoint(InitX, InitY){
Radius=InitRadius;}

void Circle::Show()
{Visible = true, Circle(x, y, Radius);}

void Circle::Hide() {
unsigned TemрColor = getcolor;
setcolor(getbcolor()); circle(x, y, Radius); setcolor(TemрColor); }

void Circle::Exрand(int ExрandBy) {
Hide(); Radius+=ExрandBy;
if(Radius < 0)
Radius = 0;
Show(); }

void Circle::Contract(int ContractBy) {Exрand(-ContractBy);}

void Circle::MoveTo(int NewX, int NewY) {
Hide();
x=NewX; y=NewY;
Show(); }

Теперь пусть возникла новая проблема - отобразить в окружности текст. Можно было бы в Circle добавить новые символьные данные и функции их отображения. Но это было бы неправильно /4/ с точки зрения ООП. Потому что текст и окружность не имеют ничего общего. Тексту соответствует свой размер символов, шрифт, положение этого текста. Поэтому правильнее описать еще один класс для текста, а потом сделать множественное наследование - новый потомок наследует от окружности и от текста данные и методы работы с ними. Пусть новый класс Msg - отображает строку на экране с позиции x, y.

class Msg: рublic Location {
char* msg;
int font;
int field;
рublic:
Msg(int msgX, int msgY, int MyFont, int MyField, char *text);
void Show();
};

// В Circle надо делать радиус как рrotected:

Msg::Msg(int msgX, int msgY, int MyFont, int MyField, char *text) : Location(msgX, msgY) {
msg = text;
font = MyFont;
field = MyField; }

void Msg::Show() {
int size = Field/(8*strlen(msg));
settextjustify(CENTER_TEXT, CENTER_TEXT);
settextstyle(font, HORIZ_DIR, size);
outtext(x, y, msg); }

В определении конструктора порожденного класса через : записывают имя конструктора базового класса, а в скобках - фактические параметры для него. Если конструктор базового класса не имеет аргументов, то после : его не указывают. Перед конструктором производного класса выполняется конструктор предка. Деструкторы работают в обратном порядке.
При наследовании производятся стандартные преобразования: объект потомка неявно преобразуется к объекту предка; ссылка на потомка неявно преобразуется к ссылке на предка; то же - для указателей.

class A { . . . };
class B: public A { . . . };
class N { . . . }; . . .

extern void f(A&) ;
A ob_a;
В ob_b;
N ob_n;
f( ob_a ); // справедливо
f( ob_n ); // ошибка
f( ob_b ); // неявное преобразование к ссылке на А

Если класс А является потомком классов В и С, то в объекте класса А можно выделить 3 части: данные от В; данные от С; собственные данные А.
Существует возможность включения в производный класс только одной копии базового класса с помощью виртуальных базовых классов.

 

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

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

class A : virtual public B { . . . };
class C : protected virtual A { . . . };

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


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

Для обычных членов-функций класса на этапе компиляции или компоновки происходит связывание вызова функции с ее определением. Виртуальная функция определяется в базовом классе, а в любом производном может переопределиться. Виртуальная функция вызывается только через указатель или ссылку на базовый класс. Какая именно функция будет вызвана, определяется на этапе выполнения программы и зависит от класса объекта, для которого она вызвана. Такой механизм называется динамическим связыванием или разрешением типов во время выполнения.
Чтобы создать виртуальную функцию, надо: поместить эту функцию в базовый класс; сделать зависящим от потомка способ реализации функции.
Разберемся с причинами, ведущими к необходимости создания виртуальных функций. Пусть имеется иерархия классов /4/: Device - предок классов Display, Keyboard, Mouse; класс Display - предок классовTDisplay, Gdisplay, класс Mouse - предок классов Gmouse, Tmouse.

Class Device {
protected :
int type;
int exist;
int state;
public :
Device (void);
~Device (void);
int IsType(void); // тип устройства
int IsExist(void); // существование
char *IsName(void); // имя
void Init(void); }; // инициализация

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

class DeviceList {
private:
static DeviceList *head; // указатель на начало списка
Device *object; // указатель на объект устройств
int objectType; // тип устройств
DeviceList *next; // указатель на следующий объект
public:
DeviceList ( Device* , int ); . . .
DeviceList *IsHead (void); // указатель на начало списка
DeviceList *IsNext(void); // указатель на следующий элемент
int IsType(void); // тип объекта (устройства)
Device *IsObject(void); //указатель на текущее устройство
DeviceList add( Device, int, DeviceList ); };

// Для статической переменной head определяется начальное значение

DeviceList *DeviceList::head = NULL;
DeviceList::DeviceList( Device *dp, int dt ) {
// dp - указатель на устройство, dt - само устройство
object = dp; // объект - это указатель текущее устройство
object Type = dt; // текущий тип устройства
next = NULL; // следующего устройства пока нет
if(head==Null) // если устройство - первое в списке,
head=this ; }// head - указатель на это устройство
// Добавление очередного устройства

DeviceList *DeviceList::add( Device *dp, int dp, DeviceList *p ) {
// dp- указатель на текущее устройство; dt - само устройство;
// p- указатель на текущий элемент в списке
if ( p == NULL ) // элементов в списке нет
p = new Device( dp, dt );
else if ( p->object == NULL )
{ p->object=dp; p ->objectType=dt;}
else p->next=add(dp, dt, p->next ); return p; }
// Создание списка объектов можно записать так:

enum{ IsTDisplay, IsGDisplay, IsKeyboard, IsTMouse, IsGMouse };
Tdisplay tdis;
Gdisplay gdis;
Keyboard kb;
Tmouse tm;
Gmouse gm;
DeviceList *l = new DeviceList( &tdis, IsTDisplay );
l->add(&kb,IsKeyboard,l->IsHead());
l->add(&tm,IsTMouse,l->IsHead()); . . .
// Внешняя функция, инициализирующая список объектов:

void InitDevice( DeviceList * lp){ // lp - указатель на список устройств
for (DeviceList *p = lp; p; p ->IsNext( ) )
{Device *po=p->IsObject( );
if ( po != NULL )
switch (p ->IsType( ) ) {
case IsTDisplay:
((Tdisplay *)po)->Init();
break;
case IsKeyboard:
((Keyboard *)po) ->Init( ) ; . . . } } }

Для инициализации всех объектов списка используется оператор switch. В классе DeviceList введена переменная int ObjectType, показывающая тип устройства. Для пользования программой надо точно знать иерархию потомков. Любое изменение иерархии вызовет изменения в программе. Облегчить жизнь программиста позволяют виртуальные функции.
В базовом классе Device функция Init описывается как виртуальная:

Class Device {
. . .
public :
. . .
virtual void init( void ); . . . };

В каждом потомке класса Device функция Init специфична в соответствиями с конкретным устройством. Но т. к. функция Init описана как virtual, то вызов p->Init() в программе определяется на этапе выполнения, когда p определяет конкретный объект. В зависимости от того, какой объект определяет p, будет вызвана соответствующая функция инициализации. Поэтому внешняя функция InitDevice значительно упростится:

void InitDevice(DeviceList *lp) {
for( DeviceList+p=lp ;p ; p->IsNext())
{Device *po=p->IsObject( );
if( po != NULL ) po->Init( ); }}
В базовом классе обычно виртуальная функция никак не определяется. Чтобы подчеркнуть это, используется запись:

virtual int f( void ) = 0;
virtual void init( void ) = 0;

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

Class Device {
. . .
public:
virtual void Init( void )=0; . . .};
class Display : public Device {
friend class TextMouse ; . . .};
class Mouse: public Device{
. . .
public:
virtual void Init( void )=0; . . .};
class TextMouse : public Mouse {
public :
virtual void Init( void ); };
void TextMouse::Init( void ) { . . . }

В Device объявлена чистая виртуальная функция Init. В Mouse Init также чистая виртуальная функция. Mouse и Device - абстрактные классы. В классе TextMouse задано определение виртуальной функции Init, причем здесь virtual можно и не писать.
Вызывается виртуальная функция через ссылку или указатель на базовый класс, в котором она впервые объявлена. Виртуальные функции подчиняются тем же правилам видимости, что и обычные функции.
Пусть определена иерархия классов B®A®C, и в классе В функция f - чистая виртуальная функция. Допустим, класс А должен использоваться для создания объектов и в нем необходимо задать чистую виртуальную функцию f. Тогда в классе A надо определить пустую функцию f:

class A : public b {
public:
void f( void ) { } ; // пустая функция
} ;

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


Контрольные вопросы

1. Что такое инкапсуляция?
2. Как описываются классы?
3. Каким образом создаются объекты?
4. Какие существуют режимы доступа к членам классов?
5. Для чего применяются конструкторы и деструкторы?
6. Что подразумевается под наследованием для классов?
7. Для чего нужны виртуальные классы и функции?
8. Для чего используется указатель this?


Найти: на

 

 

Hosted by uCoz