目录
2.1 继承
2.1.1 继承的基本语法
class 派生类 : 继承方式 基类{
//...
}
类进行继承声明之前,基类的定义必须已经存在。
派生类由两部分组成:
一是基类构成的子对象(基类的非静态成员变量),
二是派生部分(派生类的非静态成员变量)
2.1.2 继承方式
继承方式一共有三种:
公共继承(public)
保护继承(protected)
私有继承(private)
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
//公共继承
class Son1 :public Base1
{
public:
void func()
{
m_A; //可访问 public权限
m_B; //可访问 protected权限
//m_C; //不可访问
}
};
//保护继承
class Son2 :protected Base1
{
public:
void func()
{
m_A; //可访问 public权限
m_B; //可访问 protected权限
//m_C; //不可访问
}
};
//私有继承
class Son3 :private Base1
{
public:
void func()
{
//Son3是私有继承,所以继承Son3的属性在类外都无法访问到
//m_A;
//m_B;
//m_C;
}
};
2.1.3 继承中的对象模型
class Base_A {
public:
int b;
Base_A() {}
~Base_A() {}
void HelloWorld() {
cout << "Hello world" << endl;
}
protected:
int c;
private:
int a;
static int d;
};
int Base_A:: d = 0;
class Derived_B:public Base_A{
public:
Derived_B() {}
~Derived_B() {}
private:
int f;
};
int main() {
Derived_B text;
text.HelloWorld();
return 0;
}
运行结果:
派生类的内存分布
可以看出:
派生类对象可以访问基类的函数
成员函数和静态成员变量不存储在对象的内存空间中,
非静态成员变量存储在对象的内存空间中
因而继承时,派生类将继承基类成员变量(不论哪一种访问权限)以及成员函数
2.1.4 继承中构造和析构顺序
创建一个派生类对象时,先调用父类构造函数,再调用子类构造函数,
删除时,先调用派生类的构造函数,再调用基类的构造函数。
2.1.5 继承同名成员处理方式
同名变量的出现意味着我们需要解决二义性的现象
派生类和基类类出现了同名的成员变量时,派生类仅仅将父类的同名成员隐藏了,而非覆盖替换
因而1.子类对象可以直接访问到子类中同名成员
2.子类对象加作用域可以访问到父类同名成员
2.1.6 多继承语法
语法:class 子类 :继承方式 父类1 , 继承方式 父类2...
3.1 多态
我们已经知道基类和派生类同时出现同名成员函数,派生类将会隐藏父类的同名函数
如果同名成员函数是虚函数呢?派生类的虚函数表中将派生类的同名虚函数的地址覆盖为自身的同名虚函数的地址。当使用指针或引用调用虚函数就产生了多态
多态底层是通过动态绑定来实现的,基类的指针p->相应对象的vfptr->访问相应vftabl->(起始值+偏移量)调用的是对应的派生类对象的函数
多态分为两类
静态多态:在编译期间确定。这通常通过函数重载实现,即多个具有相同名称但参数列表不同的函数。静态多态与是否发生继承没有必然联系。
动态多态:运行时才能确定。动态多态必须通过虚函数实现在派生类中,可以重写虚函数以实现自己的行为。当使用基类指针或引用调用虚函数时,将根据实际对象的类型动态地调用相应的函数。
3.1.1 纯虚函数和抽象类
纯虚函数没有实现(即没有函数体)
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表)= 0 ;
抽象类概念:
抽象类是一个包含至少一个纯虚函数的类。抽象类的目的是作为其他类的基类,并定义一组通用的接口。派生类可以继承抽象类并实现其中的纯虚函数,以提供具体的功能和行为。
抽象类特点
无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚函数内存模型:
class Base_A {
public:
int a;
virtual void fa() = 0;
virtual void fb() = 0;
virtual void fc() = 0;
};
我们知道成员函数是不占用类的内存空间的,如果没有使用关键字virtual修饰函数,即虚函数,Base_A类的内存模型为
但当C++类使用了虚函数后,其内存模型会有所不同。具体来说,使用了虚函数的类会有一个虚函数表(Virtual Table,简称vtable)(不占用类的内存空间),以及一个虚函数指针(Virtual Table Pointer,简称vptr)。
Base_A类的内存模型为:
我们可以将虚函数表看成一个用于存储虚函数的地址的数组,存放虚函数地址的顺序,按照声明的顺序
每个类实例的vptr都会指向该类的虚函数表的起始部分
继承的内存模型
class Base_A {
public:
int a;
virtual void fa() {};
virtual void fb() {};
virtual void fc() {};
};
class Derived_B:public Base_A
{
public:
int a;
virtual void fa() override {
cout << "6" << endl;
}
virtual void fd() {};
};
派生类Derived_B继承了基类Base_A虚函数表,
由于Derived_B重写了函数fa,将Base_A
::fa()
覆盖为Derived_B::a(),对于基类中没有的虚函数,fd()将会被附加在虚函数表的最后,不会单独再额外建立一张虚函数表
多继承的内存模型
class Base_A {
public:
int a;
virtual void fa() {};
virtual void fb() {};
virtual void fc() {};
};
class Base_B {
public:
int b;
virtual void fe() {};
};
class Derived_B:public Base_A, public Base_B
{
public:
int a;
virtual void fa() override {
cout << "6" << endl;
}
virtual void fd() {};
};
Base_A和Base_B都有虚函数,所以它们都有自己的虚函数表,难以通过线性偏移调用函数,需要有两个虚指针分别对它们虚函数进行索引。
菱形继承的内存模型
为了避免Base_A中的成Base_B和Base_C分别虚继承Base_A。编译器会在每个子类中引入一个虚基类表(vbase table),以及一个指向虚基类表的指针。
class Base_A {
public:
int a;
virtual void fa() {};
};
class Base_B:virtual Base_A{
public:
int b;
virtual void fb() {};
};
class Base_C :virtual Base_A {
public:
int c;
virtual void fc() {};
};
class Derived_B:public Base_B, public Base_C
{
public:
int c;
virtual void fc() {};
};
如果不用虚继承的方式:可以看出Derived_B通过Base_B和Base_C重复继承了Base_A中成员
采用虚继承后,原本的虚基类Base_A部分被虚基类指针取代。编译时,无法确定它的基类Base_A
在内存中的偏移量,虚基类表中会记录Base_A中的偏移位置。
3.1.2 虚析构和纯虚析构
在多继承中,一个派生类对象通过基类指针或引用进行删除,而该派生类对象还包含有动态分配的内存,由于基类指针不能调用派生类的析构函数,那么这些动态分配的内存可能无法被正确释放,从而导致内存泄漏。我们可以引入虚析构来解决。
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;//(类内声明)
类名::~类名(){}//(类外实现)
class Base_A {
public:
int a;
virtual ~Base_A() {}
};
class Derived_B:public Base_A{
public:
int c;
void fc() {};
};
由于派生类,没有重写析构函数,所以派生类的析构函数是编译器自动为其生成的默认析构函数
并覆盖了原先虚函数表存放的内容。
从Base_A继承下来的虚函数表存放的是派生类的析构函数地址。
当用基类指针来管理为派生类对象动态分配的内存时,派生类对象析构时,基类指针可以通过vfptr访问虚函数表中派生类的析构函数地址,从而调用派生类的析构函数,再调用基类的析构函数,保证了申请堆区中的内存能正确析构
我暂时捋不清楚菱形继承的虚析构过程的底层原理,以后学明白再补上。