Шаблоны классов их создание и причины использования.
Шаблоны классов позволяют создавать параметризированные классы. Параметризированный класс создает семейство родственных классов, которые можно применять к любому типу данных, передаваемых в качестве параметра шаблона. Наиболее широкое применение шаблоны получили при создании контейнерных классов [4].
Преимуществом шаблонов классов состоит в том, что они могут применяться к любым типам данных без переопределения кода.
Синтаксис описания шаблона следующий:
template<classType>NameClass
{
// тело шаблона
};
Список шаблона <classType> состоит из служебного слова class или typename после чего идет произвольный идентификатор формального типа. Если предполагается несколько типов, то они перечисляются через запятую. Действие формального типа ограничено рамками данного шаблона.
Рассмотрим простой пример шаблона стека, предназначенного для хранения объектов различного типа.
const int Max = 100;
template<typename Type> class Stack
{
protected:
int top;
Type st[Max];
public:
Stack()
{
top = -1;
}
void Push(Type);
Type Pop();
friend ostream &operator <<<>(ostream &, const Stack<Type>&);
};
template<typename Type> ostream &operator <<(ostream &out, const Stack<Type>&s)
{
out<< " Element: " << s.st[s.top] << endl;
return out;
}
template<typename Type> void Stack<Type>:: Push(Type var)
{
st[++top] = var;
}
template<typename Type>Type Stack<Type>::Pop()
{
return st[top--];
}
Здесь приведен пример шаблона класса Stack. Для простоты методы Pop и Push не снабжены проверками на пустоту и переполнение стека. Сам стек представляется как массив объектов. Обратим внимание на то, как определяются методы за пределами классов. Во-первых, все методы шаблонов класса по умолчанию считаются шаблонами функций. Во-вторых, после имени шаблона класса в обязательном порядке должен присутствовать спецификатор типа. Если это определение метода, то в качестве спецификатора выступает имя формального типа, напримерType. Если же это объявление реального объекта, то спецификатором должен быть конкретный тип, в том числе и тип, определенный пользователем.
Обратите внимание на объявление дружественной функции (оператора) в теле и за пределами шаблона. В отличие от дружественной функции обычного класса в шаблоне в объявлении должен присутствовать спецификатор шаблона, идущий сразу за именем функции: friendostream&operator<<<Type>(ostream&, constStack<Type>&);, который в общем случае может быть пустым. При определении дружественной функции она объявляется как шаблон, а спецификатор типа после имени не допустим. Он здесь используется для передачи типа шаблона во втором аргументе: constStack<Type>&;.
Объявление объекта типа шаблон должно обязательно быть со спецификатором конкретного типа, например, Stack<long> st_1; или Stack<double> st_2;. Дальнейшие обращения к объектам шаблонов ничем не отличаются от обращений к объектам обычных классов.
Общие правила описания шаблонов классов:
1) локальные классы не могут содержать шаблоны в качестве своих элементов;
2) шаблоны могут содержать статические компоненты, дружественные функции и классы;
3) шаблоны могут входить в иерархию классов и шаблонов.
Следующий пример показывает использование в качестве параметра шаблона тип, определенный пользователем. Добавьте к предыдущему примеру определение простого класса и передайте его тип в качестве параметра шаблона.
class Test
{
int test;
public:
Test(){};
Test(int t):test(t){};
void Out()
{
cout<< " Test: " << test << endl;
}
friend ostream &operator <<(ostream &, const Test &);
};
ostream&operator <<(ostream &out, const Test &t)
{
out<< t.test << endl;
return out;
}
Теперь можно использовать тип Test в качестве параметра:
Stack<Test> st;
st.Push(100);
st.Push(200);
cout<< st.Pop();
cout << st.Pop();
9. Функции преобразования. Их создание и причины применения.
Поскольку каждый конструктор задает преобразование в своем классе, следовательно, если в некотором месте программы используется выражение Exp, представляющее данное, которое должно быть преобразовано к типу класса, то сразу же после обработки этого выражения неявно активизируется конструктор с инициализатором (Exp), что выполняет необходимые преобразования. Конструктором преобразования считается любой конструктор, имеющий один параметр, тип которого отличен от типа определяемого класса. Следующий пример показывает использование конструктора преобразования.
classTest
{
inttest;
public:
Test(){};
Test(int t)
{
cout << " Конструктор преобразования" << endl;
test = t;
}
Test(const Test &t):test(t.test)
{
cout << " Копирующий конструктор " << endl;
}
void Out()
{
cout << " Test: " << test << endl;
}
};
int main()
{
setlocale(0,"RUS");
Test tst;
tst = 300; // или tst = Test(300);
tst.Out();
return 0;
}
Преобразующий конструктор класса Test:
Test(intt)
{
cout<< " Конструктор преобразования" <<endl;
test = t;
}
осуществляющег преобразование параметра типа int к типу Test и оператор tst = 300; в теле основной функции трактуется как оператор tst = Test(300);. Такое преобразование называется типичным
В некотрых случаях преобразования, задаваемые конструкторами, можно запретить, так как они могут привести к нежелательным результатам. Для этого перед именем конструктора записывается стандартное слово explicit, которе предупреждает компилятор о том, что при объявлении некоторго объекта и инициализации его значением не относящимся к типу определяемого класса, неявных преоборазований производить не следует. Рассмотримещеразкласс Test.
class Test
{
public:
int test;
public:
Test(){};
explicit Test(int t)
{
cout << " Конструктор преобразования, запрещающий неявные преобразования типа int в тип Test" << endl;
test = t;
}
Test(const Test &t):test(t.test)
{
cout << " Копирующий конструктор " << endl;
}
void Out()
{
cout << " Test: " << test << endl;
}
};
int main()
{
setlocale(0,"RUS");
Test tst_1(38);
Test tst_2 = tst_1;
Test tst_3 = 64; // Здесьошибка!
return 0;
}
Откомпилируйте этот фрагмент и посмотрите сообщения компилятора. В операторе Test tst_3 = 64; компилятор не сможет преобразовать тип int к типу Test. Ошибку можно «исправить», если использовать явное преобразование типа Test tst_3 = (Test)64; , если это необходимо. Этот пример не единственный, более того, он очень прямолинеен. Больших неприятностей можно ожидать в случае передачи некоторой внешней функции параметра типа определяемого класса. Например:
voidFunOut(Testt)
{
cout<< " Test: "; t.Out();
}
Если вызвать эту функцию с объектом типа Test, например, FunOut(tst_1), никаких проблем не возникнет. Еси же вызвать с переменной типа int, например, FunOut(300);, то неявно вызывается конструктор преобразования, котрый приводит число 300 к типу Test. Если перед конструктором стоит спецификатор explicit, неприятностей преобразования не будет, так как компилятор выдаст соответствующее сообщение.