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



ЗНАЕТЕ ЛИ ВЫ?

Тема 4. Друзья класса. Перегрузка стандартных операций для классов

Поиск

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

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

# include < iostream. h>

class Class1; //предварительное объявление класса

class Clas s 2

{ public: void ff (Class1& ref);};

class Class1

{ private: int x;

// для дружественных функций и классов спецификатор доступа

// значения не имеет

friend int main (void); // main – может быть дружественной функцией

friend void f (Class1& ref);

friend class Class3;

friend void Class2::ff (Class1& ref);

}; // конец определения Class1

class Class3 //Class3 – дружественный классу Class1

{ public: void func (Class1& ref)

{ref.x=9; return; }

void Class2::ff (Class1& ref)

{ref.x=11; return;}

// f- обычная внешняя функция, дружественная классу Class1

void f (Class1& ref)

{ref.x=7; return;}

int main (void)

{

Class1 obj1;

obj1.x=5;

cout <<”obj1.x”<<obj1.x<< endl; //x=5

f(obj1);

cout <<”obj1.x=”<<obj1.x<< endl; //x=7

Class3 obj3;

obj3.func(obj1);

cout <<”obj1.x=”<<obj1.x<< endl; //x=9

Class2 obj2;

obj2.ff(obj1);

cout <<”obj1.x=”<<obj1.x<< endl; //x=11

return 0;

}

 

Особенности дружественной функции:

· дружественная функция не получает автоматически указатель this на объект класса, поэтому в ее вызове нужно обязательно указывать в качестве аргумента объект класса, с которым она должна работать;

· главная функция консольного приложения (main) может быть дружественной кнассу.

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

 

Перегрузка операций для классов

 

Большинство операций @ языка С++ может быть перегружено для работы с объектами пользовательских типов. Такая возможность существует и для объектов – классов. Перегрузка операции @ осуществляется с помощью функции-операции operator @, которую можно сделать либо членом класса, либо дружественной функцией(не желательно), либо обычной(глобальной) функцией. В двух последних случаях функция-оператор должна принимать хотя бы один аргумент, имеющий тип класса, указателя или ссылки на класс. Для внешней функции-операции следует учитывать доступность членов соответствующего класса.

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

Существующие правила перегрузки операций, приведения типа, вызова функции изложены в международном стандарте ISO/ IEC 14882.

 

Конструктор копирования. Поверхностное и глубинное копирование.

 

Класс может содержать специального вида конструктор – конструктор копирования. Конструктор копирования получает в качестве единственного параметра ссылку на объект этого же класса:

Classid:: Classid (const Classid& obj)

{тело конструктора}

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

· при определении нового объекта с типом класса с инициализацией его другим существующим объектом того же типа;

· при передаче в метод класса параметра-объекта с типом класса по значению;

· при возврате из метода класса значения объекта с типом класса с помощью оператора return.

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

· конструктор, выполняющий поверхностное копирование (происходит копирование указателя на объект, а копия объекта в динамической памяти не создается);

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

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

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

Для достижения указанной цели достаточно придерживаться следующих правил:

· вместо передачи в метод класса параметра с типом класса по значению следует этот параметр передавать по ссылке, а для предотвращения модификации соответствующего аргумента параметр функции надо снабдить модификатором const;

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

 

Основная литература - 5[гл.9,315-335], 7[гл.1,22-27,28-55], 8[гл. 5, 6]

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

1. Каковы правила перегрузки операций языка С++?

2. Когда вызывается конструктор копирования?

3. Как различить перегрузку префиксных и постфиксных операторов?

4. Можно ли перегрузить операцию суммирования для операндов типа short int?

5. Напишите объявление класса Circle с единственным членом-данным – Radius. В классе должны использоваться конструктор умолчания и деструктор, а также методы для чтения и записи значения Radius.

 

Тема 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 может уже не использоваться.

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



Поделиться:


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

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