前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++内存管理

C++内存管理

作者头像
devi
发布2021-08-18 10:16:51
5130
发布2021-08-18 10:16:51
举报
文章被收录于专栏:搬砖记录

本文为学习侯捷老师的C++内存管理机制的笔记。

0. 常见内存错误

修改常量

代码语言:javascript
复制
char a[] = "123"; 
char* b = "123";  

a[0] = 'X';
b[0] = 'X'; // !!错误

所有的字符串在常量区,而数组的形式,是将常量区中的字符串拷贝到数组中,因此可以修改。 指针是直接指向常量区,因此不可修改。

b[0] = 'X’试图修改常量区的内容,因此错误。

再看下面这个例子:

代码语言:javascript
复制
char* test(){
	char a[] = "hello";
	return a;
}

上面这个函数,是数组拷贝了常量区的字符串,因此返回之后,实际拷贝的字符串已经被释放,最终导致拿到的是空指针。

代码语言:javascript
复制
char* test(){
	char* a = "hello";
	return a;
}

由于指针a指向常量区的字符串,因此最终能够读取到“hello”

字符串赋值

代码语言:javascript
复制
    char a[] = "hello";
    char b[10];
//    b = a; // 错误
    strcpy(b,a);

数组的大小 当数组作为参数传递的时候,在函数内部永远是占用指针大小

代码语言:javascript
复制
void test(char a[100]){
	sizeof(a);// 64位=8
	char b[] = "hello";
	sizeof(b); // 不是参数,大小为字符串大小=6
}

修改指针

代码语言:javascript
复制
void test(char* p){
	p = malloc(sizeof(char)*10);
}

void main(){
	char* p = nullptr;
	test(p);
	strcpy(p,"hello");
}

C总是为函数形参创建一份副本,对于指针p其实在test函数里面是临时变量_p,分配的内容只是给了临时变量,无法改变真正的p指针,这一块内存属于泄露。

这就跟swap传值一样,想要改变什么,就传什么的指针想要改变指针,就得传指针的指针。

代码语言:javascript
复制
void test(char** p){
	*p = malloc(sizeof(char)*10);
}

void main(){
	char* p = nullptr;
	test(&p);
	strcpy(p,"hello");
}

使用默认的拷贝构造和拷贝赋值 个人建议,如果没有重写拷贝构造或拷贝赋值,就将其delete,不要使用默认的。 类中有指针(有new操作)的情况下,一定要重写上述的方法(最好是重写big5:构造、拷贝构造、拷贝赋值、析构、move),不然可能有如下状况:

  • 采用默认拷贝构造,会导致两个对象操作同一空间,当某个对象被析构后,该空间可能也被释放了,对于另一个对象来说该空间不可用
  • 传值、返回值都会导致临时对象,临时对象被析构的时候,导致原对象的可用空间被释放。

1. 内存使用的接口等级

在这里插入图片描述
在这里插入图片描述

2. 基本方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

new

  1. 调用::operator new()->调用malloc开辟空间,如果空间不足,则调用用户注册的newhandler(一般用于释放空间),然后再开辟空间
  2. 将malloc开辟空间得到的指针,转为我们new的对象指针
  3. 利用对象指针调用其构造方法

delete

  1. 调用析构
  2. 调用::operator delete()->调用free()释放空间

new ClassName[SIZE]: new数组,会调用SIZE次默认构造函数,如果没有默认构造函数则出错。 需要使用placement new进行赋初值(见下文)。

delete[] arrName

  1. 释放数组所占的所有空间
  2. 调用arrName中所有成员的析构

如果delete arrName(没有加[]),会释放数组所占用的所有空间(而非只有数组头指针),但是只调用一次析构(可能是第一个,也可能是最后一个),因此内存泄露发生在析构函数中,如果本身析构函数无实际操作,那么通过delete释放数组也不会发生内存泄露。

placement new: 语法: ClassName* c = new(pName)ClassName(argv); 表示创建一个ClassName对象(构造函数),放到pName指向的空间中。

代码语言:javascript
复制
// 给数组对象指针赋初值
void test1() {
    A* a = new A[10];
    A* tmp = a;
    for (int i = 0; i < 10; ++i) {
        // placement new : 调用A(i)构造器将对象放入tmp指向的空间中
        new(tmp++)A(i);
    }
    for (int i = 0; i < 10; ++i) {
        std::cout << a[i].a_ << std::endl;
    }
    // 输出 0 1 2 3 4 5 6 7 8 9
}

3. 重载

当我们重载operator new的时候,也要提供对应版本的operator delete(参数列一一对应),当operator new中抛出异常的时候,会调用对应的operator delete。 如果未提供对应的operator delete,则表示放弃处理可能因为异常而导致的分配中断。

注意,operator new和operator delete第一个参数必须为size_t

重载new能够改变new的原本调用路径,让我们能够进行内存管理,比如:系统启动的时候就malloc一大块内存,后续的new,直接从该内存上进行切分,这就减少了malloc的调用次数。


此外,原生的某些new,其实会额外分配内存去存储一些信息,比如std::string会额外分配一个extra大小的内存,new[]会额外分配一组cookie用于存放数组的size等信息。

注意:size传入检查 operator new (size_t size)和operator delete(void*,size_t size)的size是由编译器传入的,但当存在继承的时候,该size不一定等于对象原有大小,此时最好是将new的操作移交给::operator new或::operator delete。

简单内存池(仅提供给某个对象) 在对象内部维护一个链表,重载operator new, new的过程中,从链表上取下内存,若链表上的可用内存不足,就再开辟内容,切分成对象大小的内存块挂载到链表上。

delete就是将内存归还到链表头部。

内存分配器allocator 简单内存池仅提供给某个对象,自然容易想到写一个通用的对象内存分配器,这就是allocator。

4. new handler

当operator new没有能力分配出你所申请的内存(内存不足),就会调用你所注册的new_handler函数

代码语言:javascript
复制
typedef void(*new_handler)();
// 注册函数,该函数返回之前注册的new handler
new_handler set_new_handler(new_handler p) throw();

new handler其实就做两类事情:释放内存、abort或exit()。

5. 嵌入式指针 Embedded pointer

内存池是一个链表,对于每一个内存节点,必然需要一个4字节的next指针,但是这就算额外损耗了。

嵌入式指针能够去掉这个损耗。

假设有这么个obj:

代码语言:javascript
复制
union obj{
	int data;
	union obj* next;
}

data是我们需要存放的数据。 刚开始,内存块未被使用,因此该obj所占内存全被next指针拿去用了; 但是当内存块被使用的时候(分配给data了),此时next所占空间可以看为0,所有内存空间都给data使用了,这就没有任何损耗。 因为内存块分配出去之后,next指针就没用了

上述写法就叫做嵌入式指针

另一种写法:

代码语言:javascript
复制
union obj{
	union obj* next;
}

或者:

代码语言:javascript
复制
struct obj{
	struct obj* next;
}

6. cookie与allocator

malloc(以及底层调用malloc的内存分配操作)会给申请的内存分配冗余的内存块,比如我们malloc(8),实际编译器分配了不止8字节。

其中,malloc会给分配出来的内存块上下都加上一个cookie(各占4字节,共8字节),cookie记录了当前内存块的大小。

  • 首先,对于一根指针,我们无法直观的了解到该指针控制的内存区域大小,因此cookie就是记录这个大小
  • 其次,上下cookie都记录的是同样的值,看似冗余,其实cookie还充当着合并区块时的“辅助标志”。 在内存回收的时候,需要对小区块进行合并。合并过程中,会将当前指针上移8个字节(找到上一块内存的下cookie),检查cookie是否已经被回收(长度是0),如果被回收,就向上合并。随后将当前指针向下移动知道下一块内存的上cookie,进行同样的检查。

但是对于“长度已知”的对象,其实不需要cookie,因此需要通过实现自己的allocator来去除cookie。

因为容器本身就记录着容器内元素的大小,不需要cookie再去记录这个值,因此allocator非常适合容器。

标准库的std::allocator其实都只是采用默认的new/delete(带有cookie),没有特殊设计。

__gun_cxx::__pool_alloc\<className\>不带cookie,其原理如下:

在这里插入图片描述
在这里插入图片描述

alloc持有一个上图所述的链表, 横向链表表示每个对象的所需大小,以4字节为单位,最大128字节,超出128字节就转给malloc处理该次申请。

当程序向alloc申请内存的时候,alloc检查对象的大小,找到该对象所属的链表(向上对齐)。

假设该对象属于SIZE对应的链表, 如果该链表上的内存块用完了(或从未用过),就malloc(20SIZE2)的内存块,并将其中的20*SIZE切分成20份,将第0份返回,头指针指向第1份内存。

剩余的20*SIZE备用。

通过先调用malloc得到一大块内存,然后我们自己进行分配与回收,这就让cookie数量大大减少(cookie数量=malloc调用次数

__gun_cxx::__pool_alloc缺陷:

  • 回收内存资源的时候仅仅是挂回链表,没有做真正的free,这会导致该程序最终持有太多的内存资源,对多任务操作系统不友好
  • 回收内存资源的时候没有检查该内存是否是从本系统分配出去的,这会导致可能不满足8的倍数而造成灾难。这是先天缺陷:因为还回去的原始指针(带cookie)的早就已经丢了,无法还回去。

7. GUN提供的好用的allocator

__gun_cxx::pool_allocator:可查看上文第6部分的讲解 ;

__gun_cxx::bitmap_allocator

  • 如果要求的元素大于1,则转交给::operator new,只提供单次一个元素的内存管理

__gun_cxx::array_allocator:分配一个已知且大小固定的内存块(避免动态扩容),由于大小已知因此是静态数组,因此不需要free。

__gun_cxx::__mt_alloc:多线程环境下使用;

8. 通用的内存管理

  • 使用(双向)链表 来链接内存块
  • 使用嵌入式指针避免指针浪费
  • 采用分段式管理(先拿到一大块内存,然后该内存划分为A个块,A个块再划分为B个块,B个块再划分为C个块…最终操作最小单元的内存块),这样虽然复杂化了管理,但是更利于回收(回收一个小单元是很容易的,但是回收一大块是较难触发的)
  • 用户申请一块x大小的内存,实际上创建了x+N大小的内存块,其中N用于监控管理回收x大小的内存块。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/05/25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 常见内存错误
  • 1. 内存使用的接口等级
  • 2. 基本方法
  • 3. 重载
  • 4. new handler
  • 5. 嵌入式指针 Embedded pointer
  • 6. cookie与allocator
  • 7. GUN提供的好用的allocator
  • 8. 通用的内存管理
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档