首页 > 代码库 > C++模版用法和注意事项

C++模版用法和注意事项

模版与泛型编程

面向对象的多态与组合并不能完全满足实际编程中对于代码复用的全部要求,泛型编程应运而生,而且享有和面向对象等同的地位。面向对象将操作绑定到数据,泛型则是将操作应用于不同数据结构和类型。C++中泛型编程的体现就是模版。模板的技术核心体现在编译期的动态机制,模板实例化则是在编译的过程中,编译器通过“类型推导”进行实例化。而运行的时候,系统不知道模板的概念。与之相比,面向对象就是在运行时调用构造函数进行类的对象的实例化。

模版的应用:当一个类或函数的概念适用于不同类或者不同基本数据类型时,应该用模版来实现。C++提倡少用宏,鼓励使用模板。模板是C++语言内置,而且模板操作的类型在编译时是已知,是类型安全的。而宏的本质则是纯粹的文本替换,编译器不会验证宏参数是否为兼容类型,会在不进行任何特殊类型检查的情况下扩展宏。


模版的特化与偏特化

首先看一下这篇文章;IBM 编译器中国开发团队-究竟什么是特化?模版的特化,某种程度上有点儿像函数的重载。

对于函数的重载,编译器根据传递给函数的实参类型来决定调用哪个函数,这就是重载解析。在调用前,编译器有一个候选函数调用列表,每个调用函数都有各自的参数,编译器根据参数最匹配原则选择相应的函数 。

对于模版的特化,同样是对特定类型进行特殊的操作,编译器来选择最佳匹配。全特化限定模板实现的具体类型,偏特化只限定模版实现类型中一部分。一般,类模版ji既可以全特华又可以偏特化,而函数模版则只有全特化,函数模版偏特也没有必要,函数重载即可实现。

#include <iostream>
using namespace std;

template<class T1, class T2> class Test {
public:
	Test(T1 i, T2 j) :
			a(i), b(j) {
		cout << "模板类" << endl;
	}
private:
	T1 a;
	T2 b;
};

template<> class Test<int, char> {
public:
	Test(int i, char j) :
			a(i), b(j) {
		cout << "全特化" << endl;
	}
private:
	int a;
	char b;
};

template<class T2> class Test<char, T2> {
public:
	Test(char i, T2 j) :
			a(i), b(j) {
		cout << "偏特化" << endl;
	}
private:
	char a;
	T2 b;
};

int main() {
	Test<long, char> test1(1,‘a‘);		//输出:模板类
	Test<float, char> test2(1,‘a‘);		//输出:模版类
	Test<int, char> test3(1,‘a‘);		//输出:全特化
	Test<char, double> test4(‘a‘,1.0);	//输出:偏特化
	Test<char, long> test5(‘a‘,1);		//输出:偏特化
}


模版的参数与类型

决定模板参数类型前,编译器执行下列隐式类型转换:左值变换、修饰字转换、派生类到基类的转换。实际使用中,参数可以显示也可以隐式:

  • 显式类型参数:对于模板函数,在函数名后添加 < {类型参数表} >。对于模板类,在类后添加 < {类型参数表} >;
  • 隐式类型参数:对于模板函数,如果类型参数可以推导,那么可以省略类型参数表,(不过一般还是不要省略为好);

函数模版举例:

template<class T> T min(T x, T y) {
	return (x < y) ? x : y;
}
/* 对特定的类型全特化 */
template<> long min<long>(long x, long y) {
	return (x < y) ? y : x;
}

int main() {
	int n1 = 1, n2 = 2;
	char a = ‘a‘, b = ‘b‘;
	long n3 = 1, n4 = 2;
	std::cout << min<int>(n1, n2) << "\n";	//输出:1,显示类型参数,模板的特化
	std::cout << min(a, b) << "\n";   		//输出:a,隐式类型参数,编译器可以自动类型推导
	std::cout << min(n3, n4) << "\n";   		  //输出:2,隐式类型参数,函数模板的全特化
	return 0;
} //这个简单的模板在特化时基本只包含类型的查找与替换,作用类似于“类型安全的宏”。


模版的声明定义和使用

前面说过,模版的动态机制体现在编译器,模版实例化也是在编译器进行类型的确定。所以,只有将模板类.cpp文件同调用程序.cpp文件一起作为一个编译单元编译运行,才能真正确定类的真正类型。具体可以有以下两种做法:

  • C++模板类的声明和定义都放在一个文件(推荐这种做法),如 .h 或 .cpp 文件中,使用的时候加入 #include "模板类文件名.h(或.cpp)"  即可;
  • 将C++模板类的声明和定义分别放在 .h 和 .cpp 文件中,且在 .cpp 文件中包含 #include ".h",使用时和一般的类不同,一般的类是#include"类.h",但是模板类是 "模板类.cpp" ,因为要把它们作为一个编译单元编译,编译器决定。而普通类编译后由连接器连接就行。

类模版举例,作容器:

#include <iostream>
const int DefaultSize = 10;

/*一个简单的Array容器 */
template<class T> class Array {
public:
	Array(int size = DefaultSize);
	Array(const Array &rhs);
	~Array() {delete[] pType;}

	Array& operator =(const Array&);

	T& operator[](int offset) {
		return pType[offset];
	} // 运算符重载[]

	const T& operator[](int offset) const {
		return pType[offset];
	} // 运算符重载[], const版本

	int getSize() const {
		return size;
	}

private:
	T *pType;
	int size;
};

template<class T> Array<T>::Array(int size):size(size) {
	pType = new T(size);
}

/* 用此种类型对象做参数再构造一个此种类型的对象 */
template<class T> Array<T>::Array(const Array &rhs) {
	size = rhs.getSize();
	pType = new T[size];
	for (int i = 0; i < size; i++) {
		pType[i] = rhs[i];
	}
}

/* 重载 = ,形参和返回值都是引用 */
template<class T> Array<T>& Array<T>::operator =(const Array &rhs) {
	if (this == &rhs) {
		return *this;
	}
	delete[] pType;
	size = rhs.getSize();
	pType = new T(size);
	return *this;
}

class Animal {
public:
	Animal(int weight) :weight(weight) {}
	Animal():weight(0){}
	~Animal() {}

	/* 函数后面有const,表示只读,内部不能修改成员变量的值;*/
	int getWeight() const {
		return weight; // 这里如果是weight++,就报错了
	}
	void display() const {
		std::cout << weight;
	}
private:
	int weight;
};

int main() {
	Array<int> integers;
	Array<Animal> animals;
	Animal *pAnimal;
	for (int i = 0; i < integers.getSize(); i++) {
		integers[i] = i;
		pAnimal = new Animal(i * 2);
		animals[i] = *pAnimal;
	}
	for (int i = 0; i < integers.getSize(); i++) {
		std::cout << "the array[" << i << "]:\t";
		std::cout << integers[i] << "\t\t";
		std::cout << "animals[" << i << "]:\t";
		animals[i].display();
		std::cout << std::endl;
	}
	return 0;
}

输出:

the array[0]:	0		animals[0]:	0
the array[1]:	1		animals[1]:	2
the array[2]:	2		animals[2]:	4
the array[3]:	3		animals[3]:	6
the array[4]:	4		animals[4]:	8
the array[5]:	5		animals[5]:	10
the array[6]:	6		animals[6]:	12
the array[7]:	7		animals[7]:	14
the array[8]:	8		animals[8]:	16
the array[9]:	9		animals[9]:	18

函数对象Function Object 

标准库经常用到函数对象,也叫仿函数(Functor) 。函数对象就是一个重载了”()”运算符的struct或class,利用对象支持operator()的特性,来达到模拟函数调用效果的技术。函数对象有两大优势:

  • 函数对象可以包含状态;
  • 函数对象属于类型可用作模板参数;

函数对象举例:

#include<iostream>

class Add {
public:
	int operator()(int a, int b) {
		return a + b;
	}
};

int main() {
	Add add;
	std::cout << add(2, 3) << "\n"; // 输出5
}

经过近阶段对C++的了解,越来越理解:在C++语言里,存在这一种很强的趋势,就是如果你不明白C++语言的细节,你就无法做好任何事情。 – Larry Wall, developer of the Perl