Для инстацирования шаблона (создания с его помощью объекта конкретного класса) используется следующая конструкция:
имя_шаблона <аргументы> имя_объекта [(параметры_конструктора)];
Аргументы должны соответствовать параметрам шаблона. Имя шаблона вместе с аргументами можно воспринимать как уточненное имя класса.
Пример: создание объектов по шаблону List (лекция 24 раздела 2).
List <int> List_int;
List <double> List_doub1e;
List <monstr> List_monstr;
Block <char, 128> buf;
Block <monstr, 100> stado;
При использозании параметров шаблона по умолчанию список аргументов может быть пустым, но при этом угловые скобки опускать нельзя:
template<class Т = char> class String:
String <>* p;
На месте формальных параметров, являющихся переменными целого типа, должны стоять константные выражения.
После создания объектов с помощью шаблона с ними можно работать так же, как с объектами обычных классов:
for (int i = 1; i<10; i++) List_double, add(i * 0.08);
List_double.print();
//--------------------
for (int i = 1; i<10; i++) List monstr.add(i);
List_monstr.print();
// ---------------------
strcpy(buf, "Очень важное сообщение");
cout << buf << endl;
Для упрощения использования шаблонов классов можно применить переименование типов с помощью typedef:
typedef List <double> Ldbl;
Ldbl List_double;
Специализация шаблонов классов
Каждая версия класса или функции, созданная по шаблону, содержит одинаковый базовый код, а изменяется только то, что связано с параметрами шаблона. При этом эффективность работы версий может сильно различаться.
Если для какого-либо типа данных существует более эффективный код, можно предусмотреть для этого типа специальную реализацию отдельных методов, либо полностью переопределить (специализировать) шаблон класса.
Для специализации метода требуется определить вариант его кода, указав в заголовке конкретный тип данных. Например, если заголовок обобщенного метода print шаблона List имеет вид:
template <class Data> void List <Data>::print();
специализированный метод для вывода списка символов будет выглядеть следующим образом:
void List <char>::print(){
... // Тело специализированного варианта метода print
}
Если в программе создать экземпляр шаблона List типа char, соответствующий вариант метода будет вызван автоматически.
При специализации класса после описания обобщенного варианта класса помещается полное описание специализированного класса. При этом требуется заново определить все его методы.
Пример: специализировать шаблон Block для хранения 100 целых величин.
class Block<int, 100>{
public:
Вlock (){р = new int [100];}
~Block (){delete [ ] p;}
operator int * ();
protected:
int * p;
};
Block<int, 100>::operator int *(){return р;}
При определении экземпляров шаблона Block с параметрами int и 100 будет задействован специализированный вариант.
Если параметром шаблона является другой шаблон, имеющий специализацию, она учитывается при инстанцировании:
template<class Т> class А{ // Исходный шаблон
int х;
};
template<class Т> class А<Т*> { // Специализация шаблона
long х;
};
template<template<class U> class V> class C{
V<int> y;
V<int*> z;
};
...
C<A> c;
Здесь V<int> внутри C<A> использует исходный шаблон, поэтому с.у.х имеет тип int, а V<int*> - специализацию шаблона, поэтому c.z.x имеет тип long.
Достоинства и недостатки шаблонов
Шаблоны представляют собой эффективное средство параметрического полиморфизма. В отличие от макросов препроцессора шаблоны обеспечивают безопасное использование типов.
Однако программа, использующая шаблоны, содержит полный код для каждого порожденного типа. Кроме того, с некоторыми типами данных шаблоны могут работать менее эффективно, чем с другими. В этом случае необходимо использовать специализацию шаблона.
Тема 2.12
Наследование
Наследование – механизм построения иерархии классов, путем их упорядочивания и ранжирования, т.е. объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.
Производные (находящиеся ниже по иерархии) классы, наследуя элементы и свойства базовых классов, могут дополнять или изменять их.
Множественное наследование позволяет одному классу обладать свойствами двух и более родительских классов.
Наследование позволяет справиться с проблемой управления большим количеством не связанных классов.
Ключи доступа
При описании класса в его заголовке перечисляются все классы, являющиеся для него базовыми. Возможность обращения к элементам этих классов регулируется с помощью ключей доступаprivate, protected и public:
class имя: [private | protected | public] базовый_класс
{ тело класса };
По умолчанию для классов используется ключ доступа private, а для структур - public.
Если базовых классов несколько, они перечисляются через запятую. Ключ доступа может стоять перед каждым классом:
class А {... };
class В {... };
class С {... };
class D: А, protected В, public С {... };
Ключи доступа определяют доступность полей базового класса в производном классе в соответствии с таблицей 2.1. При создании иерархии классов имеет смысл использовать спецификатор protected для полей класса. Для любого элемента класса, не входящего в иерархию, этот спецификатор равносилен private. Разница между ними проявляется при наследовании.
Таблица 2.1 – Доступ к полям базового класса при наследовании
Ключ доступа | Спецификатор в базовом классе | Доступ в производном классе |
private | private protected public | Нет private private |
protected | private protected public | Нет protected protected |
public | private protected public | Нет protected public |
Элементы базового класса со спецификатором private в производном классе недоступны вне зависимости от ключа. Обращаться к ним можно только через методы базового класса.
Элементы protected при наследовании с ключом private становятся в производном классе private, в остальных случаях права доступа к ним не изменяются. Доступ к элементам public при наследовании становится соответствующим ключу доступа.
Если базовый класс наследуется с ключом private, можно выборочно сделать некоторые его элементы доступными в производном классе, объявив их в секции public производного класса с помощью операции доступа к области видимости:
class Base{ …
public: void f();
};
class Derived: private Base{ …
public: Base::void f();
};
Простое наследование
Простым называется наследование, при котором производный класс имеет одного родителя.
Пример: создадим производный от класса monstr класс daemon, добавив способность думать.
enum color {red, green, blue};
// Класс monstr
class monstr{
// Скрытые поля класса:
int health, ammo;
color skin;
char *name;
public:
// Конструкторы:
Monstr (int he = 100, int am = 10);
monstr (color sk);
monstr (char * nam);
monstr(monstr &M);
// Деструктор:
~monstr() {delete [ ] name;}
// Операции:
monstr& operator ++(){++health; return *this;}
monstr operator ++(int){ monstr M(*this); health++: return M;}
operator int(){ return health;}
bool operator >(monstr &M){
if(health > M.health) return true;
return false;
}
const monstr& operator = (monstr &M){
if (&M == this) return *this;
if (name) delete [] name;
if (M.name){
name = new char [strlen(M.name) + 1];
strcpy(name, M.name);
}
else name = 0;
health = M.health; ammo = M.ammo; skin = M.skin;
return *this;
}
// Методы доступа к полям:
int get_health() const {return health;}
int get_ammo() const {return ammo;}
// Методы, изменяющие значения полей:
void change_health(int he){ health = he;}
// Прочие методы:
void draw(int x, int y, int scale, int position);
};
// Реализация класса monstr
monstr::monstr(int he, int am):
health (he), ammo (am), skin (red), name (0){ }
monstr::monstr(monstr &M){
if (M.name){
name = new char [strlen(M.name) + 1];
strcpy(name. M.name);
}
else name = 0;
health = M.health; ammo = M.ammo; skin = M.skin;
}
monstr::monstr(color sk){
switch (sk){
case red: health =100; ammo =10; skin = red; name = 0; break;
case green: health = 100; ammo = 20; skin = green; name = 0; break;
case blue: health = 100; ammo = 40; skin = blue; name = 0; break;
}
}
monstr::monstr(char * nam){
name = new char [strlen(nam) + 1];
strcpy(name, nam);
health = 100; ammo = 10; skin = red;
}
void monstr::draw(int x, int y, int scale, int position)
{ /* Отрисовка monstr */ }
// Класс daemon
class daemon: public monstr{
int brain;
public:
// Конструкторы:
daemon(int br = 10){brain = br;};
daemon(color sk): monstr (sk) {brain = 10;}
daemon(char * nam): monstr (nam) {brain = 10;}
daemon(daemon &M): monstr (M) {brain = M.brain;}
// Операции:
const daemon& operator = (daemon &M){
if (&M == this) return *this;
brain = M.brain;
monstr::operator = (M);
return *this;
}
// Методы, изменяющие значения полей:
void think();
// Прочие методы:
void draw (int х, int у, int scale, int position);
};
// Реализация класса daemon
void daemon::think(){ /*... */ }
void daemon::draw(int x, int y, int scale, int position)
{ /*... Отрисовка daemon */ }
В классе daemon введено поле brain и метод think, определены собственные конструкторы и операция присваивания, а также переопределен метод отрисовки draw.
Все поля класса monstr, операции (кроме присваивания) и методы get_health, get_ammo и change_health наследуются в классе daemon, а деструктор формируется по умолчанию.
Функциям производного класса daemon недоступны поля, унаследованные из базового класса monstr, т.к. они определены в нем как private.
Если функциям, определенным в производном классе, требуется работать с полями базового класса, можно:
· описать их в базовом классе как protected,
· обращаться к ним с помощью функций базового класса,
· явно переопределить их в производном классе (как в примере).
В классе daemon описан метод draw, переопределяющий метод с тем же именем в классе monstr. Таким образом, производный класс может не только дополнять, но и корректировать поведение базового класса. Доступ к переопределенному методу базового класса для производного класса выполняется через имя, уточненное с помощью операции доступа к области видимости (::).