继承
继承的本质是复用,也就是沿用父类的变量和作用域,并对其做出的扩展。它会使用一个叫做继承列表的符号(:)来将两个类链接起来。例如:
class student : public people,就是在student类中沿用并扩展了people类。
在习惯上我们将被继承的类叫做基类(父类),继承的类叫做派生类(基类)。
派生类(子类)=》基类(父类),派生类继承了基类
基类=》派生类,基类派生了派生类
继承的方式有三种:
public:共有(任意位置)
protected:保护(子类类和本类类)
private:私有(本类类)
下面用一段代码来更清楚的了解派生类和基类:
class base//基类
{
public:
base(int a=0)
:ma(a)
{
}
protected:
int ma;
};
class derve :public base//派生类
{
public:
derve(int b=0)
:mb(b)
{
}
private:
int mb;
};
可以看到,在基类中定义中了四个字节大小的变量a,在派生类中定义了四个字节大小的变量b。那么在派生类继承基类后,会将基类中的变量a也继承过来。除了通过打印的方法可以知道派生类中的大小,还可以通过命令提示符来查看派生类中空间结构的存放。(具体操作在通过命令提示符查看c++中类的构造可以看到)
可以看到,在派生类中总共有8个字节大小的数据。总结如下:
派生类的内存布局:
1.派生类会继承基类中的变量
2.基类的布局优先于派生类的布局(派生类要依靠基类的变量来操作)
3.除了成员变量以外作用域也会被继承
此外派生类都可以继承基类除构造析构以外的所有成员,例如:
1.普通成员变量
2.普通成员方法
3.静态成员变量
4.静态成员方法
5.作用域
6.类型
派生类函数的构造析构顺序
调用构造函数:
1.调用基类构造
2.调用派生类构造
调用析构函数:
1.调用派生类
2.调用基类构造
3.释放内存
类和类的关系:
1.组合: a part of has_a
像链表类和节点类的关系,节点是链表的一部分
2.继承: a kinfd of is_a
class people
class student : public people
鸟和喜鹊,猫和动物,人和学生
3.代理
限制底层接口对外提供特性
同名函数的关系:
1.重载:同名不同参,同作用域
2.隐藏:同名不同作用域(继承关系)。注意,派生类中同名的函数会隐藏基类中所有的同名函数
3.覆盖:派生类中同名同参的虚函数覆盖掉基类中同名同参的虚函数。
基类和派生类的指向:只允许基类指向派生类
原因:派生类中继承了基类,里面有基类的数据。但是基类中没有派生类的数据。
多态
概念:同一接口(函数名),不同形态(功能不同)。函数名相同,传入的参数不同(函数重载就是一种多态),
多态分为三种:
1.静多态
编译阶段确定函数的调用
2.动多态(默认动多态)
运行阶段确定函数的调用,通过函数的入口地址来确定
函数的入口地址存放在符号表中,以数据的方式加载进来
在编译阶段将入口地址拷贝一份至数据段
虚函数(virtual)机制提供支持
3.宏多态
预编译阶段确定函数的调用,但是因为宏是不安全的,所以一般不提倡这种做法
虚函数的处理机制
虚函数:虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
操作方法:只需要在一个函数前面加上virtual关键字即可,它是在运行阶段编译的。但是我们知道在运行阶段我们只将.data段和.text段传入到内存中,而虚函数的入口地址是放在符号表中的。所以我们会将虚函数的函数入口地址拷贝一份放入到一个叫.ratable(只读数据段)中,然后将函数入口地址存放到虚函数表中,最后再运行阶段传入到内存中去。
vftable和vfptr
当一个类中有虚函数存在时,就会相应的生成一个虚函数表(vftable),里面存放了对象的类型信息、相对偏移、虚函数的入口地址
虚函数表的结构如下:
RTTI:运行时类型信息,运行阶段确定类型
偏移:虚函数指针相对于整体作用域的偏移(0-vfptr = 偏移)
虚函数入口地址:在运行阶段会传给寄存器(下面有讲)
那既然有一个虚函数表,我们的对象就需要去访问它,这时系统就会给我们生成一个4个字节大小的虚函数指针(vfptr)来指向它,在调动虚函数时,我们会通过虚函数指针去访问到虚函数表中的信息。
大概流程图如下,(参考下面的代码)base* pb = new derve(10);: