以下为个人学习笔记整理。参考书籍《C++ Primer Plus》
# C++ 中的代码重用
# 包含对象成员的类
简单定义一个 Student 类型,并在类中包含对象成员 string name
和 valarray<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()
。
# 访问基类成员
如果要像组合形式一样的方式使用 name
和 q_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> { | |
//... | |
}; |
使用保护继承,基类的公有函数和成员都会变成派生类的保护函数或者成员。主要区别在于如果派生类再派生出其他类,那么私有继承会导致新派生类无法访问私有成员和函数,但保护继承可以。
# 各种继承方式汇总:
# 使用 using 重新定义访问权限
保护继承和私有继承会使得 public
或 protected
函数变得非公有。
如果希望能够以公有方式访问函数,可以使用如下办法:
- 定义同名公有函数,内部调用
private
或protected
函数:
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{ | |
} |
从两个不同的基类继承同名方法。
从多个基类那里继承同一个类的多个实例。
# 有多少个 Worker
很显然,新的 SingingWaiter
类将包含两个 Worker
。
如果将派生类地址赋给基类,这里将出现二义性:
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 |
# 新的构造函数规则
使用非虚基类,唯一可以出现在初始化列表中的函数只有其基类的构造函数:
构造函数会逐层的调用从派生类到基类的构造函数。使得最终完成构造操作。
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(); | |
} |
# 其他有关多继承问题:
# 混合使用虚基类和非虚基类
假设 A
是 B
、 C
的虚基类, A
还是 X
、 Y
的非虚基类,此时有类 M
同事继承 B
、 C
、 X
、 Y
,那么它将会有「三」个 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 B:virtual 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
是非法的操作。 - 另外,实例化模板是,表达式参数必须是常量表达式。
相比于通过 new
和 delete
管理堆内存, 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]; |