以下为个人学习笔记整理。参考书籍《C++ Primer Plus》

# C++ 中的代码重用

# 包含对象成员的类

简单定义一个 Student 类型,并在类中包含对象成员 string namevalarray<int> q_vals

// .h
#pragma once
#ifndef STUDENT_H_
#define STUDENT_H_
#include<iostream>
#include<cstring>
#include<valarray>
using std::string;
using std::valarray;
class Student {
private:
	string name;
	valarray<int> q_vals;
	S s;
public:
	double Average() const;
	Student(const char* str, const int* pd, int n);
};
#endif // !STUDENT_H_
//.cpp
double Student::Average() const {
	if (q_vals.size() > 0)
		return q_vals.sum() / q_vals.size(); // use private func
	else
		return 0;
}
Student::Student(const char* str, const int* pd, int n):name(str),q_vals(pd,n) {
	// ...
}

# valarray 类简介

valarray 顾名思义,值数组,能够方便操作数组内元素的求和,求最大最小等操作。、

# 私有继承

C++ 还有另一种实现 has-a 关系的途径 —— 私有继承。使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员。这会使得基类的函数不会成为派生对象公有接口的一部分(作为私有的一部分),但可以在派生类的成员函数中使用。

# 新版本的 Student 类

相比于通过组合实现的 has-a 关系,私有继承隐藏了私有成员变量,使得类本身具备了私有继承的特性。此外,在构造函数的写法上也略有不同,私有继承的参数列表中变量名使用的是类名。

// .h
class Student_New : private std::string, private std::valarray<int> {
public:
	Student_New(const char* str, const int* pd, int n);
};
// .cpp
Student_New::Student_New(const char* str, const int* pd, int n):std::string(str),std::valarray<int>(pd,n) {
	// ...
}

# 访问基类方法

使用私有继承时,可以在派生类的方法中访问基类的非私有方法,通过类名 + 域名解析 + 函数名的形式 ClassName::Func()

# 访问基类成员

如果要像组合形式一样的方式使用 nameq_vals 两个变量,可以通过强制类型转换来实现。

const string& Student_New::Name() const{
	return (const string&) *this;
}

# 访问基类的友元函数

由于友元函数本身不属于类函数,所以只能通过显式的类型转换,转成基类并调用基类的友元函数。

ostream& operator<<(ostream& os, const Student& stu){
	os << "Scores for" << (const string&) stu << ":\n";
}

即使使用公有继承,也必须使用显式的类型转换,因为不适用类型转换,会导致 os << stu ;将和友元函数原型本身匹配,导致递归调用。

# 使用包含还是私有继承

大多数情况下,C++ 程序更倾向于使用包含。

  • 包含更加容易理解。
  • 私有继承容易产生重名冲突,定义也不直观,虽然其功能可能更强大(可以使用 protected 成员)。
  • 私有继承可以重新定义虚函数,但包含做不到。

通常,应使用包含来建立 has-a 关系;如果新类需要访问原类的 protected 成员,或需要重新定义虚函数时,可以选择私有继承。

# 保护继承

保护继承是私有继承的变体:

// .h
class Student_New : protected std::string, protected std::valarray<int> {
	//...
};

使用保护继承,基类的公有函数和成员都会变成派生类的保护函数或者成员。主要区别在于如果派生类再派生出其他类,那么私有继承会导致新派生类无法访问私有成员和函数,但保护继承可以。

# 各种继承方式汇总:

image-20210312114408668

# 使用 using 重新定义访问权限

保护继承和私有继承会使得 publicprotected 函数变得非公有。

如果希望能够以公有方式访问函数,可以使用如下办法:

  • 定义同名公有函数,内部调用 privateprotected 函数:
double PrivateClass::sum() const{
	return PrivateParentClass::sum(); // 使用私有的内部函数
}
  • 使用 using 关键字来重新定义访问权限:
class PrivateParentClass {
protected:
	double sum() const;
};
class PrivateClass :private PrivateParentClass{
public:
	using PrivateParentClass::sum;	// 重新定义访问权限为 public
};

# 多重继承(MI)

多重继承往往伴随着很多问题:

class Worker{
};
class Singer:public Worker{
public:
    void show() const;
}
class Waiter:public Worker{
public:
    void show() const;
}
class SingingWaiter:public Singer, public Waiter{
}
  • 从两个不同的基类继承同名方法。

  • 从多个基类那里继承同一个类的多个实例。

image-20210315154907307

# 有多少个 Worker

很显然,新的 SingingWaiter 类将包含两个 Worker

image-20210315161031626

如果将派生类地址赋给基类,这里将出现二义性:

SingingWaiter sw;
Worker* w = &sw // 程序没办法确定应该是哪个 work。
    
// 解决办法也很简单,强制申明所需要的类型即可:
Worker* w =(Singer *) &sw  
Worker* w =(Waiter*) &sw

# 虚基类

多重继承本质问题就是引入了多个基类实例,为此 C++ 提供了虚基类来解决该问题。

「虚基类」使得多个相同基类的派生类派生出的对象只能继承一个基类对象。相当于派生类共用一个基类对象。

class Singer: virtual public Worker{}; // is ok
class Waiter: public virtual Worker{}; // is ok
class SingingWaiter: public Singer, public Waiter{}; // one worker

image-20210315164359804

# 新的构造函数规则

使用非虚基类,唯一可以出现在初始化列表中的函数只有其基类的构造函数:

构造函数会逐层的调用从派生类到基类的构造函数。使得最终完成构造操作。

class A {
public:
	A(int n = 0) {};
};
class B : public A{
public:
	B(int n = 0, int m = 1) :A(n) {};
};
class C : public B{
public:
	C(int n = 0, int m = 1, int k = 2) :B(m, n) {}; // can not call A(n)
};

如果 Worker 是虚基类,则这种规则将不会生效。

换句话说 C++ 在基类是虚基类时,不会执行这种逐层操作,因此 Waiter(wk, p)Singer(wk, v) 将正确执行,但是由于初始化派生类必须先初始化基类的特性。所以 Waiter(wk, p)Singer(wk, v) 构造时将调用 Worker 的默认构造函数。

SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other):Waiter(wk, p), Singer(wk, v){}

如果不希望上述情况发生,则需要显示地调用基类构造函数:

SingingWaiter(const Worker& wk, int p = 0, int v = Singer::other):Worker(wk), Waiter(wk, p), Singer(wk, v){}

# 哪个方法

对于多继承情况下,如果基类出现同名函数,会使得调用存在二义性。

可以通过作用域解析运算符显式的声明:

SingingWaiter sw;
sw.show() // 存在二义性 is fail
sw.Singer::show() // 显示声明 is ok

更好的办法是显式的定义执行规则:

void SingingWaiter::show(){
	Singer::show();
}

# 其他有关多继承问题:

# 混合使用虚基类和非虚基类

假设 ABC 的虚基类, A 还是 XY 的非虚基类,此时有类 M 同事继承 BCXY ,那么它将会有「三」个 A 的对象。

class A{};
class B:virtual public A{};
class C:virtual public A{};
class X: public A{};
class Y: public A{};
class M:public B, public C, public X, public Y{}; // 3 个 A 对象

# 虚基类和支配

如果使用非虚基类从多个基类继承同名函数将会导致二义性。

如果使用虚基类则可能出现二义性,也可能不会。这取决于函数的优先级关系是否平级。

class A{
public:
	short q();
};
class Bvirtual public A{
public:
	short q(); // 重新定义了 q 函数,此处的 q 函数优先级 > A.q
	int omg();
};
class C: public B{};
class D: virtual public A{
public:
	int omg(); // 由于 D 和 B 都派生自 A 所以 D.omg 和 B.omg 属于平级
};
class F: public D, public E{};
F.omg(); //is fail;	平级调用会导致二义性 
F.q();   //is ok;		B.q 优先级 > A.q 会执行 B.q 的逻辑

此外虚基类的二义性规则不受访问控制限制。即:不论函数的访问权限为何,都满足上诉的条件。

# 类模板

没有类模板前,为了能够定义一套相对灵活,复用性的代码,往往需要像下面这样:

typedef unsigned long Item;
class Stack {
private:
	enum {MAX = 10};
	Item items[MAX];
	int top;
public:
	Stack();
	bool isempty() const;
	bool isfull() const;
	bool push(const Item& item);
	bool pop(Item& item);
};

这样会有两个缺点:

  • 每次修改类型时,必须要修改头文件。
  • 这种声明不能让 Item 时代表多种类型。

# 定义模板类

模板类可以帮助解决上述问题:

template <typename Type>
class Stack {
private:
	enum {MAX = 10};
	Type items[MAX];
	int top;
public:
	Stack();
	bool isempty() const;
	bool isfull() const;
	bool push(const Type& item);
	bool pop(Type& item);
};
template <typename Type>
Stack<Type>::Stack() {
	top = 0;
}
template <typename Type>
bool Stack<Type>::isempty() const{
	return top == 0;
}
template <typename Type>
bool Stack<Type>::isfull() const {
	return top == MAX;
}
template <typename Type>
bool Stack<Type>::push(const Type& item) {
	if (top < MAX) {
		items[top++] = item;
		return true;
	}
	return false;
}
template <typename Type>
bool Stack<Type>::pop(Type& item) {
	if (top > 0) {
		item = items[--top];
		return true;
	}
	return false;
}

# 使用模板类

通过 className<type> 的方式可以构建模板类对象。

Stack<int> st_int;
Stack<string> st_string;

# 数组模板和非类型参数

模板声明不仅仅可以指定类型,还可以设置非类型或表达式:

template<typename T, int n> // 声明一个由 < 类型,int > 组成的模板
ArrayTp<double, 12> array_tp; // 声明一个由 12 个 double 类型所组成的数组

上述定义中把 double 替换为 T ,把 12 替换为 n

表达式参数本身还有一定的限制:

  • 表达式参数可以是 「整型」、「枚举」、「引用」或「指针」;但不可以是「浮点数」。
  • 模板代码不能修改表达式参数的值,也不能使用其地址。所以 n++&n 是非法的操作。
  • 另外,实例化模板是,表达式参数必须是常量表达式。

相比于通过 newdelete 管理堆内存, template<typename T, int n> 表达式参数创建的自动标变量则是维护在栈内的,这样可以提高执行效率😄。

表达式参数方法的缺点:

  • 每种数组大小都会生成自己的模板:
// 会产生两个独立的类声明。
ArrayTp<double, 12> array_tp;  
ArrayTp<double, 2> array_tp;
// 但是非表达式参数的模板构造则不会
Stack<int> s(12);
Stack<int> ss(2);
  • 构造函数的方法更加通用,因为数组大小是作为类成员;这样,可以根据需求任意改变。

# 模板多功能性

模板类可以用于基类或是组件类,或者嵌套其他的模板。

ArrayTp< Stack<int> , 12> array_tp;   // C++98 里要求 至少用一个空白字符将两个 > 符号分开避免出现混淆,单 C++11 可以不需要这么做

# 递归使用模板

ArrayTp< ArrayTp<int, 5> , 12> array_tp; // 个数组声明类似 int arr [10][5]

# 使用多个类型参数

template<typename T1, typename T2> // 定义多个类型
class T_T{};
T_T<string, int> t_t;

# 默认类型模板参数

template<typename T1, typename T2=int> // 定义多个类型
class T_T{};
T_T<string, int> t_t;
T_T<string> t_t; // 和上面等效

# 模板的具体化

类模板和函数模板很相似,可以有隐式实例化、显式实例化、显式具体化。统称具体化(specialization)。模板以泛型的方式描述类,而具体化是使用具体的类型生成类的声明。

# 隐式实例化

当声明一个或多个对象,并指出所需的类型时,编译器使用模板提供的处方生成具体的类定义:

ArrayTp<int, 100> stuff; // 隐式实例化

编译器在需要对象之前,不会生成类的隐式实例化:

ArrayTp<int, 30>* pt; // a point, not need object
pt = new Array<int, 30>; //new a object, 触发隐式实例化

# 显式实例化

当使用关键字 template 并指出所需类型来声明类时,编译器将生成类声明显式实例化(explicit instantation)。声明必须位于模板定义所在的名称空间中:

这种情况下只是生成类声明,但不会构建类对象。

template <typename T, int V> class A {};
template class A<double, 1>; 	// #1 显式实例化 A<double, 1>
template class A<int, 100>;		// #2 显式实例化 A<int, 100>

# 显式具体化

显式具体化是指特定类型的定义,定义时需要指明定义的所操作的泛型的具体类型。

template<> class ClassName<specialized-type-name>{...};

例如:可以为一个泛型数组定义一个 const char * 类型的比较函数,因为和正常 int 类型之间的比较不同,这里需要对地址进行处理:

template <typename T> class SortedArray {};
template<> class SortedArray<int> {
public:
	bool operator>(SortedArray<int>& s);
};
template<> class SortedArray<const char*> {
public:
	bool operator>(SortedArray<const char*>& s);
};

SortedArray 被定义为 SortedArray<const char *> 时,比较大小的操作( operator> )将会使用 SortedArray<const char*> 的版本。

# 部分具体化

C++ 还允许部分具体化(partial specialization),即部分限制模板的通用性。例如,部分具体化可以给类型参数其中的某一些指定具体的类型:

template <typename T, typename K> class SortedArray {};
template<> class SortedArray<int, K> {
public:
	bool operator>(SortedArray<int, K>& s);
};
template<> class SortedArray<T, const char*> {
public:
	bool operator>(SortedArray<T, const char*>& s);
};

如果对所有的类型参数都指定了固定的类型,那么就相当于「显式具体化」的定义了类的行为。

如果有多个模板可以进行选择,那么编译器将使用具体化程度最高的模板:

// 假设定义了一下具体化声明:
template <typename T, typename K> class SortedArray {};
template<> class SortedArray<int, int> {};
template<> class SortedArray<T, int> {};
// 相关匹配
SortedArray<double, double> sa_1; 	// 生成一个新的具体化模板 SortedArray<double, double>
SortedArray<double, int> sa_2;		// 使用部分具体化模板 SortedArray<T, int>
SortedArray<int, int> sa_3;			// 使用显式具体化模板 SortedArray<int, int>

或者提供指针模板来部分具体化现有模板:

template <typename T, typename K> class SortedArray {};
template <typename T*, typename K*> class SortedArray {};
SortedArray<int*, char*> sa_1;	// use <typename T*, typename K*>
SortedArray<int, double> sa_2;	// use <typename T, typename K>

其他部分具体化模板的示例:

template <typename T, typename TT, typename TTT> class SA {};
template <typename T, typename TT> class SA<T,TT, TT> {};
template <typename T> class SA<T, T, T*> {};
SA<int, short, char*> sa_1;	// use new template SA<int, short, char*>
SA<int, int, int*> sa_3;	// use defined SA<T, T, T*>
SA<int, short, short> sa_2;	// use defined SA<T,TT, TT>

# 成员模板

模板可以用作结构、类或模板类的成员。此外模板还可以定义在另一个模板类或者函数中作为其成员使用:

template<typename T> 
class OurTpl {
private:
	template<typename V>
	class InnerTpl {
	public:
		void show();
	};
};
// 函数的定义也需要按照:
// template<typename T>
// template<typename V>
// 不能使用 template<typename T, typename V>
template<typename T>
	template<typename V>
		void OurTpl<T>::InnerTpl<V>::show() {
			// ...
		}

# 将模板用作参数

可以将一个模板作为参数传递给另一个模板类。

// 定义一个模板类 TplArg<T>。并且定义两个具体化模板 TplArg<int> TplArg<short> 和 show () 函数
template <typename T> class TplArg {};
template<> class TplArg<short> {
public:
	void show() { std::cout << "TplArg<typename short>" << std::endl; }
};
template<> class TplArg<int> {
public:
	void show() { std::cout << "TplArg<typename int>" << std::endl; }
};
//  template <template <typename T> class TPL>class 
// 可以理解为: typename TPL;  TPL = template <template <typename T> class = ClassName<T> 
template <template <typename T> class TPL>class TestTplArg{
public:
	TPL<int> tpl_int;
	TPL<short> tpl_short;
	void show() { tpl_int.show(); tpl_short.show(); }
};
// TestTplArg 使用 TplArg 模板类进行初始化,并调用了该模板类两个具体化对象的 show () 函数
TestTplArg<TplArg> tpl;
tpl.show();

当然,如果 TplArg 其中一个模板具体化没有定义并实现 show() 函数,会导致程序报错。

# 模板类和友元

模板类声明也可以有友元。模板类的友元分 3 类:

  • 非模板友元。
  • 约束(bound)模板友元,即友元的类型取决于类被实例化时的类型。(多对一)
  • 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。(多对多)

# 模板类的非模板友元函数

定义参数可以是「模板类对象」。

template<typename T>
class HasFriend {
public:
	friend void counts(HasFriend &) { std::cout << "counts: " << typeid(T).name() << std::endl; };
};
// use
HasFriend<double> hf{};
counts(hf);

# 模板类的约束模板友元函数

定义函数需要指明函数的模板类型:

template<typename T>class HasFriend;
template<class TT>
void tpl_counts();
template<class TT>
void tpl_counts_T(HasFriend<TT>&);
template<typename T>
class HasFriend {
public:
	template<typename T> friend void tpl_counts<T>() { std::cout << "tpl_counts<T>: " << typeid(T).name() << std::endl; };
	template<typename T> friend void tpl_counts_T<>(HasFriend<T>&) { std::cout << "tpl_counts_T<T>: " << typeid(T).name() << std::endl; };
};
// use
HasFriend<double> hf_d{};
HasFriend<int> hf_i{}; // 注意 2017 版本 vscode 在处理泛型类时,友元函数会发生重定义问题 C2995
// https://stackoverflow.com/questions/27946528/template-friend-function-and-return-type-deduction 
// 解决办法:把友元函数的实现提到类型声明外。
tpl_counts<int>();
tpl_counts_T(hf);

tpl_counts_T<> 为空,表示可以通过其函数参数推断出模板类似参数。

# 模板类的非约束模板友元函数

template<typename C, typename D> void show(C&, D&);
template<typename T>
class HasFriend {
public:
	template<typename C, typename D> friend void show(C&, D&) ;
};
template<typename C, typename D> void show(C&, D&) { std::cout << "show<C,D>: " << typeid(C).name() << " " << typeid(D).name() << std::endl; };
//use
HasFriend<char> hf_d{};
HasFriend<int> hf_i{};
show(hf_d, hf_i);

# 模板别名(C++11)

C++ 可以通过关键字 typedef 为模板具体化指定别名:

typedef std::array<double, 12> arrd;
arrd arr;

但是 C++11 提供了一个更好的办法 —— 使用模板提供一系列别名:

template<typename T>
using arrtype = std::array<T,12>;
arrtype<double> arr;

通过 using = 用于模板。对于非模板时,改用法等价于 typedef

typedef const char* = p1;
using p2 = const char*;
typedef const int*(*pal)[10];
using pa2 = const int *(*)[10];