番外篇--C++中的代码重用

本文最后更新于:2023年1月1日 下午

  • 实现代码重用的一些方法(这里并不是全部):
    • 包含(组合、层次化):类包含另一个类的对象
    • 使用私有继承或保护继承
  • 以上两种方法都用于实现has-a关系,常用第一种方法
  • 多重继承可以使多个基类派生出一个新类,将基类的功能组合到一起

包含对象成员的类

  • 公有继承实现is-a模型,没有增加新接口,可增加新实现
  • 包含(组合、层次化)、私有继承、保护继承实现has-a模型,没有增加新接口,可增加新实现;
  • 接口:直接暴露在外的函数;实现:函数定义;注意:这两个概念不是互斥的

私有继承

要区分开 类成员的访问类型 和 类的继承类型 之间的关系 类成员的访问类型 决定 类成员的可访问性和可继承性:

访问类型 外部可访问性 可继承性
public 可访问 可继承
protect 不可访问 可继承
private 不可访问 不可继承

类的继承类型 决定 继承下来的成员 的访问类型会变成什么:

继承类型 变化
公有继承 不改变继承下来内容的访问类型
保护继承 将继承下来的内容的访问类型都变为protect
私有继承 将继承下来的内容的访问类型都变为private

派生类的指针(引用)要赋值给私有继承下来的基类类型指针(引用),必要强转派生类的指针(引用)


包含将对象作为一个有名字的成员添加到类中,私有继承将一个没有名字的成员添加到类中;这些成员称为子对象(subobject)

私有继承是默认继承方式

访问基类成员的格式:类名::基类成员

ps:友元函数不是类成员

若要直接获得子对象,则只需要对this(*this)进行强制类型转换即可,转换成你想要的子对象类型的指针(引用)


  • 包含 与 私有继承 实现has-a关系的对比:
    • 包含易于理解,可包含多个同类子对象,不可访问子对象的保护成员、不可重新定义虚函数
    • 私有继承比较抽象,不易包含多个同类子对象,容易出问题(多重继承);但是特性多:可访问子对象的保护成员、可重新定义虚函数
  • 一般来说,能用包含就用包含,实在不行再用私有继承
  • 如果想要改变私有继承(或保护继承(可以吗?))下来的基类成员的访问属性,可以使用using关键字,将那些成员强行引入到特性的访问区块:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Student : private std::string, private std::valarray<double>
{
...
public:
//使用using将那些继承下来的成员强行引入到public中,min和max函数的访问属性由private改成了public
using std::valarray<double>::min;
using std::valarray<double>::max;
//注意:using声明只需要成员名

//实现这个功能的一种更老的方法,但即将停用
//std::valarray<double>::min;
...
};

多重继承

多重继承(MI):有多个直接基类的类,每一个基类都要写上自己的继承属性

以下面的继承关系图为例介绍MI:

如果上图中所有的继承关系都是公有继承,那么SingingWaiter实例就会有两个Worker基类对象:

多重继承引入的问题

  • 派生类对象(SingingWaiter)指针(引用)无法自动上转为相同基类类型(Worker)的指针(引用)
    • 解决方法:手动强转下派生类对象(SingingWaiter)指针(引用)到上一级基类(Singer或Waiter)就好了
  • 从不同的基类继承同名的方法
    • 解决方法:可在调用同名函数时,通过类名指定

从不同继承路线上继承相同的基类

为了删除多个相同基类,有点像融合,C++引入了新技术— 虚基类 ;

要指定哪条继承线的相同基类进行融合,则继承这个相同类(Worker)的直接派生类(Singer和Waiter)都要在继承时加上virtual关键字:

1
2
class Singer : public virtual Worker{};
class Waiter : virtual public Worker{};

然后派生类(SingingWaiter)的基类对象情况就会变成这样:

引入虚基类的多重继承需要解决的问题

构造函数的问题

只能在类(SingingWaiter)在构造函数 初始化列表 中调用虚基类(Worker)的构造函数,类(SingingWaiter)的所有基类(Singer和Waiter)都不能在构造函数 初始化列表 中调用虚基类(Worker)的构造函数;

只能由类(SingingWaiter)来 自动 调用虚基类(Worker)的构造函数,类(SingingWaiter)的所有基类(Singer和Waiter)都不能再 自动 调用虚基类(Worker)的构造函数;

总的来说就是:类(SingingWaiter)的所有基类(Singer和Waiter)已无权调用虚基类(Worker)的构造函数,只能由类(SingingWaiter)调用虚基类(Worker)的构造函数;

虚基类中虚函数的问题

如果一个类(SingingWaiter)继承了虚基类(Worker)但是没有实现虚基类(Worker)的虚函数vfun,会出现下面两种情况:

如果有两个以上 虚基类(Worker)的派生类(Singer和Waiter) 都实现了虚基类(Worker)的虚函数vfun;这个时候如果通过这个类(SingingWaiter)的对象指针去调用虚函数vfun,那么将出现二义性;因为虚函数从虚基类(Worker)开始往下走会有两个最新版本出现;因此这个类(SingingWaiter)最好重写(override)虚函数vfun,否则多态性就会异常;

如果只有一个 虚基类(Worker)的派生类(Singer或Waiter) 实现了虚基类(Worker)的虚函数vfun;这个时候如果通过这个类(SingingWaiter)的对象指针去调用虚函数vfun,那么不会有任何问题;

重写虚函数的问题

在重写(override)虚基类(Worker)的虚函数vfun时最好不要以 递增 的方式去写;所谓的 递增 指的是:在重写(override)的时候调用了基类的虚函数vfun;

后记

虚函数的调用规则不受访问类型的影响,该调用哪个就调用哪个,如果调到了私有函数或者保护函数,那运行时就报错;

基类普通函数的调用规则也不受访问类型的影响,如果基类出现两个同名普通函数函数,即使一个是私有的、一个是公有的,那么一样存在调用的二义性;

类模板

类模板的声明与定义

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
//T是类型参数
//typename和class是等价的,但是class是C++98之前的版本
template <typename T>
class A
{
public:
void fun1(){ //do something }
void fun2();

A<T> fun3(){ //do something }
A fun4();
//注意:fun3和fun4的返回类型其实是等价的,没有差别;在类的块中,所有的A<T>和A都是等价的
};

//如果函数定义与函数声明分开,那么定义格式如下:
template <typename T>
void A<T>::fun2()
{
//do something
}

template <typename T>
A<T> A<T>::fun4() //注意:返回值的写法与函数原型不同,因为在类的块外面
{
//do something
}

//跟函数模板一样,以上只是类模板,机器码中不会包含类模板的信息;编译器会根据类模板来生成相应类定义机器码
//注意:类模板函数声明与定义不能放在两个独立的文件中

类模板的使用

1
2
3
A<int> test1;
A<string> test2
//注意:类型参数必须要手动确定,编译器不会像函数模板一样自动识别

数组模板实例和非类型参数

非类型(non-type)参数or表达式(expression)参数:

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
//数组模板
template <class T,int n> //n就是非类型参数;看起来像是一个变量,但我觉得是一个可以指定类型的宏定义,编译生成具体模板实例的时候,类成员的声明和定义中的n标识符将全部被替换为n的具体数值
class ArrayTP
{
private:
T ar[n]; //由于是宏定义,所以编译时可以确定数组大小
public:
...
void set(){};
};

template <class T, int n>
void ArrayTP<T,n>::set()
{
for(int i=0;i<n;++i)
ar[i]=i;
}

int main()
{
...
ArrayTP<double,12> example1;
ArrayTP<double,20> example2;
//注意:上面将生产两个模板实例,因为非类型参数不同

example1.set();
example2.set();
...
}
  • 非类型参数的限制:
    • 类型只能是整形、枚举、引用、指针
    • 模板中的代码不能修改非类型参数的值,也不能获取参数的地址(引用也不行吧?)(因为非类型参数没有内存空间?)
    • 在实例化模板的时候,用作非类型参数的值必须是常量表达式
  • 非类型参数数组模板的优点:
    • 使用非类型参数来创建数组速度更快,因为数组是自动变量;而且不需要手动new和delete;
  • 非类型参数数组模板的缺点:
    • 不同的非类型参数数值都会生成自己的模板实例,占用内存增加;
    • 无法将一种尺寸的数组模板实例赋给另一种尺寸的数组模板实例;

模板多功能性

类模板也可以用作基类、派生类、组件类、其它模板的类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
class Array
{
private:
T entry;
};

template <typename Type>
class GrowArray:public Array<type> //派生类模板 继承 基类模板
{ ... };

template <typename Tp>
class Stack
{
Array<TP> ar; //类模板 作为 组件
};

Array<Stack<int> > asi; //类模板 作为 其它类模板的 类型参数;C++98要求后面两个 > 分开,但是C++11不用

递归使用类模板:一个类模板的类型参数还是类模板,例如:vector<vector<int,6>,7>

模板可以包含多个类型参数:

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
template<typaname T1,typename T2>
class Pair
{
private:
T1 a;
T2 b;
public:
...
void set(const T1 &aa,const T2 &bb);
};

template<typename T1,typrname T2>
void Pair<T1,T2>::set(const T1 &aa,const T2 &bb)
{
a=aa;
b=bb;
}

int main()
{
...
Pair<int,double> example1;
Pair<float,string> example2;

example1.set(3,6.268);
example2.set(3.1415,"hello world");
...
}

默认模板参数:

1
2
3
4
5
6
7
8
9
10
template <typename T1,typename T2 = int>  //一定要确保有默认值的类型参数右边的类型参数都是有默认值的
class Topo //还可以为非类型参数设置默认值
{};

int main()
{
...
Topo<double> m2; //由于没有给出第二个类型参数,所以默认第二个类型参数为int
...
}

模板的具体化

隐式实例化(implicit instantiation) 、 显式实例化(explicit instantiation) 、 显式具体化(explicit specialization) 统称为 具体化(specialization)

假设有这样一个类模板:

1
2
3
4
5
template <typename T1,typename T2>
class ArrayTP
{
...
};

隐式实例化

1
2
3
4
5
ArrayTP<int,100> stuff;			//隐式实例化
...

ArrayTP<double,30> *pt; //不会实例化
pt = new ArrayTP<double,30>; //隐式实例化

这种实例化在函数模板中是显式实例化的一种形式

显式实例化

1
template class ArrayTP<string,100>;		//显式实例化

显式具体化

1
2
3
4
5
template<>
class ArrayTP<string,100>
{
...
};

当同时存在具体化模板和通用模板时,编译器优先选择具体化模板;如果还存在普通函数,是不是普通函数最优先

部分具体化

1
2
3
4
5
template <typename T1>		//没有具体化的参数就保留下来了;是否要确保已具体化参数右边的函数都是被具体化了???
class ArrayTP<T1,int>
{
...
};

部分具体化的更多例子:

1
2
3
4
5
6
7
8
9
10
11
//general template
template <class T1, class T2, class T3> class Trio{...};
//specialization tith T3 set to T2
template <class T1, class T2> class Trio<T1, T2, T2>{...};
//specialization with T3 and T2 set to T1*
template <class T1> class Trio<T1, T1*, T1*>{...};

//给定上述声明,编译器将作出如下选择:
Trio<int, short char*> t1; //use general template
Trio<int, short> t2; //use Trio<T1, T2, T2>
Trio<char, char*, char*> t3; //use Trio<T1, T1*, T1*>

函数调用时,模板匹配优先度:显式具体化>部分具体化>实例化

指针(引用)类型优先与T*(T&)匹配,非指针(引用)类型优先与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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
template <typename T>
class beta
{
private:
template <typename V>
class hold
{
private:
V val;
};

hold<T> q;
hold<int> n;
public:
template <typename U>
U blad(U u,T t,hold<T> ht,hold<int> hi)
{ ... }
};

//以上的类模板和函数模板都是在类中定义的,接下来看看在类外要怎么定义
//注意:部分编译器不接受类外定义

template <typename T>
class beta
{
private:
template <typaneme V>
class hold; //声明

hold<T> q;
hold<int> n;
public:
template<typename U>
U blad(U u,T t,hold<T> ht,hold<int> hi);
};

template <typename T>
template <typename V>
class beta<T>::hold //注意作用域解析运算符
{
private:
V val;
};

template <typename T>
template <typename U>
U beta<T>::blad(U u,T t,hold<T> ht,hold<int> hi) //注意作用域解析运算符
{ ... }

番外篇--C++中的代码重用
https://roudersky.com/posts/7230a012.html
作者
Rouder
发布于
2022年1月5日
许可协议