从 C 向 C++ 进阶系列导航
1. 类模版
C++ 提供了类模板。与函数模板相同,类模板本质也是数据类型的替换,在定义类对象时指定数据类型可以控制类中的数据类型等。
类模板定义方式如下:
template <typename T>
class ClassName
{
...
}
类模板也会进行二次编译。第一次为类模板本身的编译,第二次为数据类型替换后的类编译,每定义一个类模板对象,都会产生一个新的指定数据类型的类副本。
类模板主要用于存储和组织数据元素,类中数据组织的方式和数据元素的具体类型无关,如:数组类、链表类、Stack 类等。C++ 中将模板的思想应用于类,使得类的实现不关注数据元素的具体类型,而只关注类所需要实现的功能。
- 实验:
template <typename T>
class Test
{
public:
T mVar;
Test(T num) : mVar(num)
{
cout << "mVar = " << mVar << endl;
}
};
int main()
{
Test<int> obj_A(3); // mVar = 3
Test<float> obj_B(3.14); // mVar = 3.14
Test<char> obj_C('a'); // mVar = a
// cout << "sizeof(Test)" << sizeof(Test) << endl; // error: missing template arguments before ‘)’ token
cout << "sizeof(Test<int>) = " << sizeof(Test<int>) << endl; // sizeof(Test<int>) = 4
cout << "sizeof(Test<char>) = " << sizeof(Test<char>) << endl; // sizeof(Test<char>) = 1
return 0;
}
需要注意的是,在子模版类的成员函数中使用父模版类声明的非私有成员变量时,需使用 this 指针指示,表示该成员变量为子类的成员,否则编译器在模版类的第一次编译时会因找不到该成员变量所属的父模版类而报错。
2. 工程中的类模版形态
类模板在工程中的应用遵循以下规则:
- 类模板在头文件中定义。
- 类模板中类的实现与成员函数的实现必须在同一文件中。
- 类模板外部定义的成员函数需要加上模板<>声明。
这里解释一下为什么类模板中类的实现与成员函数的实现必须在同一文件中。之前有提到过,一般地在工程中,类的实现放在头文件中,而成员函数的实现会单独放在一个源文件中,这样子更方便阅读与管理。但类模板属于特殊情况,模板类实际会被编译两次。如果模板类中类的实现与成员函数的实现分开在不同的文件,则意味着是在程序中定义模板类对象即第二次编译时,编译器会查找模板中类的实现并进行数据类型替换,产生类的副本。但由于头文件中仅包含类的实现,不包含成员函数的实现,因此这个副本也仅包含类的实现的副本,并没有包含成员函数的实现的副本。只要程序中调用了模板类对象的成员函数,在链接阶段时,链接器会找不到类对象副本的成员函数的具体实现,进而报错停止编译。
- 正确形态示例:
// operator.h
#ifndef _OPERATOR_H_
#define _OPERATOR_H_
template <typename T>
class Operator
{
public:
T add(T a, T b);
T minus(T a, T b);
};
template <typename T>
T Operator<T>::add(T a, T b)
{
return a + b;
}
template <typename T>
T Operator<T>::minus(T a, T b)
{
return a - b;
}
#endif
// main.c
#include <iostream>
#include "Operator.h"
using namespace std;
int main()
{
Operator<int> obj;
cout << "obj.add(1, 2) = " << obj.add(1, 2) << endl; // obj.add(1, 2) = 3
cout << "obj.minus(4, 5) = " << obj.minus(4, 5) << endl; // obj.minus(4, 5) = -1
return 0;
}