文章目录
- 一、继承的功能
- 二、继承的本质
- 三、继承方式
- 四、派生类的实例化过程
- 五、重载、隐藏、覆盖
- 六、基类对象和派生类对象的转换
- 七、虚函数,静态绑定和动态绑定
- 八、多继承下的对象内存分布
- 九、虚析构函数(消失的析构函数)
一、继承的功能
- 代码复用
二、继承的本质
派生类可以将基类的所有成员都继承过来
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
九、虚析构函数(消失的析构函数)
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出来的派生类对象时。