C++基础——函数模板与类模板的一些细节

这里做一下总结 and 深入吧~

类模板继承

这里我们需要知道一点,模板类可以被继承,但是你必须指定类继承,比如你声明了一个

1
2
3
4
5
class A<T>{
public:
A();
~A();
}

A 并不是一个类,不能被继承,你只能继承一个 A<int> 类,或者是其它类的一个类实例,或者如果你想让子类也保持为泛型,那么你可以给子类定义一个模板类,然后这里继承的时候给 A<T>

这里两种继承还是有很大区别的,至少在编译部分就会让你产生写法上的很大的差异,指定实例化的类继承这个我们很好理解,它就相当于直接指定完整了一个类,你把你指定的类用模板类替换上去类型就成了一个具体的类,然后去继承,和直接继承没有任何差别,我们主要关心第二种情况:子类传泛型进去给父类继承。

关注以下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
template<typename T>
class A {
public:
T s;
A() {
s = 1;
std::cout << "默认构造:" << s << std::endl;
}
A(T val) :s(val) {
std::cout << "A构造:" << s << std::endl;
}
void test() {
std::cout << "A::test()" << std::endl;
}

};
template<typename T>
class B :public A<T> {
public:
B(int val){
s = val;
std::cout << "B构造:" << s << std::endl;
}
};

int main() {
B<int> obj(5);
obj.test();
obj.s = 1;
}

我们可以发现,在B的构造函数中,s 完全调用不起来,它会报一个错误叫找不到标识符。

这是因为我们继承的类是不确定的,因为 T 是不确定的,T在没有实例化之前不存在一个真正的类而是一个模板,因此在子类这里调用 s 会导致编译器发现这个类里面没有这个成员于是报错,但是如果我们继承的类在编译期就可以确定的类,那么就不会报这个错误了。但是发现,加了 this 指针就能去访问了,这里我也是请教了一下老师,老师也给出了较为专业的答复:

C++的编译有俩阶段,模板在第一阶段,模板实例化在第二阶段。第一个阶段时候 只对模板中和模板无关的名字进行查找,无视模板那些参数的部分,由于父类是模板,所以在第一阶段编译时候那些泛型啥的变量会被无视掉,所以模板继承想访问变量必须加上this告诉编译器我要访问的是基类

大概意思就是,编译错误出现在第一次编译,第一次编译因为父类是模板,没有被实例化,里面的各种变量都不能被继承,因此我子类访问不到父类的成员,在第二次编译的时候,父类已经实例化为一个具体类了,因此可以找到。加个this只是改变了检查变量的时间,把从第一阶段移动到了第二阶段,第二阶段父类模板已经被实例化因此可以找到。

零初始化

定义一个模板类型 T 并进行零初始化的写法是

1
T x=T();

零初始化的规则为:

  • 如果是标准类型,就会初始化为0
  • 如果是非union的类类型,基类和非静态成员初始化为零,所有填充位初始化为零。忽略构造函数
  • 如果是union,第一个非静态的数据初始化为零,填充位初始化为零
  • 如果是数组,所有元素初始化为零
  • 如果是引用,不做任何处理

总结一句就是会初始化为0,数字是0,字符是\0,指针是null

参数引用

参数引用当中有一个规则:不会对传入的实参进行指针转化,也就是说,我如果给到一个字符串常量,它会被视为 const char [n] 的类型而不是 char * 类型,在用模板的时候如果没有对这个进行合理的操作,可能出现问题。

比如常见的比大小:

1
2
3
4
template<typename T>
bool cmp(const T &val1, const T &val2) {
return val1 < val2;
}

我们如果调用

1
cmp("123","1234");    

就会出问题,此时我们需要把 val1 和 val2 的引用标记去掉才可以,这样它们俩都会被当成 char *,所以这里出现了一个类型退化的概念,至于它们具体怎么比大小,那就是要用到运算符重载了,比如我们直接重载 char * 的小于运算符,改成 strcmp 两个参数即可。

头文件引用

我们写一个 .cpp 文件和一个 .h 文件,在头文件定义模板,在 .cpp 实现模板,然后新建一个 .cpp 文件去包含头文件调用这个模板会发现编译失败。因为在编译我们新建的那个 .cpp 文件的时候找不到模板的实例化对象,因此编译会失败。解决的办法就是让定义和声明放到一起,但是如果我们放到 .cpp 里面的话,只有当前 .cpp 文件可以调用,我们的模板要解决的问题是实现一次,然后解决很多场景下面的问题,那么放到 cpp 文件可能就不太可以了,那么如果放到 .h 文件的话,刚刚的问题解决了,但是会导致头文件很大。

如果要以上两个优点兼得的话,我们需要用 export 关键字把模板导出,这样就能同时解决泛用性过小和头文件过大的问题了。

模板非类型参数

我想到的一个应用就是 bitset,我们在定义的时候给模板的参数是 bitset<123> 给我们定义了一个集合大小为 123 的位集合。在这里我们传了一个非类型参数,而这个参数的改变会导致我们实例化出很多的类。比如 bitset<1>bitset<2> 它就会被视为不同的类型,这个参数在它们内部的代码当中是固定死的。

初始化模板静态成员

比如这样的一个定义:

1
2
3
4
5
template<typename T>
class CA {
template<typename W>
static W s;
};

这里我们如果要初始化这个静态成员,我们需要这么写:

1
2
3
template<typename T>
template<typename W>
W CA<T>::s=0;

记一下吧就~

不定个数的类模板参数

我们声明模板的时候可以这样声明:

1
2
3
4
5
6
template<typename T,typename  ... args>
void print(T header,args ... res){
std::cout << header << std::endl;
print(res...);
}
print<int>(1,2,3,4,5)

这样我们调用这个函数就能实现不定参数个数地循环打印了。

但是感觉没有体会到不定个数的类模板参数的精髓,因为这里只是函数传参个数不定罢了,后面遇到再看看吧。

获取模板参数类名

用以下的方法:

1
2
3
4
template<typename T>
void GetType(T s) {
std::cout << typeid(s).name() << std::endl;
}

这个 typeid 关键字好像不止可以获取模板里的变量类型,但是其实其它有没有利用场景,其它场景下面我们变量什么类型都是在编译前可以确定的,而模板属于是实现了哪个模板才知道是什么类型。