
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf770 |
Часть IV # Расширенное использование С'^-^ |
Та же идея используется, когда методы общего класса реализуются за фигурными скобками области действия класса. Эти функции являются видовыми функция ми — они должны работать с любым типом, если он определяется как фактиче ский тип. (Конечно, функция может ожидать некоторых свойств от объектов фактического типа, например, способности поддерживать операции присваива ния, которые зависят от реализованного алгоритма функции.)
void push(const Туре&); |
/ / плохо, вне фигурных скобок класса |
Из-за прототипа функции могут возникнуть проблемы. Во-первых, он должен указывать, что эта функция принадлежит к классу Stack. Во-вторых, следует об ратить внимание на то, что идентификатор Туре является параметром типа, определяемым позже, а не именем типа, заданного ранее. Если это не сделать, компилятор сообш^ит, что Туре не определен.
C + + интерпретирует функции-члены шаблонных классов как шаблонные функции. Определение (или объявление) шаблонной функции начинается с заре зервированного слова template, за которым в угловых скобках следует список параметров шаблона. Каждый компонент списка параметров шаблона включает ключевое слово class, за которым располагается идентификатор, определяюи;ий имя параметра типа.
template <class Туре> |
/ / |
список |
параметров шаблона |
void Stack::push(const Туре&); |
/ / |
лучше, |
но недостаточно хорошо |
Теперь компилятору известно, что идентификатор Туре — это имя параметра типа. Он будет ожидать имя фактического типа в реализации объекта класса. Еш,е одна проблема связана с именем класса. Используется имя Stack, но в программе нет такого класса. Имя Stack обозначает шаблонный класс, а не класс. Для ком пилятора имя Stack при отсутствии какого-либо спецификатора не определено. Он должен знать типы компонентов, имеюш^ихся в Stack.
Что надо сообш^ить компилятору? Каким станет тип Stack, неизвестно, по скольку на момент определения класса, он может быть любого типа. Фактический тип станет известен в момент реализации. Вы знаете лишь предположительно, каким он может быть. Он будет соответствовать типу, определенному в списке
параметров функции. Тем не менее это тип, |
который должен быть определен |
|
в угловых скобках после имени типа. |
|
|
template <class Туре> |
/ / |
список параметров шаблона |
void Stack<Type>::push(const Туре&); |
/ / |
это достаточно хорошо |
При использовании шаблонных классов помните об этой концепции. Подобный прототип функции строится в соответствии с теми же правилами, что и любой другой прототип функции в C+ + . Он определяет типы параметров функции (в данном случае Туре) и классы, к которым эти функции принадлежат (здесь Stack<Type>).
Список параметров шаблона должен повторяться для каждой функции-члена, определенной вне спецификации класса.
template <class Туре> |
|
/ / |
список параметров шаблона |
|||
void |
Stack<Type>::push (const Type& c) |
/ / |
префикс шаблона |
|||
{ |
i f (top |
< size) |
|
/ / |
обычный случай: занесение шаблона |
|
|
items[top++] = c; |
|
|
|||
|
else |
|
|
/ / восстановление после переполнения стека |
||
|
{ Type *р = new Type[size*2]; |
/ / |
будет использован фактический тип |
|||
|
i f |
(р - |
0) |
|
|
|
|
|
{ cout |
« |
"Out of memory\n"; |
e x i t ( l ) ; J |
|
|
for |
(int |
i=0; |
i < size; i++) |
|
|
|
|
p [ i ] |
= items[i]; |
/ / |
копирование существующих данных |
|
|
|
Глава 17 • Шаблоны как еще одно средство проектирования |
773 "I |
|||||||
|
bool isEmptyO const; |
|
|
|
// стек пуст? |
|
|||||
|
""Stack О ; |
|
|
|
|
|
// возвращение памяти динамически распределяемой области |
||||
} |
|
|
|
|
|
|
|
|
|
|
|
template <class Туре> |
|
|
|
size(sz),top(0) |
|
||||||
Stack<Type>: : Stack (int sz = 100) |
|
||||||||||
{ |
items = new Type[sz]; |
|
|
// выделение динамически распределяемой области памяти |
|||||||
|
if (items==0) |
|
|
|
|
|
|
} } |
|
||
|
{ cout « |
"Out ofmemoryXn"; exit(l); |
|
||||||||
template <class Т> |
|
|
|
|
|
|
|
||||
void Stack<T>::push (const T& c) |
// обычный случай: занесение символа |
|
|||||||||
|
{ if (top < size) |
|
|
|
|
|
|||||
|
items[top++] = c; |
|
|
// восстановление после переполнения стека |
|
||||||
|
else |
|
|
|
|
|
|
|
|||
|
{ T *p = new T[size*2]; |
|
// получить дополнительную память в динамически |
|
|||||||
|
|
if (р ==0) |
|
|
|
|
// распределяемой области |
|
|||
|
|
|
|
|
|
// проверка науспех |
|
||||
|
|
|
{ cout « |
"Out of memory\n"; exit(l); } |
|
||||||
|
|
for (int i=0; i < size; i++) // копировать существующий стек |
|
||||||||
|
|
|
p[i] = items [i]; |
|
// возвращение памяти динамически распределяемой области |
||||||
|
|
delete [] items; |
|
|
|
||||||
|
|
items = p; |
|
|
|
|
// присоединение новой памяти |
|
|||
|
|
size *= 2; |
|
|
« |
|
// корректировка размера стека |
|
|||
|
|
cout « |
"New size: |
size « |
endl; |
|
|||||
|
|
items [top++] = c; } |
} |
// занесение символа в вершину |
|
||||||
template <class Type> |
|
|
|
|
|
|
|
||||
Type Stack<Type>::pop() |
|
|
|
// безусловное извлечение |
|
||||||
{ return items[-top]; } |
|
|
|
|
|||||||
template <class Tp> |
|
|
|
|
// что-либо извлечь |
|
|||||
bool Stack<Tp>::isEmpty() const |
|
||||||||||
{ |
return top ==0; } |
|
|
|
|
|
|
|
|||
template <class Type> |
|
|
|
|
|
|
|
||||
Stack<Type>: : ""Stack ( ) |
|
|
|
// возвращение памяти динамически распределяемой области |
|||||||
{ delete [] items; } |
|
|
|
|
|||||||
int mainO |
|
|
|
|
|
|
|
|
|
||
|
Point data [5] |
; |
data[1] .set(3, 4); data[2].set (5, 6); |
|
|||||||
|
data[0].set(1, 2) |
|
|||||||||
|
data[3].set(7, 8) |
data[4].set(9, |
0); |
|
|||||||
|
Stack<Point> s(4) |
|
|
|
|
// объект Stack |
|
||||
|
int n = sizeof (data)/sizeof |
(Point); |
// количество компонентов |
|
|||||||
|
cout « |
"Initial data: "; |
) |
|
|
|
|
||||
|
for (int j = 0; j < n; j++ |
|
// вывод входных данных |
|
|||||||
|
{ cout « data[j] « |
" "; } |
|
||||||||
|
cout « |
endl; |
|
|
i++) |
|
|
|
|
||
|
for (int i = 0; i < n; |
|
// занесение данных в стек |
|
|||||||
|
{ s.push(data[i]); } |
|
|
|
|||||||
|
cout « |
" Inversed data: "; |
|
|
// извлечение, пока стек не опустеет |
|
|||||
|
while ( ! s.isEmptyO) |
|
|
|
|
||||||
|
cout « |
s.popO « " "; |
|
|
|
|
|
||||
|
cout « |
endl; |
|
|
|
|
|
|
|
|
return 0 ;
}
I 774 |
Часть IV # Росширенное использование С4*Ф |
Имена функций-членов становятся доступными для использования в клиентской программе, как только объекту класса приписываются конкретные значения аргу ментов типа. Когда функция-член отправляется как сообщение объекту конкрет ного класса, имя функции в клиентской программе определяется без префиксов.
Stack<Point> s(4); |
|
/ / |
объекту присвоено |
конкретное значение |
for (int i = 0; i < n; |
i++) |
|
|
|
{ s.push(clata[i]); |
} |
/ / |
спецификаторы типа |
отсутствуют |
Это справедливо для любой клиентской программы — нет указаний, что сообще ния отправляются объектам шаблонных классов, а не регулярным классам. Внут ри определения класса ситуация другая. За именем класса может либо следовать, либо нет список параметров в квадратных скобках. Например, для реализации контейнера в виде связного списка класс-узел будет шаблоном, содержащим указатель на следующий узел. Следовательно, определение класса-узла должно использовать имя класса для своего собственного компонента класса.
template <class Т> |
|
|
struct Node { |
/ / |
общедоступные данные |
Т item; |
|
|
Node *next; |
/ / |
поле next указывает на Node |
Node(const Т&); |
/ / |
конструктор |
} ; |
|
|
Здесь имя параметра не используется в предположении, что компилятор поймет, что следующее поле соответствует определяемому классу. Более строгий подход предполагает, что класс Node не существует, ес/ш не определен тип компонентаузла.
template <class Т> |
|
|
struct Node { |
/ / |
общедоступные данные |
Т item; |
|
|
Node<T> *next; |
/ / |
поле next указывает на Node<T> *next |
Node(const Т&); |
/ / |
конструктор |
} ; |
|
|
С точки зрения программной инженерии второй вариант класса Node лучше первого. Однако компилятор должен скомпилировать каждую версию без затруд нений.
Вспомним, что реализация каждого класса генерирует отдельный экземпляр кода объекта класса. В зависимости от версии компилятора код объекта может помещаться в отдельный объектный файл, который позже связывается с другими файлами объектного кода. Как следствие этого — исходные файлы и объектные файлы не соответствуют друг другу. Существуют объектные файлы, происхожде ние которых с трудом устанавливает проектировщик.
Распространение реализаций шаблонов может увеличить время компиляции и компоновки, а также размер объектной программы. Некоторые компиляторы могут предложить способы возможного уменьшения этого негативного влияния.
Когда функции-члены шаблонов встраиваются в определение класса, не требуется определять префикс шаблона и оператор явного задания с именами параметров.
template <class Туре> |
|
|
class Stack { |
|
|
Type *items; |
/ / |
стек элементов типа Type |
int top, size; |
/ / |
текущая вершина, общий размер |
Stack(const Stack&); |
|
|
operator = (const |
Stack&); |
|
Глава 17 • Шаблоны кок еще одно средство проектирования |
775 |
||||||
public: |
|
|
|
|
|
|
|
Stack(int sz = 100) : size(sz),top(O) |
// конструктор преобразования |
||||||
{ |
items = new Type[sz]; |
// выделение памяти в динамически |
|
||||
|
if (items==0) |
// распределяемой области |
|
||||
|
|
|
|
|
|
||
|
{ cout « |
"Out ofmemory\n"; exit(l); } } |
|
|
|||
void push(const Type& c) |
// занесение в вершину стека |
|
|||||
{ |
if (top < size) |
// обычный случай: занесение символа |
|||||
|
items[top++] = с; |
// recover from stack overflow |
|
||||
|
else |
|
памяти |
||||
|
{ Type *p = new Type[size*2]; |
// получение дополнительной |
|||||
|
if (р ==0) |
// вдинамически распределяемой области |
|||||
|
// проверка науспешность выполнения |
|
|||||
|
{ cout « "Out ofmemory\n"; exit(l); } |
|
стека |
||||
|
for (int i = 0; i < size; i++) |
// копирование существующего |
|||||
|
p[i] = items[i]; |
// возвращение |
памяти динамически |
|
|||
|
delete |
[] items; |
|
||||
|
items = р; |
// распределяемой |
области |
|
|||
|
// присоединение |
новой памяти |
|
||||
|
size *=2; |
// корректировка |
размера стека |
|
|||
|
cout « |
"New size: " « |
size « |
endl; |
|
|
|
|
items[top++] = c; } } |
// занесение символа в вершину |
|
||||
Type рорО |
|
// извлечение |
верхнего символа |
|
|||
{ return items[-top]; } |
// безусловное |
выполнение |
|
||||
bool isEmptyO |
const |
// стек пуст? |
|
|
|
||
{ |
return top ==0: } |
|
|
|
|
|
|
"StackO |
|
// возвращение памяти динамически |
|
||||
|
|
|
// распределяемой |
области |
|
{delete [] items; }
};
Определение этого класса выглядит почти как обычное определение класса, где тип компонента объявляется оператором typedef (см. листинг 17.2).
Вложенные шаблоны
Шаблонный класс может использовать другие шаблоны в качестве его эле ментов данных. Например, шаблон-стек Stack<T> с компонентами класса Т может включать элемент данных шаблона типа List<T>. Компоненты списка должны быть того же самого типа, что и компоненты стека. Функции-члены шаблонастека могут посылать сообш.ения объекту шаблона-списка для реализаций опера ций со стеком.
Пусть шаблон-список обеспечивает такие операции, как insert_as_first() и remove_f irst(), добавляюш^ие компонент как первый элемент в список и удаляюш,ие первый компонент списка.
template |
<class Т> |
|
|
class List { |
|
|
|
public: |
|
|
|
void |
insert_as_first(const |
T& x); |
|
T remove_first(); |
|
|
|
bool |
emptyO; |
/ / |
список пуст? |
. . .} |
; |
/ / |
остальная часть класса List |
Часть IV » Расширенное использование С^-^-
Использование шаблона-списка как элемента данных стека упрощает реализа цию стека. О реализации динамического управления памятью заботится класс List. Не требуется включать конструкторы или деструкторы или беспокоиться о пере полнении памяти. Клиентская программа должна структурировать алгоритмы обработки стека так, чтобы избегать опустошения.
template <class Т> |
/ / |
тот же тип Т для Stack и List |
|
class Stack { |
|
|
|
List<T> 1st; |
|
/ / |
элемент данных шаблона |
public: |
|
|
|
void push(const |
Т&); |
/ / |
константа, ссылка на Т |
Т рорО; |
|
/ / |
возвращенное значение типа Т |
bool isEmptyO; |
} ; |
/ / н е требуется для деструктора |
Реализация функции-члена стека становится очень простой. Методы push() вызыва ют соответствующую функцию-список и полагаются на управление памятью в них.
template <class Т> |
|
void Stack<T>::push(const Т& item) |
|
{ lst . insert _ as _ first(item); } |
/ / передать работу в List |
Функции-члены стека рор() и isEmpty() передают работу функциям-членам списка.
template |
<class Т> |
|
|
Т Stack<T>::рор() |
/ / |
возвращение значения типа Т |
|
{ return |
1st. remove_first(); } |
/ / |
передать работу в список |
template |
<class Т> |
|
|
bool Stack<T>::isEmpty() |
|
|
|
{ return |
Ist.emptyO; } |
/ / |
вызов подобной функции |
Когда клиентская программа реализует объект Stack<int>, функции-члены будут следующими:
void Stack<int>::push(const |
int& item) |
{ 1st. insert_as_first(item); |
} |
int Stack<int>::pop() |
/ / возвращение значения-типа Т |
{ return 1st.remove_first(); |
} |
Когда клиентская программа объявляет экземпляр объекта стека, например Stack<int>, процесс реализации повторяется, и компилятор формирует объектный код для List<int>. В свою очередь, объект-шаблон класса List<int> может потре бовать другие шаблоны.
Если объект класса List<int> не может быть реализован, реализация стека приводит к ошибке реализации. Это может быть либо из-за ошибки в шаблонесписке, либо недопустимой операции над параметром шаблона (например, для класса не определяется сравнение, или операция класса применяется к встроенно му параметру). Отладка шаблонных классов становится труднее отладки обычных классов.
Шаблонные классы с несколькими параметрами
в предыдуш,их примерах использовались шаблонные классы только с одним параметром типа, хотя и упоминалось, что они могут иметь несколько параметров типов.
Это могут быть параметры типов, подобные рассмотренным в предыдуш^их примерах, или параметры, которые имеют сходство с обычными параметрамизначениями в функциях. За счет использования параметров и совокупности пара метров типов и параметров выражений повышается гибкость шаблонов С + 4-. В то же время появляются дополнительные синтаксические сложности.
Глава 17 • Шаблоны как еще одно средство проектирования |
777 |
Несколько параметров типов
Рассмотрим шаблонный класс с более чем одним параметром типа. Имена таких параметров должны использоваться в рамках класса для его элементов данных, локальных переменных или параметров методов. При реализации объекта клиентская программа предоставляет имена фактических типов. В СН-+ использу ется принцип передачи параметров. Первый параметр, определенный клиентской программой, соответствует первому параметру, заданному в шаблонном классе, и т.д.
Определяя список параметров типов в шаблонном классе, следует повторить зарезервированное слово class для каждого параметра типа. Вот пример шаблон ного класса для словарных статей. У класса имеются два компонента: ключевой компонент и компонент информационного наполнения. Они могут быть компо нентами произвольных классов в той степени, в которой они поддерживают присваивание и конструкторы копирования.
template <class Key, class Data> |
|
|
|
||||||
class DictEntry |
{ |
|
|
|
|
|
|
||
Key key; |
|
|
|
|
|
/ / |
поле ключа |
|
|
Data info; |
|
|
|
|
/ / |
поле информации |
|
||
public: |
|
|
|
|
|
|
|
|
|
DictEntry |
0 |
{ |
} |
|
|
/ / |
пустой конструктор по умолчанию |
||
DictEntry(const Кеу& к, |
const |
Data& d) |
|
|
|||||
: key(k), info(d) |
{} |
//инициализация компонентов данных |
|||||||
Key getKeyO |
const |
|
|
|
|
|
|
||
{ return key; |
|
} |
|
|
/ / |
возвращение значения |
ключа |
||
Data getlnfoO |
const |
|
|
|
|
|
|||
{ return info; |
} |
|
|
/ / |
возвращение значения |
информации |
|||
void setKey(const |
Key& k) |
|
|
|
|
||||
{ key = k; |
} |
|
|
|
|
/ / |
установить |
значение |
ключа |
void setInfo(const |
Data& d) |
|
|
|
|
||||
{ info = d; |
} |
|
|
|
|
/ / |
установить |
значение информации |
|
} ; |
|
|
|
|
|
|
|
|
|
Это простая структура, поскольку набор функций get() и set() для каждого клас са поля данных можно исключить, а поля данных сделать обш^едоступными — качество структуры останется без изменений. Но это не важно. Пример проде монстрирует использование нескольких параметров типов.
Такой класс словарных статей реализует два объекта, которые могут обраба тываться как пара. Например, объект данного класса можно возвратить из вызова функции клиентской программой. Это прош,е, чем передача указателя или ссылки объекту, созданному клиентской программой. Это удобнее для реализации связан ных массивов или словарей, в которых объект-ключ служит индексом для поиска связанных значений информационного поля. Чтобы алгоритм поиска работал, класс Key должен поддерживать операцию сравнения дополнительно к оператору присваивания и конструктору копирования.
Отсутствует необходимость обеспечения деструктора DictEntry, поскольку сам класс не управляет динамически своей памятью (элементами данных key и info). Если любой из компонентных классов (Key или Data) обрабатывает дополнитель ные ресурсы и включает деструктор, он будет вызываться автоматически в про цессе разрушения объекта словарной статьи. Это аналогично последней версии класса Stack, реализованного с элементом данных List. В классе List суш^ествует деструктор, но для этой версии класса Stack он не требуется.
Для реализации объекта класса DictEntry клиентская программа должна опре делить два фактических типа: один для поля ключа и второй для поля данных.
|
|
|
Глава 17 • Шаблоны как еще одно средство проектирования |
779 |
|||
DictEntry(const Кеу& к, const Data& d) |
// инициализация |
полей данных |
|
||||
|
|
: кеу(к), info(cl) {} |
|
|
|||
Key getKeyO const |
|
// возвращения значения key |
|
||||
{ return key; } |
|
|
|||||
Data getlnfoO const |
|
// возвращения значения info |
|
||||
{ return info; } |
|
|
|||||
void setKey(const Key& k) |
|
// задание значения key |
|
||||
{ key = k; } |
|
|
|||||
void setInfo(const Data& d) |
|
// задание значения info |
|
||||
{ info = d; } |
|
|
|||||
} ; |
|
|
|
|
|
|
|
int mainO |
|
|
|
|
|
||
{ |
DictEntry<Point,char*> data[5]; |
|
|
|
|||
data[0].setKey(Point(1,2)); data[0].setInfo("Initial stage"); |
|
||||||
data[l].setKey(Point(3,4)); data[l].setInfo("Analysis"); |
|
|
|||||
data[2].setKey(Point(5.6)); data[2]. setInfo("Design"); |
|
|
|||||
data[3].setKey(Point(7,8)); data[3].setInfo("Implementation"); |
|
||||||
data[4].setKey(Point(9,0)); data[4].setInfo("Testing"); |
// рискованно |
|
|||||
int n = sizeof(data)/sizeof(DictEntry<Point,char*>); |
|
||||||
cout « |
"Associated Data:\n"; |
|
|
|
|
||
for (int j = 0; j < n; j++) |
|
|
|
|
|||
{ |
cout |
« |
data[j].getKey() « |
" " |
|
|
|
|
|
« data[j] . getInfo() « |
endl; } |
/ / вывод на печать |
входных данных |
|
|
cout « |
endl; |
|
|
|
|
||
return |
0; |
} |
|
|
|
|
В этом примере показано применение шаблонных классов с более чем одним параметром типа. Использование нескольких параметров вызывает вопрос о кон фликтах имен. Можно ли определить несколько шаблонных классов, которые ис пользуют одинаковое имя класса? Если у класса имеется только один параметр типа, компилятор не сможет решить, какой класс использовать, когда клиентская программа создаст экземпляр объекта класса.
А как насчет нескольких параметров? Можно ли определить шаблонные клас сы, предусматривая, что они имеют различное количество параметров типа?
Ответ — нет. Имена шаблонов класса не могут перегружаться. Программа содержит только один шаблон класса с указанным именем.
Шаблоны с параметрами — константные выражения
Как уже упоминалось, параметры шаблона также могут быть выражениями, а не типами. Эти выражения могут быть любого встроенного или определенного программистом типа. Тип указывается явно в определении шаблона (а не как параметр типа). Клиентская программа подставляет значение данного типа во время реализации объекта.
Ниже приведен пример шаблона Stack, в котором исходный размер массива в динамически распределяемой области памяти указывается как параметр шаб лона, а не как параметр конструктора.
template <class Type, int sz> |
/ / |
параметр-выражение |
class Stack { |
|
|
Type *items; |
/ / |
стек элементов типа Type |
int top, size; |
/ / |
текущая вершина, общий размер |
Stack(const Stack&); |
|
|
operator = (const Stack&); |
|
|