4 семестр / Методические материалы / metodicheskie-ukazaniia-po-teme-29
.pdfStack& Stack::operator<<(int x) { // Добавление элемента в push(x); // стек операцией "<<". return *this;
}
Теперь добавлять элементы в стек можно так:
... |
// |
Содержимое |
стека (с |
вершины): |
s << 78 << 910 << 37; |
// |
11, 910, |
78, 345, |
12. |
3.2. Шаблон класса, реализующего стек
Предположим, что необходимо работать с двумя типами стеков: первый содержит целые числа типа int, а второй – указатели на символьные строки
(тип const char*).
Для описания класса такого строкового стека можно скопировать разработанный выше класс и в нем все места, где встречается тип данных int (выделены подчеркиванием в коде предыдущем примера) заменить на тип данных const char*. Новый класс строкового стека будет отличаться от класса целочисленного стека только этим изменением, остальной код полностью дублируется.
Избежать такого нежелательного и неоправданного дублирования кода позволяет использование шаблона класса:
template <class |
T> |
|
|
|
|
class Stack { |
|
|
|
|
|
struct Elem |
{ |
// Элемент стека: |
|
||
T value; |
|
// |
- |
информационное поле; |
|
Elem *next; |
// |
- |
указательное |
поле. |
|
}; |
|
|
Elem *top; // Поле класса |
- указатель на вершину стека. |
|
public: |
// |
Методы класса: |
Stack(): top(0) { } |
// |
- конструктор (обнуляет вершину); |
~Stack(); |
// - деструктор (освобождает память); |
|
void push(T x); |
|
// - поместить элемент x в стек; |
void pop(); |
|
// - извлечь элемент из стека; |
Stack<T>& operator<<(T x); |
// - перегрузка операции "<<". |
|
}; |
|
|
template <class T> |
|
|
void Stack<T>::push(T x) { |
|
// Добавление в стек элемента |
Elem *temp = new Elem; |
|
// со значением x типа int, |
temp->value = x; |
|
// выделяя динамическую память. |
temp->next = top; |
|
|
top = temp; |
|
|
} |
|
|
11
template <class T> |
|
void Stack<T>::pop() { |
// Удаление элемента из вершины стека, |
if (top) { |
// освобождая динамическую память. |
Elem *temp = top; |
|
top = temp->next; |
|
delete temp; |
|
} |
|
} |
|
template <class T>
Stack<T>& Stack<T>::operator<<(T x) { // Добавление элемента в
push(x); |
// стек операцией "<<". |
return *this; |
|
} |
|
template <class T> |
|
Stack<T>::~Stack() { |
// Деструктор освобождает всю выделенную |
while (top) { |
// память путем удаления всех элементов |
pop(); |
// стека с помощью метода pop. |
} |
|
} |
|
Обратите внимание: каждый метод, описанный вне класса, тоже описывается как шаблон (template <class T>). При этом везде к имени Stack добавляется параметр шаблона <T>, за исключением имени класса (после ключевого слова class), имен конструктора и деструктора. (В коде примера эти исключения выделены подчеркиванием.) Кроме этих трех исключений, везде,
где по смыслу предполагается имя типа, при использовании шаблона необходимо указывать параметры шаблона, чтобы получить тип.
При создании объектов стека необходимо после имени шаблона Stack указывать значение его параметра, например, <int> или <const char*> :
Stack<int> |
sint; |
// Содержимое |
стека sint: |
|
sint << 12 |
<< 345 << 67; |
// |
67, 345, |
12; |
sint.pop(); |
|
// |
345, 12. |
|
... |
|
|
|
|
|
|
|
|
Stack<const char*> sstr; |
// Содержимое стека sstr: |
|||||||
|
|
|
|
|
|
|
|
|
sstr << "abc" << "de" << "fghi"; |
// |
|
fghi, |
|
de, |
abc. |
|
|
|
|
|
|
|
||||
sstr.pop(); |
// |
|
de, |
abc. |
|
|
|
|
При компиляции создаются классы Stack<int> |
и Stack<const char*>. Это |
|||||||
различные классы, хотя они сгенерированы из одного шаблона.
3.3. Инстанцирование шаблонов классов
Процесс генерации класса по шаблону класса для конкретных значений параметров (аргументов) шаблона называется инстанцированием шаблона класса. Инстанцирование классов происходит во многом схоже с инстанцированием функций, но имеются и довольно существенные различия, связанные с более сложным устройством классов.
12
Для каждого шаблонного класса, имеющего уникальный набор значений параметров шаблона, компилятор выполняет инстанцирование только один раз. Если в программе создаются несколько объектов такого класса, то код объявления класса (как типа данных) генерируется лишь единожды.
Компилятор проводит инстанцирование шаблона класса неявно (при создании объекта класса) или явно (с помощью ключевого слова template). Инстанцирование возможно проводить только там, где доступна реализация методов шаблонного класса. Для инстанцирования класса одного его объявления не достаточно, необходимо иметь описание всех методов класса в той же единице трансляции.
Сначала рассмотрим вариант неявного инстанцирования класса Stack при создании объекта этого класса из предыдущего примера. Пусть код шаблона класса Stack, код реализации всех его методов и код создания объектов этого класса с разными значениями параметра T (то есть с аргументами шаблона int и const char*) содержатся в одном файле (в одной единице трансляции):
main.cpp – основной файл: #include <iostream>
using namespace std;
template |
<class T> |
class Stack { |
|
... |
// Тело шаблона объявления класса (из предыдущего примера). |
}; |
|
template |
<class T> |
void Stack<T>::push(T x) { |
|
... |
// Тело шаблона метода push (из предыдущего примера). |
} |
|
template |
<class T> |
void Stack<T>::pop() { |
|
... |
// Тело шаблона метода pop (из предыдущего примера). |
} |
|
template |
<class T> |
Stack<T>& Stack<T>::operator<<(T x) { |
|
... |
// Тело шаблона метода, перегружающего операцию "<<" |
}// (из предыдущего примера).
template <class T>
Stack<T>::~Stack() {
... // Тело шаблона деструктора (из предыдущего примера).
}
int main() {
Stack<int> sint; // Неявное инстанцирование при создании
//объекта sint - целочисленный стек.
sint.push(12); |
// Вызов метода |
push. |
|
sint << 345; |
// |
Вызов метода, перегружающего операцию "<<". |
|
sint.pop(); |
// |
Вызов метода |
pop. |
...
Stack<const char*> sstr; // Неявное инстанцирование при создании
//объекта sstr - символьный стек.
sstr.push("ab"); // Вызов метода |
push. |
||
sstr << "cde"; |
// |
Вызов метода, перегружающего операцию "<<". |
|
sstr.pop(); |
// |
Вызов метода |
pop. |
... |
|
|
|
}
13
Здесь инстанцирование происходит неявно, когда компилятор в main.cpp находит код создания каждого из двух объектов шаблонного класса Stack.
При создании объектов в ранних стандартах языка Си++ шаблонный класс (в отличие от шаблонной функции) нельзя использовать, не указывая значения параметров (то есть аргументов) шаблона. Только начиная со стандарта языка
C++17 поддерживается возможность автоматического выведения аргументов шаблона конструкторов класса. Поэтому если для разработки важна переносимость кода между версиями компилятора языка Си++, то не следует пользоваться этой возможностью.
Теперь рассмотрим вариант явного инстанцирования класса Stack с помощью ключевого слова template. Создадим модуль для класса Stack, который состоит из заголовочного файла и отдельного файла с реализацией:
объявление шаблона класса Stack выносится в заголовочный файл,
описание реализации шаблона класса помещается в файл реализации,
создание класса находится в основном файле кода программы.
Stack.h – заголовочный файл модуля:
template |
<class T> |
|
|
|
class Stack { |
// Объявление шаблона класса. |
|||
struct Elem { |
// Элемент стека: |
|||
|
T value; |
// - информационное поле; |
||
|
Elem *next; |
// - указательное поле. |
||
}; |
|
|
|
|
Elem |
*top; |
// Поле класса - указатель на вершину стека. |
||
public: |
|
|
|
// Методы класса: |
Stack(): top(0) { } |
// - конструктор (обнуляет вершину); |
|||
~Stack(); |
|
|
// - деструктор (освобождает память); |
|
void |
push(T x); |
|
// - поместить элемент x в стек; |
|
void |
pop(); |
|
|
// - извлечь элемент из стека. |
Stack& operator<<(T x); // - перегрузка операции "<<". |
||||
}; |
|
|
|
|
Stack.cpp – файл с реализацией модуля: |
||||
#include |
"Stack.h" |
|
|
|
template |
<class T> |
|
|
|
void Stack<T>::push(T x) { |
|
|||
... |
// Тело шаблона метода push (из предыдущего примера). |
|||
} |
|
|
|
|
template |
<class T> |
|
|
|
void Stack<T>::pop() { |
|
|
||
... |
// Тело шаблона метода pop (из предыдущего примера). |
|||
} |
|
|
|
|
template |
<class T> |
|
|
|
Stack<T>& Stack<T>::operator<<(T x) { |
||||
... |
// Тело шаблона метода, перегружающего операцию "<<" |
|||
}// (из предыдущего примера).
template <class T>
Stack<T>::~Stack() {
... // Тело шаблона деструктора (из предыдущего примера).
}
template
class Stack<int>; // Явное инстанцирование класса для int.
14
main.cpp – основной файл: #include <iostream> #include "Stack.h"
using namespace std;
int main() {
Stack<int> sint; // Создание объекта sint (целочисленный стек)
//инстанцированного класса Stack<int>.
sint.push(12); |
// Вызов метода |
push. |
|
sint << 345; |
// |
Вызов метода, перегружающего операцию "<<". |
|
sint.pop(); |
// |
Вызов метода |
pop. |
...
Stack<const char*> sstr; // Создание объекта sstr с помощью
//конструктора пройдет успешно, но объектный код
//деструктора отсутствует, что приведет к ошибке. sstr.push("ab"); // Ошибка: объектный код метода push отсутствует.
sstr << "cde"; // Ошибка: объектный код метода "<<" отсутствует.
sstr.pop(); // Ошибка: объектный код метода pop отсутствует.
...
}
Создание объекта sint проходит успешно, поскольку класс Stack<int> явно инстанцирован в файле реализации модуля Stack.cpp. При работе с объектом sint доступны все методы класса Stack<int>.
Для класса Stack<const char*> явное инстанцирование не проведено, то есть объектный код методов этого класса не сгенерирован из шаблона. Поэтому при создании объекта sstr компилятор попытается выполнить неявное инстанцирование класса Stack<const char*> следующим образом.
Из заголовочного файла Stack.h компилятор получит описание шаблона класса Stack и подставит в него значение параметра T, соответствующее const char*, чтобы получить объектный код полей и методов класса. Внутри шаблона класса имеется полное описание только конструктора класса, хотя его тело пусто, но имеется инициализация поля top:
Stack(): top(0) { } // Конструктор из шаблона класса Stack.
Поэтому компилятор сможет сгенерировать объектный код конструктора класса Stack<const char*>::Stack(). Но описание остальных методов класса вынесено в отдельную единицу трансляции (в файл реализации модуля Stack.cpp), поэтому их объектный код компилятор сгенерировать не сможет.
В результате инстанцирование класса Stack<const char*> произойдет неявно, но объектный код будет сгенерирован не для всех его методов. Это приведет к сообщениям об ошибках компиляции при попытке вызова таких методов в коде программы.
3.4. Специализация шаблонов классов
Также, как для шаблонов любых других функций, для шаблонов методов класса можно задавать частные (специальные) значения параметров шаблона. При этом для таких специализированных случаев код методов генерируется отдельно.
15
В рассмотренном выше примере в стеке Stack<const char*> хранятся указатели на строки, а сами строки хранятся вне стека. Это нарушает принцип инкапсуляции содержимого класса, что может привести к ошибкам: например, если внешняя строка хранилась в блоке динамической памяти, и этот блок был освобожден, то элемент стека продолжит хранить адрес уже недоступного блока памяти. Для целочисленного стека Stack<int> такого не происходит, поскольку он хранит сами значения чисел, а не их адреса.
Чтобы решить эту проблему в строковом стеке станем хранить ссылки на копии помещаемых в стек строк, для которых выделяется отдельная динамическая память. При этом шаблоны методов push и pop необходимо специализировать для типа const char* :
template <class T> |
// Общий шаблон метода push. |
||||
void Stack<T>::push(T x) { |
|
|
|||
... |
// Тело шаблона метода push (из предыдущего примера). |
||||
} |
|
|
|
|
|
template <> |
// Специализированный метод push для const char*. |
||||
void Stack<const char*>::push(const char* x) { |
|||||
char *str = new char[strlen(x)+1]; |
// Выделение памяти и |
||||
strcpy(str, x); |
// копирование строки в выделенную память. |
||||
Elem *temp = new Elem; |
|
|
|||
temp->value = str; |
|
|
|||
temp->next = top; |
|
|
|||
top = temp; |
|
|
|
||
} |
|
|
|
|
|
template <class T> |
// Общий шаблон метода pop. |
||||
void Stack<T>::pop() { |
|
|
|||
... |
// Тело шаблона метода pop (из предыдущего примера). |
||||
} |
|
|
|
|
|
template <> |
// Специализированный метод pop для const char*. |
||||
void Stack<const char*>::pop() { |
|
||||
if (top) { |
|
|
|
||
|
Elem *temp = top; |
|
|
||
|
top = temp->next; |
|
|
||
|
const char *str = temp->value; |
// Получение адреса |
|||
|
delete [] str; |
// строки и освобождение памяти. |
|||
|
delete temp; |
|
|
|
|
}
}
Остальные методы шаблона класса Stack не нуждаются в специализации для типа const char*, поскольку их код не требует изменений.
В коде программы специализированные методы должны следовать после описания шаблонных методов, но не наоборот.
Для шаблонов классов тоже можно задавать частные (специальные) случаи, описывающие конкретные значения параметров шаблона (аргументы шаблона). Для такого специализированного шаблона класса код генерируется отдельно от остальных случаев, для которых значения параметров шаблона не заданы.
Допустим требуется, чтобы рассмотренный выше шаблон класса стека (Stack), для элементов типа char реализовывал работу со стеком не через линейный список, а через массив задаваемого размера, расположенный в
16
динамической памяти. Опишем специализированный шаблон стека Stack<char>, используя ключевое слово template с пустыми угловыми скобками <>:
#define |
MAXSIZE 512 |
|
|
|
template <> |
|
|
|
|
class Stack<char> { |
|
|
|
|
int |
maxsize; |
// Максимально возможный размер стека. |
||
int |
itop; |
// Индекс в массиве элемента-вершины стека. |
||
char *arr; |
// Указатель на массив, в котором |
|||
public: |
|
// |
|
размещается стек. |
Stack(int ms = 10); // Конструктор. |
||||
~Stack(); |
|
// Деструктор. |
||
int |
push(char x); |
// Поместить элемент x в стек. |
||
int |
pop(char& x); |
// Извлечь элемент x из стека и вернуть |
||
}; |
|
|
// |
его значение через параметр-ссылку x. |
Stack<char>::Stack(int ms) { // ms – максимальный размер стека. maxsize = (0<ms && ms<MAXSIZE) ? ms : MAXSIZE; // Проверка. itop = -1; // В стеке отсутствуют элементы.
arr = new char[maxsize]; // Выделение памяти для массива.
} |
|
|
|
|
Stack<char>::~Stack() { |
|
|||
delete [] arr; |
обождение памяти массива. |
|||
} |
|
|
|
|
int Stack<char>::push(char x) { |
// Добавление элемента в стек. |
|||
if (itop < maxsize-1) { // Есть места для элементов стека. |
||||
++itop; |
|
|
// Изменить индекс вершины стека. |
|
arr[itop] = x; |
// Добавить в стек новый элемент. |
|||
return 0; // Вернуть 0, если добавление прошло успешно. |
||||
} else |
|
|
|
|
return 1; |
// Вернуть 1, если добавление не выполнено. |
|||
} |
|
|
|
|
int Stack<char>::pop(char& x) { |
// Удаление элемента из стека. |
|||
if (itop > -1) { |
|
// Стек не пуст. |
||
x = arr[itop]; |
// Вернуть из стека значение вершины. |
|||
--itop; |
|
|
// Изменить индекс вершины стека. |
|
return 0; // Вернуть 0, если удаление прошло успешно. |
||||
} else |
|
|
|
|
return 1; |
// Вернуть 1, если удаление не выполнено. |
|||
} |
|
|
|
|
Обратите внимание: описания методов, вынесенные вне объявления класса, не являются шаблонами (не содержат параметров), поэтому перед ними не требуется указывать ключевое слово template.
Описание шаблона класса для частного случая, которое не зависит от параметров, называется полной специализацией шаблона.
17
3.5. Частичная специализация
Описание специализированного шаблона класса может само зависеть от параметров, этот случай называется частичной специализацией. При этом получившийся класс тоже является шаблоном класса (но уже другим).
Частичная специализация возможна только для шаблонов классов. Для шаблонов функций она запрещена, поскольку не сочетается с перегрузкой функций. Из-за этого также не допускается частичная специализация отдельных методов параметризованного шаблона класса.
Простейший случай частичной специализации: шаблон класса имеет несколько параметров, и при специализации задается значение не всех из них.
Например, первичный шаблон класса MyClass с двумя параметрами T и U:
template<class T, class U> |
// Первичный шаблон класса. |
class MyClass { |
|
... |
|
}; |
|
может быть частично специализирован только по второму параметру U со значением int:
template<class X> // Частичная специализация шаблона. class MyClass<X, int> {
...
};
Тогда этот частично специализированный шаблон станет использоваться для создания (инстанциации) классов вида MyClass<X,int>, например:
MyClass<int, int> a; |
|
|
MyClass<doudle, int> b; |
|
|
MyClass<const char*, int> |
c; |
|
MyClass<GeomFigure, int> |
d; |
// GeomFigure – имя класса. |
... // и т.д. |
|
|
Для всех остальных комбинаций двух параметров класс будет создаваться на основании первичного шаблона.
У частично специализированного шаблона класса список параметров в угловых скобках после ключевого слова template не может быть пуст. Если он оказался пустым, то описан полностью специализированный класс.
Все полные и частичные специализации шаблона класса должны быть объявлены в том же пространстве имен, что и первичный шаблон.
3.6. Сложные случаи частичной специализации
В общем случае частичная специализация шаблона класса – это некоторая конфигурация параметров первичного класса, которая служит аргументом специализированного шаблона.
18
Рассмотрим более сложный пример шаблона класса MyClass, который имеет только один параметр. Опишем специализированные шаблоны этого класса для указателей и ссылок:
template<class T> |
// Первичный шаблон класса. |
class MyClass { |
|
... // Реализация общего шаблона. |
|
}; |
|
template<class T> |
// Частичная специализация шаблона. |
class MyClass<T*> { |
|
... // Реализация специализированного шаблона для указателей.
}; |
|
template<class X> |
// Частичная специализация шаблона. |
class MyClass<X&> {
... // Реализация специализированного шаблона для ссылок.
};
Здесь каждая частичная специализация является шаблоном класса с параметром, на который задается указатель или ссылка. Причем параметр шаблона не является выражением, переданным шаблону в качестве аргумента, а выводится из конструкции аргумента на основе шаблона первичного класса.
Тогда при создании любых объектов с аргументами-указателями на различные типы всегда используется частично специализированный шаблон класса MyClass<T*> :
MyClass<int*> ap; |
// Все объекты созданы |
по одному |
|
MyClass<char*> bp; |
// |
специализированному |
шаблону |
MyClass<double*> cp; |
// |
класса для указателей MyClass<T*>. |
|
То же самое можно получить для объектов с аргументами-ссылками на различные типы, используя один шаблон MyClass<X&>:
MyClass<int&> ar; |
// Все объекты созданы |
по одному |
|
MyClass<char&> br; |
// |
специализированному |
шаблону |
MyClass<double&> cr; |
// |
класса для ссылок MyClass<X&>. |
|
19
