很多人都把 C++ 继承当成一张权限表来背,但先别急着背表,先回答一个问题:
继承的本质是什么?
我的结论就多了内存和访问权限,改变父类在子类的权限
子类继承的是父类的"完整子对象",所以内存变多了;同时父类的公有、保护成员
在子类里的访问权限被重新映射,所以权限变了。这就是继承的全部。
而"完整子对象"的内容,可以用一条公式记死:
父类完整子对象 = 父类所有权限的非静态成员变量 + 父类虚指针 + 内存对齐填充
这条公式贯穿全文:
sizeof(类) = 所有权限的非静态成员 + 可能的虚指针 + 内部对齐填充
也就是说,sizeof(类) 恰好等于完整子对象的空间。
单一继承时,子类内存 = 原有空间 + 父类子对象空间;
三种继承方式,改变的只是权限,内存一点都不会少;
私有的非静态成员同样在子类里面,只是代码访问不到。
抓住"内存"与"权限"这两条线,继承就再也不会答乱。
一、类的继承本质是什么
父类就是基类意思,派生类就是子类意思
是多了内存和访问权限,改变父类在子类的权限
子类会继承父类的完整子对象就多了内存和对父类公有保护成员访问的权限
父类完整子对象 = 父类手写的所有权限的非静态成员变量 +父类虚指针+内存对齐填充 //死记
完整子对象 = 所有权限的非静态成员变量 +虚指针+内存对齐填充 //死记
所以这才是子类真正继承的东西,也是内存真正多的东西,说白了就是多增加了一个sizof父类就是增加了一整个父类
为什么加内存对齐填充,因为子类还多继承了父类内存对齐填充的一些内存
二、类的空间怎么计算
sizeof类=所有权限的非静态成员 + 可能的虚指针 + 内部对齐填充。
刚好所有非静态成员 + 可能的虚指针 + 内部对齐填充。等于完整子对象的内存空间
所以sizeof类=完整子对象的空间
如果是单一继承
子类内存空间大小=原有内存空间大小加父类的非静态成员变量的空间大小+可能存在的虚指针和字节对齐
为什么强调非静态,因为静态的东西不属于任何对象,放在静态区,就不算进类的内存
私有的非静态成员变量也在子类里面,只是因为私有访问不了代码写错报错
子类没有继父类的私有权限
继承说白了就是不同的继承模式,父类的公有保护成员在子类的权限变了
三、类的继承基本语法:
class 子类名:继承方式 父类名
{
//正常书写子类
}
继承方式有三种分别是public,prviate,protected
三种继承方式只是代表一个继承形式,不是代表写了什么只继承什么
|-----------|---------------|------------------|----------------|
| 父类中权限 | public 继承后子类里 | protected 继承后子类里 | private 继承后子类里 |
| public | public | protected | private |
| protected | protected | protected | private |
| private | ❌不可访问 | ❌不可访问 | ❌不可访问 |
1.三种类的继承方式:
public继承:父类的public变成子类的public,父类的protected变成子类的protected
protected 继承:父类的 public、protected,到子类统一变成 protected
private 继承:父类的 public、protected,到子类统一变成 private
父类的私有永远继承不了,每次继承继承父类的公有和保护,选择什么继承只是代表一种方式不是代表只继承这个
公有模式继承(公有和保护)变原本两个,私有模式变全私有,保护模式变全保护
公有继承就原本两个权限不变,私有就两个都变私有,保护就两个都变保护
继承会继承父类的公有和保护同时改变父类的公有和保护在子类的权限,通过子类访问父类就得看继承后的权限
2.类的继承思路:
子类继承后内存得到父类的完整子对象但是子类只能读取公有和保护,读取不了私有的非静态变量,然后父类的公有和保护在子类的权限变了
子类只得到父类的完整子对象,不得到父类的任何函数,甚至是构造函数这些
因为类的函数是独一无二的,不能跑到去地方甚至是子类,但是子类可以访问
子类内存空间大小=原有内存空间大小加父类的非静态成员变量的空间大小+可能存在的虚指针和字节对齐
继承之后,子类里面虽然可以随便访问父类的公有保护,但是父类的公有和保护成员在子类的权限变了
如私有继承,私有继承之后子类里面随便访问父类的公有保护,但是私有继承之后父类的公有保护在子类对外权限
都变成了私有,你就不可能在类外从子类里面直接访问父类的东西,但是访问子类的公有还是可以的
一定记住了,权限一定是对外的
3.继承了后会发生什么?
Son 继承了 Base:
- ✅ 继承的含义:子类得到父类的完整子对象,然后Son 对象里面,可以访问base的公有和私有成员。
- 为了把这份 Base 子对象初始化,编译器自动调用 Base 的构造函数,这是对象组装,不是 Son 里面代码去读取 / 调用 Base 的成员。
- 你的 Son 构造函数内部,确实没有写任何代码去访问 Base 里面的变量或者函数,完全没问题!继承不强制要求子类一定要去访问父类成员。
继承 = ①子类对象内嵌父类子对象(自动触发父类构造) + ②子类拥有访问父类 public/protected 成员的权限。
cpp
#include<iostream>
#include<string>
using namespace std;
class father //father是父类
{
public:
int age;
father()
{
cout<<"父构造函数"<<endl;
}
~father()
{
cout<<"父析构函数"<<endl;
}
};
class son:public father //就在这里,这里只是表面父类和子类的继承关系声明,不是在这里调用父类构造函数
{ //这里是son,子类
public:
int sno;
son()
{
cout<<"子构造函数"<<endl;
}
~son()
{
cout<<"子析构函数"<<endl;
}
};
int main()
{
son s; //创建子类对象后先调用父类构造函数,然后调用子类构造函数,然后子类析构函数,然后父类析构函数
return 0;
}
子类有了protected,protected还是只能在类里面访问,类外面还是访问不了
只是多了一个子类可以在子类里面访问父类的protected
4.类继承后的内存空间
类继承后,两个类的内存空间有没有变化呢?
父类本身的内存大小和地址不会变
类继承后会继承的父类完整子对象,类的空间大小就是完整子对象空间大小就是sizeof类
sizeof父类已经包含父类所有非静态成员 + 父类可能自带虚指针 + 父类内部对齐填充
父类本身地址大小不会变,但是子类不是继承了父类的东西嘛
单一继承内存连续指的是子类继承的东西内存地址和子类本身的东西内存地址是连续的
单一继承
sizeof (子类) = sizeof (父类) + 子类自己新增的非静态成员变量 +子类可能的虚指针+ 内存对齐填充
子类空间大小=父类空间大小加子类原本自己的非静态成员变量加子类可能的虚指针加子类内存对齐填充
多继承
子类空间就多几个父类空间呗
继承后的内存地址连续
继承的是完整子对象,内存连续的也是完整子对象
父类本身地址大小不会变,变的是子类继承父类的东西在子类的内存地址
继承的实质是内存和访问权限,这是我自己总结的
单继承:子类里面继承下来的父类的完整子对象和子类原本的完整子对象内存地址是连续的,父类在前
当然面试的时候要说子列原本完整子对象是子类新增片段
多继承,依旧父在前,多个父就按父继承的顺序
我只是加载这个子类已经存在还没继承,方便我记忆而已
多继承的内存连续就是父类按着继承顺序连续然后子类呗
先继承羊类就先拿到羊类东西,再继承驼类就拿到驼类
内存空间连续就是先羊后驼后子
如
5.类的单一继承:
父类本身地址大小不会变,但是子类不是继承了父类的东西嘛
单一继承内存连续指的是子类继承的东西内存地址和子类本身的东西内存地址是连续的
如:
cpp
#include<iostream>
#include<string>
using namespace std;
class father
{
public:
int age;
};
class son:public father
{
public:
int sno;
};
int main()
{
father s;
son ns;
cout<<"父类的地址是"<<&ns.age<<endl; //父类本身地址大小不会变,但是子类不是继承了父类的东西嘛
//单一继承内存连续指的是子类继承的东西内存地址和子类本身的东西内存地址是连续的
cout<<"子类的地址是"<<&ns.sno<<endl;
return 0;
}
打印结果是
类的地址是0xffffcc04
子类的地址是0xffffcc08
6.类的多继承
如
cpp
#include<iostream>
using namespace std;
class sheep
{
public:
int age;
};
class caml
{
public:
int sno;
};
class sheep_caml:public sheep,public caml //父类的内存连续就是按照继承的顺序
{
public:
int tno;
};
int main()
{
sheep_caml a;
cout<<"a.age的地址是"<<&a.age<<endl;
cout<<"a.sno的地址是"<<&a.sno<<endl;
cout<<"a.tno的地址是"<<&a.tno<<endl;
return 0;
}