c++(4`OPP,多态、模板、虚函数)
面向过程(OPP)、面向对象(OOP)
面向过程 vs. 面向对象
- 面向过程:问题分解成一系列的步骤(方法和数据),然后按照顺序执行这些步骤
- 例子:插电源-》放衣服-》加洗衣液-》漂洗-》甩干
- 优点:效率高
- 缺点:不易复用,不易扩展,不易组合,安全性差,不易使用
- C语言完全面向过程
- 面向对象:问题分解抽象为一系列的对象,执行对象的方法
- 例子:
- 人(插电源,放衣服,加洗衣液)
- 洗衣机(漂洗,甩干)
- 人.插电源-》人.放衣服-》人.加洗衣液-》洗衣机.漂洗-》洗衣机.甩干
- 优点:
- 易复用,易扩展,易组合,提高安全性,简化代码使用
- 缺点:效率低
- C++语言半面向对象,java完全面向对象
- 例子:
OOP三大特性:
- 封装,分解抽象为一系列对象/设置访问权限
- 继承,允许一个类继承一个现有类,并可以 继承/修改/扩展 父类的功能
- 多态,是通过统一的接口操作不同类型的对象,从而产生不同的行为
多态
静态多态
- 静态:编译时期
重载:
1
2
3
4
5
6
7
void print(int i) {
cout << "整数: " << i << endl;
}
void print(double f) {
cout << "浮点数: " << f << endl;
}
展开
- 函数重载:同作用域中,函数名相同,其余函数签名特性至少有一样不同
- 转换等级
- 精确匹配:
- 类型完全相同
- 数组/函数 转换到指针
- const转换
- 类型提升(同一类型少字节->多字节)
- 算数类型转换(同一类型多字节->少字节 / 不同类型)
- 类类型转换
- 精确匹配:
- 函数匹配:
- 候选函数集:函数名相同,在同一作用域内
- 可行函数:形参数量相等(默认实参可算可不算),类型相同/类型可转换
- 找到最佳匹配:
- 为每个可行函数的每个参数评定转换等级
- 如果某个函数在所有参数上的转换等级都优于(>=)其他函数,并且至少一个参数严格优于(>非>=,防止(int,int)(int,int)情况出现)对方,选择它
- 否则则出现二义性
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
returnType ClassName::operator重载的运算符(parameterList){
}
class A {
public:
A operator+(const A &b){//2元
A ret;
ret.x = this->x + b.x;
ret.y = this->y + b.y;
return ret;
}
A& operator++() {//前置:先递增,再返回值
x ++;
y ++;
return *this;
}
const A operator++(int){//后置:先返回原值,再递增
A temp(x,y);
x ++;
y ++;
return temp;//返回原值
}
public:
double x, y;
//友元
friend A operator+(const A &, const A &);
friend ostream& operator<< (ostream &out , const A &a);
}
A operator+(const A &a, const A &b){//类外
A ret;
ret.x = a.x + b.x;
ret.y = a.y + b.y;
return ret;
}
ostream& operator<< (ostream &out , const A &a){//2元
out << "<A>( " << a.x << ", " << a.y << ")";
return out;
}
展开
- 运算符重载:
- 限制:仅能在类内定义/ 类外但形参列表中必须包含至少一样自定义类类型(并且需要加友元)
- 作用:允许自定义类型使用和内置类型相同的运算符,避免了函数调用的方式,让代码更直观
- 使用:
- 运算符操作数数量如果为n个,在类内定义时,形参列表有n-1个参数,在类外定义时形参列表有n个参数
- 要注意是否应该修改左操作数,是否应该返回自身引用
- 只能重载现有的运算符
- 不能改变运算符的操作数数量,不能改变运算符的优先级和结合性
- 某些运算符不能被重载(如 ::, .*, ., ?: 等)
动态多态
- 动态:运行时期,调用一系列的指令
重写:
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
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
int main() {
A* basePtr = new B();
basePtr->vfunc1();
delete basePtr;
return 0;
}
展开
- 重写
- 当通过基类的引用或指针来调用一个虚函数时,会根据对象的实际类型来决定调用哪个函数版本(比如A基类指针,BC派生类,A可以指向B,调用B的函数,可以更换指向C,调用C的函数)
模板
泛型编程是一种编程范式,目的是编写与类型无关的代码(都是统一接口,模板注重类型,多态注重行为)
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
template <typename T> int compare (T t1, T t2);
template <typename T> class compare;
template <typename T>
int compare(T & t1, T & t2)
{
if(t1 > t2)
return 1;
if(t1 == t2)
return 0;
if(t1 < t2)
return -1;
}
template <typename T>
class compare
{
private:
T _val;
public:
explicit compare(T & val) : _val(val) { }
explicit compare(T && val) : _val(val) { }
bool operator==(T & t)
{
return _val == t;
}
};
template<typename N>
void NormalClass::templateMember(N value) {
}
template<typename T>
void TemplateClass<T>::normalMember(T value) {
}
template<typename T>
template<typename N>
void TemplateClass<T>::templateMember(T t, N n) {
}
展开
- 声明和定义
- template <模板形参列表> 函数/类 声明/定义模板形参列表>
- 模板形参前需要用typename/class关键字,它们没有区别,但typename可能更直观
- 作用域:模板参数会隐藏外部作用域的同名的类型别名,不能在定义内包含同名标识符
- 成员函数类外定义:(类变量不能在类外定义)
- 普通类的模板成员函数:template<typename N>
- 模板类的普通成员函数:template<typename T>, TemplateClass
:: - 模板类的模板成员函数:template<typename T> template<typename N>,TemplateClass
::
1
2
3
4
5
6
7
8
9
10
11
//函数
compare(1,0);
compare<type>(1,0);
//类
compare<int> com;
//普通成员
com.member;
com.func();
//模板成员
conv.process<int, double>(2, 1.5);
conv.process(1, 3.14);
展开
- 实例化:在编译阶段,编译器检测函数调用/类创建/类成员函数调用,根据模板代码和模板实参,生成一个特定版本的函数或类,此过程叫做模板实例化,生成的版本称为实例化版本
- 错误检测时机:
- 一阶段:编译模板本身,只会检测模板本身的语法错误
- 二阶段:遇到使用,检测参数数量类型是否匹配
- 三阶段:模板实例化时,发现类型相关错误
- 模板定义通常写在头文件中:和类类型定义一样,模板定义同样不会产生实际定义,因此可以放在头文件,编译时需要看到完整定义,放在头文件可以被多个源文件使用,因此最好放在头文件中
- 实例化方式:
- 隐式(编译器推断模板形参类型):函数可以隐式/显示去实例化
- 显示(手动指定模板形参类型):
- 对于类模板,必须使用显示模板实参
- 对于函数,显示模板实参按照从左到右顺序与模板形参匹配(可以数量少于模板形参数量),根据函数实参去隐式推断对应的模板形参,最后如果有剩余未推断的模板形参,这是错误的
1 2 3 4 5 6
//声明 extern template class A<string>; extern template int compare(const int &, const int &); //定义(显示实例化) template class A<string>; template int compare(const int &, const int &);
展开
- 显示实例化:
- 由于模板实例化的触发情况,是当检测函数调用/类创建/类成员调用,会导致相同地实例化可能出现在多个文件对象中,会消耗内存
- 通过extern声明,它们是特定类型的,表示在其他地方有定义,这样就不会导致此编译单元生成实例化版本
- 使用方式:
- 对于定义版本,由于会生成实际的代码,因此需要放在源文件
- 对于声明版本,放在头文件
- 在其他文件使用时,#include头文件
1
2
3
4
5
6
template <size_t N, size_t M>
int str_compare(const char (&str1)[N], const char (&str2)[M])//由于数组不可拷贝,因此形参为引用
{
return strcmp(str1,str2);
}
str_compare("hello","nihao")//6,6//C风格字符串终结符\0也会计算
展开
- 非类型参数:模板形参具有类型(而不是typename/calss了),类型可以为整形/指针/左值引用,绑定到整形形参的模板实参必须是常量表达式(因为模板是在编译期间实例化的,因此值必须能在编译期确定),绑定到指针/左值引用形参的模板实参必须有静态声明周期(全局空间/命名空间定义的/static)/nullptr/0
- inline/constexpr需要放在<模板形参列表>后
- 友元:
- 由于一个模板可以有多个实例化,因此要注意哪些实例化版本有友元关系
- 非模板类:
- 非模板友元:P是此类的友元(一对一)
- 模板友元:
- friend class p<type> ,P的type实例化版本是此类的友元(一对一)
- template<typename T> friend class p,P的所有实例化版本都是此类的友元(一对多)
- 模板类:
- 非模板友元:P是此类所有实例化版本的友元(多对一)
- 模板友元:
- friend class p<T>,P的T实例化版本是此类T实例化版本的友元(一对一)
- template<typename X> friend class p ,P的所有实例化版本都是此类所有实例化版本的友元(多对多)
1
2
3
typedef className<type> 别名;
using 别名 = className<T>;
别名<type> 标识符;
展开
- 别名:
- typedef只能针对特定实例化的版本,无法为T起别名
- 而using是可以的,使用时要指定特定类型
- 类static成员,每个实例化版本都有自己的static成员,和普通成员一样,类外定义同样包含模板形参,在访问时类名要指定特定类型版本
- 默认模板实参: 像默认实参一样,也可以为模板指定默认模板实参,如果为类模板提供默认实参,那么<>可以空实例化,将会使用默认的
- 二义性:
- 类中定义的类型别名和静态成员,都可以通过::直接访问
- 在普通类中,编译器知道类定义,因此可以区分它访问的是类型别名还是静态成员
- className<T>:: 成员访问/ T:: 类型成员访问(类型为类时)
- 而在模板类中,有时需要非特定版本访问,这时无法区分直到实例化时,默认情况会当作静态变量来处理,因此如果它是类型,可以在前面加上typename来说明它是类型
- 类型转换:
- 它支持类型转换,但非常有限:
- 忽略实参的顶层const
- 如果形参非引用,则可以将数组/函数,转为指针
- 如果函数形参有普通类型(非T)/ 形参类型被显示指定,则会支持普通类型转换
- 它支持类型转换,但非常有限:
- 模板实参推断
- 当隐式实例化模板函数时,需要编译器自动根据实参来推断模板参数
- 模板类型推导失败:T是推断而来的,但也会出现不合法情况,比如推导类型不一致
- 模板重载
- 函数模板可以被另一个函数模板/非模板重载
- 函数匹配:
- 候选函数集:函数名相同,在同一作用域内(包括函数模板)
- 可行函数:形参数量相等,类型相同/可转换,对于模板额外需要实参推断成功
- 找到最佳匹配:
- 为每个可行函数的每个参数评定转换等级
- 如果某个函数在所有参数上的转换等级都优于(>=)其他函数,并且至少一个参数严格优于(>)对方,选择它
- 如果多个函数在阶段一同样好,应用决胜规则:
- 如果有非模板,并且唯一,选择它
- 否则如果都是模板,选择更特化的模板
- 否则出现二义性
虚函数表
静态绑定、动态绑定:
静态绑定:构建时根据指针对象类型确定/绑定函数地址,它在运行时效率更高,但灵活性较差
动态绑定:运行时根据对象的实际类型确定/绑定函数地址,它在运行时效率更低,但灵活性较高
虚表:
虚函数表是动态绑定的底层实现(函数查找表)
每个包含虚函数的类都有且仅有一个虚表(vtbl),vtbl是一个函数指针数组,每个函数指针指向该类的一个虚函数
通过*__vptr指针来访问vtbl
vtbl是在编译阶段生成的,vptr是在运行时调用构造函数时进行初始化(vptr指向vtbl)
内存开销不大,因为都已指针形式存储
实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
展开
A_vtbl: 包含两个函数指针,分别指向A::vfunc1()和A::vfunc2()这两个虚函数
B_vtbl: 包含两个函数指针,分别指向B重写了B::vfunc1()函数,继承了A::vfunc2()这两个虚函数
C_vtbl: 包含两个函数指针,分别指向继承了B::vfunc1()函数,重写了C::vfunc2()这两个虚函数
1
2
3
4
5
6
7
8
9
int main()
{
B bObject;
A *p = & bObject;
p->vfunc1();
p->vfunc2();
delete p;
return 0;
}
展开
查表:
- 首先创建了B对象,会首先调用A的构造函数,创建A_vptr指向A_vtbl, 然后调用B的构造函数,将B_vptr指向B_vtbl
- 创建一个基类指针p指向B对象,由于基类指针/引用根据实际的对象查表,并且vptr的共享性,和vtbl独立存在不会消失,那么p是可以访问到B_vtbl
- 使用p来调用vfunc1()函数时,会根据B_vtbl查找vfunc1()函数,也就是B_vtbl中第一个函数指针指向的B::vfunc1()
- 使用p来调用vfunc2()函数时,会根据B_vtbl查找vfunc2()函数,也就是B_vtbl中第二个函数指针指向的A::vfunc2()
构造析构与虚函数
构造函数不能为虚函数:因为调用虚构需要通过vptr去访问vtbl来访问虚构,但是vptr是在构造函数内被初始化的,也就是在调用构造函数前没有初始化(vptr没有指向vtbl),也就无法调用虚构
1
2
A *p = new B;
delete p;
展开
析构函数可以为虚函数,并且当要使用基类指针/引用调用子类时,最好将基类的析构函数声明为虚函数,否则可能存在内存泄露的问题
- 如果类A的析构函数不是虚函数,那么delete p;将会仅仅调用A的析构函数,释放B继承的A部分,而新增的部分未释放掉
- 如果类A的析构函数是虚函数,delete p; 将会先调用B的析构函数,释放掉新增的部分,再调用A的析构函数,释放B继承的A部分
虚函数 vs.纯虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
public:
virtual void vfunc() = 0;
virtual ~A() {} //虚折构
};
class B : public A {
public:
virtual void vfunc(){}
};
class C: public B {
public:
virtual void vfunc(){}
};
int main() {
//A* basePtr0 = new A(); ❌ 错误
A* basePtr1 = new B();
A* basePtr2 = new C();
basePtr1->vfunc();
basePtr2->vfunc();
delete basePtr1;
delete basePtr2;
return 0;
}
展开
virtual:首次声明不可省略,在类外定义/重写声明定义时可以省略
函数未实现:
- 普通函数:
- 没有调用此函数,不会发生错误
- 有调用此函数,在链接时发生错误,找不到定义,无法解析外部符号
- 虚函数:
- 没有实例化,不会发生错误
- 有实例化,如果基类有实现,则继承实现,否则基类没有实现,在链接时发生错误:undefined reference to `Base::func()’
- 纯虚函数:
- 纯虚函数可以实现,但必须通过Base::调用,基类作为抽象类不能被实例化:cannot instantiate abstract class
- 派生类不实现,将继承基类实现,仍作为抽象类不能被实例化
使用场景:
- 虚函数:
- 应用于继承体系中需要重写的行为,它支持多态性,比普通的隐藏方式更灵活
- 纯虚函数:
- 定义接口,A->B->C,B是接口(抽象基类),C是实现类(派生类),A是调用方,降低AC耦合度
- 类是抽象的概念, 不需要被实例化
override
用于显式标识派生类中重写了基类虚函数,当没有正确重写时,编译器会报错,因此当我们重写时尽量添加override关键字,从而提高代码安全性和可读性
final
1
2
class Derived final : public Base{};
void speak() override final{}
展开
在类/函数后:防止类被继承或虚函数被重写

