本文为学习侯捷老师的C++内存管理机制的笔记。
修改常量
char a[] = "123";
char* b = "123";
a[0] = 'X';
b[0] = 'X'; // !!错误
所有的字符串在常量区
,而数组的形式,是将常量区中的字符串拷贝到数组中,因此可以修改。
指针是直接指向常量区,因此不可修改。
b[0] = 'X’试图修改常量区的内容,因此错误。
再看下面这个例子:
char* test(){
char a[] = "hello";
return a;
}
上面这个函数,是数组拷贝了常量区的字符串,因此返回之后,实际拷贝的字符串已经被释放,最终导致拿到的是空指针。
char* test(){
char* a = "hello";
return a;
}
由于指针a指向常量区的字符串,因此最终能够读取到“hello”
字符串赋值
char a[] = "hello";
char b[10];
// b = a; // 错误
strcpy(b,a);
数组的大小 当数组作为参数传递的时候,在函数内部永远是占用指针大小
void test(char a[100]){
sizeof(a);// 64位=8
char b[] = "hello";
sizeof(b); // 不是参数,大小为字符串大小=6
}
修改指针
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传值一样,想要改变什么,就传什么的指针
。
想要改变指针,就得传指针的指针。
void test(char** p){
*p = malloc(sizeof(char)*10);
}
void main(){
char* p = nullptr;
test(&p);
strcpy(p,"hello");
}
使用默认的拷贝构造和拷贝赋值
个人建议,如果没有重写拷贝构造或拷贝赋值,就将其delete,不要使用默认的。
类中有指针(有new操作
)的情况下,一定要重写上述的方法(最好是重写big5:构造、拷贝构造、拷贝赋值、析构、move
),不然可能有如下状况:
new
:
delete
:
new ClassName[SIZE]:
new数组,会调用SIZE次默认构造函数
,如果没有默认构造函数则出错。
需要使用placement new进行赋初值(见下文)。
delete[] arrName
:
如果delete arrName(没有加[]),会释放数组所占用的所有空间(而非只有数组头指针),但是只调用一次析构(可能是第一个,也可能是最后一个),因此内存泄露发生在析构函数中,如果本身析构函数无实际操作,那么通过delete释放数组也不会发生内存泄露。
placement new
:
语法:
ClassName* c = new(pName)ClassName(argv);
表示创建一个ClassName对象(构造函数),放到pName指向的空间中。
// 给数组对象指针赋初值
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
}
当我们重载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。
当operator new没有能力分配出你所申请的内存(内存不足),就会调用你所注册的new_handler
函数
typedef void(*new_handler)();
// 注册函数,该函数返回之前注册的new handler
new_handler set_new_handler(new_handler p) throw();
new handler其实就做两类事情:释放内存、abort或exit()。
内存池是一个链表,对于每一个内存节点,必然需要一个4字节的next指针,但是这就算额外损耗了。
嵌入式指针能够去掉这个损耗。
假设有这么个obj:
union obj{
int data;
union obj* next;
}
data是我们需要存放的数据。 刚开始,内存块未被使用,因此该obj所占内存全被next指针拿去用了; 但是当内存块被使用的时候(分配给data了),此时next所占空间可以看为0,所有内存空间都给data使用了,这就没有任何损耗。 因为内存块分配出去之后,next指针就没用了。
上述写法就叫做嵌入式指针
。
另一种写法:
union obj{
union obj* next;
}
或者:
struct obj{
struct obj* next;
}
malloc(以及底层调用malloc的内存分配操作)会给申请的内存分配冗余的内存块,比如我们malloc(8),实际编译器分配了不止8字节。
其中,malloc会给分配出来的内存块上下都加上一个cookie(各占4字节,共8字节),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缺陷:
这是先天缺陷
:因为还回去的原始指针(带cookie)的早就已经丢了,无法还回去。__gun_cxx::pool_allocator
:可查看上文第6部分的讲解
;
__gun_cxx::bitmap_allocator
:
__gun_cxx::array_allocator
:分配一个已知且大小固定的内存块(避免动态扩容),由于大小已知因此是静态数组
,因此不需要free。
__gun_cxx::__mt_alloc
:多线程环境下使用;