文章目录
一、继承的功能
- 代码复用;
- 在基类中提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态了。
二、继承的本质
派生类可以将基类的所有成员都继承过来,等于说,在派生类的内存空间中也存在了一份基类的成员变量,只不过这些变量都是带作用域的,如下图所示:
class A {
int ma;
};
class B : A {
int mb;
};
class C : B {
int mc;
};
int main() {
A a;
B b;
C c;
cout << sizeof(a) << endl; //4
cout << sizeof(b) << endl; //8
cout << sizeof(c) << endl; //12
return 0;
};
三、继承方式
public,protected,private
- 继承方式是继承下来的成员在类外访问权限的上限;
- 多重继承下,派生类的继承方式看直接继承的基类;
- 外部只能访问对象public成员,protected和private的成员无法直接访问。
- 在继承结构中,派生类从基类可以继承过来private成员,但是派生类却无法直接访问
- 默认的继承方式:class定义派生类,默认继承方式是private;struct定义派生类默认继承方式是public
四、派生类的实例化过程
- 派生类无法直接初始化从基类继承来的变量,而只能通过基类的构造函数来初始化
class Base {
public:
Base(int data = 10) : ma(data) {}
int ma;
};
class Derive : public Base {
public:
// Derive(int data = 10) : ma(), mb(data) {} //会报错,无法直接对继承来的成员进行初始化
Derive(int data = 10) : Base(data), mb(data) {} //必须使用基本的构造函数进行初始化
int mb;
};
- 派生类实例化的时候会先调用基类的构造函数初始化从基类继承来的成员,派生类实例消亡的时候会先将派生类实例销毁(使用派生类析构函数释放资源),再销毁基类实例(使用基类析构函数释放资源)。
五、重载、隐藏、覆盖
- 重载:一组函数在同一个作用域下,函数名相同,参数列表不同,构成重载关系。
- 隐藏:在继承结构当中(即不同作用域),派生类的同名成员把基类的同名成员隐藏起来了。
- 覆盖:虚函数表中,派生类虚函数地址覆盖基类虚函数地址
class Base {
public:
Base(int data = 10) : ma(data) { cout << __FUNCTION__ << endl; }
~Base() { cout << __FUNCTION__ << endl; }
void show() { cout << this << endl; } //1号函数
void show(int a) { cout << a << endl; } //2号函数
int ma;
};
class Derive : public Base {
public:
Derive(int data = 10) : mb(data) { cout << __FUNCTION__ << endl; }
void show() { cout << __FUNCTION__ << endl; } //3号函数
~Derive() { cout << __FUNCTION__ << endl; }
int mb;
};
int main() {
Derive b(10);
// b.show(100); //报错因为带int参数的show函数被隐藏了
b.Base::show(100); //如果要调用上述函数,需要加作用域
system("pause");
return 0;
};
1号函数和2号函数是重载关系
3号函数隐藏了1号函数和2号函数,所以,如果构造了Derive的对象,并调用show(int a)
函数会出错,因为现在Derive对象只能看到自己的show()
函数。
六、基类对象和派生类对象的转换
继承结构是一种从上(基类)向下(派生类)的结构
- 派生类对象赋值给一个基类对象,类型从下到上的转换;
- 基类对象不能赋值给派生类对象;
- 基类指针可以指向派生类对象,类型从下到上的转换,但是该指针只能访问到基类囊括的成员;
- 派生类指针不能指向基类对象。
总结来说,派生类是基类,基类不是派生类,类似于,白马是马,马不是白马。
七、虚函数,静态绑定和动态绑定
- 一个类里如果定义了虚函数,编译时期,编译器就会为该类生成唯一的虚函数表vftable,虚函数表中存储的信息如下。程序运行时,每一张虚函数表都会加载到内存的.rodata区。
- 一个类中如果定义了虚函数,那个这个类的对象,运行时,内存中开始的部分,会多存储一个vfptr虚函数指针,指向相应函数的虚函数表vftable,一个类型定义的n个对象指向的是同一个虚函数表。
- 一个类中的虚函数个数,不影响对象的内存大小,影响的只是虚函数表的大小。
- 如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是虚函数,那么这个派生类的方法会被自动处理成虚函数。
- 编译器处理派生类虚函数表的时候会先把基类虚函数表中的虚函数都继承下来,然后如果派生类中有和基类中相同的虚函数,就用派生类中的虚函数覆盖/重写基类的虚函数。
静态绑定(编译时期的绑定):编译器在编译时期根据对象的类型调用相应作用域下的成员函数,如果该成员函数是非虚函数,就直接调用该类型作用域下的函数生成指令。
动态绑定(运行时期的绑定):编译器在编译时期根据对象的类型调用相应作用域下的成员函数,如果该成员函数是虚函数,就在运行期间调用实际对象的虚函数表中对应的虚函数。
注意:
- 使用对象调用某个方法的时候只会发生静态绑定,因为已经有了对象,调用哪个作用域中的方法已经很明确了。
- 使用指针或者引用调用虚函数的时候才会发生动态绑定
- 构造函数中调用的虚函数并不会发生动态绑定
八、多继承下的对象内存分布
class Derive: public Base1, public Base2
注意:vfptr是在对象构造函数运行之前就写入对象内存的
九、虚析构函数(消失的析构函数)
class Base {
public:
Base() { cout << __FUNCTION__ << endl; }
~Base() { cout << __FUNCTION__ << endl; }
};
class Derive : public Base {
public:
Derive() { cout << __FUNCTION__ << endl; }
~Derive() { cout << __FUNCTION__ << endl; }
};
int main() {
Base *pb = new Derive;
delete pb;
return 0;
};
以上程序输出如下:
Base
Derive
~Base
问题:~Derive
析构函数并没有调用,这样会导致内存泄漏
问题产生原因:delete pb
运行时,会去调用pb指针类型(Base类)的析构函数,编译器发现Base类的析构函数不是virtual,于是发生静态绑定,直接调用Base类的析构函数,而不会调用Derive的析构函数
解决办法:将基类析构函数~Base声明成virtual ~Base()
,如此操作之后输出如下:
Base
Derive
~Derive
~Base
- 虚函数依赖:
虚函数要能产生地址,存储在vftable中;
对象必须存在,这样才能依据对象中的虚函数表指针寻找函数(vfptr----> vftable ---->虚函数);
- 构造函数不能是virtual,因为构造函数调用完成之后才有对象
构造函数中调用的任何函数(虚函数或非虚函数)都是静态绑定;
- virtual 和static不能共存,因为static函数并不依赖于对象;
- 把基类的析构函数实现成虚函数的时机:
基类的指针(引用)指向堆上new出来的派生类对象时。
十、多态(多种多样的形态)
静态(编译时期)多态:
- 函数重载,在编译阶段就确定好要调用的函数版本;
- 函数模板,在编译时期使用具体的类型对函数模板进行实例化。
动态(运行时期)多态:
- 在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的覆盖方法,称为多态。多态是通过动态绑定来实现的。
十一、抽象类
class Animal {
public:
virtual void bark() = 0; //纯虚函数,Animal类成为抽象类,纯虚函数必须被派生类重写
};
十二、虚继承和虚基类
虚继承的时候派生类对象的内存分布如下:
如图所示,虚继承的时候,基类的内容会被移到派生类对象内存区域的最后。(以上只是msvc中的对象内存结构,在gcc中,内存结构并没有发生后移的现象)
基类指针指向派生类对象的时候,指针指向的永远是派生类对象中基类内容的开始部分
class Base {
public :
virtual void func() { cout << "Base::func" << endl; }
void operator delete(void *p) {
cout << "operate delete p:" << p << endl;
free(p);
}
};
class Derive : virtual public Base {
public :
void func() { cout << "Derive::func" << endl; }
void *operator new(size_t size) {
void *p = malloc(size);
cout << "operate new p:" << p << endl;
return p;
}
};
int main() {
Base *pb = new Derive();
cout << "main p:" << pb << endl;
pb->func();
delete pb; //msvc编译器和gcc编译器在处理此处处理有所不同
system("pause");
return 0;
};
如上代码所示在delete pb
的时候会出现问题,由于pb指向的并不是Derive对象的内存起始位置,而是Derive对象中从基类继承来的内容的起始部分,所以delete的时候会出错,gcc编译器会自动处理上述问题,而msvc会报错。
十三、多重继承的问题
菱形继承和圆形继承如下图所示
以上继承方式,最后的派生类会重复继承到最初基类的成员。
解决办法:将最初基类继承的方式变成虚继承。
因为虚继承的时候会把基类的内存转移到派生类对象最后,然后覆盖掉基类重复的变量。