Тезисы лекционных материалов 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Тезисы лекционных материалов



Тема 5. Шаблоны

 

Двумя важнейшими характеристиками языка С++ верхнего уровня являются: шаблоны (templates) и обработка исключительных ситуаций (exception handling). Эти характеристики поддерживаются всеми современными компиляторами и позволяют достичь двух наиболее заманчивых целей программирования: создания многократно используемых и отказоустойчивых программ.

С помощью шаблонов можно создавать родовые функции (generic functions) и родовые классы (generic classes). В родовой функции или классе тип данных, с которым функция или класс работают, задается в качестве параметра. Это позволяет одну и ту же функцию или класс использовать с несколькими различными типами данных без необходимости программировать новую версию функции или класса для каждого конкретного типа данных. Таким образом шаблоны дают возможность создавать многократно используемые программы.

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

Родовая функция создается с помощью ключевого слова template. Типовая форма определения функции-шаблона:

template < class T> возвращаемое_значение имя_функции (список_параметров)

{тело функции}

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

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

 

template < class T> T MyMin (T x, T y)

{ if (x<=y) return x; else return y;}

 

которая потенциально поможет работать с неизвестным типом Т.

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

 

template < class T> T Fun1 (T x, T y, int z)

template < class T> double Fun2 (T x, T y)

template < class T, class W> W Fun3 (T x, W y, bool z)

 

· Идентификаторы формальных параметров шаблона (T, W) должны хотя бы один раз появляться в списке формальных параметров функции.

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

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

· Инструкция с ключевым словом template должна находиться сразу перед определением шаблонной функции

· Текст определений шаблонов функции нужно располагать в тех же самых файлах, в которых осуществляется их вызов, либо в заголовочных файлах (h-файлы).

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

 

Родовые классы

 

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

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

Синтаксис описания шаблона класса имеет следующий вид:

 

template <описание_параметров_шаблона> определение_класса

 

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

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

 

#include < iostream. h>

template < class T> class List

{ T data;

List *next;

public:

List(T d); //конструктор класса

void add(List* node)

{node -> next= this; next=0;}

List* getnext() { return next;}

T getdata() { return data;}

};

 

template < class T> List <T>:: List(T d)

{data=d; next=0;}

int main (void)

{

List < char > start (‘a’); //start - объект

List < char > *p, *last;

// создание списка

last=&start;

for (int i=1; i<26; i++)

{ p= new List< char > (‘a’+i);

p -> add (last); last=p;

}

//вывод списка

p=&start;

while (p)

{ cout <<p->getdata();

p=p ->getnext();

}

return 0;

}

 

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

Подведем некоторые итоги и перечислим основные правила описания шаблонов:

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

2. Область действия параметра шаблона – от точки описания параметра шаблона до конца шаблона класса

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

template <описание_параметров_шаблона>

возвращаемый-тип имя-класса < параметры-шаблона >::

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

4. Локальные классы не могут содержать шаблоны в качестве своих элементов.

5. Методы шаблона класса не могут быть виртуальными.

6. Шаблоны классов могут содержать статические элементы, дружественные функции и классы.

7. Внутри шаблона класса нельзя определять шаблоны дружественных функций.

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

 

 

Основная литература – 8 [гл.11, 332-337], 7 [гл.2, 89-93], 6 [гл.1, 35-42],5[Гл.10,375-378]

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

1. Что позволяют определить шаблоны классов?

2. Когда и как происходит реальная генерация машинного кода для шаблонной функции?

3. Каким образом осуществляется конкретизация при вызове шаблонной функции?

4. Какая функция называется специализацией некоторого функционального шаблона?

5. Создайте и продемонстрируйте родовой класс, реализующий очередь.

 

Тема 6. Наследование

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

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

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

class имя_проивзодного класса: as имя_базового_класса

{определение производного класса}

Здесь as – спецификатор доступа (access specifier) определяет то, как элементы базового класса (base class) наследуются производным классом (derived class).

Если as – есть public, то все открытые члены базового класса остаются открытыми и в производном. Если as – есть ключевое слово private, то все открытые члены базового класса в производном классе становятся закрытыми. В обоих случаях все private члены базового класса в производном классе остаются закрытыми и недоступными, независимо от того, как он наследуется!

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

Технически спецификатор доступа не обязателен! Если он не указан и производный класс определен с ключевым словом class, то базовый класс по умолчанию наследуется как закрытый. Если спецификатор доступа не указан и производный класс определен с ключевым словом struct, то базовый класс по умолчанию наследуется как открытый. Тем не менее для ясности предпочтительно явно задавать спецификатор доступа.

 

Покажем пример наследования со спецификатором public:

#include < iostream. h>

class Base

{ int x; // закрытый член -данное

public:

void setx(int n) {x=n;}

void showx(){ cout <<x<< endl;}

};

class Derived: public Base

{ int y;

public:

void sety(int n){y=n;}

void showy() (cout <<y<< endl;}

};

int main (void)

{Derived obj;

obj.setx(10);

obj.sety(20);

obj.showx();

obj.showy();

return 0;

}

 

Поскольку класс Base наследуется как открытый, его открытые функции становятся открытыми производному классу и поэтому доступны из любой части программы. Следовательно, совершенно правильно вызывать эти функции из функции main ().

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

Если класс наследуется как закрытый, то все члены-данные и функции базового класса становятся закрытыми в производном классе и недоступными вне его (для объектов типа Derived они становятся закрытыми).

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

 

#include < iostream. h>

class Base

{ int x;

public:

void setx (int n) {x=n;}

void showx (){ cout <<x<< endl;}

};

class Derived: private Base

{ int y;

public:

void setxy (int n, int m) {setx(n); y=m;}

void showxy () {showx(); cout <<y<< endl;}

};

 

int main (void)

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

obj.setxy(10,20);

obj.showxy();

return 0;

}

 

В данном случае функции showx() и setx() доступны внутри производного класса, что совершенно правильно, поскольку они являются закрытыми членами этого класса.

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

· все открытые члены-данные и все открытые методы базовых классов;

· функции, не являющиеся членами классов;

· глобальные переменные.

Внутри производного класса доступны все защищенные члены базового класса.

В объявлении производного класса в списке базовых классов могут использоваться модификаторы public: protected: private:, с помощью которых может быть еще ограничен доступ к членам базовых классов. Это является ценным свойством языка С++ - признаком хорошего стиля объектно-ориентированного программирования.

 

Защищенные члены класса

 

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

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

Вне базового или производных классов защищенные члены недоступны.

Спецификатор protected: может находиться в любом месте объявления класса, обычно после private -членов и перед public -членами класса.

Когда базовый класс наследуется производным классом как открытый (public:), защищенный член базового класса становится защищенным членом производного класса. Когда базовый класс наследуется как закрытый (private:), то защищенный член базового класса становится закрытым членом производного класса.

Базовый класс может также наследоваться производным классом как защищенный (protected:). В этом случае открытые и защищенные члены базового класса становятся защищенными членами производного класса и следовательно внутри функции main () они недоступны.

Спецификатор доступа protected: можно также использовать со структурами.

 

Конструкторы, деструкторы и наследование

 

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

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

конструктор_производного_класса (список_арг): базовый_класс (список_арг)

{тело конструктора производного класса}

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

 

Основная литература – 5 [гл.10, 336-349], 6 [гл. 2, 58-75], 8 [гл. 7, 205-223]

 

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

1. Методы базового и производного классов могут иметь одинаковые имена. Как при этом можно будет различить разные варианты этих методов?

2. Наследуются ли данные и методы базового класса в последующие поколения производных классов?

3. Пусть класс Three произведен от класса Two, а класс Two произведен от класса One. В классе Two замещен метод, описанный в классе One. Какой вариант этого метода получит класс Three?

4. Можно ли в производном классе описать как private: метод, который перед этим был описан в базовом классе как public:?

5. С какой целью используется служебное слово protected:?

Тема 7. Иерархия классов

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

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

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

 

class имя_производного_класса: as имя_базового_класса1,…, as имя_базового_классаN

{тело производного класса}

 

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

констр_произв_класса(список_арг):имя_базового_класса1(список_арг), имя_баз_класса2(список_арг),…, имя_баз_класса N(список_арг)

{тело конструктора производного класса}

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

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

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

Класс с ключом union, не может быть ни базовым, ни производным по отношению к какому бы то ни было другому классу.

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

· наследует класс, производный от другого;

· прямо наследует два базовых класса;

· прямо наследует несколько базовых классов.

 

#include < iostream.h >

class Base1

{ int a;

public:

Base1(int x) {a=x;}

int geta(){ return a;}

};

class Der1: public Base1

{ int b;

public:

Der1(int x, int y):Base1(y)

{b=x;}

int getb(){ return b;}

};

class Der2: public Der1

{ int c;

public:

Der2(int x, int y, int z):Der1(y,z)

{c=x;}

void show()

{ cout <<geta()<<’ ‘<<getb() <<’ ‘<<getc()<< endl;}

};

int main (void)

{

Der2 obj(1,2,3);

obj.show(); //на экране будет 3 2 1

cout <<obj.geta()<<’ ‘<<obj.getb()<< endl;

return 0;

}

 

При наследовании открытых членов базового класса они становятся открытыми членами производного класса. Поэтому, если класс Der1 наследует класс Base1, то функция geta() становится открытым членом класса Der1 и затем открытым членом класса Der2.

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

#include < iostream.h >

class Base1

{ int a;

public:

Base1(int x) {a=x;}

int geta() { return a;}

};

class Base2

{ int b;

public:

Base2(int x) {b=x;}

int getb(){ return b;}

};

class Der: public Base1, public Base2

{ int c;

public:

Der(int x, int y, int z):Base1(z), Base2(y)

{c=x;}

void show()

{ cout <<geta()<<’ ‘<<getb() <<’ ‘<<c<< endl;}

};

int main (void)

{

Der obj(1,2,3);

obj.show();

return 0;

}

 

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

 

#include < iostream.h >

class Base1

{ public:

Base1() { cout <<”Работа конструктора класса Base1\n”;}

~Base1() { cout <<”Работа деструктора класса Base1\n”;}

};

class Base2

{ int b;

public:

Base2() { cout <<”Работа конструктора класса Base2\n”;}

~Base2() { cout <<”Работа деструктора класса Base2\n”;}

};

class Der: public Base1, public Base2

{ public:

Der(){ cout <<”Работа конструктора класса Der\n”;}

~Der() { cout <<”Работа деструктора класса Der\n”;}

};

int main (void)

{

Der obj;

return 0;

}

 

Когда прямо наследуются несколько базовых классов, конструкторы вызываются слева направо в порядке, задаваемом списком, а деструкторы – в обратном порядке, поэтому программа выведет на экран:

 

 

Работа конструктора класса Base1

Работа конструктора класса Base2

Работа конструктора класса Der

Работа деструктора класса Der

Работа деструктора класса Base2

Работа деструктора класса Base1

 

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

 

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

Синтаксис языка С++ запрещает непосредственную передачу базового класса в производный более одного раза:

 

class A{…};

class B: A, A{…}; //ошибка

 

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

class A {…};

class B: public A {…};

class C: public A, public B{…}; // ошибка

 

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

 

class A{…};

class B: virtual public A{…};

class C: virtual public A, public B //так можно

{…};

 

Пример, где для предотвращения появления в классе Der3 двух копий класса Base используется виртуальный базовый класс:

 

#include < iostream.h >

class Base

{ public: int i;};

class Der1: virtual public Base

{ public: int j;};

class Der2: virtual public Base

{ public: int k;};

class Der3: public Der1, public Der2

{ public: int f(){ return i*j*k;}};

 

int main (void)

{ Der3 obj;

//неоднозначности не будет, так как представлена

// только одна копия класса Base

obj.i=10; obj.j=3; obj.k=5;

cout <<”Результат равен”<<obj.f()<< endl;

return 0;

}

Основная литература – 5 [гл.10, 349-359], 6 [гл. 2, 58-81], 8 [гл.7, 223-238]

 

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

1. Что позволяет определять иерархия классов?

2. Какова схема обработки сообщений в иерархии объектов?

3. Если при наследовании имена членов базового класса по-новому (повторно) определены в производном классе, будут ли они доступны из производного класса?

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

5. В каких случаях обращаются к механизму виртуальных классов?

Тема 8. Полиморфизм

Третьим принципом ООП является полиморфизм («многоформенность»).

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

В языке С++ полиморфизм имеет две формы.

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

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

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

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

 

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

 

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

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

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

Виртуальная функция (virtual function) объявляется внутри базового класса и переопределяется в производном классе. По существу, виртуальная функция реализует идею «один интерфейс, множество методов», которая лежит в основе полиморфизма. Виртуальная функция внутри базового класса определяет вид интерфейса этой функции. Каждое переопределение виртуальной функции в производном классе определяет ее реализацию, связанную со спецификой производного класса.

Таким образом, переопределение создает конкретный метод.

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

Рассмотрим пример программы с иерархическим порядком виртуальных функций.

 

#include < iostream.h >

class Base

{ public:

int i;

Base(int x){i=x;}

virtual void func()

{ cout <<”Выполнение функции func() базового класса:”;

cout <<i<< endl;

}

};

class Der1: public Base

{ public:

Der1(int x): Base(x) {}

void func()

{ cout <<” Выполнение функции func() класса Der1:”;

cout <<i*i<< endl;

}

};

class Der2: public Base

{ public:

Der2 (int x): Base(x) {}

//функция func() не подменяется

};

int main (void)

{ Base *p;

Base obj(10);

Der1 d1_obj(10);

Der2 d2_obj(10);

p=&obj;

p->func(); //функция func() базового класса

p=&d1_obj;

p->func(); //функция func() производного класса Der1

p=&d2_obj;

p->func(); //функций func() базового класса

return 0;

}

 

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

 

Приведем ряд важных правил работы с виртуальными методам:

· Виртуальными могут быть не любые функции, а только компонентные функции какого-либо класса.

· После того как функция определена как виртуальная, ее повторное определение в производном классе(с той же самой сигнатурой) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может уже не использоваться.

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

· Функция производного класса с другой сигнатурой параметров не будет виртуальной

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

· Членом производного класса вполне может быть метод с именем, совпадающим с именем виртуального метода базового класса, но с другой сигнатурой. Тогда он будет другим, не виртуальным методом.

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

· Если виртуальный метод переопределен в производном классе, объекты этого класса могут получить доступ к виртуальному методу базового класса с помощью операции доступа к области видимости (операция ::).

·

Виртуальные деструкторы

 

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

 

Основная литература 5 [гл.10, 359-365], 6 [гл. 3,84-96], 8 [гл.10, 303-313]

 

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

1. Можно ли для объекта производного класса вызвать метод какого-либо из базовых классов и что для этого нужно указать?

2. Для чего нужны виртуальные методы в иерархии классов?

3. Сформулируйте правила работы с виртуальными методами.

4. Может ли перегруженный метод содержать параметры, заданные по умолчанию?

5. В каких случая не следует делать методы класса виртуальными?

6. Каким образом в языке С++ реализуется полиморфизм?

 

Тема 9. Абстрактные классы

 

Как правило, виртуальная функция объявленная в базовом классе, не выполняет никаких значимых действий. Это вполне обычная ситуация, поскольку часто в базовом классе законченный тип данных не определяется. Вместо этого в нем просто содержится базовый набор функций-членов и переменных, для которых в производном классе определяется все недостающее. Когда в виртуальной функции базового класса отсутствует значимое действие, в любом классе, производном от этого базового, такая функция обязательно должна быть переопределена. Для реализации этого положения в языке С++ поддерживаются так называемые чистые виртуальные функции (pure virtual function).

Чистые виртуальные функции (абстрактные методы) не определяются в базовом классе. В базовый класс включаются только прототипы этих функций. Для чистой виртуальной функции используется такая основная форма:

virtual тип имя_функции (список_параметров)=0;

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

Класс, содержащий хотя бы один абстрактный метод, называется абстрактным классом.

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

Предположим, что имеется абстрактный класс:

 

class Base

{ protected:

virtual void f (char)=0;

void func(int);

};

На основе абстрактного класса Base можно по-разному построить производные классы:

 

class Der1: public Base

{... void f(char); };

class Der2: public Base

{... void func(int); };

 

В классе Der1 абстрактный метод f() заменен конкретной виртуальной функцией того же типа. Функция Base::func() наследуется классом Der1 и доступна в нем и в его методах. Класс Der1 не абстрактный. В классе Der2 переопределена функция Base::func(), а виртуальная функция Base::f() унаследована. Тем самым класс Der2 становится абстрактным и может использоваться только как базовый.

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

Механизм абстрактных классов разработан для представления общих понятий предметной области, которые в дальнейшем предполагается конкретизировать. Эти общие понятия обычно невозможно использовать непосредственно, но на их основе можно, как на базе, построить частные производные классы, пригодные для описания конкретных объектов. Например, из абстрактного класса «Фигура» можно сформировать класс «Треугольник», «Окружность» и так далее.

Сформируем наиболее важные правила работы с абстрактными методами и классами:

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

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

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



Поделиться:


Последнее изменение этой страницы: 2016-12-13; просмотров: 377; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.139.81.58 (0.347 с.)