Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf780 |
Часть IV * Расширенное исоользованме С^^^ |
|
|||
|
public: |
|
|
|
|
|
StackO; |
Туре&); |
/ / |
конструктор no умолчанию |
|
|
void push(const |
/ / |
занесение |
в вершину стека |
|
|
Type popO; |
const; |
/ / |
извлечение |
верхнего символа |
|
bool isEmptyO |
/ / |
стек пуст? |
|
|
|
"StackO; |
|
/ / |
возвращение памяти динамически |
|
|
|
|
/ / |
распределяемой области |
|
|
} |
|
|
|
|
Когда создается экземпляр объекта этого класса, совсем недостаточно опреде лить фактический тип элементов массива. Необходимо указать размер массива.
Stack<int,4> s; |
/ / объект S стека |
Обратите внимание, что объект-стек сам не содержит каких-либо параметров. Конструктору по умолчанию они не требуются. Заметьте, что параметр должен передаваться по значению. Информация пересылается только в одном направле нии, от клиентской программы к объекту шаблона. Параметры-ссылки не допус каются. Для этой цели могут использоваться только обычные параметры функции.
Параметры — шаблонные выражения, по своей сути константы, хотя в опре делении параметра не используется модификатор const. Они не могут изменяться в коде шаблонных функций. Их фактические значения могут быть только констант ными выражениями. Не допускается использование переменной, не являюш^ейся константой, как фактического аргумента для реализации шаблона. При реализа ции в качестве фактических параметров разрешаются только литеральные значе ния или идентификаторы, помеченные как const.
int size = 4; const int |
length |
|
|
Stack<int,size> s; |
/ / |
синтаксическая |
ошибка: не константа |
Stack<int,length> s; |
/ / |
OK: константа |
во время компиляции |
Когда функции-члены шаблона реализуются за пределами определения класса,
всписке должны быть перечислены все параметры шаблона. Если класс-шаблон содержит параметры-выражения, последние должны быть перечислены как
всписке параметров шаблонов, так и в префиксе шаблона класса.
template <class Type, int sz> |
/ / |
параметр-выражение |
|
Stack<Type,sz>::Stack() |
/ / |
параметр-выражение |
|
: |
size(sz),top(O) |
|
|
{ items = new Type[sz]; |
/ / |
использование параметра-выражения |
|
i f |
(items==0) |
|
|
{ |
cout « "Out of memory\n"; |
e x i t ( l ) ; } } |
|
Подобно параметрам типа, параметры-выражения являются "заместителями". Их имена не важны до тех пор, пока они не противоречат функции. Они не должны противоречить различным функциям шаблонного класса. Приведем еш,е одну функцию-стек, в которой используются различные имена для параметра типа и для параметра-выражения.
template <class Т, int s> |
|
|
/ / |
разные имена для параметров |
|||
void |
Stack<T,s>::push |
(const |
T& c) |
|
/ / |
непротиворечивые имена параметров |
|
{ |
i f (top |
< size) |
|
|
|
/ / |
обычный случай: занесение символа |
|
items |
[top++] |
= c; |
|
|
|
|
|
else |
|
|
|
|
|
|
|
{ T *p = new T[size*2]; |
/ / |
получить дополнительную память |
||||
|
i f (р == 0) |
|
|
/ / |
в динамически распределяемой области |
||
|
|
|
|
|
|
||
|
{ |
cout « |
'Out of |
memory\n"; |
e x i t ( l ) ; } |
||
Глава 17 • Шаблоны как еще одно средство проектирования |
781 |
||
for (int i=0; i < size; i++) |
// копирование существующего стека |
||
p[i] = items[i]; |
// возвращение памяти динамически |
||
delete [] items; |
|||
items = р; |
// распределяемой области |
|
|
// корректировка размера стека |
|||
size *=2; |
|||
cout « "New size: " « size « |
endl; |
|
|
items[top++] = c; } } |
// занесение символа на вершину |
||
Подобно параметрам типа, параметры выражения должны перечисляться как в списке параметров шаблона, так и в префиксе имени класса для всех функ ций-членов. Даже если параметр-выражение не используется в теле функции, он все равно должен быть указан всписке.
template <class Type, int sz> |
// параметры не используются |
Type Stack<Type.sz>::pop() |
|
{ return items[-top]; }. |
|
template <class Tp, int s> |
// параметры не используются |
bool Stack<Tp,s>::isEmpty() const |
|
{ return top ==0; } |
|
template <class Type, int sz> |
|
Stack<Type,sz>::~Stack() |
// параметры не используются |
{ delete [] items; } |
Основная характеристика шаблонных классов с параметрами выражениями состоит в том, что каждый создаваемый экземпляр представляетдругой тип C + + .
Как разные типы они не совместимы, и объект одного типа не может использова
ться там, где ожидается объект другого типа.
Stack<int,4> |
s; |
// объект-стек |
Stack<int,8> |
s1; |
// несовместимый объект-стек |
Рассмотрим глобальную функцию DebugPrint(). У нее есть параметр класса Stack<int, 4>. Обратите внимание, что объект параметра передается по ссылке — закрытое объявление Stack<Type, sz> конструктора копирования предотвраш.ает передачу объектов-стеков по значению. Кроме того, заметьте, что параметр не помечен как const, поскольку он изменяется при выполнении функции.
void DebugPrint(Stack<int,4>& s) |
// модификатор const отсутствует |
|
{ Stack<int,4> temp; |
|
|
cout « |
"Debugging print: "; |
// извлечение, пока стек не опустеет |
while |
(! s. isEmptyO) |
|
{ int X = s.popO; temp.push(x); |
// сохранение во временном стеке |
|
cout « X « " "; } |
// печать каждого компонента |
|
cout « |
endl; |
// извлечение, пока стек не опустеет |
while (! temp. isEmptyO) |
||
{ S.push(temp.рорО); } } |
// восстановление исходного состояния |
|
Объекты стека s и s1 — это объекты разных типов. Объект s можно передать
как параметр в DebugPrint(). При попытке передать подобным образом объект s1 появляется синтаксическая ошибка.
DebugPrint(s); |
// |
OK |
DebugPrint(sl); |
// синтаксическая ошибка |
|
Фактические выражения, определяемые одним значением, эквивалентны.
const intlength = 4; |
// совместимо со Stack<int,4> |
Stack<int,length> s2; |
782 |
Часть IV * Расширенное использование 0-^+ |
Что касается только шаблонов с параметрами типа, все реализации с одинако выми фактическими параметрами типа бывают одного типа, и один объект может использоваться вместо другого. Рассмотрим повторно шаблонный класс с одним параметром типа.
template <class Туре> |
|
|
|
class Stack { |
|
|
|
Type *itemns; |
|
/ / |
стек элементов типа Type |
int top, size; |
|
/ / |
текущая вершина, общий размер |
Stack(const Stack& = 100); |
|
|
|
operator = (const Stack&); |
|
|
|
public: |
|
|
|
Stack(int); |
|
/ / |
конструктор преобразования |
void push(const |
Type&); |
/ / |
помещение на вершину стека |
Type рорО; |
|
/ / |
извлечение верхнего символа |
bool isEmptyO |
const; |
/ / |
стек пуст ? |
"StackO; |
/ / возвращение памяти динамически распределяемой области |
||
} ; |
|
|
|
Эти два объекта относятся к одному классу, поэтому один объект может исполь зоваться вместо другого.
Stack<int> |
stackl(20); |
/ / того же типа, что и другие объекты Stack<int> |
Stack<int> |
stack2(50); |
|
Это более гибкая и удобная реализация, чем реализация с использованием пара метра-выражения. Шаблонный класс с параметрами типа и параметром-конст руктором может выполнять все, что делает шаблонный класс с дополнительным параметром-выражением и без параметра конструктора. Кроме того, объекты различной исходной длины совместимы. Следует избегать шаблонов с параметра ми-выражениями, за исключением тех случаев, когда очевидны их преимуи;ества по сравнению с шаблонами с параметрами-классами.
Взаимосвязи между реализациями шаблонных классов
Реализации шаблонов могут использоваться как фактические параметры типа для реализации других шаблонных классов. Например, можно создать стек сло варных статей в следуюи;ем объявлении.
Stack<DictEntry<Point,char*> > stackofEntries; / / 100 записей
Обратите внимание на большой промежуток между двумя знаками "больше". Если не включить эти пробелы, компилятор поймет код неправильно и покажет огромное количество сообщений об ошибках, совершенно не относяш,ихся к делу. Ни одно из этих сообш^ений не подскажет, что требуются дополнительные пробе лы. Это второй случай, когда язык C++ учитывает пробелы. Они имеют значение при определении значения параметра по умолчанию для параметра типа указателя функции, когда имя параметра не используется.
В приведенном выше объявлении реализация Stack подсказывает реализацию DictEntry. Оптимизированный компилятор может занести код объекта в кэш память для повторного использования при выполнении компиляции. Если компи лятор этого не делает, время компиляции и компоновки увеличивается.
Реализации шаблонов для различных фактических типов (и значений выраже ний) обособлены, и между ними нет связи или доступа друг к другу.
Например, DictEntry<int, int> и DictEntry<float, recorcl> представляют собой два независимых индивидуальных класса. Такими же являются реализации д/ш Stack<int> и Stack<float>. Объекты этих типов не могут заменять друг друга.
Глава 17 • Шаблоны как еще одно средство проектирования |
783 |
Класс может объявить все свои реализации шаблонов как имеющие общий базо вый нешаблонный класс.
template <class Т>
class Stack : public BaseStack {
Все реализации класса Stack будут иметь доступ к объектам BaseStack в соот ветствии с правилами наследования. Эти реализации не имеют доступа к необще доступным компонентам друг друга.
Шаблонные классы как ^^друзья
Нешаблонный класс (или функцию) можно объявить "другом" всех реализаций шаблонного класса, если использование созданных экземпляров объектов не за висит от их типа:
template <class Т> class Stack {
friend class StackUser;
Здесь класс StackUser имеет доступ к необщедоступным компонентам любой реа лизации класса Stack независимо от типа, используемого в качестве фактического.
Шаблонный класс можно объявить другом нешаблонного класса, даже когда его параметр типа не связан с каким-либо фактическим значением.
class Node {
template <class Т> friend class Stack; int item;
Node *next; |
/ / Node *next' также OK |
Node(const int val) |
: item(val) |
{ next = NULL; } |
|
} ; |
|
Здесь класс Node может поддерживать информационное поле и связь со следую щим узлом в связанном списке. Для него не требуется никаких функций-членов. Необходим конструктор, который инициализирует оба поля данных.
Каждая реализация класса Stack является другом нешаблонного класса Node и имеет доступ к его необщедоступным компонентам. Это может быть полезным, если при вынесении за скобки общего кода уменьшается размер (и время компи ляции/компоновки) объектной программы. Вместо динамически распределяемой области памяти, выделенной при реализации (или при переполнении массива), класс Stack может выделять объект Node и освобождать верхний объект Node.
Однако это не слишком полезно. Node одного типа (например, с полем инфор мации целого типа) не сможет вместить объекты разных типов, которые клиент ская программа хотела бы поместить в стек. Следовательно, класс Node также должен быть шаблоном.
Кроме того, класс Node должен определяться как шаблонный класс. Тогда объекты стека разных типов смогут создавать экземпляры и осуществлять доступ к объектам класса Node разных типов с различными типами поля item.
template <class |
Туре> |
/ / |
шаблонный класс |
class Node { |
|
|
|
friend class Stack<T>; |
/ / |
компонент любого типа |
|
Type item; |
|
|
|
Node<Type> |
*next; |
/ / |
Node *next' также OK |
Node(const |
Type& val) : item(val) |
|
|
{ next = NULL; }
784 |
Часть IV • Расширенное использование C-f^ |
Технический термин для такого использования шаблонов — неограниченные типы. Параметр Туре не зависит от параметра Т и каждый параметр может при нимать любые фактические значения. Каждый созданный экземпляр класса Stack (например, типа float) имеет доступ к любой реализации класса Node (например, класса Point). Данная программа не реализует в полной мере модель реального мира.
Вам следует отобразить связанные реализации так, чтобы стек целых значений стал "другом" только узла целого типа, а не узла с другими типами поля item. Чтобы получить такое отображение, нужно связать "дружественный" (клиент ский) шаблонный класс (в данном случае Stack) с таким же типом, что и шаблон ный класс, который предоставляет сервисы (в данном случае Node).
template <class |
Туре> |
|
/ / |
шаблонный класс |
class Node { |
|
|
/ / |
компонент одинакового типа |
friend class Stack<Type>; |
||||
Type item; |
|
|
/ / |
Node *next', также OK |
Node<Type> |
*next |
; |
||
Node(const |
Type& val) : item(val) |
|
|
|
{ next = NULL; |
} |
|
|
|
Здесь для каждого созданного экземпляра Node конкретного типа (например, класса Point) реализация Stack для того же типа (класс Point) объявляется "другом" этой реализации класса Node.
Теперь у класса Stack имеется элемент данных указателя класса Node, который инициализируется в нулевое значение с помош,ью конструктора Stack. Когда в стек заносится следуюш^ий узел, этот указатель ссылается на следующий узел (а новый узел указывает на узел, который обычно является первым узлом в спис ке). Функция-член isEmptyO проверяет, является ли данный указатель нулевым и указывает ли он на узел. Следовательно, функция рор() должна установить такой указатель в нулевое значение при удалении стека последнего узла.
template <class Т> class Stack {
Node<T> *top; |
|
/ / |
Node *top; не допустима здесь |
|
public: |
|
|
|
|
StackO |
|
/ / |
по умолчанию: исходная длина отсутствует |
|
{ top |
= NULL; |
} |
|
|
void |
push(const T&); |
|
|
|
T popO; |
|
|
|
|
int isEmptyO |
const |
/ / |
указывает ли вершина на узел? |
|
{ return top == NULL; |
||||
"StackO; |
|
|
|
|
} :
Как и для любого шаблона, использование Node вне определения Node уточня ется списком параметров. Именно поэтому элемент данных top, принадлежаи|ий Stack, не может иметь тип Node*. Он должен быть типа Node<T>*, где Т является параметром типа в Stack. В результате определения Stack создание экземпляра объекта класса Stack ведет к автоматическому заданию поля данных класса Node того же самого типа.
Метод pushO класса Stack выделяет новый объект Node в динамически рас пределяемой области памяти. Вызов конструктора Node инициализирует поле item объекта Node в значение параметра push(). Следуюш.ее поле нового Node устанав ливается в указатель на узел, на который в настояш,ее время указывает поле top класса Stack, а поле top устанавливается в указатель на объект нового Node.
Глава 17 • Шаблоны кок еще одно средство проектирования |
785 |
||
template <class Т> |
|
|
|
void Stack<T>::push (const T&val) |
// тип Node<T>, неNode |
|
|
{ Node<T> *p = new Node<T>(val); |
|
||
if (p == NULL) |
|
|
|
{ cout « "Out of memory\n";exit(l); } |
|
||
p->next =top; |
/ / |
установить указатель |
на первый узел |
top = p; } |
/ / |
обозначает новый узел |
|
В push О не надо проверять переполнение массива, потому что в этой реализа ции массив отсутствует. Советуем уточнить, является ли успешным выделение объекта Node. Обратите внимание на тип указателя — это Node<T>*, а не Node*. Подобным же образом, когда оператор new запрашивает память в динамически распределяемой области, тип этого оператора Node<T>, а не Node. Тип Т задается при создании экземпляра Stack.
Метод рорО класса Stack устанавливает локальный указатель (типа Node<T>, а не просто Node) на первый узел стека, копирует поле информации в локальную переменную (типа Т), изменяет поле top так, чтобы оно указывало на второй узел, и удаляет узел top, поскольку он больше не нужен.
template <class Т> |
|
|
|
Т Stack<T>::рор() |
/ / |
возвращаемое значение типа Т |
|
{ Node<T> *р = top; |
/ / |
Node типа Т, а не Node |
|
Т val |
= top->item; |
/ / |
получить значение типа Т |
top = top->next; |
/ / |
обозначает второй узел |
|
delete |
p; |
/ / |
вернуть узел в динамически |
|
|
/ / |
распределяемую область памяти |
return |
val; } |
|
|
Когда рорО удаляет последний узел списка (второй узел для указания в поле top отсутствует), указатель top снова становится NULL. Когда в push() заносится пер вый узел, оператор p->next = top устанавливает это поле в NULL. Убедитесь, что функции-члены одного класса тесно связаны друг с другом через данные класса. Исходный код функций-членов должен быть скоординирован для того, чтобы убедиться в правильном взаимодействии функции.
Деструктор Stack должен сканировать связанный список остальных узлов и возвраш[,ать их в динамически распределяемую область памяти. Снова использу ется локальный указатель р типа Node<T> с компонентом типа Т. Он устанавлива ется в указатель на первый узел списка. Обратите внимание, что указатель на первый узел списка, элемент данных top, того же типа, что и указатель р: Node<T>. В цикле while указатель top перемеидается на следуюш.ий узел. Узел, на который указывал указатель р, удаляется, а указатель р смеш^ается на следуюш^ий узел.
template |
<class Т> |
|
|
Stack<T>: rStackO |
|
|
|
{ Node<T> *p = top; |
/ / |
тип Node типа T |
|
while |
(top != NULL) |
/ / |
если узлы отсутствуют, то top равен О |
|
{ top = top->next; |
/ / |
указатель на следующий узел |
|
delete p; |
/ / |
удалить предыдущий узел |
|
P = top; } } |
/ / |
проход к следующему узлу |
Преимуш,ество этого подхода состоит в том, что класс Node не зависит от класса Stack. Значит, класс Node может использоваться другими "дружественны ми" классами, например Queue и List. Поскольку все компоненты Node (включая конструктор) являются закрытыми в этой структуре, то не "дружественные" клиенты не могут создать или осуш,ествить доступ к объектам Node.
Другой подход состоит в обеспечении каждого клиента своим собственным сер верным классом. Если определение Node вложено в закрытую часть клиента, тогда этот клиент (и его "друзья") могут осуш^ествить доступ к классу Node.
