跳过正文
P17 内存高级话题
  1. S/
  2. C/C++/
  3. C/
  4. C语言程序设计/

P17 内存高级话题

·3228 字·7 分钟· ·
Li
作者
Li
往前走,别回头!
目录
C++ 新经典笔记 - 这篇文章属于一个选集。

知识点
#

18.1 new、delete 的进一步认识
#

  • @ new与delete深入图 ![[白板/P56 new与delete深入.canvas|new与delete深入]]

18.1.1 总述与回顾
#

[!tip] 回顾

  • new/delete 的基本使用已在 13.4.2 节和 16.2 节讲解
  • 本章从高级角度深入探讨内存相关知识
  • 理解内存高级话题有助于理解模板中的内存分配等概念
  • 实战中常遇到内存池等概念

18.1.2 从 new 说起
#

new 类对象加与不加括号的差别
#

[!tip] new类对象的括号差异

  • new A() vs new A

    • 空类:无区别
    • 有成员变量无构造函数:new A() 成员变量初始化为 0new A 成员变量为随机值
    • 有构造函数:两者相同,初始化工作交给构造函数
  • 简单类型:

    • new int:初值为随机值
    • new int():初值为 0
    • new int(100):初值为 100

new 做了什么事
#

[!tip] new关键字的调用关系

new 关键字主要做了两件事:

  1. 调用 operator new() 分配内存
  2. 调用类的构造函数

完整调用关系:

A *pa = new A();       // 操作符
    operator new();    // 函数
        malloc();      // C风格函数分配内存
    A::A();            // 有构造函数就调用构造函数
  • ! operator new 是一个函数,可以被直接调用:operator new(12);

[!tip] delete关键字的调用关系

delete 关键字的调用关系(注意顺序):

delete pa;
    A::~A();               // 先调用析构函数
    operator delete();     // 函数
        free();            // C风格函数释放内存

new 与 malloc 的区别
#

[!tip] new与malloc区别(面试必答)

  • new 是关键字/操作符,malloc 是函数
  • new 不但分配内存,还会调用类的构造函数
  • new 可以将成员变量初始化为 0malloc 没这个能力
  • delete 不但释放内存,还会调用析构函数;free 只释放内存
  • ! 一般提倡使用 new/delete,不提倡使用 malloc/free(C编程风格)

18.2 new 内存分配细节探秘与重载类内 operator new、delete
#

  • @ 内存分配细节图 ![[白板/P56 new与delete深入.canvas|new与delete深入]]

18.2.1 new 内存分配细节探秘
#

[!tip] malloc分配内存的真相

  • 分配 10 字节内存,实际分配远不止 10 字节
  • 额外内存用于记录:分配字节数、调试信息、边界调整填充、回收内存标记等
  • 在分配的内存首地址前 12 字节位置记录了分配出去的字节数
  • free 释放内存时影响的范围远大于分配的字节数

==程序员拿到的地址是 malloc 分配的整块内存中间的某个位置==

[!tip] 内存分配与释放的合并

  • 释放内存时,free 函数负责将临近的空闲内存块合并
  • 合并空闲块、登记空闲块大小、设置标记等都是 free 的职责
  • 频繁分配小块内存浪费更加严重

18.2.2 重载类中的 operator new 和 operator delete 操作符
#

[!tip] 重载类内operator new/delete

  • 在类中重载 operator newoperator delete 可以取代系统的同名函数
  • 重载后的函数负责分配和释放内存
  • operator new 应为 static 函数(new 对象时还没对象存在)
class A {
public:
    static void *operator new(size_t size);
    static void operator delete(void *phead);
};

void *A::operator new(size_t size) {
    cout << "A::operator new被调用了" << endl;
    A *ppoint = (A *)malloc(size);
    return ppoint;
}

void A::operator delete(void *phead) {
    cout << "A::operator delete被调用了" << endl;
    free(phead);
}
  • ! 调用 ::new A() 使用全局 new,不调用类内重载的 operator new,但仍会调用构造函数

18.2.3 重载类中的 operator new[] 和 operator delete[] 操作符
#

[!tip] 数组操作符重载

  • new A[3]() 调用的是 operator new[],不是 operator new

  • operator new[]operator delete[] 只调用 1

  • 构造函数和析构函数分别调用 3 次(数组大小)

  • 对象数组分配时,额外 4 字节记录数组大小

  • malloc 返回的首地址与程序员拿到的指针相差 4 字节

[!tip] 对象数组内存概貌

| 记录字节数(4B) | 调试信息(~几十B) | 数组大小(4B) | 3个对象(3B) | 边界填充 | 回收标记(4B) |
                                                 ↑ 程序员拿到的指针
  • 多出的 4 字节记录数组元素个数,delete[] 时据此知道调用多少次析构函数

18.3 内存池概念、代码实现和详细分析
#

  • @ 内存池图 ![[白板/P57 内存池.canvas|内存池]]

18.3.1 内存池的概念和实现原理简介
#

[!tip] 内存池概念

内存池: ==用 malloc 申请一大块内存,分配时从大块中一点点分配,用完再申请一大块==

  • 解决的主要问题:减少 malloc 调用次数,减少内存浪费
  • 提高运行效率是顺带的,malloc 本身执行速度极快
  • 实现原理:一次申请一大块,小块分配,用完再申请

18.3.2 针对一个类的内存池实现演示代码
#

[!tip] 内存池实现核心思路

  • 通过重载类内 operator new/operator delete 实现内存池
  • operator new:从空闲链表中取出一块返回;链表为空时一次性 malloc 多块
  • operator delete:不调用 free,而是将内存块链回空闲链表
class A {
public:
    static void *operator new(size_t size);
    static void operator delete(void *phead);
    static int m_iCount;       // 分配计数
    static int m_iMallocCount; // malloc次数
private:
    A *next;                   // 空闲链表指针
    static A *m_FreePosi;      // 指向可分配内存的首地址
    static int m_sTrunkCount;  // 一次分配多少块
};

[!tip] 内存池分配流程

  1. 第一次 newm_FreePosinullptrmalloc 一大块(m_sTrunkCount 块),用 next 指针链成链表
  2. 后续 new:从 m_FreePosi 取出一块,m_FreePosi 指向下一块
  3. 链表用完:再次 malloc 一大块,链入空闲链表
  4. delete:将释放的内存块链回空闲链表头部,m_FreePosi 指向它
  • ! 内存池的 operator delete 并不真正归还内存给系统(不调用 free),而是链回空闲链表
  • ! m_sTrunkCount 不宜设置太大,否则首次分配浪费;几十之间较合适

[!tip] 内存池性能对比

方式500万次分配用时malloc 次数
内存池(每次5块)~500-800ms100万次
内存池(每次500块)~300-500ms1万次
原生 malloc~1000-1500ms500万次
  • malloc 速度本身极快,内存池提升效率有限
  • 内存池主要价值在于==减少内存浪费==

18.3.3 内存池代码后续说明
#

  • % 每 5 个分配的内存地址是挨着的(间隔 4 字节),说明内存池在发挥作用
  • ! 内存池代码不完善:释放时没有真正 delete,只是链回空闲链表
  • ! 程序退出时建议真正释放内存池占用的内存

18.4 嵌入式指针概念及范例、内存池改进版
#

  • @ 嵌入式指针图 ![[白板/P57 内存池.canvas|内存池]]

18.4.1 嵌入式指针
#

[!tip] 嵌入式指针(embedded pointer)

嵌入式指针: ==借用对象所占内存空间的前 4 字节来保存空闲链表指针,省去额外的 next 成员变量==

  • 前提条件:类的 sizeof 必须 >= 4 字节
  • 工作原理:空闲时前 4 字节保存链表指针;分配出去后被对象数据覆盖,无影响
  • 常用于内存池代码实现中

[!tip] 嵌入式指针实现方式

class TestEP {
public:
    int m_i;
    int m_j;
private:
    struct obj {
        struct obj *next;  // 嵌入式指针
    };
};
  • struct obj 定义在类内部(private),限制外部使用
  • next 指针借用对象前 4 字节,空闲时链住空闲块

18.4.2 内存池代码的改进
#

[!tip] 独立内存池类 myallocator

  • 将内存池逻辑提取为独立的 myallocator
  • 使用嵌入式指针替代 next 成员变量
  • 任何类只需声明静态 myallocator 成员即可使用
class myallocator {
public:
    void *allocate(size_t size);   // 分配内存接口
    void deallocate(void *phead);  // 释放内存接口
private:
    struct obj { struct obj *next; }; // 嵌入式指针
    int m_sTrunkCount = 5;
    obj *m_FreePosi = nullptr;
};

[!tip] 使用宏简化内存池声明

#define DECLARE_POOL_ALLOC() \
public: \
    static void *operator new(size_t size) { return myalloc.allocate(size); } \
    static void operator delete(void *phead) { return myalloc.deallocate(phead); } \
    static myallocator myalloc;

#define IMPLEMENT_POOL_ALLOC(classname) \
    myallocator classname::myalloc;

// 使用
class A {
    DECLARE_POOL_ALLOC();
public:
    int m_i;
    int m_j;
};
IMPLEMENT_POOL_ALLOC(A)

18.5 重载全局 new/delete、定位 new 及重载
#

  • @ 定位new图 ![[白板/P58 定位new.canvas|定位new]]

18.5.1 重载全局 operator new 和 operator delete 操作符
#

[!tip] 重载全局operator new/delete

void *operator new(size_t size) { return malloc(size); }
void *operator new[](size_t size) { return malloc(size); }
void operator delete(void *phead) { free(phead); }
void operator delete[](void *phead) { free(phead); }
  • 必须放在全局空间,不能放在自定义命名空间
  • 影响面太广,==很少在实际项目中使用==
  • 类内重载会覆盖全局重载

18.5.2 定位 new(placement new)
#

[!tip] 定位new(placement new)

定位new: ==在已经分配好的原始内存中初始化一个对象(调用构造函数),不额外分配内存==

  • 不分配内存,内存必须事先分配好
  • 只初始化对象(调用构造函数)
  • 没有 placement delete

[!tip] 定位new用法

// 格式:new (地址) 类类型(参数)

// 1. 先分配内存
void *mymemPoint = (void *)new char[sizeof(PLA)];

// 2. 定位new:在已分配内存上构造对象
PLA *pmyAobj1 = new(mymemPoint) PLA();       // 无参构造
PLA *pmyAobj2 = new(mymemPoint2) PLA(12);    // 带参构造

// 3. 手动调用析构函数
pmyAobj1->~PLA();

// 4. 释放原始内存
delete[](void *)pmyAobj1;

[!tip] 定位new的调用关系

PLA *pa = new(分配好的内存首地址) PLA();  // 定位new操作符
    operator new();    // 函数,不调用malloc
    PLA::PLA();        // 调用构造函数
  • 定位new的 operator new 不调用 malloc,不分配内存
  • 可以在类中重载定位new的 operator new,参数比传统 new 多一个

[!tip] 重载定位new的operator new

// 在类PLA中重载
void *operator new(size_t size, void *phead) {
    // 不要分配内存
    return phead;  // 返回传入的内存地址
}

18.5.3 多种版本的 operator new 重载
#

[!tip] 多版本operator new重载

  • 可以重载多个版本的 operator new,只要参数不同
  • 第一个参数固定为 size_t(对象的 sizeof 值)
  • 其他参数通过 new 时指定
// 类中重载
void *operator new(size_t size, int typ1, int typ2) {
    return NULL;
}
void operator delete(void *phead, int typ1, int typ2) {
    return;
}

// 调用
PLA *pla = new(1234, 56) PLA();
  • 这种用法不会调用类的构造函数
  • 主要目的是扩大见闻,不建议常规使用
Reply by Email
C++ 新经典笔记 - 这篇文章属于一个选集。

相关文章