cpp内存管理
内存分布
补充知识:
代码就是在操作内存(如取值写值)
编译器将代码分为三部分写入可执行文件:
各函数代码(因为此时并没有执行)(代码区)
已初始化的全局及静态变量及常量数据(如字符串常量)(数据区)
未初始化的全局及静态变量,内核在加载时清零(BSS)
在将可执行程序转载进内存后成为进程,除去PCB控制块,整个进程虚拟空间如上(32位),除了可执行文件外还有低位区域,栈,共享库地址映射,堆以及高位的内核空间,总共4G空间
- 低位区域:如nullptr,只读
- 栈帧:
- 形参,调用时才会从右到左压进栈顶esp
- 父函数中调用函数的下一条指令,用于返回
- 父函数的栈底ebp
- 函数中的临时变量
- 内核空间:
- 占据最高的1GB(
0xC0000000-0xFFFFFFFF),用户进程无法直接访问,需通过系统调用进入内核态- 注意:内核空间被所有进程共享,但每个进程的页表会映射到同一物理内存,匿名管道其实就是在这块区域开辟了内存,多个进程同时借助系统调用来访问该区域实现通信
- 函数返回值,大小如果
- <=4 eax
- >4&&<=8 eax edx
- >8 产生临时变量带出
类
- 类的核心作用是为编译器提供内存布局规则,同时通过继承和多态实现动态行为
- 类的成员一部分来自自定义,一部分来自继承
成员函数
- 所有的成员函数(包括非静态)都存储在代码区
- 非静态成员函数调用时转换为普通函数,并将当前对象传递为第一个参数this:
obj.func()等价于func(&obj) - 静态成员函数无this指针,只能访问静态成员或其他静态函数
全局静态函数:使用
static修饰的全局符号仅限当前文件访问,避免命名冲突
- 注意区分:隐藏、覆盖和重载
- c++编译器一般情况下会给类添加6个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符 operator=, 对属性进行值拷贝
- 移动构造函数c11
- 移动赋值运算符c11
静态数据成员
- 初始化则存储在全局数据区。
- 没有初始化则在BSS
虚函数表
- 如果类存在虚函数,则在编译阶段就会写入可执行文件的数据区只读数据段.rodata
- 注意虚函数表是属于类的,所有对象共享这张表
- 类可能不止一张虚函数表,比如多个基类都有虚函数表时
- 派生类的虚函数表是拷贝的基类虚函数表,两者并不共享
非静态数据成员
- 这部分是在对象内,在实际内存中看对象是位于哪,如果对象位于全局数据区,则非静态数据成员也是位于全局数据区
- 后续是分析这部分在对象内部的布局
最简单
- 数据的内存分配上基本与结构体无异,也存在内存对齐
- 注意如果对象没有数据成员,则内存大小为1,而不是0;
- 无法在成员函数中使用this定义成员数据,因为对象内存在编译时可能就已经确定,而成员函数调用是在运行时
class A
{
private:
short pri_short_a;
public:
int i_a;
double d_a;
static char ch_a;
void funcA1() {}
};
增加虚函数
通常会在对象添加自己的数据(非继承)之前增加虚函数表指针,指向虚函数表,虚函数表存储着当前对象存在的虚函数(继承或自定义)
- 纯虚函数:
virtual double area() = 0;- 若基类中某成员函数被声明为
virtual,则所有派生类中同名、同参数列表的函数自动成为虚函数,无论是否显式添加virtual关键字
class A
{
private:
short pri_short_a;
public:
int i_a;
double d_a;
static char ch_a;
void funcA1() {}
virtual void funcA2_v();
};
简单继承
- 会将父对象按照继承顺序放前面,最后放上自己的数据
class A
{
public:
int i_a;
static char ch_a;
void funcA1() {}
};
class B : public A
{
public:
int i_b;
void funcB1() {}
};
2. 如果派生类和基类成员名(包括成员函数)相同,派生类会隐藏基类成员,即使成员函数的参数不同
- 直接访问成员
派生对象.成员名会访问到派生类自定义的成员- 若想访问父类的成员,则需要在成员前加限定
派生对象.基类名::成员名- 若想实现成员函数的重载(即调用函数时根据参数选择调用方法),则需要将基类中实现的方法暴露在派生类中:
using 基类::成员名
class Base {
public:
void func(int) { /*...*/ } // 虚函数
};
class Derived : public Base {
public:
using Base::func; // 引入基类函数
void func(double) { /*...*/ }
};
Derived d;
d.func(1); // 调用 Base::func(int)
d.func(1.0); // 调用 Derived::func(double)
继承且存在虚函数
优先放父对象,当放自己数据时,决定是否添加虚函数指针;
- 每个类都是独立的虚函数表,继承会出现虚函数表的拷贝
- 如果基类有虚函数,则基类就是虚函数表指针开头,此时派生类的虚函数表就是这张表,如果有单独的虚函数,则直接添加到这张表上
override显式标记虚函数覆盖,确保派生类函数签名与基类虚函数完全匹配(防止因拼写错误或参数不一致导致意外重载而非覆盖)。final禁止进一步覆盖:标记虚函数或类,阻止派生类覆盖该函数或继承该类。
class A
{
public:
int i_a;
static char ch_a;
void funcA1() {}
virtual void funcA_v1();
virtual void funcA_v2();
};
class B : public A
{
public:
int i_b;
void funcB1() {}
virtual void funcA_v1 override final();
};
- 如果基类没有虚函数,且派生类有单独的虚函数,则先把基类数据写入,写自己的数据时先加上虚函数表指针
class Base {
public: int base_data; // 基类无虚函数,无vptr
};
class Derived : public Base {
public: virtual void foo() {} // 派生类新增虚函数
int derived_data;
};
多继承
- 类可以拥有多张虚函数表
- 派生类中按照其继承的基类的顺序,存放了各个基类的数据成员(基类会包括自己的基类),然后才是自己的数据成员,如果对象起始地址不是虚函数表指针,则在自己数据成员前插入虚表指针
- 如果多个基类存在相同的成员,派生类依然可以使用增加作用域的方式来实现访问(见简单继承)
class A
{
public:
int i_a;
void funcA1() {}
virtual ~A() {}
};
class B
{
public:
int i_b;
void funcB1() {}
virtual ~B() {};
};
class C :public A, public B
{
public:
int i_c;
virtual ~C() {}//这个会存放在A虚表的副本之中
};
如果多个父类有相同的祖父类,则子类中会存在多个相同的祖父类成员,导致冲突,此时就需要虚继承
虚拟继承
- 普通继承基类都是在最前面,虚拟继承基类会放到最后
- 虚拟继承会增加虚基指针,指向虚基类表。表中存储了虚基类对象相对于当前类对象的偏移量;(为了依然能满足多态)
- 虚基类指针通常位于虚函数表指针的之后
- 虚基类表结构:虚基指针vbptr自身的偏移量,从vbptr到共享基类子对象的偏移
- 虚基类成员的实际地址:
对象地址 + vbptr偏移 + 虚基类偏移 + 成员偏移- 调用构造函数顺序:虚基类-->基类(按照声明顺序)-->派生类;最终派生类负责初始化虚基类,中间派生类对虚基类的构造调用会被忽略
- 析构函数调用顺序相反
class B
{
public:
int ib;
public:
B(int i = 1) :ib(i) {}
virtual void f() { cout << "B::f()" << endl; }
virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : virtual public B
{//B1中会增加vbptr,以维持多态
public:
int ib1;
public:
B1(int i = 100) :ib1(i) {}
virtual void f() { cout << "B1::f()" << endl; }
virtual void f1() { cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
class B2 : virtual public B
{
public:
int ib2;
public:
B2(int i = 1000) :ib2(i) {}
virtual void f() { cout << "B2::f()" << endl; }
virtual void f2() { cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
class D : public B1, public B2
{
public:
int id;
public:
D(int i = 10000) :id(i) {}
virtual void f() { cout << "D::f()" << endl; }
virtual void f1() { cout << "D::f1()" << endl; }
virtual void f2() { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
};
多态
基类类型的指针指向派生类,调用被派生类覆盖的函数时,实际调用的是派生类的函数
覆盖要求:
- 基类中该成员函数是虚函数
- 派生类中对应成员函数返回值类型 函数名 参数列表完全一致
class Animal {
public:
virtual void speak() { cout << "Animal sound" << endl; }
};
class Dog : public Animal {
public:
void speak() override { cout << "Woof!" << endl; }
};
Animal* animal = new Dog();
animal->speak(); // 输出 "Woof!"(运行时决定)
实现:
调用虚函数时先去函数表查看函数的入口地址
- 编译器生成代码,通过
animal的vptr找到Dog的虚表。 - 从虚表中取出
speak()的地址并调用。
抽象类
拥有纯虚函数的类为抽象类
- 不能被实例化
- 强制派生类实现所有纯虚函数,否则派生类也是抽象类
虚析构函数
- 也是虚函数,且并不是按照类型名字存储在虚函数表中,故派生类和基类的构造/析构函数存在覆盖
- 基类是虚析构函数,派生类析构函数即使不加virtual默认就是虚析构函数
- 如果派生类没有堆区数据,其实没必要使用虚析构函数
作用
用于确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数,避免资源泄漏。
原理
- 派生类析构函数执行后会自动调用基类析构函数
- 基类指针指向派生类时,调用
delete 基类对象时,会调用析构函数,如果不是基类不是虚析构,则会调用基类的析构函数,而不是子类的,导致子类中自己的数据可能没有得到释放
纯虚虚构函数
虽然可以使用,但是必须得给出函数定义,因为派生析构会调用该析构函数
class base {
public:
virtual ~base() = 0; // 声明为纯虚函数
};
base::~base() { } // 必须提供定义