Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лабник по C.pdf
Скачиваний:
74
Добавлен:
01.06.2015
Размер:
876.9 Кб
Скачать

– 49 –

 

 

Таблица 4

Доступ в базовом классе

Модификатор доступа

Унаследованные права

 

 

 

доступа

 

 

 

 

 

private

private

не доступен

 

protected

private

private

 

public

private

private

 

private

public

не доступен

 

protected

public

protected

 

public

public

public

 

Свойство protected аналогично свойству private, но доступ к такому компоненту возможен как через интерфейс базового класса, так и через интерфейс производного. По умолчанию для базового класса типа class в качестве модификатора доступа используется private, для класса struct public.

Ниже следует программа, в которой класс Node задает тип данных, которым является элемент одностороннего списка; класс List является базовым, класс Stack – производным

(List Stack): struct Node { int elem;

Node *next;

Node (int el, Node *ptr) {elem=el; next=ptr;} };

class List { protected: Node *head; public:

List() {head=0;}

List(int a) {head=new Node(a,0);} void Show(void);

~List();

};

class Stack: public List { public:

Stack (int el): List(el){}; void Push(int);

int Pop(); };

Унаследованная переменная head может быть использована в теле функций Push и Pop, но в программе она доступна только через интерфейс классов – функции Push, Pop, Show.

Отметим следующий момент: если производный класс был унаследован из базового с модификатором public, то указатель на базовый класс можно использовать и как указатель на производный класс без какого-либо преобразования типов:

указатель_на_базовый = указатель_на_производный;

Существуют виртуальные базовые классы. Необходимость таковых появляется при множественном наследовании в тех случаях, когда в разных родительских классах есть одинаковые поля (например, при следующей иерархии: AС; ABC). Наследуя каждому родителю, производный класс может получить несколько копий одних и тех же полей, что приводит к неоднозначности обращения к этим полям. Добавление слова virtual в списке базовых классов (перед модификатором доступа) устраняет дублирование.

14. ВИРТУАЛЬНЫЕ ФУНКЦИИ

14.1. ОБЩИЕ ПОЛОЖЕНИЯ

Рассмотрим следующую программу. class Parent{

© 1998 Калачев Д.П., Лутай В.Н.

– 50 –

public:

double F1(double x){ return x*x;} double F2(double x { return F1(x)/2; }

};

class Child : public Parent { public:

double F1( double x) { return x*x*x; } //Переопределение функции F1 };

void main(){ Child child;

cout<<child.F2(3)<<endl;

}

Здесь класс Child, производный от класса Parent, наследует функцию F2, но переопределяет функцию F1. Компилятор оттранслирует выражение child.F2(3) в обращение к унаследованной функции Parent::F2, которая в свою очередь вызовет Parent::F1, а не Child::F1. Полученный результат поэтому равен 4,5 вместо ожидаемого 13,5. Объявление функции F1 виртуальной приведет к правильному результату.

Свойство виртуальности задается ключевым словом virtual перед типом функции class Parent { ...

public:

virtual double F1(double x){ return x*x; };

//В базовом слово virtual обязательно

...

};

class Child :public Parent { ...

public:

virtual double F1( double x) { return x*x*x; }

//Повторение virtual здесь необязательно

};

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

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

Механизм виртуальных функций основан на позднем (динамическом) связывании. Раннее связывание характерно для языка С – код вызова функции является частью откомпилированного кода всей программы. При позднем связывании виртуальная функция не связывается с объектом при компиляции. Если в некотором классе имеется хотя бы одна виртуальная функция, то все объекты этого класса содержат указатель на связанную с их классом виртуальную таблицу указателей функций этого класса. Доступ к виртуальной функции осуществляется посредством косвенной адресации – через этот указатель и соответствующую таблицу. (Отметим, что виртуальная функция не может быть статическим компонентом класса).

Использование виртуальных функций снижает быстродействие и увеличивает размер объектов класса, содержащих такие функции. Но это – неизбежная плата за эффективность.

14.2. ВИРТУАЛЬНЫЕ ДЕСТРУКТОРЫ

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

#include <iostream.h>

struct pet{ virtual void speak(void){cout<<“ Привет.“;} virtual ~pet(){cout<<“Я-деструктор животных”<<endl;} };

struct dog: public pet{

void speak(void){pet::speak(); cout<<"Гав-гав"; } ~dog(){cout<<“Я-деструктор Гав-Гав”<<endl;} };

© 1998 Калачев Д.П., Лутай В.Н.

– 51 –

struct cat: public pet{

void speak(void){pet::speak(); cout<<"Мяу-мяу";} ~cat(){cout<<“Я -деструктор Мяу-мяу”<<endl;} };

void main() {

pet *members[]={new dog, new cat}; for (int i=0; i<2; i++)

members[i]->speak(); // Вызов виртуальных функций через указатели на базовый класс for(i=0; i<2; i++) // Вызов деструкторов

delete members[i];

}

Информация на дисплее:

Привет.Гав-Гав Привет.Мяу-мяу Я- деструктор Гав-гав

Я- деструктор животных Я- деструктор Мяу-мяу Я- деструктор животных

Рассмотрим деструкторы классов. Во-первых, создание массива объектов в свободной памяти требует для вызова деструкторов обязательного использования delete. Во-вторых, деструктор класса pet объявлен как виртуальный; вследствие этого и все деструкторы производных классов являются виртуальными. Если бы они не были таковыми, то не были бы вызваны деструкторы классов cat и dog. Виртуальный деструктор самостоятельно определяет объект того класса, который подлежит разрушению.

Если в классе есть хотя бы одна виртуальная функция, то рекомендуется деструктор класса делать виртуальным. Конструктор виртуальным быть не может.

14.3. АБСТРАКТНЫЕ КЛАССЫ

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

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

#include <graphics.h> #include <iostream.h> class Shape { public:

int xb,yb,xe,ye; // Координаты прямоугольника public:

Shape(int hb,int vb,int he,int ve): xb(hb),yb(vb),xe(he),ye(ve) {}

virtual void draw()=0; // Рисует фигуру virtual void move(int,int)=0; // Перемещает virtual void copy(int,int)=0; // Копирует virtual void rotate(int)=0; // Поворачивает

};

struct Rectangle : public Shape{ Rectangle(int hb, int vb, int he, int ve):

Shape (hb,vb,he,ve) { int gd=DETECT,gm; initgraph(&gd,&gm, "");//Инициализация графического //режима

~Rectangle() {closegraph(); }

virtual void move(int x, int y){/* Какие-то действия */ } virtual void copy(int x, int y){} /* Какие-то действия */

© 1998 Калачев Д.П., Лутай В.Н.

– 52 –

virtual void rotate(int r){} /* Какие-то действия */

virtual void draw(){::rectangle(xb,yb,xe,ye);} // вызов библиотечной функции };

void main() {

Rectangle r(10,10,20,20); r.draw();

}

15.ПОТОКИ ВВОДА-ВЫВОДА

Воснову системы ввода-вывода положена библиотека iostream, заключенная в соот-

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

cin – стандартный ввод;

cout – стандартный вывод;

cerr – стандартный вывод ошибок.

15.1. ВЫВОД ДАННЫХ

Вывод потока осуществляется перегруженной операцией <<, которая здесь называется операцией вставки. Примеры использования операторов вывода приводились ранее. Упомянем о компонентах класса cout.

Функция write – компонент класса cout – выводит n символов, в том числе, в отличие от вставки, и пробелы:

cout.write(arg, n)

Функция-компонент того же класса put выводит одиночный символ.

Форматирование вывода производится с помощью специальных операций, называемых манипуляторами:

cout<<setw(4)<<i<<setw(6)<<j;

setw(n) – манипулятор, устанавливающий ширину поля для работы следующей после него вставки. В результате для представления i будет отведено поле шириной 4 символа, для j-6 символов.

С помощью манипуляторов можно изменять основание системы счисления выводимых данных (dec,hex,oct), устанавливать точность представления чисел с плавающей точкой (setprecizion), устанавливать символы-заполнители (setfill). Для использования манипуляторов к программе надо подключить заголовочный файл iomanip.h.

Пример использования функций write и setw:

#include <iostream.h> #include <iomanip.h> #include <conio.h> void main() {

char* s=new char[3]; s=“ABC”;

cout.write(s,1); // Выводится A cout<<endl;

cout<<setw(3)<<s<<endl; //Выводится ABC getch();

}

Приведем пример использования потока cerr:

Дополним класс String перегруженной операцией индексирования [] и функцией

error:

char& operator [] (int i) {

if (i<0 || this->length<i) error("Индекс за границей массива");} void error(char* p) {

cerr<<p<<endl ; exit (1);}

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

© 1998 Калачев Д.П., Лутай В.Н.