- •Чистые виртуальные функции и абстрактные классы
- •Производные классы с конструкторами и деструкторами
- •Виртуальные деструкторы
- •Обобщенный пример наследования
- •Модульность классов
- •Расширяющяся иерархия классов
- •Узловые классы
- •Множественное наследование классов
- •Шаблоны
- •Шаблоны функций
- •Перегрузка шаблонов функций
- •Шаблоны классов
- •Друзья Дружественные функции
- •Дружественные классы
- •Статические элементы класса
Шаблоны классов
Шаблонные классы предоставляют пользователям еще большие возможности, чем шаблонные функции, позволяя построить отдельные классы аналогично шаблонам функций. Имя шаблона класса должно быть уникальным, то есть перегрузка для шаблона класса не возможна (в отличие от шаблона функции). Шаблон класса задает параметризованный тип и обеспечивает скелет обобщенного класса для его последующей реализации с помощью типов данных, определенных пользователем. В качестве параметра при задании шаблона класса можно использовать не только тип, но и другие параметры, например, целочисленные значения, но они должны быть константными выражениями.
Класс, генерируемый из шаблона класса, является совершенно нормальным классом. Поэтому использование шаблона не подразумевает каких-либо дополнительных механизмов времени исполнения сверх того, что использовались бы для написания класса вручную.
Объявление шаблона класса имеет следующий общий вид:
template <class T>
class Anyclass
{ // закрытые, защищенные и открытые элементы класса
}; // точка с запятой обязательна, как при описании класса,
где Т – неопределенный тип (возможен любой идентификатор), который необходимо задать при использовании класса. Как и в случае шаблонных функций, Т в последствии можно заменить любым встроенным типом, другим классом, указателем и т.д. Anyclass – имя шаблонного класса.
В теле шаблона класса тип Т может использоваться для объявления данных-элементов, типов возвращаемых методами значений, параметров и прочих элементов пока еще неопределенных типов, например:
Т* ptr; // указатель типа Т
Как и шаблоны функций, шаблоны классов чаще всего объявляются в заголовочных файлах.
Параметры шаблонов.
Параметрами шаблонов могут быть: параметры-типы, параметры обычных типов, такие как int, и параметры-шаблоны. Естественно, у шаблонов может быть несколько параметров. Например:
template<class T, T val> class Buffer {/*...*/};
Как видно, параметр шаблона может быть использован для определения последующих параметров шаблона. Целые аргументы используются обычно для задания размеров и границ. Например:
template<class T, int n>
class Buffer
{ T v [n]; // массив типа Т
int sz; // переменная для размера массива
public:
Buffer (): sz (n) { } // конструктор с инициализацией размера
// ...
};
Объекты класса Buffer:
Buffer<char, 127> cbuf; // буфер для 127 символов
Buffer<Record, 8> rbuf; // буфер для 8 записей
Аргумент шаблона может быть константным выражением, адресом объекта или функции, или неперегруженным указателем на элемент. Указатель, используемый в качестве аргумента шаблона, должен иметь форму &of, где of является именем объекта или функции, либо в форме f, где f является именем функции. Указатель на элемент должен быть в форме &X::of, где of является именем элемента. В частности, строковый литерал не допустим в качестве аргумента шаблона. Целый аргумент шаблона должен быть константой:
void f (int i)
{ Buffer<int, i> bx; } // ошибка: требуется константное выражение
И наоборот, параметр шаблона, не являющийся типом, должен быть константой в теле шаблона, поэтому попытка изменения значения этого параметра приводит к ошибке.
Элементы шаблона класса объявляются и определяются так же, как и для обычного класса. Их можно определить как внутри, так и вне шаблона класса. Элементы шаблона класса в свою очередь являются шаблонами, параметризованными при помощи аргументов шаблона.
Пример 48.
Представим объявление шаблона класса Database, реализующего базу данных с небольшим числом записей.
template <class T> // шаблон класса с неопределенным типом
class Database // имя класса
{ T* rp; // указатель на записи
int num; // число записей
public:
Database (int n) // встроенный конструктор
{ rp = new T[num=n]; } // указатель на динамическую память
~Database() // встроенный деструктор
{ delete [ ] rp;} // очистка динамической памяти
void Donothing (); // прототип некоторой функции
T& Getrecord (int recnum); // прототип функции с параметром
// номера записи, возвращающей ссылку на объект типа Т
};
// описание функций шаблона класса:
// образец шаблона некоторой функции из шаблона класса Database:
template <class T> // шаблон типа Т
void Database <T>:: Donothing () // заголовок функции
{ // тело функции пустое
}
// описание функции-элемента шаблона класса Database:
template <class T> // шаблон типа Т
T& Database <T>:: Getrecord (int recnum) // метод получения записи
{ T* crp = rp; // текущий указатель
if (0 <= recnum && recnum < num) // проверка диапазона
while ( recnum-- > 0) // цикл, пока число записей > 0
crp++; // сдвиг указателя на одну запись
return *crp; // возврат указателя на запись
}
Комментарии:
Рассмотрим объявление шаблона класса Database более внимательно. Природа типа Т неизвестна, но уже можно описать указатель rp типа Т и конструктор выделяет динамическую память для массива объектов типа Т, присваивая адрес начала массива указателю rp и устанавливая элемент num равным требуемому числу записей. Деструктор должен удалить массив с указателем rp с помощью оператора delete [ ] rp.
Как и для обычных классов, методы шаблонного класса могут быть встраиваемыми, как конструктор и деструктор, либо могут описываться вне класса. Например, функция Donothing, которая не выполняет действий, демонстрирует формат описания метода шаблонного класса как шаблонной функции с оператором разрешения области видимости (Database <T>::).
В классе объявляется также метод Getrecord, возвращающий ссылку на объект типа Т, идентифицируемый номером записи recnum. Это еще один пример операции, не требующей знания реального типа объекта. В функции указатель rp типа Т присваивается указателю того же типа crp, который ссылается на первую запись, сохраненную в объекте класса Database. Оператор if проверяет, соответствует ли параметр recnum допустимому диапазону значений. Если да, то цикл while уменьшает параметр recnum до нуля и смещает указатель crp на одну запись размера sizeof (T) в базе данных. При компиляции вместо Т будет использован тип реальных объектов. Функция возвращает разыменованное значение указателя crp, то есть ссылку на любой объект, адресуемый crp.
Поскольку шаблонный класс Database и его шаблонные функции являются объявлениями, включим их в заголовочный файл db.h, чтобы воспользоваться им затем в основной программе.
Пример 49.
Заголовочный файл db.h используем для реализации действительного класса Record с целью получения базы данных четырьмя способами создания объектов класса с помощью шаблона класса. Программу сохраним в файле database.cpp в директории, где находится файл db.h:
# include<iostream.h>
#include<string.h>
#include"db.h"
// реальный класс Record для создания записей в базе данных:
class Record
{ char name{41}; // символьный массив – private
public:
Record () // конструктор по умолчанию
{ name[0] = 0;} // обнуление базы данных (БД)
Record ( const char* s) // конструктор заполнения БД
{ Assign(s); } // функцией копирования
void Assign (const char* s) // метод копирования строки s
{ strncpy(name, s, 40); } // как записи в БД
char* Getname () // метод возврата указателя
{ return name; } // на строку записи в БД
};
void main() // главная функция
{ int rn; // индекс числа записей
clrscr();
// создание объектов с помощью шаблона класса Database:
Database <Record> db (3); // БД из 3 записей типа Record
Database <Record*> dbp (3); // БД из 3 указателей типа Record
Database <Record> *pdb; // указатель на БД
Database <Record*> *ppdb; // указатель на БД указателей
// создание БД из записей типа Record:
cout << "Database of 3 Records:" << endl;
db.Getrecord(0). Assign ("A. Pushkin");
db.Getrecord(1). Assign ("M. Lermontov");
db.Getrecord(3). Assign ("L. Tolstoy");
for (rn = 0; rn < 3; rn++)
cout << db.Getrecord (rn).Getname() << endl;
// создание БД из указателей типа Record:
cout << "Database of 3 Record pointers:" << endl;
dbp.Getrecord(0) = new Record ("I. Repin");
dbp.Getrecord(1) = new Record ("V. Serov");
dbp.Getrecord(2) = new Record ("M. Vrubel");
for (rn = 0; rn < 3; rn++)
cout << dbp.Getrecord (rn)->Getname() << endl;
// создание указателя на БД записей типа Record:
cout << "Pointer to database of 3 Records:" << endl;
pdb = new Database <Record> (3);
pdb->Getrecord (0). Assign ("Rafael");
pdb->Getrecord (1). Assign ("Leonardo");
pdb->Getrecord (2). Assign ("Mikelangelo");
for (rn = 0; rn < 3; rn++)
cout << pdb->Getrecord (rn).Getname() <<endl;
// создание указателя на БД из указателей типа Record:
cout << "Pointer to database of 3 Record pointers:" << endl;
ppdb = new Database <Record*> (3);
ppdb->Getrecord(0) = new Record ("A. Einshtein");
ppdb->Getrecord(1) = new Record ("L. Landau");
ppdb->Getrecord(2) = new Record ("A. Sakharov");
for (rn = 0; rn < 3; rn++)
cout << pdb->Getrecord (rn)->Getname() << endl;
getch();
}
Комментарии к программе:
Объявлен класс Record, который программа использует для запоминания в Database. Класс Record очень прост, в нем объявлен в качестве элемента только символьный массив name. На самом деле вместо Record может быть любой произвольный класс, так как в шаблоне класса Database нет ограничений на тип объектов.
В функции используются четыре способа создания объектов с помощью шаблона класса. В объявлении
Database <Record> db (3);
определяется объект db с именем шаблонного класса Database и задается Record в качестве класса, замещающего Т в шаблоне. Выражение в скобках (3) – это инициализатор, передаваемый конструктору класса Database.
Вместо использования типа Record можно определить базу данных из 100 вещественных значений:
Database <double> dbd (100);
Поскольку класс Database написан для сохранения объектов произвольных типов, он называется контейнерным классом. Обычно, наилучшие контейнерные классы являются полностью обобщенными.
В функции следующее объявление
Database <Record*> dbp (3);
определяет объект dbp как указатель класса Database, состоящий из трех указателей на Record.
Третье объявление
Database <Record> *pdb;
определяет pdb как указатель на объект класса Database с незаданным числом объектов типа Record.
Последнее объявление
Database <Record*> *ppdb;
объединяет предыдущие два объявления для создания указателя на базу данных указателей, которые ссылаются на объекты типа Record.
В программе объект класса Database используется так же, как и любой другой не шаблонный объект. Например, для создания базы данных первым способом в операторе
db.Getrecord (0). Assign ("A. Pushkin");
объектом db вызывается функция Getrecord (0) для доступа к записи с индексом 0. Затем в строке вызывается метод Assign этого объекта для задания строкового значения.
В программе демонстрируется также использование других объектов класса Database. Представляет интерес второй способ заполнения базы данных с помощью указателя следующим оператором:
dbp.Getrecord (0) = new Record ("I. Repin");
Поскольку функция Getrecord возвращает ссылку на объект указателю dbp, она может использоваться в левой части оператора присваивания. В данном случае создается новый объект типа Record, а его адрес присваивается в базе данных указателю записи с индексом 0, ссылку на которую возвращает функция Getrecord.
В третьем способе для заполнения базы данных оператором
pdb = new Database <Record> (3);
создается указатель pdb типа Database на базу данных из трех объектов-записей класса Record, которые заполняются методом Assign с использованием оператора, например,
pdb->Getrecord (0). Assign ("Rafael");
Четвертый способ объединяет два предыдущих для создания указателя ppdb типа Database на массив указателей типа Record:
ppdb = new Database <Record*> (3);
С помощью указателей создаются новые объекты-записи типа Record:
ppdb->Getrecord(0) = new Record ("A. Einshtein");
Важно то, что во всех этих примерах не нужно информировать компилятор с помощью приведения типов о типах данных, используемых классом Database. Благодаря обобщенной природе шаблонов, можно избежать приведения типов и создавать классы, которые могут поддерживать множество различных типов данных.
Примечание.
Четыре примера объявлений Database из функции main можно использовать в качестве образца для создания объектов большинства типов шаблонов класса. Создание шаблонов с нуля – довольно трудная задача. Поэтому проще всего создать рабочий класс, который обрабатывает реальные данные, а затем преобразовать его в универсальный шаблон, заменив реальные типы данных метками-заполнителями <class T>.
