一、模板
前面我们简单的介绍了什么是模板 ,具体请看C++ string 全面指南这篇文章。今天,我们需要对模板有更深层次的理解。
1. 模板参数
模板参数分为 类型形参与非类型形参。
类型形参 :出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参 :就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

注 :浮点数,类对象以及字符串是不允许作为非类型模板参数的。
非类型模板参数必须在编译期就能确认结果。




c++20以后才支持这种非类型模板参数。
2. 模板特化
2.1 函数模板特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果。


那么,为什么代码是一样的,比较出来的结果却是不确定的呢 ?这是因为我们并没有比较Date里面的内容,而是比较Data对象的地址,所以,结果是不确定的。
那么,针对于这种情况,我们要如何处理呢?
有两种处理办法 ,第一种方法就是模板特化。

第二种方法 :定义一个普通函数。

特化分为函数模板特化与类模板特化。
上面演示的就是函数模板特化 。一般,我们在比较大小时,是不会改变对象的大小的,所以可以加const ,如果是自定义类型,还会涉及到拷贝,因此,可以加&。那么,这种情况下的函数模板特化要如何书写呢?


按照函数模板的一致性写,const在模板参数T的左边,所以特化时,也这样去特化,但是这样写是有问题的,为什么呢?
因为函数模板里的 const 是修饰引用本身 left 的(本质是修饰 left 所引用的对象,即实参),函数特化里的 const 是修饰 left 指向的内容的不能修改(修饰的是*left),所以不对。
那么,应该如何正确的书写呢?

一般情况下,不建议使用函数模板特化,建议直接将该函数给出。
函数模板特化的步骤:
1.必须要先有一个基础的函数模板。
2.关键字template后面接一对空的尖括号<>
3.函数名后跟一对<>,尖括号中指定需要特化的类型
4.函数形参表:必须要和模板函数的基础参数类型完全相同。
2.2 类模板特化

在类名后加上一对<>,在尖括号中指定需要特化的类型。
我们所演示的这个叫做全特化 ,所谓全特化就是将模板参数列表中所有的参数都确定化。
有全特化,自然也有偏特化。偏特化就是将模板参数的一部分参数进行特化。

偏特化不仅仅是指特化部分参数,而是对模板参数更进一步的条件限制所设计出来的一个特化版本。

3. 模板的分离编译
什么是分离编译呢?
一个程序由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有的目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模板一般来说是不建议分离的 。如果非要对模板进行分离,就必须在模板定义的位置显示实例化。


经过验证,模板确实是不支持分离编译的。但是可以显示实例化来达到分离编译的目的。

不建议这样做,一般情况下,我们是不会将模板分离编译的。
那么,模板为什么不支持分离编译呢?
程序要想运行,必须经过预编译,编译,汇编,链接4个阶段,预编译阶段会进行宏替换,去注释,展开头文件,条件编译,编译阶段是将程序翻译成汇编语言,再经过汇编,生成机器可识别的二进制码,最后经过链接,合并成可执行程序,链接函数地址等,才可以形成一个可执行程序。在形成可执行程序之前,每个源文件都是单独编译的。
比如:main.cc文件里使用了一个Add加法函数,但该函数的实现在a.cc文件里,函数的声明在a.h文件里,使用该函数时,main.cc文件里会包含头文件a.h,所以,函数的声明有了,在编译的时候,编译Add函数时,由于main.cc文件里没有该函数的实现,所以本不应该编译通过,但是编译器默认有了函数声明,就允许编译成功,它不会去其它文件里查找该函数。它会在编译Add函数时,给一个预指令地址,等到链接时,才会去其它文件里查找该函数,确定该函数的地址 。这样做的目的是为了提高编译的效率。
调用函数本质上是一个call语句,call语句里面就是函数的地址。但是在编译阶段,生成的中间文件调用函数的地方是没有函数的地址的,也就是call语句里面没有,这是因为要有函数的地址就要有函数的定义,函数的定义不在main.cc文件里,在其它文件里,所以call语句里的内容为空。在链接时,会进行地址重定向,汇编阶段会生成符号表,符号表里就是函数的地址,链接时就会在符号表里查找,进行地址重定向。



这里我们看到函数编译之后是有地址的 ,是因为这是处理后的最终结果,即生成了可执行程序。
所以,为什么模板不支持分离呢 ?因为模板在距离被编译还差一步实例化,main.cc里只有函数模板的声明(包含了头文件),虽然编译通过了,但是函数定义的地方不知道要实例化成什么类型,所以符号表里没有该函数的地址,链接时自然就找不到了,因此会报错,函数模板不支持分离。
二、继承
1. 认识继承
继承的概念 :继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。
我们来体会一下。


student和teacher都有一些相同的成员变量,成员函数,两个类里面都要去写一份就太冗余了,所以,我们可以这样去做。
我们可以将相同的成员变量放在一个类里面,student和teacher去继承这个类,就可以复用这些成员。


2. 继承定义

继承的方式有3种 :public,protected,private。
访问限定符也有3种 :public,protected,private。
所以,子类在继承基类时,对于基类当中的成员访问需要注意以下几个:

1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员依然被继承到了子类当中,但是无法在子类当中访问基类成员。



通过观察可以看到,private和protected在一个类中作为成员的访问限定符,是没有本质区别的,但是在继承这里却有很大的区别 。private成员是无法在子类当中访问的,而protected成员是允许在子类中访问的。
2.基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中访问,就定义为protected。
3.基类的私有成员在派生类中都是不可见的,基类的其它成员在派生类当中的访问方式 = Min(成员在基类的访问限定符,继承方式),public > protected > private。
4.关键字 class 的默认继承方式是 private,struct 的默认继承方式时 public,最好显示继承。
3. 继承类模板

主函数实例化stack对象时,也实例化了vector,调用push函数时,只实例化了push,没有实例化vector的push_back函数,所以找不到,需要指定类域。
4. 基类和派生类之间的转换
. public继承的派生类对象可以赋值给基类的指针/基类的引用,我们把它叫做切片或者切割,就是把派生类中基类那部分切出来,基类的指针或者引用指向的是派生类当中切出来的基类那部分。
. 基类对象不能赋值给派生类对象。
. 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast来进行识别后进行安全转换。


