Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
274
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

I 760

 

 

 

Часть IV # Расширенное использование С^-г

 

 

 

 

 

 

 

 

 

обмен информацией межлу классом стека и его клиентами. Как же поступить

 

 

 

 

 

 

клиенту, если делается попытка извлечения из пустого стека? В большинстве

 

 

 

 

 

 

случаев (см., например, главу 8 и примеры в листингах 8.10—8.13) пустой стек

 

 

 

 

 

 

является сигналом для завершения одного этапа обработки и начала нового. Тем

 

 

 

 

 

 

не менее отсутствует необходимость вовлекать серверный класс в это решение,

 

 

 

 

 

 

связанное с приложением. Клиентская программа должна вызывать метод стека

 

 

 

 

 

 

isEmptyO перед каждым вызовом рор() и либо вызывать метод рор(), если стек не

 

 

 

 

 

 

является пустым, либо делать что-то еще. В листинге 17.1 алгоритм завершается.

 

 

 

 

 

 

Пустой стек означает, что надо закончить обработку.

 

 

 

 

 

 

 

 

Последние два метода, метод isEmpty() и деструктор Stack, являются три­

 

 

 

 

 

 

виальными. Метод isEmptyO проверяет, возвратился ли индекс стека в свою

 

 

 

 

 

 

исходную позицию. Деструктор возвращает память в динамически распределяе­

 

 

 

 

 

 

мую область, выделенную объекту на время его существования.

 

 

 

 

 

 

 

Объекты класса Stack могут использоваться только д/ш сохранения элементов

 

 

 

 

 

 

указанного типа, а не для всех операций. Эти объекты не предназначены для ини­

 

 

 

 

 

 

циализации друг друга или для присваивания одного другому. Можно выполнять

 

 

 

 

 

 

подобные операции с любыми переменными C+ + , включая объекты Stack. Если

 

 

 

 

 

 

кто-то использует объект Stack при инициализации или присваивании, это не

 

 

 

 

 

 

должно поддерживаться. Следовательно, добавление конструктора копирования

Initial data: abcdefghij

и оператора присваивания к классу Stack выходит за пределы

допустимого. С другой стороны, полезно объявить их прототипы

New size: 8

 

 

 

закрытыми. Например, если требуется передать объект Stack по

New size:

16

 

 

значению, возникнет синтаксическая ошибка.

 

Inversed data: Jihgfedcba

 

 

 

 

 

 

 

 

Для иллюстрации подобного подхода в листинге 17.1 первона­

Рис. 17.1.

 

 

 

чально выделяется небольшой массив для объекта Stack. Именно

программы

поэтому можно заметить отладочные сообщения, которые говорят

Вывод

для

 

об изменении размера массива. Вывод программы представлен

из листинга

17.1

 

 

 

 

 

 

 

 

на рис. 17.1.

 

 

 

Листинг 17.1. Класс Stack, содержащий символы

 

 

 

#inclucle

<iostream>

 

 

 

 

 

using

namespace std;

 

 

 

 

 

class

Stack

{

 

// стек символьных знаков

 

 

 

char

*items;

 

 

 

 

int

 

top,

size;

 

// текущая вершина, общий размер

 

 

 

Stack(const Stack&);

 

 

 

 

 

 

operator

= (const Stack&);

 

 

 

 

 

public:

 

 

 

// конструктор преобразований

 

 

 

Stack(int);

 

 

 

 

void

push(char);

// помещение в вершину

стека

 

 

 

char

popO;

 

// извлечение верхнего

символа

 

 

 

Pool isEmptyO

const;

// стек пуст?

 

распределяемой

области

}

"StackO;

 

 

// возвращение памяти динамически

 

 

 

 

 

 

 

 

 

 

 

Stack::Stack(int

sz = 100) : size(sz),top(O)

 

распределяемой

области

{

items = new char[sz];

// выделение памяти динамически

 

if

(items==0)

 

 

} }

 

 

 

 

{ cout «

"Out ofmemory\n"; exit(l);

 

 

 

void Stack::push

(char c)

// обычный случай: внесение символа

 

 

{ if (top < size)

 

 

 

 

items[top++] = с;

// восстановление после переполнения стека

 

 

else

 

 

 

 

 

 

 

{ char *р = new char[size*2];

// получение дополнительной

памяти для

 

 

 

 

 

 

 

 

 

// динамически распределяемой области

 

 

 

Глава 17 • Шаблоны как еще одно средство проектирования

761

 

 

if (р ==0)

 

 

// проверка успешного выполнения

 

 

 

{ cout «

"Out of memory\n"; exit(l); }

 

 

 

for (int i=0; i < size; i++)

// копирование существующего стека

 

 

 

p[i] = items[i];

// возвращение памяти динамически распределяемой области

 

 

delete [] items;

 

 

items = р;

 

// присоединение новой памяти

 

 

 

size *= 2;

size «

// корректировка размера стека

 

 

 

cout « "New size: «

endl;

 

 

 

items[top++] = c; } }

 

// внесение символа на вершину

 

char Stack::pop()

 

 

// безусловное извлечение

 

{ return items[-top]; }

 

 

bool Stack::isEmpty() const

 

// извлечь еще что-нибудь

 

{

return top ==0; }

 

 

 

 

Stack::~Stack()

 

// возвращение памяти динамически распределяемой области

{ delete [] items; }

 

int mainO

 

 

 

// заранее подготовленные входные данные

 

{

char data[] = "abcdefghij";

 

 

 

Stack s(4);

 

 

// объект Stack

 

 

int n = sizeof(data)/sizeof(char)-1;

// счетчик входных данных

 

 

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 (Is.isEmptyO)

 

 

 

 

cout « s. pop ( ) « " " ;

 

 

 

 

cout «

endl;

 

 

 

 

return 0;

Проблема стакой структурой заключается втом, что если требуется контейнер с компонентами другого типа,то проектирование необходимо повторить с самого начала.Все объекты стипом предыдущего компонентадолжны заменяться экзем­ плярами компонентов другого типа. Например, если нужен стек с целыми значе­ ниями вместо символов, то спецификации стека должны выглядеть следующим образом:

class Stack {

// стек целых символов

int *items;

int top, size;

// текущая вершина, общий размер

Stack (const Stack&);

 

operator = (const Stack&);

 

public:

// конструктор преобразования

Stack(int);

void push(int);

// внесение в вершину стека

int popO;

// извлечение верхнего символа

bool isEmptyO const;

// стек пуст?

~Stack();

// возвращение памяти динамически

 

// распределяемой области

}

762

Чость IV * Расширенное использование C-i-^

Для стека значений двойной длины с плаваюидей точкой класс необходимо снова изменить.

class Stack {

// стек символов двойной длины

double *items;

int top, size;

// текущая вершина, общий размер

Stack(const Stack&);

 

 

operator = (const Stack&);

 

 

public:

// конструктор преобразования

Stack(int);

void push(double);

// внесение в вершину стека

double popO;

// извлечение верхнего символа

bool isEmptyO const;

// стек пуст?

"StackO;

/ /

возвращение памяти динамически

 

/ /

распределяемой области

Обратите внимание, что для выполнения данной задачи недостаточно исполь­ зовать обычный редактор. Чтобы получить эту структуру, необходимо изменить тип int на double при определении указателя в списке параметров push() и в рор() для типа возвращаемого значения. Определение элементов данных вершины top и размера size не должно изменяться. Кроме того, тип параметра конструктора должен остаться без изменений. Тем^ не менее повторное использование этой структуры требует внимания.

Другой метод повторного использования контейнера состоит в проектировании его с видовым типом "параметра". Он не соответствует ни типу, определенному программистом, ни встроенному типу. Например, класс Stack можно определить так:

class

Stack {

/ /

стек символов типа Type

Type

*items;

int top, size;

/ /

текущая вершина, общий размер-

Stack(const Stack&);

 

 

 

operator = (const Stack&);

 

 

 

public:

 

 

 

Stack(int);

/ /

конструктор преобразования

void

push(Type);

/ /

занесение

в вершину стека

Type popO;

/ /

извлечение

верхнего символа

bool

isEmptyO const;

/ /

стек пуст?

 

"StackO;

/ /

возвращение памяти динамически

 

 

/ /

распределяемой области

}

Эта программа не компилируется только в том случае, когда компилятору не известен тип Туре. Как только он определяется — программа компилируется. Это замечательно, поскольку упрощает задачу замещения и вызывает меньше ошибок. Требуется заместить только используемые экземпляры типа Туре. Более того, тип Туре невозможно определить, используя typedef, например:

typedef char Type;

/ / тип эквивалентен char

Initial data: 1234567890 New size: 8

New size: 16

Inversed data: 0987654321

Рис. 17.2.

Вывод для программы из листинга 17.2

Это определение должно рассматриваться компилятором до об­ работки Stack. Компилятор заменит каждый экземпляр иденти­ фикатора Туре зарезервированным словом char и скомпилирует получаемый в результате класс.

При таком подходе повторное использование структуры класса больше не повреждается случайными ошибками. Все это требу­ ется, чтобы сгенерировать вариант стека для другого типа ком­ понентов, поскольку надо заменить зарезервированное слово char

Глава 17 • Шаблоны как еще одно средство проектирования

763

в операторе typedef на имя другого типа. Отсутствует опасность случайных оши­ бок. В листинге 17.2 представлена версия класса Stack, где тип Туре обозначает int. Вывод этой программы показан нарис. 17.2.

Листинг 17.2. Повторное использование структуры класса для Stack,

содержащего целые значения

include <iostream> using namespace std;

typedef int Type;

 

// определение изменяемого типа

class Stack {

 

// стек элементов типа Туре

 

Type *items;

 

 

int top, size;

 

// текущая вершина, общий размер

 

Stack(const Stack&);

 

 

 

 

operator = (const Stack&);

 

 

 

public:

 

// конструктор преобразований

 

Stack(int);

 

 

void push(const Type&);

 

// занесение в вершину стека

 

Type рорО;

 

// извлечение верхнего символа

 

bool isEmptyO const;

 

// стек пуст?

 

~Stack();

 

// возвращение памяти в динамически

 

 

 

// распределяемую область памяти

Stack::Stack(int sz = 100) size(sz),top(0)

 

{

items = new Type[sz];

 

// выделить динамически распределяемую

 

if (items==0)

 

// область памяти

 

 

} }

 

 

{ cout « "Out ofmemory\n"; exit(l);

 

void Stack::push (const Type& c)

 

// передача поссылке

 

{ if (top < size)

 

// обычный случай: занесение символа

 

items[top++] = c;

 

// восстановление после переполнения стека

 

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;

size «

// корректировка размера стека

 

cout «"New size: " «

endl;

 

 

items[top++] = c; } }

 

// занесение символа в вершину

Type Stack::pop()

 

// безусловное извлечение

{ return items[-top]; }

 

bool Stack::isEmpty() const

 

// что-либо извлечь?

{

return top ==0; }

 

 

 

Stack: :"'Stack()

{ delete [] items; } // возвращение памяти в динамически

// распределяемую область

764

 

 

Часть IV • Расширенное использование C-^'f

int mainO

 

 

3, 4, 5, 6. 7, 8, 9, 0 }

;

{ Type data[] = { 1 , 2 ,

 

Stack s(4);

 

.,

// объект-стек

int n = sizeof(data)/sizeof(Type);

// счетчик введенных данных

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;

 

 

 

 

 

}

 

 

 

 

 

 

 

 

 

 

Подход с использованием typedef позволяет повторно использовать структуру

 

 

 

класса не только для встроенных типов, но также и для произвольных типов, опре­

 

 

 

деленных программистом,— счетов, инвентаризационных записей, прямоугольни­

 

 

 

ков и т. д. Здесь требуется пояснить способность типа компонента поддерживать

 

 

 

операции, которые выполняются с объектами в контейнерном классе. Это не

 

 

 

слишком сложно, но следует убедиться, что в программе контейнера эти операции

 

 

 

можно распознать, часто они являются неявными.

 

 

 

В примере со Stack контейнерный класс создает массив компонента в дина­

 

 

 

мически распределяемой области памяти. Значит, компонентный класс должен

 

 

 

обеспечить конструктор по умолчанию. Это не проблема для встроенных типов,

 

 

 

но может оказаться трудным для типа, определенного программистом.

 

 

 

Когда объект компонента вносится в контейнер с применением метода push(),

 

 

 

используется присваивание. Если компонентный класс не обрабатывает свою

 

 

 

память динамически, это не проблема. Если он не обрабатывает динамически вы­

 

 

 

деленную область памяти, то компонентный класс должен обеспечивать перегру­

 

 

 

женный оператор присваивания. Обратите внимание, что оператор присваивания

 

 

 

для контейнера остается закрытым.

 

 

 

Другим вопросом, связанным с повторным использованием, который требует

 

 

 

поддержки со стороны компонентного класса, является передача параметра и воз­

 

 

 

вращение значения из методов контейнера. Для встроенных типов данных такой

 

 

 

вопрос будет тривиальным. Именно поэтому в листинге 17.1 метод push() полу­

 

 

 

чил параметр-значение, а метод рор() возвратил значение. В листинге 17.2 метод

 

 

 

push О передает параметр

как ссылку на константу, чтобы избежать проблем,

связанных со снижением производительности (см. главу 11). Метод рор() все еще возвращает значение для совместимости с первой версией программы в листин­ ге 17.1. Однако многие проектировщики контейнера избегают возвращения зна­ чений из методов контейнера и передают параметры (не константы) по ссылке.

Такой подход позволяет повторно использовать структуру контейнера в другой программе для любого типа, который поддерживает присваивание и копирование. Однако если в одной и той же программе используются стеки разных типов, этот подход не работает. Тип Туре может иметь при компиляции только одно значение.

Когда тот же контейнер должен использоваться для компонентов разных типов в одной программе, следует возвратиться к способу ручного редактирования структуры. Каждый стек должен иметь другое имя, например charStack, intStack, pointStack и т. д. Советуем также отредактировать их код и интерфейсы.

class

doubleStack

{

 

double *items;

/ /

редактирование компонента

int

top, size;

/ /

оставить тот же тип

Глава 17 • Шаблоны как еще одно средство проектирования

765 3

Stack(const Stack&);

 

operator = (const Stack&);

 

public:

// оставить тот же тип

Stack (int);

void push(double);

// отредактировать тип параметра

double рорО;

// отредактировать возвращаемый тип

bool isEmptyO const;

 

""StackO;

 

} ;

 

Если исходный кодд/ш каждого класса редактируется по отдельности, то рас­ пространение будущих модификаций становится громоздким и вызывает ошибки. Уникальные имена классов засоряют пространство имен проекта и приводят к конфликтам имен.

Использование возможностей макроопределений может автоматизировать фор­ мирование новых имен классов и программ, но этот метод повторного использо­ вания способствует появлению ошибок. Есть сомнения, что в настояш,ее время программисты на С-+-+ должны изучать, как писать макросы,— это устаревший подход к повторному использованию структур. Покажем, как выглядят макрооп­ ределения для этого стека, чтобы удовлетворить любопытство читателя.

#define

MakeName(a,b)

a/**/b

 

 

 

 

 

 

#define

DefineStack(Type)

\

 

 

 

 

 

class MakeName( Type, Stack) {

\

 

 

 

 

 

Type

*items;

 

 

 

 

 

 

 

\

 

 

 

 

int top,

size;

 

 

 

 

 

 

\

 

 

 

 

Stack(const

Stack&);

 

 

 

\

 

 

 

 

 

operator

= (const Stack&) ;

\

 

 

 

 

 

public:

 

 

 

 

 

 

 

 

 

 

\

 

 

 

 

Stack(int sz = 100)

: size(sz),top(O)

\

 

 

{

items

= new Type[sz];

 

 

 

\

 

 

 

i f

(items==0)

 

 

of

 

 

 

 

 

\

 

 

 

{

cout

«

"Out

memoryXn";

e x i t ( l ) ;

} }

\

void

push(const

Type& c)

 

\

 

 

 

 

{

i f

(top

< size)

 

 

 

 

 

\

 

 

 

 

 

items

[top++]

= c;

 

 

\

 

 

 

 

else

 

 

 

 

 

 

 

 

 

 

 

\

 

 

 

 

 

{

Type *p = new Type [size*2];

\

 

 

 

 

 

 

i f

(P

== 0)

 

 

 

 

 

\

 

 

 

 

 

 

 

{

cout

«

"Out of memory\n";

e x i t ( l ) ; }

\

 

 

 

for

(int

i=0;

Ksize; i++)

 

\

 

 

 

 

 

 

 

 

p [ i ]

= items[i];

\

 

 

 

 

 

 

 

 

delete

[ ]

items;

\

 

 

 

 

 

 

 

 

items

= p;

 

 

 

 

\

 

 

 

 

 

 

 

size

*=

2;

 

 

 

 

\

 

 

 

 

 

 

 

 

 

 

cout «

"New size:

"

«

size «

endl;

\

 

 

 

 

 

 

items[top++] = c;

} }

 

 

\

 

 

Type popO

 

 

 

 

 

 

 

 

 

 

\

 

 

{

return

items[-top];

}

 

 

 

\

 

 

bool

isEmptyO

const

 

 

 

 

 

\

 

 

{

return

top

== 0;

}

 

 

 

 

 

\

 

 

"StackO

 

 

 

 

 

 

 

 

 

 

 

\

 

 

{

delete

[ ]

items;

}

 

 

 

 

 

\

 

 

766 Часть IV • Расширенное использование С^+

Клиент должен определить типы стека, используя имя DefineStack, заданное в начале макроопределения.

DefineStack(int);

Оно сгенерирует имя intStack как объединение типа (определенного в скобках) и имени стека (второй аргумент в макроопределении MakeName). Это также опре­ деление программы для стека целых значений. Затем клиент будет в состоянии объявить и использовать любой подходящий объект стека.

intStack s(4);

Поскольку весь код вмещается в одну строку, сгенерированную препроцессо­ ром, его сложно отладить. Лексическая подстановка, как часто случается с макро­ определениями, может генерировать неверный код. Это не очень хороший способ повторного использования структуры класса.

Синтаксис определения шаблонного класса

СН-+ поддерживает еще один метод повторного использования структуры клас­ са. Этот инструмент называется шаблонный класс. Вместо класса с фиксирован­ ным типом компонентов создается класс, где тип компонентов интерпретируется как параметр класса.

Имя этого параметра определяется программистом, например Туре, Т, Тр и т. д. (Как и для любого параметра, именно программист решает, какое имя ему при­ своить.) Его действительное значение может быть как встроенным типом, так и определенным программистом. На этапе компиляции определения шаблона, как и для любого другого параметра, его фактическое значение не может быть известным. Когда клиентская программа присваивает значение объекту этого класса, она определяет фактический тип, который должен использоваться в классе вместо параметра класса.

Спецификация шаблонного класса

Ниже показаны спецификации для примера класса. Тип параметра обозначает­ ся именем Туре, определенным программистом.

template <class Туре>

/ /

Type задается при реализации

class

Stack {

 

 

Type

*items;

/ /

будет использоваться фактический тип

int top, size;

 

 

Stack(const Stack&);

 

 

operator = (const Stack&);

 

 

public:

 

 

Stack (int);

 

 

void push(const Type&);

/ /

будет использоваться фактический тип

Type popO;

/ /

будет использоваться фактический тип

bool

isEmptyO const;

 

 

"StackO; } ;

Теоретически представлено расширенное понятие параметров функции. Когда записывается функция, каждому параметру присваивается имя, но оно является просто псевдонимом для имени, которое будет определено позже. Функция уста­ навливает все выполняемые операции для значений фактического аргумента. Однако значение фактического аргумента при написании функции неизвестно. Вы знакомитесь с ним только в момент вызова функции. Затем все появления имени

Глава 17 • Шаблоны как еще одно средство проектирования

767

формального параметра в функции заменяются именем фактического аргумента и с этим значением аргумента выполняются вычисления.

Преимущество использования функций с параметрами состоит в том, что на момент проектирования алгоритма не требуется подтверждать значения, по кото­ рым выполняются вычисления. Вычисления могут быть записаны с любым значе­ нием, которое становится известным только в момент вызова функции. Если этот же алгоритм нужен в другом месте программы и для другого значения аргумента, совсем не обязательно в исходной программе реализовывать его снова. Данная функция может вызываться в других местах программы (без каких-либо измене­ ний) с указанием имен фактических аргументов.

Подобным образом шаблоны указывают компилятору, как сгенерировать код при запросе с использованием конкретного типа. Когда пишется шаблон, типу присваивается имя, но оно является просто псевдонимом для имени, которое будет определено позже. Шаблон обозначает все операции, которые должны выполня­ ться над значениями данного типа. На момент написания шаблона имя фактиче­ ского аргумента неизвестно. Вы узнаете о нем только в момент создания объекта. В этом случае вычисления выполняются для значения подобного типа.

Преимущество использования шаблонных классов состоит в том, что в ходе проектирования алгоритма не требуется осуществлять связывание с типом, над которым выполняются вычисления. Они могут выполняться над любым типом (до тех пор, пока данный тип будет поддерживать эти операции). Этот тип станет известным только на момент присвоения значения объекту. Если данная структура потребуется в другом месте программы, но с применением другого типа, необяза­ тельно реализовывать ее повторно в исходной программе. Можно реализовать один и тот же шаблонный класс для различных типов в других местах программы. Реализации отличаются друг от друга только именем фактического типа. Если не­ обходимо, объект может создаваться с тем же фактическим типом, что и раньше.

Класс проектируется только раз, а затем он используется как шаблон (здесь берется имя) для генерирования любого количества конкретных классов. Они обозначают фактические типы, которые должны использоваться вместо типа па­ раметра, применяемого при проектировании класса. Другие элементы д/ш шаб­ лонных классов являются родовыми классами и параметризованными классами. Они могут использоваться с различными фактическими типами компонентов.

Template является зарезервированным словом C+ + . В определении класса после этого слова указывается непустой список параметров в угловых скобках. Эти угловые скобки (в отличие от круглых скобок в функциях C+ + ) используются для обозначения типа параметров. Каждый параметр шаблонного класса в списке параметров является комбинацией зарезервированного слова class (класс) и иден­ тификатора, определенного программистом.

Каждый параметр шаблона в угловых скобках представляет собой "заполни­ тель" для типа. Параметризованный класс (родовой класс) может иметь любое количество параметров типа. Несколько шаблонных параметров в списке разде­ ляются запятыми.

template <class Т1, class Т2, class Т3>

/ /

три параметра типа

class Triple;

/ /

объявление класса

Как и в других классах C+ + , зарезервированное слово class в списке пара­ метров имеет иное значение, чем в других контекстах. Оно показывает, что иден­ тификатор, расположенный за ним, является "заполнителем" фактического типа. Этот тип не обязательно должен быть классом. Он также может быть любым встроенным типом. Кроме того, допускается использование конкретных типов параметров выражений.

template <class Type, int size>

/ / тип и значение

class Array;

 

768

Часть fV ^ Расширенное использование СФ**"

Это подобно параметрам функций — в момент реализации класса Array значение указанного типа (в данном случае int) должно предоставляться клиентской про­ граммой.

Реализация шаблона

Создание объекта шаблонного класса называется реализацией (instantiation). Оно подобно созданию объекта любого класса в С+Н-.

Когда клиентская программа приписывает значения конкретному объекту шаблонного класса, она должна предоставить для каждого шаблонного параметра аргумент фактического типа. Шаблон Stack не является классом, это всего лишь шаблон. Он не поддерживает прямое создание объектов Stack. Объект Stack без указания фактического типа при вызове функции является абсурдом. Например, push О без указания фактического значения, которое должно вноситься в стек.

Фактический тип определяется при реализации шаблона как имя типа в угло­ вых скобках, которые добавляются к имени шаблонного класса. Имя объекта за­ дается таким же образом, как и для неродовых классов. Параметры конструкторов используются там, где необходимо.

Stack<int>

is(50);

/ /

стек целых длиной 50 значений

Stack<char>

cs(200);

/ /

стек символов длиной в 200 символов

Реализация напоминает вызов функции, в которой формальные параметры являются "заместителями", принимающими значения фактических аргументов. В определении функции фактические аргументы указываются в скобках. В опре­ делении шаблона параметры шаблонов обозначаются в угловых скобках. В реа­ лизации объекта шаблонов, фактические типы перечисляются в угловых скобках. Если в определении шаблона содержится более одного параметра класса, то в реализации шаблона, между левой и правой угловыми скобками, должно указываться такое же количество фактических типов.

В отличие от вызова функции в реализации объекта шаблона могут использо­ ваться только фактические значения периода компиляции. Они жестко запрограм­ мированы в клиентской программе. C + + не реализует переменные, тип которых определяется во время выполнения. Вы не можете использовать переменную, значением которой является тип. Значения параметров по умолчанию не могут применяться для параметров типа шаблона.

Обратите внимание, что когда компилятор детально прорабатывает шаблонн^эш класс, он не генерирует объектный код для класса. Он не может это выполнить, потому что фактический тип компонента класса неизвестен. Компилятор сохраня­ ет во внутренних таблицах информацию о шаблоне, но не добавляет объектный код к объектному файлу, который соответствует исходному файлу с определением шаблона.

Компилятор, анализируя реализацию шаблона, например Stack<int> is(50), создает определение фактического класса (в данном случае Stack<int>), заменяя формальные параметры шаблона фактическими аргументами типа шаблона. Этот код не должен переходить в генерируемый файл объектного кода, поскольку класс может использоваться также и в другом исходном файле. Итак, компилятор генерирует дополнительный объектный файл для Stack<int>. Затем он генерирует объект класса Stack<int>.

template <class

Туре>

 

 

class

Stack<int>

{

/ /

тип передается во время реализации

int

*items;

 

/ /

используется фактический тип

int

top, size;

 

 

 

Stack(const Stack&); operator = (const Stack&);

Глава 17 • Шаблоны кок еще одно средство проектирования

769

public:

 

 

 

Stack(int);

int&);

// используется фактический тип

 

void push(const

 

int рорО;

const;

// применяется фактический тип

 

bool isEmptyO

 

 

~Stack();

 

 

 

} ;

 

 

 

Обратите внимание, что в определении родового шаблона интерфейс функции push О был определен как ссылка на константу. Именно поэтому такая функция обозначается для Stack<int> как push (const int&). Это является избыточным для встроенных типов, но повышает эффективность для фактических типов, когда они представляют собой сложные классы. Однако функция рор() возвраш^ает зна­ чение, а не ссылку, чтобы клиентская программа не зависела от времени жизни объекта-стека и его элементов.

Объект is типа Stack<int> имеет такую же природу, как и любой другой объект C-f+. В нем ничто не указывает на то, что это объект шаблонного класса, а не объект обычного класса. Данный объект можно использовать там же, где применяются объекты, не являюш^иеся шаблонами. Его можно передавать как параметр и отвечать на сообщения, определенные для родового класса.

I f (! is . isEmptyO)

/ /

пересылка сообщения объекту

DebugPrint(is);

/ /

передача объекта как параметра

Имя шаблонного класса может применяться там же, где и имя нешаблонного класса, но должен указываться список параметров с именами фактических типов. Например, функция DebugPrintO должна определяться как функция, которая принимает аргументы конкретного типа, в данном случае Stack<int>.

void DebugPrint(Stack<int>& stack); / / прототип функции

Реализация шаблонных функций

в примере функция DebugPrintO является обычной функцией СН- + . Единст­ венное различие между этой и другими функциями С4- + , представленными ранее, состоит в том, что ее параметр является объектом шаблонного класса. В теле данной функции параметр-стек интерпретируется как обычный объект С+ + с известным классом.

Если алгоритм отладки одинаков для объектов стека разных типов, то, скорее всего, вам придется указать его в интерфейсе функции. Тип Stack<int> является специфическим для такой функции. С таким типом функция может принимать фактические аргументы конкретного типа, но не других типов стека. Интерфейс функции должен показывать, что допустим объект-стек любого типа. В СН-+ это поддерживается за счет понятия шаблонной (родовой) функции.

Шаблонная функция определяется так же, как и шаблонный класс. Вслед за зарезервированным словом template в угловых скобках указывается список параметров типа. Каждая запись в списке параметров состоит из зарезервирован­ ного слова class и идентификатора, который является "заместителем" имени типа. За списком параметров шаблона приводится заголовок функции с именем функции и списком параметров. В списке параметров функции используется нота­ ция реализованных шаблонных классов. Вместо фактических типов указываются имена параметров типов из списка параметров шаблона. Вот как выглядит родо­ вая функция DebugPrintO, которая может принимать параметр-стек любого типа.

template <class Туре>

/ /

список

параметров шаблона

void DebugPrint(Stack<Type>& stack);

/ /

список

параметров функции

Соседние файлы в предмете Программирование на C++