一、继承对象的内存空间
构造函数调用顺序,先调用父类,再调用子类
cpp
#include<iostream>
using namespace std;
//基类 父类
class Base{
public: //公有权限 类的外部 类的内部
Base(){cout<<"Base()"<<endl;}
Base(int a):baseA(a)
{
cout<<"Base(int a)"<<endl;
}
~Base(){cout<<"~Base()"<<endl;}
void setData(int a){
baseA = a;
}
void show(){
cout<<"baseA:"<<baseA<<"\t addr:"<<&baseA<<endl;
}
protected: //保护权限 类的内部 子类
private: //私有权限 类的内部
int baseA;
};
//子类 派生类
class Child: public Base
{
public: //公有权限 类的外部 类的内部
//在参数列表中指定 父类的构造函数
Child():Base(100)
{
cout<<"Child()"<<endl;
}
Child(int a,int b):Base(a),childA(b)
{
cout<<"Child(int a)"<<endl;
}
~Child(){cout<<"~Child()"<<endl;}
void show(){
Base::show();
cout<<"childA:"<<childA<<"\t addr:"<<&childA<<endl;
}
protected: //保护权限 类的内部
private: //私有权限 类的内部
int childA;
};
int main()
{
Child mya(100,200);
cout<<"mya size:"<<sizeof(mya)<<"\t addr:"<<&mya<<endl;
mya.show();
}
打印结果:
cpp
gec@ubuntu:/mnt/hgfs/GZ22229/14C++/05/1-code$ ./a.out
Base(int a)
Child(int a)
mya size:8 addr:0x7ffed6dc6630
baseA:100 addr:0x7ffed6dc6630
childA:200 addr:0x7ffed6dc6634
~Child()
~Base()
注意:
1、子类调用成员函数的时候会检测该成员函数在子类中是否存在,如果存在就调用自己的, 如果不存在就调用 父类的(前提是父类要有这个函数)
2、如果子类和父类存在同名函数,那么在子类中 父类的函数成员会被隐藏,默认调用的就是子类函数成员。如果要调用父类的函数成员必须添加类名和作用域。
cpp
mya.Base::showData();
练习1:
设计一个基类动物类(属性:体重,颜色,年龄 行为:跑,吃,睡),构造方法初始化属性
设计一个猫类继承动物类(属性:体重,颜色,年龄,品种 行为:跑吃睡,抓老鼠,叫)
定义一个猫对象--咖菲猫,调用猫的喊叫行为,抓老鼠行为 ,输出猫的属性
练习2:编写一个输出学生和教师数据的程序,学生数据有编号、姓名、年龄、学号和成绩;教师数据有编号、姓名、年龄、职称和部门。要求声明一个 person 类,并作为学生数据操作类 student 和教师数据操作类 teacher 的基类
cpp
#include <iostream>
#include <cstring>
//将命名空间打开
/*
设计一个基类 动物类(属性:体重、颜色、年龄 、性别 行为:吃喝拉撒)
设计一个猫类 继承动物类(属性:体重、颜色、年龄 、性别、 行为:吃喝拉撒、抓老鼠)
定义一个猫对象 ---断尾猫,调用猫的行为*/
using namespace std;
//动物类
class zoon{
public: //公有权限
zoon(int weight,int age, const char * color){
this->weight = weight;
this->age = age;
strcpy(this->color,color);
}
void show(){
cout << "weight:" << weight <<endl;
cout << "age:" << age <<endl;
cout << "color:" << color <<endl;
}
protected:
//私有属性
int weight; //体重
int age; //年龄
char color[256]; //颜色
};
//猫类
class cat:public zoon
{
public: //公有权限
cat(int weight,int age,const char * color,const char *type):zoon(weight,age,color) {
strcpy(this->type,type);
}
void show(){
cout << "weight:" << weight <<endl;
cout << "age:" << age <<endl;
cout << "color:" << color <<endl;
cout << "type:" << type <<endl;
}
void behavior(){
cout << "behavior" <<endl;
}
private:
//私有
char type[256];
// cat Broken_tail(10,6,"white","BrokenTail");
};
int main()
{
cat mya(10,6,"white","BrokenTail");
mya.show();
mya.behavior(); //行为
return 0;
}
二、多继承
1、格式
cpp
class 子类:继承方式 基类1,继承方式 基类2
{
变量成员
函数成员
};
2、例子
cpp
#include <iostream>
using namespace std;
//羊类
class Sheep{
public:
Sheep(){
cout<<"Sheep()"<<endl;
}
Sheep(int a):dataA(a)
{
cout<<"Sheep(int a)"<<endl;
}
~Sheep(){
cout<<"~Sheep()"<<endl;
}
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
}
int dataA;
};
//驼类
class Tuo{
public:
Tuo(){
cout<<"Tuo()"<<endl;
}
Tuo(int a):dataB(a)
{
cout<<"Tuo(int a)"<<endl;
}
~Tuo(){
cout<<"~Tuo()"<<endl;
}
void show(){
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
}
int dataB;
};
//羊驼类
//多继承 的时候 基类的构造函数执行的顺序 跟 继承的先后顺序有关
class SheepTuo:public Sheep,public Tuo
{
public:
//在构造函数的参数列表中 指定 基类的构造函数
SheepTuo(int a,int b):Tuo(b),Sheep(a)
{
cout<<"SheepTuo()"<<endl;
}
~SheepTuo(){
cout<<"~SheepTuo()"<<endl;
}
//在子类中重写 基类的同名函数,那么基类中的同名函数就会自动隐藏
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
cout<<"dataC value:"<<dataC<<"\t addr:"<<&dataC<<endl;
}
int dataC;
};
int main()
{
//派生类对象的实例化
SheepTuo mya(10,20);
cout<<"mya size:"<<sizeof(mya)<<"\tmya addr:"<<&mya<<endl;
mya.show();
//通过基类的类名显示调用
//mya.Sheep::show();
return 0;
}
结果:
mylinux@ubuntu:/mnt/hgfs/13C++编程/04/1-code$ ./a.out
A()
B()
C()
dataA addr:0x7fffd8f7f420 dataB addr:0x7fffd8f7f424 dataC addr:0x7fffd8f7f428
~C()
~B()
~A()
总结:
- 多继承的时候,创建子类对象构造函数执行的顺序 羊---》驼----》羊驼 ,跟基类继承的先后顺序有关
- 在派生类的构造函数初始化列表中指定基类的构造函数
3、内存结构图
思考:如果多个基类中有同名的函数,那么创建子类的对象,然后进行调用,会发生什么情况?怎么解决
情况:编译错误,语法不通过
解决: 1. 在子类中重新实现函数,父类函数就会自动隐藏
- 通过父类类名显示调用
三、多继承中的菱状继承(环状继承)
1、概念
多继承中的多级继承的时候,父类的父类出现公共的基类
2、例子
bash
A--->D C-->D B-->A、C
这个继承会使D创建两个对象
cpp
#include <iostream>
using namespace std;
//动物类
class Animal{
public:
Animal(){
cout<<"Animal()"<<endl;
}
Animal(int a):dataD(a)
{
cout<<"Animal(int a)"<<endl;
}
~Animal(){
cout<<"~Animal()"<<endl;
}
int dataD;
};
//羊类 --继承于 动物类
class Sheep:public Animal{
public:
Sheep(){
cout<<"Sheep()"<<endl;
}
Sheep(int a):dataA(a)
{
cout<<"Sheep(int a)"<<endl;
}
~Sheep(){
cout<<"~Sheep()"<<endl;
}
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
}
int dataA;
};
//驼类
class Tuo:public Animal{
public:
Tuo(){
cout<<"Tuo()"<<endl;
}
Tuo(int a):dataB(a)
{
cout<<"Tuo(int a)"<<endl;
}
~Tuo(){
cout<<"~Tuo()"<<endl;
}
void show(){
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
}
int dataB;
};
//羊驼类
//多继承 的时候 基类的构造函数执行的顺序 跟 继承的先后顺序有关
class SheepTuo:public Sheep,public Tuo
{
public:
//在构造函数的参数列表中 指定 基类的构造函数
SheepTuo(int a,int b):Tuo(b),Sheep(a)
{
cout<<"SheepTuo()"<<endl;
}
~SheepTuo(){
cout<<"~SheepTuo()"<<endl;
}
//在子类中重写 基类的同名函数,那么基类中的同名函数就会自动隐藏
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
cout<<"dataC value:"<<dataC<<"\t addr:"<<&dataC<<endl;
// cout<<"dataD value:"<<dataC<<"\t addr:"<<&dataD<<endl;
}
int dataC;
};
int main()
{
//派生类对象的实例化
SheepTuo mya(10,20);
cout<<"mya size:"<<sizeof(mya)<<"\tmya addr:"<<&mya<<endl;
mya.show();
//发生二义性问题
//mya.Animal::dataD = 100;
//通过基类的类名显示调用
//mya.Sheep::show();
return 0;
}
分析:
使用qtcreator的断点调试可看到:
内存图如下:
通过分析可得, 创建B myb 对象的时候,构造函数顺序为 D---->A----->D--->C---->B
思考:如果myb访问D类中的成员,会发生什么情况??
答案: 会发生歧义,编译错误
如何解决:使用虚拟继承
四、虚拟继承(虚基类)
1、概念
为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题, 将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个 拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题
2、格式
cpp
class 子类名称:virtual 父类名
{
};
3、例子
cpp
#include <iostream>
using namespace std;
//动物类
class Animal{
public:
Animal(){
cout<<"Animal()"<<endl;
}
Animal(int a):dataD(a)
{
cout<<"Animal(int a)"<<endl;
}
~Animal(){
cout<<"~Animal()"<<endl;
}
int dataD;
};
//羊类 --继承于 动物类 --虚基类 Animal
class Sheep:virtual public Animal{
public:
Sheep(){
cout<<"Sheep()"<<endl;
}
Sheep(int a):dataA(a)
{
cout<<"Sheep(int a)"<<endl;
}
~Sheep(){
cout<<"~Sheep()"<<endl;
}
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
}
int dataA;
};
//驼类
class Tuo:virtual public Animal{
public:
Tuo(){
cout<<"Tuo()"<<endl;
}
Tuo(int a):dataB(a)
{
cout<<"Tuo(int a)"<<endl;
}
~Tuo(){
cout<<"~Tuo()"<<endl;
}
void show(){
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
}
int dataB;
};
//羊驼类
//多继承 的时候 基类的构造函数执行的顺序 跟 继承的先后顺序有关
class SheepTuo:public Sheep,public Tuo
{
public:
//在构造函数的参数列表中 指定 基类的构造函数
SheepTuo(int a,int b):Tuo(b),Sheep(a)
{
cout<<"SheepTuo()"<<endl;
}
~SheepTuo(){
cout<<"~SheepTuo()"<<endl;
}
//在子类中重写 基类的同名函数,那么基类中的同名函数就会自动隐藏
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
cout<<"dataC value:"<<dataC<<"\t addr:"<<&dataC<<endl;
cout<<"dataD value:"<<dataC<<"\t addr:"<<&dataD<<endl;
}
int dataC;
};
int main()
{
//派生类对象的实例化
SheepTuo mya(10,20);
cout<<"mya size:"<<sizeof(mya)<<"\tmya addr:"<<&mya<<endl;
mya.show();
//发生二义性问题
//mya.Animal::dataD = 100;
//通过基类的类名显示调用
//mya.Sheep::show();
return 0;
}
现象:
cpp
A:virtual public D
C:virtual public D
分析:
总结:
- 在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。
- 虚拟继承在创建对象的时候在子类对象中会有一个虚基类表指针,该虚基类表指针存储 虚基类 对象空间的最开始位置。
- 类对象空间的大小 由 非静态成员数据+虚基类指针+字节对齐 组成。
五、虚函数、虚函数表
1、格式
cpp
virtual 函数返回类型 函数名(参数表) {函数体};
当一个类中有一个或多个虚拟函数,那么这个类在编译的时候就会创建一个虚函数表(简称虚表) 。虚表就是一个存放虚函数指针的表格,而这个表格的首地址存放在对象的地址开始位置。
2、例子
cpp
#include <iostream>
using namespace std;
class Data{
public:
Data(){cout<<"Data()"<<endl;}
~Data(){cout<<"~Data()"<<endl;}
private:
//在普通的函数前面加一个 关键字virtual ---虚函数
virtual void setA(int val){
cout<<"setA"<<endl;
//dataA = val;
}
virtual void setB(int val){
cout<<"setB"<<endl;
//dataB = val;
}
virtual void setC(int val){
cout<<"setC"<<endl;
dataC = val;
}
int dataA;
int dataB;
int dataC;
private:
};
int main()
{
Data mya;
cout<<"mya size:"<<sizeof(mya)<<"mya addr:"<<&mya<<endl;
//如何在类的外部 去调用类中的私有函数成员
//可以将 私有函数成员 声明为 虚函数 ,通过虚表指针 去调用
//mya.setA(10);
// mya.setB(20);
// mya.setC(30);
//unsigned long 在window平台 是 4个字节 unsigned long long 是 8个字节
//unsigned long 在linux平台 是 8个字节
//求出虚表的地址
unsigned long long *vptr = (unsigned long long *)(*(unsigned long long*)&mya);
cout<<"vptr:"<<vptr<<endl;
cout<<"vptr+1:"<<vptr+1<<endl;
return 0;
}
通过断点调试 可以看到
3、注意:
1)、如果一个类中包含(一个或者多个)虚函数,那么这个类的对象中会包含一个虚表指针vptr
2)、虚表指针保存在对象内存空间的最前面
3)、虚表中存储的是 类中 虚函数的地址
4)、对象调用类中的虚函数,会查询虚表指针再去执行函数
4、虚表与虚表指针,虚基类表与虚基类表指针的区别
https://blog.csdn.net/qq_42719751/article/details/104656429
六、通过虚表指针调用虚函数
1、虚函数
在类的成员函数声明前面添加virtual,这个函数就被声明为虚函数, 那么这个类的对象中包含一个指向虚表的指针
2、虚表
存储虚函数地址
3、通过虚函数指针调用虚函数
cpp
#include <iostream>
using namespace std;
class Data{
public:
Data(){}
~Data(){}
private:
int a;
//virtual关键字还可以用来修饰函数 ,称之为 虚函数
virtual int test01(){
cout<<__FUNCTION__<<endl;
}
virtual int test02(){
cout<<__FUNCTION__<<endl;
}
int test03(){
cout<<__FUNCTION__<<endl;
}
};
int main()
{
Data d1;
//d1.test01();
//d1.test02();
cout<<"size:"<<sizeof(Data)<<endl;
//通过得到虚表中的函数的地址去调用函数
//如果该函数是私有权限下的函数,能够突破类中权限的限制
//1)先获取虚表的地址
//*(unsigned long long*)&d1; 地址常量 0x100ff1a
unsigned long long*vptr = (unsigned long long*)*(unsigned long long*)&d1;
cout<<"vptr:"<<vptr<<endl;
//求test01函数的地址
//*vptr 等价于vptr[0] --得到test01函数的地址--但是该地址是一个常量,必须转换成函数类型的地址
typedef int (*PFunc_t)(void);
// PFunc_t test01_p = (PFunc_t)vptr[0];
// test01_p();
((PFunc_t)vptr[0])();
// PFunc_t test02_p = (PFunc_t)vptr[1];
// test02_p();
return 0;
}
4、总结
- 同一个类的不同实例共用同一份虚函数表, 它们都通过一个所谓的虚函数表指针__vfptr指向该虚函数表.
- 虚表存储在进程的数据段.rodata中,虚函数存储在代码段中的.text段中
- 当类中有虚函数的时候,编译器在编译的时候就会为该类创建一张虚表,而且只存在一份。定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表。
- 通过虚函数表能够突破虚函数访问权限的控制。比如我们可以将虚函数定义为private类型,这样在类外面就不能通过对象调用private类型的函数了。但是在类的虚函数表中,不论其访问类型是public还是private的,其所有虚函数都会存放其中。