继承
继承的本质是复用,也就是沿用父类的变量和作用域,并对其做出的扩展。它会使用一个叫做继承列表的符号(:)来将两个类链接起来。例如:
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);:
虚函数表的合并
在派生类继承基类的时候,如果基类中有虚函数,那么在派生类中同名同参的函数也会成为虚函数。因为覆盖的关系派生类中的虚函数最终会覆盖掉基类中的虚函数。最终实现虚函数表合并。
同理,那么相应的基类和派生类中都会有虚函数指针。我们知道虚函数指针(vfptr)是占四个字节大小的,所以虚函数指针也会进行合并,它的合并规则是由外向内合并的。
class base
{
public:
base(int a = 10)
:ma(a)
{}
void show()
{
std::cout << "base::ma" << ma << std::endl;
}
protected:
int ma;
};
class derve :public base
{
public:
derve(int b)
:mb(b)
{}
virtual void show()
{
std::cout << "derve::mb" << mb << std::endl;
}
private:
int mb;
};
int main()
{
std::cout << "base::size:" << sizeof(base) << std::endl;//4
std::cout << "derve::size:" << sizeof(derve) << std::endl;//8
base* pb = new derve(10);
std::cout << "pb type:" << typeid(pb).name() << std::endl;//class base*
std::cout << "*pb type:" << typeid(*pb).name() << std::endl;//class base
pb->show();//ma 10 class base.show()基类的show函数
return 0;
}
创建一个base类,输出a的值,然后创建一个derve类并以共有的方式继承基类base。分别在调用点打印出基类的大小、派生类的大小定义一个基类指针来指向派生类、打印指针的类型、打印给指针解引用的类型、调动show()函数。打印结果如下:
当我们在基类的show函数中加上一个virtual后,再去运行程序时,结果如下:
这是因为系统会分配一个虚函数指针vfptr指向虚函数表,实现对象针对于虚函数表的共享,而pb在调用show函数时,会通过vfptr指针来找到派生类中的虚函数表,此时就发生了动多态。
动多态的发生时机
第一:我们可以转到反汇编去看,在调动show函数时,传入的是地址还是寄存器。我们知道如果是在编译阶段处理的化,就会直接传入地址,这时候发生的是静多态**。如果是在运行阶段处理的化,就会在编译阶段传入一个寄存器,然后再运行阶段将虚函数入口地址传给寄存器,这时就发生了动多态**
第二:通过对象定义的指针来调动虚函数也会发生动多态,但是要注意:对象必须是完整的(也就是说构造和析构函数是不可以发生动多态的)
虚析构
首先我们要知道,当基类中的指针指向派生类对象时,它指向的是基类中的那部分。在调用析构函数时,会发现派生类的析构函数并没有调动,这样是会发生内存泄漏的。我们知道在调用析构函数时,是先调用派生类的析构函数再调用基类的虚构函数的。但是当我们直接释放掉基类的内存时,那么派生类中的内存就会无法得到释放。这时就产生了内存泄漏。
解决方法:将基类中的析构函数设为虚函数,这样再派生类中就会覆盖掉基类中的虚函数,从而在调动析构函数时,可以将基类和派生类的空间一块释放掉。
虚函数的写入时机
虚函数是在构造函数之前写入的。。如果有基类的指针指向派生类对象,将构造函数全部置为0,会不会出错呢?是不会的,因为基类指针指向的是派生类中基类的那部分空间,所以不会将派生类的构造函数置为0。
纯虚函数
概念:基类中保留接口,通过派生类来实现功能
virtual+函数名 = 0;
抽象类:拥有纯虚函数的类,不能实例化对象,但可以通过指针来引用对象
class anmial//基类anmial
{
public:
anmial( std::string name)
:mname(name)
{}
virtual void bark() = 0;//纯虚函数,只保留接口
protected:
std::string mname;
};
class dog:public anmial//派生类dog
{
public:
dog(std::string name)
:anmial(name)
{}
virtual void bark()//对基类中纯虚函数的实现
{
std::cout << mname << "wang wang wang" << std::endl;
}
};
class cat :public anmial//派生类cat
{
public:
cat(std::string name)
:anmial( name)
{}
virtual void bark()//对基类中纯虚函数的实现
{
std::cout << mname << "miao miao miao" << std::endl;
}
};
void showanmioal(anmial* pb)//基类的指针可以指向任意一个派生类对象
{
if (typeid(*pb) == typeid(cat))//类型比较,如果pb指向的类型和派生类cat相同
{
std::cout << "cat can 喵喵喵" << std::endl;
}
else//如果不同调用bark函数
{
pb->bark();
}
}
int main()
{
dog DOG("dog");
cat CAT("cat");
showanmioal(&DOG);//dog类和cat类不同,执行else()
showanmioal(&CAT);//类型相同,打印cat can 喵喵喵
return 0;
}
四种转换类型
1.const_cast:去除常性,将一个常量去除常性
2.static_cast:安全性更高,如果有安全隐患就会报错
3.reinterpret_cast:指针类型的转换
4.dynamic_cast:RTTI信息转换
多继承和虚继承
概念:派生类继承了两个或两个以上的基类
典型的堆积成就是‘菱形继承’,假设基类为A,基类派生出两个派生类B和C,同时派生类D又继承了B和C。
虚继承
如果B和C都继承A的情况下,再让D同时去继承B,C。呢么就会再D的内存布局下同时线两份A的数据,后果就是在给ma赋值时系统将无法选择从而报错。解决这个问题的方法就是让B,C在继承A的时候是以虚继承的方式来继承。
class A
{
public:
A(int a)
:ma(a)
{}
protected:
int ma;
};
class B :virtual public A//以虚继承的方式继承基类A
{
public:
B(int b)
:A(b),mb(b)
{}
protected:
int mb;
};
class C :virtual public A//以虚继承的方式继承基类A
{
public:
C(int c)
:A(c), mc(c)
{}
protected:
int mc;
};
class D :public B, public C
{
public:
D(int d)
:A(d), B(d),C(d),md(d)
{}
protected:
int md;
};
布局:非虚基类的布局优先于虚基类
在D类中会把重复的数据移到后面去,同时在原来的地方添加一个虚继承指针(vbptr),vbptr指针指向的是一张虚继承表里面存放着两个偏移:vbptr相对于作用域的偏移和vbptr相对于虚基类数据的偏移
虚基类的处理和继承顺序相关:
构造顺序:虚基类的构造优先于非虚基类
析构顺序:非虚基类的析构优先于虚基类