C++总结(五)——多态与模板
C++总结(五)——多态与模板
向上转型
回顾
在C++总结四中简单分析了派生类转换为基类的过程,在讲多态前需要提前了解这种向上转型的过程。类本身也是一种数据,数据就能进行类型的转换。如下代码
int a = 10.9;
printf("%d\n", a); //输出为10
float b = 10;
printf("%f\n", b);//输出为 10.000000
上面代码中,10.9属于float类型的数据,讲10.9赋值给z整型的过程属于是float->int的过程,所以会丢失小数点后面的数据。
同理,10是整型数据,将其赋值给b相当于int->float的过程,这个过程是数据填充过程,b数据后面会添加上小数点后6位
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。
类向上转型主要有三种方式:将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用
将派生类赋值给基类对象
#include <iostream>
using namespace std;
class A{
public:
A(int a);
public:
void display();
public:
int m_a;
A::A(int a): m_a(a){ }
void A::display(){
cout<<"Class A: m_a="<<m_a<<endl;
//派生类
class B: public A{
public:
B(int a, int b);
public:
void display();
public:
int m_b;
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
int main(){
A a(10);
B b(66, 99);
//赋值前
a.display();
b.display();
//赋值后
a = b;
a.display();
b.display();
return 0;
输出结果:
Class A: m_a=10
Class B: m_a=66, m_b=99
Class A: m_a=66
Class B: m_a=66, m_b=99
分析:主要看a=b这段代码。由于a是基类b是派生类,当使用a=b的时候,就是将派生类转换为基类的过程。由于对象继承的时候内存模型如下图,基类与派生类中的成员函数是独立存储在公共的代码空间当中的。所以,当(a=b)派生类转为基类的过程本质就是派生类中的数据覆盖掉基类的数据,而成员函数依旧是使用基类中的成员函数。所以在赋值后的a.display()是调用基类中的函数。
将派生类指针赋值给基类对象
#include <iostream>
using namespace std;
//基类A
class A{
public:
A(int a);
public:
void display();
protected:
int m_a;
A::A(int a): m_a(a){ }
void A::display(){
cout<<"Class A: m_a="<<m_a<<endl;
//中间派生类B
class B: public A{
public:
B(int a, int b);
public:
void display();
protected:
int m_b;
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
//基类C
class C
public:
C(int c);
public:
void display();
protected:
int m_c;
C::C(int c): m_c(c){ }
void C::display()
cout<<"Class C: m_c="<<m_c<<endl;
//最终派生类D
class D: public B, public C
public:
D(int a, int b, int c, int d);
public:
void display();
private:
int m_d;
D::D(int a, int b, int c, int d): B(a, b), C(c), m_d(d){ }
void D::display()
cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
int main()
A *pa = new A(1);
B *pb = new B(2, 20);
C *pc = new C(3);
D *pd = new D(4, 40, 400, 4000);
pa = pd;
pa -> display();
pb = pd;
pb -> display();
pc = pd;
pc -> display();
cout<<"-----------------------"<<endl;
cout<<"pa="<<pa<<endl;
cout<<"pb="<<pb<<endl;
cout<<"pc="<<pc<<endl;
cout<<"pd="<<pd<<endl;
return 0;
输出结果
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400
pa=0x9b17f8
pb=0x9b17f8
pc=0x9b1800
pd=0x9b17f8
分析:pd派生类指针指向它继承的基类时,本质上也类似于指针变量之间的类型转换。指的注意的是这里的pc的指针应该与pa、pb、pd的相同才是为什么会比他们大?
要理解这个问题,首先要清楚 D 类对象的内存模型,如下图所示:
首先要明确的一点是,对象的指针必须要指向对象的起始位置。对于 A 类和 B 类来说,它们的子对象的起始地址和 D 类对象一样,所以将 pd 赋值给 pa、pb 时不需要做任何调整,直接传递现有的值即可;而 C 类子对象距离 D 类对象的开头有一定的偏移,将 pd 赋值给 pa 时要加上这个偏移,这样 pc 才能指向 C 类子对象的起始位置。也就是说,执行pc = pd;语句时编译器对 pd 的值进行了调整,才导致 pc、pd 的值不同。
将派生类赋值给基类对象
#include <iostream>
using namespace std;
//基类A
class A{
public:
A(int a);
public:
void display();
protected:
int m_a;
A::A(int a): m_a(a){ }
void A::display(){
cout<<"Class A: m_a="<<m_a<<endl;
//中间派生类B
class B: public A{
public:
B(int a, int b);
public:
void display();
protected:
int m_b;
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
//基类C
class C{
public:
C(int c);
public:
void display();
protected:
int m_c;
C::C(int c): m_c(c){ }
void C::display(){
cout<<"Class C: m_c="<<m_c<<endl;
//最终派生类D
class D: public B, public C{
public:
D(int a, int b, int c, int d);
public:
void display();
private:
int m_d;
D::D(int a, int b, int c, int d): B(a, b), C(c), m_d(d){ }
void D::display(){
cout<<"Class D: m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
int main(){
D d(4, 40, 400, 4000);
A &ra = d;
B &rb = d;
C &rc = d;
ra.display();
rb.display();
rc.display();
return 0;
运行结果:
Class A: m_a=4
Class B: m_a=4, m_b=40
Class C: m_c=400
ra、rb、rc 是基类的引用,它们都引用了派生类对象 d,并调用了 display() 函数,从运行结果可以发现,虽然使用了派生类对象的成员变量,但是却没有使用派生类的成员函数,这和指针的表现是一样的。
扩展:前面提到了引用,引用是什么,引用与指针有什么区别?
引用本质是指一个变量的别名,它在C++中被用来传递参数和返回值。引用的声明方式为在变量名前加上&符号,例如:int& ref = a; 这里的ref就是a的引用。
与指针相比,引用有以下几点不同:
- 引用必须被初始化,指针可以不初始化。
- 引用一旦被初始化,就不能再指向其他变量,指针可以重新指向其他变量。
- 引用在使用时不需要解引用,指针需要使用*运算符解引用。
- 引用不存在空引用,指针可以为空指针。
- 算数转换:例如 int 转换为 float,char 转换为 int,double 转换为 int 等。
- 派生类向基类的转换:也就是向上转型
- const 转换:也即将非 const 类型转换为 const 类型,例如将 char * 转换为 const char *。
- 数组或函数指针转换:如果函数形参不是引用类型,那么数组名会转换为数组指针,函数名也会转换为函数指针
- 用户自定的类型转换。
int a = 10;
int* p = &a; // 指针指向a的地址
int& ref = a; // 引用指向a
cout << *p << endl; // 输出10
cout << ref << endl; // 输出10
int b = 20;
p = &b; // 指针重新指向b的地址
ref = b; // 引用依然指向a,但是a的值被改为20
cout << *p << endl; // 输出20
cout << ref << endl; // 输出20
int* nullPtr = nullptr; // 空指针
int& nullRef; // 错误,引用必须被初始化
多态
概念
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。简单来说就是一个基类下有多个派生类,或者基类下派生类之间又存在着继承。最后,形成的层次结构。 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
在C++中,多态的实现主要是通过虚函数和继承来实现的。
前面说到,派生类可以向上转换为基类,那么基类该如何访问到派生类?
#include <iostream>
using namespace std;
class Animal
public:
void sound()
cout << "Animal makes sound" << endl;
class Cat : public Animal
public:
void sound()
cout << "Cat meows" << endl;
class Dog : public Animal
public:
void sound()
cout << "Dog barks" << endl;
int main()
Animal *animal;
Cat cat;
Dog dog;
animal = &cat;
animal->sound();
animal = &dog;
animal->sound();
return 0;
输出结果
Animal makes sound
Animal makes sound
分析:如上代码,基类得Animal访问派生类Dog与Cat对应得sound函数。本想着直接把Cat与Dog的地址直接赋值给animal就能访问,但是这是一种向上隐式转换方式,只会覆盖基类中的变量,基类对象数据是不包含成员函数的。所以,最终只会输出基类中的sound()函数内容。可如果我就要通过基类访问派生类内容怎么办?
分析:如上代码,基类得Animal访问派生类Dog与Cat对应得sound函数。本想着直接把Cat与Dog的地址直接赋值给animal就能访问,但是这是一种向上隐式转换方式,只会覆盖基类中的变量,基类对象数据是不包含成员函数的。所以,最终只会输出基类中的sound()函数内容。可如果我就要通过基类访问派生类内容怎么办?
使用友元,这里就不分析友元,前面C++总结四中已经友例子了。
1,使用虚函数
#include <iostream>
using namespace std;
class Animal
public:
virtual void sound()
cout << "Animal makes sound" << endl;
class Cat : public Animal
public:
void sound()
cout << "Cat meows" << endl;
class Dog : public Animal
public:
void sound()
cout << "Dog barks" << endl;
int main()
Animal *animal;
Cat cat;
Dog dog;
animal = &cat;
animal->sound();
animal = &dog;
animal->sound();
return 0;
输出结果
Cat meows
Dog barks
如上代码直接在基类的sound函数前面加上virtual关键字,输出就正常了。原因是Virtual关键字声明的成员函数,它可以在派生类中被重写,并且在运行时根据对象的类型调用相应的函数。在基类中声明为虚函数的函数,在派生类中可以使用相同的函数名和参数列表进行重写。当通过基类指针或引用调用虚函数时,会根据实际对象的类型来调用相应的函数。
下面是一个简单的例子,演示了如何使用虚函数实现多态:
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() {
cout << "Drawing a shape." << endl;
class Circle : public Shape {
public:
void draw() {
cout << "Drawing a circle." << endl;
class Square : public Shape {
public:
void draw() {
cout << "Drawing a square." << endl;
int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Square();
shape1->draw();
shape2->draw();
delete shape1;
delete shape2;
return 0;
在上面的代码中,定义了一个基类Shape和两个派生类Circle和Square。其中Shape类中有一个虚函数draw(),在派生类中分别重写了这个函数。在main函数中,分别用基类指针指向Circle和Square对象,并调用它们的draw()函数。在运行时,根据指针所指对象的类型,分别调用了Circle和Square的draw()函数,实现了多态。
除了虚函数,C++中还有纯虚函数和抽象类的概念。纯虚函数是在基类中声明但没有实现的虚函数,它的目的是为了让派生类必须实现这个函数。抽象类是包含纯虚函数的类,它不能被实例化,只能作为基类使用。
下面是一个例子,演示了如何使用纯虚函数和抽象类:
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() = 0; // 纯虚函数,没有实现
class Circle : public Shape {
public:
void draw() {
cout << "Drawing a circle." << endl;
class Square : public Shape {
public:
void draw() {
cout << "Drawing a square." << endl;
int main() {
// Shape* shape = new Shape(); // 错误,抽象类不能被实例化
Shape* shape1 = new Circle();
Shape* shape2 = new Square();
shape1->draw();
shape2->draw();
delete shape1;
delete shape2;
return 0;
在上面的代码中,Shape类中的draw()函数被声明为纯虚函数,没有实现。Circle和Square类都继承自Shape类,并分别实现了draw()函数。在main函数中,同样使用基类指针指向Circle和Square对象,并调用它们的draw()函数。由于Shape类是抽象类,不能被实例化
C++静态绑定和动态绑定
函数调用实际上是执行函数体中的代码。函数体是内存中的一个代码段,函数名就表示该代码段的首地址,函数执行时就从这里开始。说得简单一点,就是必须要知道函数的入口地址,才能成功调用函数。
找到函数名对应的地址,然后将函数调用处用该地址替换,这称为函数绑定。
一般情况下,在编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定,程序运行后直接使用这个地址即可。这称为静态绑定(Static binding)。
但是有时候在编译期间想尽所有办法都不能确定使用哪个函数,必须要等到程序运行后根据具体的环境或者用户操作才能决定这称为动态绑定(dynamic binding).
int n;
cin>>n;
if(n > 100)
p = new Student("王刚", 16, 84.5);
p = new Senior("李智", 22, 92.0, true);
p -> display();
如上代码,如果用户输入的数字大于 100,那么 p 指向 Student 类的对象,否则就指向 Senior 类的对象,这种情况编译器如何逆推呢?鬼知道用户输入什么数字!所以编译器干脆不会向前逆推,因为编译器不知道前方是什么情况,可能会很复杂,它也无能为力。
这就是动态绑定的本质:编译器在编译期间不能确定指针指向哪个对象,只能等到程序运行后根据具体的情况再决定。
模板
概念
模板是一种通用的编程机制,它可以实现在编写程序时不考虑具体数据类型的情况下,编写一些通用的代码。模板可以看做是一个模板类或模板函数的声明,其中某些类型可以是变量类型。模板中使用的类型可以在实例化时被具体的类型代替。
问题
int Add1 (int *a,int *b)
int temp;
*temp = *a +*b;
return temp;
float Add2 (float *a,float *b,float *temp)
float temp;
*temp = *a +*b;
return temp;
如上代码,其实Add1的函数与Add2函数的功能是一样的只是参数的类型不一样。那么是否有一种形式能够将上面两个函数统一为一个函数?这就需要通过模板来实现了。
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。
一但定义了函数模板,就可以将类型参数用于函数定义和函数声明了。说得直白一点,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。如上代码就可以变为
#include <iostream>
using namespace std;
template<typename T>
T Add(T *a, T *b)
T temp = *a+*b;
return temp;
int main()
int a1 =100,a2=200;
cout<<Add(&a1,&a2)<<endl;
float b1=100.0 float b2= 120.0
cout<<Add(&b1,&b2)<<endl;
输出:
300
220.0
格式
在C++中,模板的格式为:
template <typename T>
class A {
//...
template <typename T>
void fun(T t) {
//...
template <typename 类型参数1 , typename 类型参数2 , ...> 返回值类型 函数名(形参列表){
//在函数体中可以使用类型参数
类型参数可以有多个,它们之间以逗号,分隔。
其中,typename关键字可以用class替代,两者等价。
template <class T>
class A {
//...
template <class T>
void fun(T t) {
//...
多参数类模板格式
template<typename T1, typename T2>
class Point
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const; //获取x坐标
void setX(T1 x); //设置x坐标
T2 getY() const; //获取y坐标
void setY(T2 y); //设置y坐标
private:
T1 m_x; //x坐标
T2 m_y; //y坐标
template<typename T1, typename T2> //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {
return m_x;
template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x){
m_x = x;
template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const{
return m_y;
template<typename T1, typename T2>
void Point<T1, T2>::setY(T2 y){
m_y = y;
int main()
Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, char*> p3(12.4, "东经180度");
// 或者
Point<float, float> *p4 = new Point<float, float>(10.6, 109.3);
Point<char*, char*> *p5 = new Point<char*, char*>("东经180度", "北纬210度")
除了 template 关键字后面要指明类型参数,类名 Point 后面也要带上类型参数,只是不加 typename 关键字了。另外需要注意的是,在类外定义成员函数时,template 后面的类型参数要和类声明时的一致。
实例化
模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。
另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。
#include <iostream>
using namespace std;
template<class T1, class T2>
class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; };
void setY(T2 y){ m_y = y; };
void display() const;
private:
T1 m_x;
T2 m_y;
template<class T1, class T2>
void Point<T1, T2>::display() const{
cout<<"x="<<m_x<<", y="<<m_y<<endl;
int main(){
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
Point<char*, char*> p2("东经180度", "北纬210度");
p2.display();
return 0;
输出
x=40, y=50
x=东经180度, y=北纬210度
p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。
值得提醒的是,Point<int, int>和Point<char*, char*>是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如p1 = p2;这样的语句是错误的,除非重载了=运算符。
模板重载
当需要对不同的类型使用同一种算法(同一个函数体)时,为了避免定义多个功能重复的函数,可以使用模板。然而,并非所有的类型都使用同一种算法,有些特定的类型需要单独处理,为了满足这种需求,C++ 允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义。
下面是一个重载函数模板的完整示例:
#include <iostream>
using namespace std;
template<class T> void Swap(T &a, T &b); //模板①:交换基本类型的值
template<typename T> void Swap(T a[], T b[], int len); //模板②:交换两个数组
void printArray(int arr[], int len); //打印数组元素
int main(){
//交换基本类型的值
int m = 10, n = 99;
Swap(m, n); //匹配模板①
cout<<m<<", "<<n<<endl;
//交换两个数组
int a[5] = { 1, 2, 3, 4, 5 };
int b[5] = { 10, 20, 30, 40, 50 };
int len = sizeof(a) / sizeof(int); //数组长度
Swap(a, b, len); //匹配模板②
printArray(a, len);
printArray(b, len);
return 0;
template<class T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
template<typename T> void Swap(T a[], T b[], int len){
T temp;
for(int i=0; i<len; i++){
temp = a[i];
a[i] = b[i];
b[i] = temp;
void printArray(int arr[], int len){
for(int i=0; i<len; i++){
if(i == len-1){
cout<<arr[i]<<endl;
}else{
cout<<arr[i]<<", ";
运行结果
99, 10
10, 20, 30, 40, 50
1, 2, 3, 4, 5
实参推断
在使用类模板创建对象时,程序员需要显式的指明实参
Point<int, int> p1(10, 20); //在栈上创建对象
Point<char*, char*> *p = new Point<char*, char*>("东京180度", "北纬210度"); //在堆上创建对象
我们可以在栈上创建对象,也可以在堆上创建对象:因为已经显式地指明了 T1、T2 的具体类型,所以编译器就不用再自己推断了,直接拿来使用即可。
而对于函数模板,调用函数时可以不显式地指明实参(也就是具体的类型)。请看下面的例子
//函数声明
template<typename T> void Swap(T &a, T &b);
//函数调用
int n1 = 100, n2 = 200;
Swap(n1, n2);
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);
然没有显式地指明 T 的具体类型,但是编译器会根据 n1 和 n2、f1 和 f2 的类型自动推断出 T 的类型。这种通过函数实参来确定模板实参(也就是类型参数的具体类型)的过程称为模板实参推断。
对于普通函数(非模板函数),发生函数调用时会对实参的类型进行适当的转换,以适应形参的类型。这些转换包括:
例如有下面两个函数原型:
void func1(int n, float f);
void func2(int *arr, const char *str);
它们具体的调用形式为:
int nums[5];
char *url = "http://www.cdsy.xyz";
func1(12.5, 45);
func2(nums, url);
对于 func1(),12.5 会从double转换为int,45 会从int转换为float;对于 func2(),nums 会从int [5]转换为int *,url 会从char *转换为const char *
而对于函数模板,类型转换则受到了更多的限制,仅能进行「const 转换」和「数组或函数指针转换」,其他的都不能应用于函数模板。例如有下面几个函数模板:
template<typename T> void func1(T a, T b);
template<typename T> void func2(T *buffer);
template<typename T> void func3(const T &stu);
template<typename T> void func4(T a);
template<typename T> void func5(T &a);
它们具体的调用形式为:
int name[20];
Student stu1("张华", 20, 96.5); //创建一个Student类型的对象
func1(12.5, 30); //Error
func2(name); //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int
func3(stu1); //非const转换为const,T 的真实类型为 Student
func4(name); //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int *
func5(name); //name的类型依然为 int [20],不会转换为 int *,所以 T 的真实类型为 int [20]
对于func1(12.5, 30),12.5 的类型为 double,30 的类型为 int,编译器不知道该将 T 实例化为 double 还是 int,也不会尝试将 int 转换为 double,或者将 double 转换为 int,所以调用失败。
对于func2(name)和func4(name),name 的类型会从 int [20] 转换为 int *,也即将数组转换为指针,所以 T 的类型分别为 int * 和 int。
对于func5(name),name 的类型依然为 int [20],不会转换为 int *,所以 T 的类型为 int [20]。
可以发现,当函数形参是引用类型时,数组不会转换为指针。这个时候读者要注意下面这样的函数模板:
template<typename T> void func(T &a, T &b);
如果它的具体调用形式为:
int str1[20];
int str2[10];
func(str1, str2);
由于 str1、str2 的类型分别为 int [20] 和 int [10],在函数调用过程中又不会转换为指针,所以编译器不知道应该将 T 实例化为 int [20] 还是 int [10],导致调用失败。
函数模板的实参推断是指「在函数调用过程中根据实参的类型来寻找类型参数的具体类型」的过程,这在大部分情况下是奏效的,但是当类型参数的个数较多时,就会有个别的类型无法推断出来,这个时候就必须显式地指明实参。
template<typename T1, typename T2> void func(T1 a){
T2 b;
func(10); //函数调用
func() 有两个类型参数,分别是 T1 和 T2,但是编译器只能从函数调用中推断出 T1 的类型来,不能推断出 T2 的类型来,所以这种调用是失败的,这个时候就必须显式地指明 T1、T2 的具体类型。「为函数模板显式地指明实参」和「为类模板显式地指明实参」的形式是类似的,就是在函数名后面添加尖括号< >,里面包含具体的类型。
对于上面的 func(),虽然只有 T2 的类型不能自动推断出来,但是由于它位于类型参数列表的尾部(最右),所以必须同时指明 T1 和 T2 的类型。对代码稍微做出修改
template<typename T1, typename T2> void func(T2 a){
T1 b;
//函数调用
func<int>(10); //省略 T2 的类型
func<int, int>(20); //指明 T1、T2 的类型
由于 T2 的类型能够自动推断出来,并且它位于参数列表的尾部(最右),所以可以省略。
上面我们提到,函数模板仅能进行「const 转换」和「数组或函数指针转换」两种形式的类型转换,但是当我们显式地指明类型参数的实参(具体类型)时,就可以使用正常的类型转换(非模板函数可以使用的类型转换)了。
template<typename T> void func(T a, T b);
func(10, 23.5); //Error
func<float>(20, 93.7); //Correct
在第二种调用形式中,我们已经显式地指明了 T 的类型为 float,编译器不会再为「T 的类型到底是 int 还是 double」而纠结了,所以可以从容地使用正常的类型转换了。
模板显示具体化
C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。
template<class T> const T& Max(const T& a, const T& b){
return a > b ? a : b;
注意a > b这条语句,>能够用来比较 int、float、char 等基本类型数据的大小,但是却不能用来比较结构体变量、对象以及数组的大小,因为我们并没有针对结构体、类和数组重载>。
除了>,+-*/==<等运算符也只能用于基本类型,不能用于结构体、类、数组等复杂类型。总之,编写的函数模板很可能无法处理某些类型,我们必须对这些类型进行单独处理。
模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的显示具体化(Explicit Specialization)。
#include <iostream>
#include <string>
using namespace std;
typedef struct{
string name;
int age;
float score;
} STU;
//函数模板
template<class T> const T& Max(const T& a, const T& b);
//函数模板的显示具体化(针对STU类型的显示具体化)
template<> const STU& Max<STU>(const STU& a, const STU& b);
ostream & operator<<(ostream &out, const STU &stu);
int main(){
int a = 10;
int b = 20;
cout<<Max(a, b)<<endl;
STU stu1 = { "王明", 16, 95.5};
STU stu2 = { "徐亮", 17, 90.0};
cout<<Max(stu1, stu2)<<endl;
return 0;
template<class T> const T& Max(const T& a, const T& b){
return a > b ? a : b;
//实例化
template<> const STU& Max<STU>(const STU& a, const STU& b){
return a.score > b.score ? a : b;
ostream & operator<<(ostream &out, const STU &stu){
out<<stu.name<<" , "<<stu.age <<" , "<<stu.score;
return out;
输出结果
20
王明 , 16 , 95.5
本例中,STU 结构体用来表示一名学生(Student),它有三个成员,分别是姓名(name)、年龄(age)、成绩(score);Max() 函数用来获取两份数据中较大的一份。
要想获取两份数据中较大的一份,必然会涉及到对两份数据的比较。对于 int、float、char 等基本类型的数据,直接比较它们本身的值即可,而对于 STU 类型的数据,直接比较它们本身的值不但会有语法错误,而且毫无意义,这就要求我们设计一套不同的比较方案,从语法和逻辑上都能行得通,所以本例中我们比较的是两名学生的成绩(score)。
不同的比较方案最终导致了算法(函数体)的不同,我们不得不借助模板的显示具体化技术对 STU 类型进行单独处理。【template<> const STU& Max(const STU& a, const STU& b)具显化声明】,template<> const STU& Max(const STU& a, const STU& b)【实例化】
注意Max中的STU表明了要将类型参数 T 具体化为 STU 类型,原来使用 T 的位置都应该使用 STU 替换,包括返回值类型、形参类型、局部变量的类型。
Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写作template<>
另外,Max中的STU是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够逆推出 T 的具体类型。简写后的函数声明为:
部分显式具体化
在上面的显式具体化例子中,我们为所有的类型参数都提供了实参,所以最后的模板头为空,也即template<>。另外 C++ 还允许只为一部分类型参数提供实参,这称为部分显式具体化。
部分显式具体化只能用于类模板,不能用于函数模板。
以 Point 为例,假设我现在希望“只要横坐标 x 是字符串类型”就以|来分隔输出结果,而不管纵坐标 y 是什么类型,这种要求就可以使用部分显式具体化技术来满足。请看下面的代码:
#include <iostream>
using namespace std;
//类模板
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
template<class T1, class T2> //这里需要带上模板头
void Point<T1, T2>::display() const{
cout<<"x="<<m_x<<", y="<<m_y<<endl;
//类模板的部分显示具体化
template<typename T2> class Point<char*, T2>{
public:
Point(char *x, T2 y): m_x(x), m_y(y){ }
public:
char *getX() const{ return m_x; }
void setX(char *x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
char *m_x; //x坐标
T2 m_y; //y坐标
template<typename T2> //这里需要带上模板头
void Point<char*, T2>::display() const{
cout<<"x="<<m_x<<" | y="<<m_y<<endl;
int main(){
( new Point<int, int>(10, 20) ) -> display();
( new Point<char*, int>("东经180度", 10) ) -> display();
( new Point<char*, char*>("东经180度", "北纬210度") ) -> display();
return 0;
运行结果
x=10, y=20
x=东经180度 | y=10
x=东经180度 | y=北纬210度
本例中,T1 对应横坐标 x 的类型,我们将 T1 具体化为char ,模板头template中声明的是没有被具体化的类型参数;类名Point<char , T2>列出了所有类型参数,包括未被具体化的和已经被具体化的。类名后面之所以要列出所有的类型参数,是为了让编译器确认“到底是第几个类型参数被具体化了”,如果写作template class Point<char*>,编译器就不知道char*代表的是第一个类型参数,还是第二个类型参数。
模板多文件编程
C++模板在多文件编程中的应用主要体现在两个方面:头文件和源文件的分离,以及模板的实例化。
头文件和源文件的分离
在多文件编程中,通常会将类的声明和实现分别放在不同的文件中。对于模板类来说,它的声明和实现同样可以分离到不同的文件中。一般来说,模板类的声明放在头文件中,而模板类的实现放在源文件中。
例如,我们定义一个模板类
Array
,用于存储任意类型的元素:
// main.cpp
#include "Array.h"
int main() {
Array<int> arr(10);
for (int i = 0; i < 10; i++) {
arr[i] = i;
return 0;
在源文件
main.cpp
中,我们只需要包含头文件
Array.h
,就可以使用
Array
类了。
这种方式的好处是,可以将模板类的声明和实现分别放在不同的文件中,使得代码更加清晰和易于维护。
实例化
在使用模板类时,需要对模板进行实例化,即指定模板参数的类型。在多文件编程中,模板的实例化需要注意一些细节。
首先,模板类的声明和实现必须都要放在头文件中。这是因为,模板的实例化是在编译期间进行的,编译器需要知道模板的完整定义才能进行实例化。
其次,如果模板类的实现放在源文件中,那么在使用模板类时,需要在源文件中显式实例化模板。例如,我们在源文件
main.cpp
中使用
Array
类:
// main.cpp
#include "Array.h"
template class Array<int>; // 显式实例化模板
int main() {
Array<int> arr(10);
for (int i = 0; i < 10; i++) {
arr[i] = i;
return 0;
在源文件
main.cpp
中,我们需要显式实例化
Array<int>
模板,告诉编译器我们要使用
Array<int>
类型的对象。如果没有显式实例化模板,编译器会在链接时报错,因为无法找到
Array<int>
的定义。
需要注意的是,如果模板类的实现放在头文件中,就不需要显式实例化模板了,因为头文件中的模板已经包含了完整的定义。
模板类继承
C++模板类继承是指一个模板类可以从另一个模板类继承,来获取一部分或全部成员。模板类继承的语法与普通类继承相同,只不过需要在模板参数列表中指定继承的模板类参数。下面是一个简单的示例代码:
template<typename T>
class Base {
public:
T data;
void print() {
std::cout << "Base data: " << data << std::endl;
template<typename T>
class Derived : public Base<T> {
public:
void print() {
std::cout << "Derived data: " << this->data << std::endl;
int main() {
Derived<int> d;
d.data = 10;
d.print(); // 输出:Derived data: 10
return 0;
在上面的示例中,类
Derived
从类
Base
中继承了一个成员变量
data
和一个成员函数
print
。在
print
函数中,我们使用
this->data
来访问
Base
类中的
data
变量,因为
Derived
类中也有一个名为
data
的变量。
需要注意的是,在模板类继承中,如果基类中有依赖于模板参数的成员或函数(例如
Base
类的
data
成员),子类中要使用这些成员或函数时需要加上
this->
或
Base<T>::
来指定作用域。
另外,模板类继承也可以多层继承,例如:
template<typename T>
class Base {
public:
T data;
void print() {
std::cout << "Base data: " << data << std::endl;
template<typename T>
class Derived1 : public Base<T> {
public:
void print() {
std::cout << "Derived1 data: " << this->data << std::endl;
template<typename T>
class Derived2 : public Derived1<T> {
public:
void print() {
std::cout << "Derived2 data: " << this->data << std::endl;
int main() {
Derived2<int> d;
d.data = 10;
d.print(); // 输出:Derived2 data: 10
return 0;
派生方式:类模板从类模板派生
template <class T1, class T2>
class A
Tl v1; T2 v2;
template <class T1, class T2>
class B : public A <T2, T1>
T1 v3; T2 v4;
template <class T>
class C : public B <T, T>
T v5;
int main()
B<int, double> obj1;
C<int> obj2;
return 0;
[ B<int, double> obj1;]编译器用 int 替换类模板 B 中的 T1,用 double 替换 T2,生成 B<int, double> 类如下:
类模板从模板类派生
template<class T1, class T2>
class A{ T1 v1; T2 v2; };
template <class T>
class B: public A <int, double>{T v;};
int main() { B <char> obj1; return 0; }
A<int, double> 是一个具体的类的名字,而且它是一个模板类,因此说类模板 B 是从模板类派生而来的。Bobj1;时会自动生成两个模板类:A<int, double> 和 B。
类模板从普通类派生
class A{ int v1; };
template<class T>
class B: public A{ T v; };
int main (){ B <char> obj1; return 0; }
普通类从模板类派生
template <class T>
class A{ T v1; int n; };
class B: public A <int> { double v; };
int main() { B obj1; return 0; }
模板与友元
函数、类、类的成员函数作为类模板的友元
void Func1() { }
class A { };
class B
public:
void Func() { }
template <class T>
class Tmpl
friend void Func1();
friend class A;
friend void B::Func();
int main()
Tmpl<int> i;
Tmpl<double> f;
return 0;
类模板实例化时,除了类型参数被替换外,其他所有内容都原样保留,因此任何从 Tmp1 实例化得到的类都包含上面三条友元声明,因而也都会把 Func1、类 A 和 B::Func 当作友元。
函数模板作为类模板的友元
#include <iostream>
#include <string>
using namespace std;
template <class T1, class T2>
class Pair
private:
T1 key; //关键字
T2 value; //值
public:
Pair(T1 k, T2 v) : key(k), value(v) { };
bool operator < (const Pair<T1, T2> & p) const;
template <class T3, class T4>
friend ostream & operator << (ostream & o, const Pair<T3, T4> & p);
template <class T1, class T2>
bool Pair <T1, T2>::operator< (const Pair<T1, T2> & p) const
{ //“小”的意思就是关键字小
return key < p.key;
template <class Tl, class T2>
ostream & operator << (ostream & o, const Pair<T1, T2> & p)
o << "(" << p.key << "," << p.value << ")";
return o;
int main()
Pair<string, int> student("Tom", 29);
Pair<int, double> obj(12, 3.14);
cout << student << " " << obj;
return 0;
程序的输出结果是:(Torn, 29) (12, 3.14)
上面的代码中【friend ostream & operator << (ostream & o, const Pair<T3, T4> & p);】函数模板 operator<< 声明为类模板 Pair 的友元。在 Visual Studio 中,这两行也可以用下面的写法替代:
friend ostream & operator<< <T1, T2>(ostream & o, const Pair<T1, T2> & p);
编译本程序时,编译器自动生成了两个 operator << 函数,它们的原型分别是:
ostream & operator << (ostream & o, const Pair<string, int> & p);
ostream & operator << (ostream & o, const Pair<int, double> & p);
前者是 Pair <string, int> 类的友元,但不是 Pair<int, double> 类的友元;后者是 Pair<int, double> 类的友元,但不是 Pair<string, int> 类的友元。
函数模板作为类的友元
#include <iostream>
using namespace std;
class A
int v;
public:
A(int n) :v(n) { }
template <class T>
friend void Print(const T & p);
template <class T>
void Print(const T & p)
cout << p.v;
int main()
A a(4);
Print(a);
return 0;
程序的输出结果是:4
编译器编译到Print(a);时,就从 Print 模板实例化出一个 Print 函数,原型如下:
void Print(const A & p);
这个函数本来不能访问 p 的私有成员。但是编译器发现,如果将类 A 的友元声明中的 T 换成 A,就能起到将该 Print 函数声明为友元的作用,因此编译器就认为该 Print 函数是类 A 的友元。
类模板作为类模板的友元
#include <iostream>
using namespace std;
template<class T>
class A
public:
void Func(const T & p)
cout << p.v;
template <class T>
class B
private:
public:
B(T n) : v(n) { }
template <class T2>
friend class A; //把类模板A声明为友元
int main()
B<int> b(5);
A< B<int> > a; //用B<int>替换A模板中的 T
a.Func(b);
return 0;
程序的输出结果是:5
类模板中的静态成员
C++类模板中的静态成员是指在类模板中使用static关键字定义的成员变量或成员函数。它们与普通的静态成员的定义和使用方式相同,但是在类模板中需要特别注意一些问题。
静态成员变量的定义和初始化
在类模板中定义静态成员变量时,需要在前面加上template<>关键字,表示这是一个类模板的特化版本。例如:
template<class T>
class MyClass
public:
static int count; // 声明静态成员变量
template<class T>
int MyClass<T>::count = 0; // 定义并初始化静态成员变量
在这个例子中,MyClass::count表示一个静态成员变量,它的类型为int,初值为0。由于它是一个静态成员变量,所以它属于类而不属于对象,它的值在所有对象中是共享的。
需要注意的是,静态成员变量必须在类外面进行初始化,否则编译器会报错。在类外面定义和初始化静态成员变量的方式与普通的静态成员变量相同,例如:
template<class T>
int MyClass<T>::count = 0; // 在类外面定义并初始化静态成员变量
静态成员函数的定义和调用.
在类模板中定义静态成员函数时,需要在前面加上template<>关键字,表示这是一个类模板的特化版本。例如:
template<class T>
class MyClass
public:
static void print() // 声明静态成员函数
cout << "This is a static member function.\
在这个例子中,MyClass::print()表示一个静态成员函数,它的返回值为void,没有参数。由于它是一个静态成员函数,所以它属于类而不属于对象,可以通过类名直接调用,例如:
MyClass<int>::print(); // 调用静态成员函数
需要注意的是,静态成员函数不能访问非静态的成员变量和成员函数,因为它们属于对象而不属于类。如果需要访问非静态的成员变量和成员函数,可以把对象作为参数传递进去,例如:
template<class T>
class MyClass
public:
void set(T value)
this->value = value;
T get() const
return value;
static void print(const MyClass<T>& obj)
cout << "The value is " << obj.value << ".\