C++语言学习(十七)——模板
一、模板简介
泛型(Generic Programming)即是指具有在多种数据类型上皆可操作的含意。 泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。
泛型编程最初诞生于C++中,目的是为了实现C++的STL(标准模板库)。其语言支持机制就是模板(Templates)。模板的核心思想是参数化类型,即把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数T。
二、函数模板
1、宏实现交换函数
定义一个交换两个数的宏
#define SWAP(t, a, b) \
do \
{ \
t c = a; \
a = b; \
b = c; \
}while(0);
宏代码块实现的优点是代码复用,适合所有类型;缺点是缺少类型检查。
2、函数重载实现
#include <iostream>
using namespace std;
void swap(int &a, int& b)
{
int t = a;
a = b;
b = t;
}
void swap(double &a,double b)
{
double t = a;
a = b;
b = t;
}
int main()
{
int ia = 10; int ib = 20;
swap(ia,ib);
cout<<ia<<ib<<endl;
double da = 10, db = 20;
swap(da,db);
cout<<da<<db<<endl;
return 0;
}
函数重载实现的优点是真正进行函数调用,C++编译器进行类型检查;缺点是根据类型重复定义函数,无法代码复用。
3、函数模板
函数模板是可用不同类型进行调用的特殊函数,关键在于类型参数化。
函数模板的语法格式如下:
template<typename/class 类型参数表>
返回类型 函数模板名(函数参数列表)
{
函数模板定义体
}
template关键字用于声明开始进行泛型编程。
typename关键字用于声明泛指类型。
函数模板可以自动推导类型进行调用,也可以显示指定具体类型进行调用。
4、函数模板实现
#include <iostream>
using namespace std;
template <typename T>
void Swap(T& a,T &b )
{
T t = a;
a = b;
b = t;
}
int main()
{
int ia = 10; int ib = 20;
Swap(ia,ib); //Swap<int>(ia,ib);
cout<<ia<<ib<<endl;
double da = 10, db = 20;
Swap(da,db); //Swap<double>(da,db);
cout<<da<<db<<endl;
string sa ="china"; string sb = "America";
Swap(sa,sb);
cout<<sa<<sb<<endl;
return 0;
}
判断一个变量是不是指针类型示例:
template
<typename T>
bool isPtr(T *p)
{
return true;
}
template
<typename T>
bool isPtr(T t)
{
return false;
}
函数模板,只适用于函数的参数个数相同而类型不同,且函数体相同的情况。如果个数不同,则不能用函数模板。
5、函数模板分析
C++编译器从函数模板通过具体类型产生不同的函数,C++编译器会对函数模板进行两次编译,一次是函数模板代码进行编译,一次是参数替换后的函数代码进行编译。
#include <iostream>
using namespace std;
template <typename T>
void Swap(T& a, T& b)
{
T c = a;
a = b;
b = c;
}
class Test
{
};
typedef void (*pFuncInt)(int&, int&);
typedef void (*pFuncDouble)(double&, double&);
typedef void (*pFuncTest)(Test&, Test&);
int main(int argc, char *argv[])
{
pFuncInt pi = Swap;//Swap<int>
printf("0x%x\n", pi);
pFuncDouble pd = Swap;//Swap<double>
printf("0x%x\n", pd);
pFuncTest pt = Swap;//Swap<Test>
printf("0x%x\n", pt);
return 0;
}
函数模板本身不允许隐式类型转换,因此,自动推导类型时需要严格匹配,但当显示指定类型参数时可以进行隐式类型转换。
函数模板中的返回值类型必须显示指定。
add<int>(ia,ib);
6、多类型参数函数模板
函数模板可以定义多个不同的类型参数,但无法自动推导返回值类型,可以从左向右部分指定类型参数,实际工程中将返回值作为第一个类型参数,必须显式指定。
#include <iostream>
#include <string>
using namespace std;
template
< typename T1, typename T2, typename T3 >
T1 Add(T2 a, T3 b)
{
return static_cast<T1>(a + b);
}
int main()
{
// T1 = int, T2 = double, T3 = double
int r1 = Add<int>(0.5, 0.8);
// T1 = double, T2 = float, T3 = double
double r2 = Add<double, float>(0.5, 0.8);
// T1 = float, T2 = float, T3 = float
float r3 = Add<float, float, float>(0.5, 0.8);
cout << "r1 = " << r1 << endl; // r1 = 1
cout << "r2 = " << r2 << endl; // r2 = 1.3
cout << "r3 = " << r3 << endl; // r3 = 1.3
return 0;
}
7、普通函数与函数模板的关系
函数模板可以被重载,C++编译器优先考虑普通函数,但如果函数模板可以产生更好的匹配,则使用函数模板,可以通过空模板实参列表限定只能使用函数模板。
#include <iostream>
#include <string>
using namespace std;
template < typename T >
T Max(T a, T b)
{
cout << "T Max(T a, T b)" << endl;
return a > b ? a : b;
}
int Max(int a, int b)
{
cout << "int Max(int a, int b)" << endl;
return a > b ? a : b;
}
template < typename T >
T Max(T a, T b, T c)
{
cout << "T Max(T a, T b, T c)" << endl;
return Max(Max(a, b), c);
}
int main()
{
int a = 1;
int b = 2;
cout << Max(a, b) << endl; // 普通函数 Max(int, int)
cout << Max<>(a, b) << endl; // 函数模板 Max<int>(int, int)
cout << Max(3.0, 4.0) << endl; // 函数模板 Max<double>(double, double)
cout << Max(5.0, 6.0, 7.0) << endl; // 函数模板 Max<double>(double, double, double)
cout << Max('a', 100) << endl; // 普通函数 Max(int, int)
return 0;
}
三、类模板
1、类模板的定义
C++语言中将模板的思想应用于类,使得类的实现不关注数据元素的具体类型,只关注类需要实现的功能。
类模板的定义语法如下:
template <typename T>
class classname
{
};
在类声明前使用template进行标识,<typename T>
用于说明类中使用泛指类型T。
类内定义成员函数
template<typename T>
class classname
{
public:
void push(int size)
{
}
}
类外定义函数
template<typename T>
void classname<T>::push(T data)
{
}
类模板实例化为模板类:
classname<double> object;
类模板是类的抽象,类是类模板的实例。
2、类模板应用
类模板只能显示指定类型参数,无法自动推导。声明的泛型类型参数可以出现在类模板的任意地方。
类模板必须在头文件中实现,不能分开实现在不同文件中。类模板的成员函数需要定义在外部定义时,每个成员函数需要加上类模板template<typename T>
声明。
类模板适合以相同的逻辑处理不同的数据类型的数据,因此非常适合编写数据结构相关代码。
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;
template<typename T>
class Stack
{
public:
Stack(int size)
{
space = new T[size];
top = 0;
}
~Stack();
bool isEmpty();
bool isFull();
void push(T data);
T pop();
private:
T* space;
int top;
};
template<typename T>
Stack<T>::~Stack()
{
delete []space;
}
template<typename T>
bool Stack<T>::isEmpty()
{
return top == 0;
}
template<typename T>
bool Stack<T>::isFull()
{
return top == 1024;
}
template<typename T>
void Stack<T>::push(T data)
{
space[top++] = data;
}
template<typename T>
T Stack<T>::pop()
{
return space[--top];
}
int main()
{
Stack<double> s(100); //Stack<string> s(100);
if(!s.isFull())
s.push(10.3);
if(!s.isFull())
s.push(20);
if(!s.isFull())
s.push(30);
if(!s.isFull())
s.push(40);
if(!s.isFull())
s.push(50);
while(!s.isEmpty())
cout<<s.pop()<<endl;
return 0;
}
3、类模板分析
类模板通过具体类型产生不同的类,C++编译器在类模板声明的地方对类模板代码本身进行编译,在使用的地方对类模板参数替换后产生的代码进行编译。
类模板可以定义多个不同类型参数。
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class Operator
{
public:
Operator()
{
cout << "Operator()" << endl;
}
T add(T a, T b)
{
cout << "T add(T a, T b)" << endl;
return a + b;
}
T minus(T a, T b)
{
return a - b;
}
T multiply(T a, T b)
{
return a * b;
}
T divide(T a, T b)
{
return a / b;
}
};
int main(int argc, char *argv[])
{
Operator<int> op1;
cout << op1.add(1, 2) << endl;
cout << op1.add(1, 2) << endl;
Operator<string> op2;
cout << op2.add("D.T.", "Software") << endl;
return 0;
}
// output:
// Operator()
// T add(T a, T b)
// 3
// Operator()
// T add(T a, T b)
// 3
// Operator()
// T add(T a, T b)
// Hello World
上述代码中,类模板中的函数代码在使用的时候才会被分别编译。
四、模板的特化
1、类模板的特化
类模板可以被特化,以下情况需要特化类模板:
A、指定特定类型的实现
B、部分参数类型必须显示指定
C、根据类型参数分开实现类模板
类模板的特化分为部分特化和完全特化。部分特化是指用特定规则约束类型参数,完全特化是指完全显示指定类型参数。
类模板的特化是模板的分开实现,本质上是同一个类模板,特化类模板必须显示指定每一个类型参数。编译器会自动优先选择特化类模板。
#include <iostream>
using namespace std;
template
<typename T1, typename T2>
class Test
{
public:
void add(T1 a, T2 b)
{
cout << "void add(T1 a, T2 b)" << endl;
cout << a + b << endl;
}
};
//部分特化
template
<typename T>
class Test<T,T>
{
public:
void add(T a, T b)
{
cout << "void add(T a, T b)" << endl;
cout << a + b << endl;
}
void print()
{
cout << "class Test <T,T>" << endl;
}
};
//完全特化
template
<>
class Test<int,int>
{
public:
void add(int a, int b)
{
cout << "void add(int a, int b)" << endl;
cout << a + b << endl;
}
void print()
{
cout << "class Test<int,int>" << endl;
}
};
int main(int argc, char *argv[])
{
Test<int, int> t1;//完全特化
t1.add(1,2);
t1.print();
Test<double, double> t2;//部分特化
t2.add(3.14,2.0);
t2.print();
Test<float, double> t3;//类模板
t3.add(3.14,2.0);
return 0;
}
// output:
// void add(int a, int b)
// 3
// class Test<int,int>
// void add(T a, T b)
// 5.14
// class Test <T,T>
// void add(T1 a, T2 b)
// 5.14
2、函数模板的特化
函数模板只支持模板的完全特化。
#include <iostream>
using namespace std;
//函数模板
template
<typename T>
bool Equal(T a, T b)
{
cout << "bool Equal(T a, T b)" << endl;
return a == b;
}
//函数特化模板
template
< >
bool Equal<double>(double a, double b)
{
const double delta = 0.00000000000001;
double r = a - b;
cout << "bool Equal<double>(double a, double b)" << endl;
return (-delta < r) && (r < delta);
}
//函数重载
bool Equal(double a, double b)
{
const double delta = 0.00000000000001;
double r = a - b;
cout << "bool Equal(double a, double b)" << endl;
return (-delta < r) && (r < delta);
}
int main(int argc, char *argv[])
{
Equal<double>(0.1,0.1);//函数特化模板
Equal<int>(10,10);//函数模板
Equal(0.1,0.1);//函数重载
return 0;
}
// output:
// bool Equal<double>(double a, double b)
// bool Equal(T a, T b)
// bool Equal(double a, double b)
工程实践中当需要重载函数模板时,优先使用函数模板特化,当函数模板特化无法满足需求时,使用函数重载。
五、数组类模板
1、数值型模板
模板参数可以是数值型参数,数值型模板参数存在限制:
A、变量不能作为模板参数
B、浮点数不能作为模板参数
C、类对象不能作为模板参数
模板参数是在编译阶段处理的,因此在编译阶段需要唯一确定。
使用最高效方式求1+2+3+4......+100
#include <iostream>
using namespace std;
template
<int N>
class Sum
{
public:
static const int value = Sum<N-1>::value + N;
};
template
<>
class Sum<1>
{
public:
static const int value = 1;
};
int main(int argc, char *argv[])
{
cout<<Sum<100>::value<<endl;
return 0;
}
2、数组模板
#ifndef _ARRAY_H_
#define _ARRAY_H_
template
< typename T, int N >
class Array
{
T m_array[N];
public:
int length();
bool set(int index, T value);
bool get(int index, T& value);
T& operator[] (int index);
T operator[] (int index) const;
virtual ~Array();
};
template
< typename T, int N >
int Array<T, N>::length()
{
return N;
}
template
< typename T, int N >
bool Array<T, N>::set(int index, T value)
{
bool ret = (0 <= index) && (index < N);
if( ret )
{
m_array[index] = value;
}
return ret;
}
template
< typename T, int N >
bool Array<T, N>::get(int index, T& value)
{
bool ret = (0 <= index) && (index < N);
if( ret )
{
value = m_array[index];
}
return ret;
}
template
< typename T, int N >
T& Array<T, N>::operator[] (int index)
{
return m_array[index];
}
template
< typename T, int N >
T Array<T, N>::operator[] (int index) const
{
return m_array[index];
}
template
< typename T, int N >
Array<T, N>::~Array()
{
}
#endif
六、智能指针类模板
智能指针是C++开发库的重要类模板之一,是自动内存管理的主要手段,可以避开内存的相关问题。
1、STL的智能指针
STL中的智能指针分为auto_ptr、shared_ptr、weak_ptr、unique_ptr四类。
auto_ptr智能指针的特性:
A、生命周期结束时,销毁指向的内存空间
B、不能指向堆数组,只能指向堆对象
C、一块堆空间只能属于一个智能指针
D、多个智能指针对象不能指向同一块空间
shared_ptr智能指针的特性:
带有引用计数机制,支持多个指针指向同一对象内存空间。
weak_ptr智能指针的特性:
weak_ptr是一种弱引用,指向shared_ptr所管理的对象。
unique_ptr智能指针的特性:
一个指针对象指向一片内存空间,不能拷贝构造和赋值
STL智能指针使用实例:
#include <iostream>
#include <memory>
using namespace std;
class Test
{
string m_name;
public:
Test(const char* name)
{
cout << "Hello, " << name << "." << endl;
m_name = name;
}
void print()
{
cout << "I'm " << m_name << "." << endl;
}
~Test()
{
cout << "Goodbye, " << m_name << "." << endl;
}
};
int main(int argc, char *argv[])
{
auto_ptr<Test> pt(new Test("D.T.Software"));
cout << "pt = " << pt.get() << endl;
pt->print();
cout << endl;
auto_ptr<Test> pt1(pt);
cout << "pt = " << pt.get() << endl;//NULL
cout << "pt1 = " << pt1.get() << endl;//
return 0;
}
2、QT中的智能指针
QT中的主要智能指针有:QPointer、QSharedPointer、QWeakPointer、QScopedPointer、QSharedDataPoiner、QExplicitlySharedDataPointer。
QPointer智能指针特性:
A、当其所指向的对象被销毁时会被自动置空
B、析构时不会自动销毁所指向的对象
多个QPointer指针对象可以指向同一内存空间,当所指向对象被销毁时指针会被自动置空,但是指针对象析构时不会自动销毁所指向的对象。
QSharedPointer智能指针特性:
A、引用计数型智能指针
B、可以被自由的拷贝和赋值
C、当引用计数为0时才删除指向的对象
QT中智能指针使用实例:
#include <QPointer>
#include <QSharedPointer>
#include <QDebug>
class Test : public QObject
{
QString m_name;
public:
Test(const char* name)
{
qDebug() << "Hello, " << name << ".";
m_name = name;
}
void print()
{
qDebug() << "I'm " << m_name << ".";
}
~Test()
{
qDebug() << "Goodbye, " << m_name << ".";
}
};
int main()
{
QPointer<Test> pt(new Test("D.T.Software"));
QPointer<Test> pt1(pt);
QPointer<Test> pt2(pt);//多个QPointer指针对象可以指向同一内存空间
pt->print();
pt1->print();
pt2->print();
delete pt;//QPointer智能指针指向的对象被销毁时,指针对象被置空
qDebug() << "pt = " << pt;//NULL
qDebug() << "pt1 = " << pt1;//NULL
qDebug() << "pt2 = " << pt2;//NULL
qDebug() << endl;
QSharedPointer<Test> spt(new Test("Delphi Tang"));
QSharedPointer<Test> spt1(spt);
QSharedPointer<Test> spt2(spt);
spt->print();
spt1->print();
spt2->print();
return 0;//指针对象都被销毁时引用计数为0,自动析构指针指向的对象
}
3、智能指针类模板
#ifndef _SMARTPOINTER_H_
#define _SMARTPOINTER_H_
template
< typename T >
class SmartPointer
{
T* mp;
public:
SmartPointer(T* p = NULL)
{
mp = p;
}
SmartPointer(const SmartPointer<T>& obj)
{
mp = obj.mp;
const_cast<SmartPointer<T>&>(obj).mp = NULL;
}
SmartPointer<T>& operator = (const SmartPointer<T>& obj)
{
if( this != &obj )
{
delete mp;
mp = obj.mp;
const_cast<SmartPointer<T>&>(obj).mp = NULL;
}
return *this;
}
T* operator -> ()
{
return mp;
}
T& operator * ()
{
return *mp;
}
bool isNull()
{
return (mp == NULL);
}
T* get()
{
return mp;
}
~SmartPointer()
{
delete mp;
}
};
#endif
七、单例类模板
某些类在整个系统的生命周期中只能有一个对象存在,即单例模式。
要控制类的对象数目必须隐藏类的构造函数,即构造函数声明为private。
定义一个instance标识符,初始化为NULL,当需要使用对象时查看instance的值,如果instance为NULL则创建对象并用instance标识,如果instance非空则返回instance标识的值。
#ifndef _SINGLETON_H_
#define _SINGLETON_H_
template
< typename T >
class Singleton
{
static T* c_instance;
public:
static T* GetInstance();
};
template
< typename T >
T* Singleton<T>::c_instance = NULL;
template
< typename T >
T* Singleton<T>::GetInstance()
{
if( c_instance == NULL )
{
c_instance = new T();
}
return c_instance;
}
#endif
使用代码:
#include <iostream>
#include <string>
#include "Singleton.h"
using namespace std;
class SObject
{
friend class Singleton<SObject>; // 当前类需要使用单例模式
SObject(const SObject&);
SObject& operator= (const SObject&);
SObject()
{
}
public:
void print()
{
cout << "this = " << this << endl;
}
};
int main()
{
SObject* s = Singleton<SObject>::GetInstance();
SObject* s1 = Singleton<SObject>::GetInstance();
SObject* s2 = Singleton<SObject>::GetInstance();
s->print();
s1->print();
s2->print();
return 0;
}
八、模板应用示例
判断一个变量是不是指针
C++编译器匹配的调用优先级:
A、重载函数
B、函数模板
C、变参函数
可以根据C++编译器匹配的调用优先级,将函数模板匹配指针变量,返回true,变参函数匹配非指针变量,返回false。
template
<typename T>
bool IsPtr(T *pt)
{
return true;
}
bool IsPtr(...)
{
return false;
}
但是,由于变参函数是C语言的内容,无法解析C++自定义类型对象,可能造成程序崩溃。
template
<typename T>
char IsPtr(T* v) // match pointer
{
return 'd';
}
int IsPtr(...) // match non-pointer
{
return 0;
}
#define ISPTR(p) (sizeof(IsPtr(p)) == sizeof(char))
上述代码中,C++编译器在编译时会进行函数匹配,不会进行调用,避免了参数为自定义对象时调用变参函数导致的程序崩溃。
九、class与typename关键字
C++语言在引入了面向对象编程思想时,使用class关键字定义类类型。C++语言发展过程中引入了泛型编程,直接复用class关键字来定义模板。但泛型编程针对的不只是类类型,直接复用class关键字会使代码出现二义性。因此,C++直接引入了typename关键字,用于在模板定义中声明泛指类型,明确告诉C++编译器声明的标识符为类型。
C++语言中允许类定义中嵌套类型,因此当自定义类类型中嵌套类型的标识符与其它类类型中定义的成员变量标识符重名时将会造成二义性。不同类中的同名标识符代表可能导致二义性,因此C++编译器无法识别标识符的确切意义。
#include <iostream>
using namespace std;
class Test1
{
public:
static const int NS = 1;
};
class Test2
{
public:
struct NS
{
int value;
};
};
int a = 0;
template <class T>
void func()
{
T::NS* a;
}
int main(int argc, char *argv[])
{
func<Test1>();
//func<Test2>();//error
//error: dependent-name 'T:: NS' is parsed as a non-type,
//but instantiation yields a type
//say 'typename T:: NS' if a type is meant
return 0;
}
上述代码中,C++编译器不会将func函数模板中NS解析为类型,因此使用Test2作为参数时,C++编译器会报错。因此,为了将NS明确声明为类型,需要使用typename关键字对NS标识符进行声明。代码如下:
#include <iostream>
using namespace std;
class Test1
{
public:
static const int NS = 1;
};
class Test2
{
public:
struct NS
{
int value;
};
};
int a = 0;
template <class T>
void func()
{
typename T::NS* a;
}
int main(int argc, char *argv[])
{
//func<Test1>();//error
func<Test2>();
return 0;
}
上述代码中,NS被明确声明为类型,因此如果使用Test1作为参数,func函数模板将会报错。