前言
內存是计算机中的“脑”吗?CPU才是脑,CPU才是计算机的三魂六魄。但若沒有內存,一切只存在于虚无缥缈间,等同 于不存在。C 有一个很大的优势是用户可以动态分配内存,并加以控制,以达到最优性能。对于一个C 程序员,对内存有深厚的了解将使你的C 功力更上一层楼。而这篇文章则是分享学习侯捷老师的内存管理课的笔记,在此分享,希望各位学习内存管理有所帮助。
测试环境:Clion2018.2 GNU C 14
C Primitives
这一节介绍C 与内存相关的所有组件,包括malloc/free, new/delete, operator new/operator delete, placement new/placement delete,将探讨它们的意义,运作方式和重载方式,并以此开发一个极小的内存池。
对于一个应用程序而言,使用内存的途径有下面几种方式:
看图形知道越往底层调用越费劲,并且高层内部调用低层,这里不会讲述O.S.API,这是操作系统级的API了,太过底层,所讲述的最低只到malloc和free这一层级,对于学过C语言的朋友来说,malloc和free是C语言动态分配内存的两个函数,malloc主管内存分配,free主管内存释放。new和delete是C 语言中动态分配的语法,类属表达式,分配一个对象的内存,相同的还有new[ ]和delete[ ],这也是表达式,不过分配的是对象的数组。::operator new和::operator delete就是C 标准库的函数了,可以自己重载定制。allocator是C 标准库的分配器类,用于容器的参数,有默认分配器。下面的表格是它们的区别:
分配 | 释放 | 类属 | 可否重载 |
---|---|---|---|
malloc() | free() | C函数 | 否 |
new | delete | 表达式 | 否 |
::operator new | ::operator delete | C 函数 | 可以重载 |
allocator::allocate() | allocator::deallocate() | C 标准库 | 可自由设计并搭配任何容器 |
现在来小试牛刀,使用上述的各种动态分配操作:
代码语言:javascript复制#include <iostream>
#include <complex>
#include <memory> //std::allocator
#ifdef __GNUC__
#include <extpool_allocator.h> //欲使用 std::allocator 以外的 allocator, 就得自行 #include <ext/...>
#endif
using namespace std;
namespace jj01
{
void test_primitives()
{
cout << "ntest_primitives().......... n";
void* p1 = malloc(512); //512 bytes
free(p1);
char* p1_c = (char*)malloc(512); //char*指向512字节
free(p1_c);
complex<int>* p2 = new complex<int>; //one object
delete p2;
complex<int>* p2_array = new complex<int>[10]; //10 objects
delete [] p2_array;
void* p3 = ::operator new(512); //512 bytes
::operator delete(p3);
//以下两函数都是 non-static,定要通过 object 调用。以下分配 3 个 ints.
int* p4 = allocator<int>().allocate(3);
allocator<int>().deallocate(p4,3);
#ifdef __GNUC__ //GNU C
//以下两函数都是 non-static,定要通过 object 调用。以下分配 9 个 ints.
int* p5 = __gnu_cxx::__pool_alloc<int>().allocate(9);
__gnu_cxx::__pool_alloc<int>().deallocate(p5,9);
#endif
}
} //namespace
int main(int argc, char** argv)
{
jj01::test_primitives();
return 0;
}
现在来看看new和delete的底层逻辑,对于下面这一行代码:
代码语言:javascript复制complex<int>* pc = new complex<int>(1, 2);
编译器会将它演绎成下面的形式:
代码语言:javascript复制complex* pc;
try{
void* mem = operator new( sizeof(complex) ); //分配内存
pc = static_cast<complex<int>*>(mem); // 指针转型
pc->complex::complex(1, 2); //调用构造函数,注意,只有编译器才可以这样调用构造函数。不过老版本VC6支持。
}
catch( std::bad_alloc ) {
//若分配内存失败,就不执行指针转型以及构造操作
}
对于new表达式来说,编译器会编译成上面三步:分配内存,指针转型,以及构造对象。对于上面的operator new函数,其VC98源代码如下:
代码语言:javascript复制void* operator new(size_t size, const std::nothrow_t&) _THROW0() //第二参数是为了不抛出异常
{
void* p;
while ((p == malloc(size)) == NULL)
{
//buy more memory or return null pointer
_TRY_BEGIN // typedef
if (_callnewh(size) == 0) break;
_CATCH(std::bad_alloc) return NULL;
_CATCH_END // typedef
}
return p;
}
可以看到operator new的内部调用的是malloc分配内存,当分配成功,直接返回指向分配好内存的指针,如果失败,malloc函数返回空指针,进入循环体,这里有两种方式处理,一种是调用_callnewh函数,这个是调用new handler函数(后面会讲,可以自己设定),当内存分配失败,此时说明内存不足,可以在new handler函数中释放掉某些不需要的内存,然后再进入循环判断条件调用malloc,第二种是直接返回空指针。
然后再来看看delete表达式,承接上面的new代码,当我们写出:
代码语言:javascript复制delete pc;
编译器会将它转为下面的程序:
代码语言:javascript复制pc->~complex(); //先析构 与上面通过指针调用构造函数不同,可以这样调用析构函数
operator delete(pc); //然后释放内存
对于一个complex对象而言,complex内部没有指针,也就是它的内部没有指向其他内存,析构函数什么都不会做,operator delete(pc) 则是释放pc所指向的内存,也就是动态分配的complex对象。
对于operator delete函数,它的VC98源码如下:
代码语言:javascript复制void _cdecl operator delete(void* p) _THROW0()
{
free(p);
}
可以看到operator delete内部调用就是free函数,直接释放内存。
通过上面的讲述,知道了malloc/free,new/delete以及operator new/operator delete的关系,在这里小结一下。new表达式内部会调用operator new,而operator new内部则会调用malloc,通过malloc分配内存,然后对指针转型,最后调用构造函数。而delete表达式则是先调用对象的析构函数,确保先将对象(如string)内部的动态分配内存在析构中释放,然后再通过operator delete函数,里面调用free函数释放对象的内存。
还剩下array new/array delete和placement new,现在来讲array new和array delete。
分下面两种情况:
代码语言:javascript复制//情况a
complex<int>* pca1 = new complex<int>[3]; //动态分配3个complex的空间,调用三次构造函数
delete [] pca1; // 调用三次析构函数和释放内存
//情况b
string* pca2 = new string[3]; //动态分配3个string(指针)的空间,调用三次构造函数
//对三个string进行赋值...
*pca2 = "fang yang";
*(pca2 1) = "love";
*(pca2 2) = "chun xiao";
delete [] pca2; //调用三次析构函数和释放内存
分两种情况是因为它们的内存情况不同(有简化),如下所示:
对于complex对象而言,析构函数可有可无,因为只需要让free(pca1)就可以达到释放内存的目的,而对于string对象来说,析构函数很重要,因为string内部的指针指向字符串动态分配的内存,需要在析构函数将这部分内存释放,然后再调用free(pca2)释放string内部指针的内存。
对于free(pca1)为什么能够正确释放内存,有一个重要原因,那就是cookie,它的作用是记录内存块的起始位置和终止位置,所以就知道要释放多大的内存块。另外,cookie占用四个字节,上下两个占用8个字节,内存管理其中很重要的一个原因就是减小cookie占用量。可以想想,动态分配一个4个字节的对象,对象内存布局中cookie占用8个字节,并且由于内存对齐问题,分配内存需要是16字节的倍数,这样还需要填充4个字节,对象内存占用率才25%,如果是这样的一百万个对象,那太糟糕!
知道了上面的原理,现在来说说为什么array new和array delete需要搭配使用,不搭配使用就是报错呢?
其实上面已经给出了答案,那就是构造函数和析构函数的次数问题,看看下面的代码:
代码语言:javascript复制string* pca2 = new string[3];
编译器转换为:
代码语言:javascript复制string* pca2[3];
try{
void* mem = operator new[]( sizeof(string) * 3 ); //分配内存
for (int i = 0; i < 3; i ){ //三次转型和构造函数
pca2[i] = static_cast<string*>((mem i)); // 指针转型
pca2[i]->string::string();
}
}
catch( std::bad_alloc ) {
//若分配内存失败,就不执行指针转型以及构造操作
}
同理,对于array delete:
代码语言:javascript复制delete [] pca2;
编译器转换为:
代码语言:javascript复制for (int i = 3 - 1; i <= 0; --i){ //在GNU C 中,是从上往下构造,从下往上析构
pca2[i]->~string();
}
operator delete[](pca2);
如果将delete [] pca2
换成delete pca2
,那么析构函数就只调用了一次(调用一次发生在string没有析构函数,具体原因见下一段话),剩下的两个没有调用,那么当operator delete释放掉三个指针的内存后,其中两个指针当初所指向的内存就悬空了,没有指针指向,发生内存泄漏。
另外,上面的图,complex对象没有3(内置类型也不会有),而string对象有3,这是因为complex没有重要的析构函数(需要释放内存),编译器不会记录3这个长度,而string则需要记录,delete [] pca2
并没有带有3,这是因为当编译器看到[]
,此时pca2没有指向string数组的首地址,而是回跳到3的地址(所以上面一段话中delete pca2其实会报错,多了一个3,内存布局变了),所以就知道析构多少次。
下面的是一个事例,帮助理解:
代码语言:javascript复制namespace jj02
{
class A
{
public:
int id;
A() : id(0) { cout << "default ctor. this=" << this << " id=" << id << endl; }
A(int i) : id(i) { cout << "ctor. this=" << this << " id=" << id << endl; }
~A() { cout << "dtor. this=" << this << " id=" << id << endl; }
};
void test_call_ctor_directly()
{
int size = 3;
A* buf = new A[size]; //动态分配调用default ctor 3 次. [0]先于[1]先于[2])
//A必须有 default ctor, 否则 [Error] no matching function for call to 'jj02::A::A()'
A* tmp = buf;
cout << "buf=" << buf << " tmp=" << tmp << endl;
for(int i = 0; i < size; i)
new (tmp ) A(i); //placement new, 3次 ctor
cout << "buf=" << buf << " tmp=" << tmp << endl;
delete [] buf; //dtor three times (次序逆反, [2]先于[1]先于[0])
}
} //namespace
int main(int argc, char** argv)
{
jj02::test_call_ctor_directly();
return 0;
}
程序输出如下:
代码语言:javascript复制default ctor. this=0xf91258 id=0
default ctor. this=0xf9125c id=0
default ctor. this=0xf91260 id=0
buf=0xf91258 tmp=0xf91258
ctor. this=0xf91258 id=0
ctor. this=0xf9125c id=1
ctor. this=0xf91260 id=2
buf=0xf91258 tmp=0xf91264
dtor. this=0xf91260 id=2
dtor. this=0xf9125c id=1
dtor. this=0xf91258 id=0
上面的程序出现了placement new, 现在就进入placement new和placement delete。
如果operator new接受的参数除了一定会有的size_t,还有其他参数,这就是placement new。众多placement new版本特别有用的一个是接受一个指针让对象构建在该指针所分配的已分配内存当中,就像上面程序中的new (tmp ) A(i)
, tmp是指向已分配内存的指针,A(i)则是在已分配内存当中执行构造函数,这也是最早的placement new,该版本也已纳入C 标准库。
《Effective C 》第52条款指出写了placement new,也要写placement delete,placement delete用来处理placement new中分配内存成功,但构造失败的情况,此时如果不释放内存,就会发生内存泄漏,但侯捷老师在G2.9测试确实会调用,但新版本的G4.9却不会调用。
现在来看看上述的placement new到底做了什么,有以下程序:
代码语言:javascript复制char* buf = new char[sizeof(complex<int>) * 3]; //分配三个
complex<int>* pc = new(buf) complex<int>(1, 2);
//...
delete [] buf;
对于complex* pc = new(buf) complex(1, 2)
这句话,编译器会转换为下面代码:
complex* pc;
void* mem = operator new( sizeof(complex<int>), buf ); //直接返回buf指针
pc = static_cast<complex<int>*> (mem); //指针转型
pc->complex::complex(1, 2); //调用构造函数
对于上面的operator new,就和之前的operator new不太一样了,它现在多了第二参数,它的代码如下:
代码语言:javascript复制void* operator new(size_t, void* loc)
{
return loc;
}
可以看到直接返回buf指针,啥都没做,所以placement new做的事有两件,一件转型,一件调用构造函数。
现在来梳理一下前面的,谈谈作用,如下图所示:
在应用程序中的内存分配表达式是不可变的,而且无法重载,这部分是不可以变的,编译器会将表达式解析为中间部分,调用operator new函数,placement new函数以及operator delete函数,这时候就有两条路了,一条是让编译器调用全局函数,另一条就是让编译器调用重载对象的动态分配成员函数,而内存管理就是要让编译器走重载的这条路,构造适合当前对象的内存池,减小内存消耗以及多次内存分配所带来的时间开销。
上面说的是重载对象的动态分配成员函数,那么是否可以在全局直接重载全局函数,这是可以的,但不推荐这么做,影响无比深远,所有的动态分配内存都将改变。
下面通过一个例子来看看如何重载,代码如下:
代码语言:javascript复制#include <cstddef>
#include <iostream>
#include <string>
using namespace std;
namespace jj06
{
class Foo
{
public:
int _id;
long _data;
public:
static void* operator new(size_t size);
static void operator delete(void* deadObject, size_t size);
static void* operator new[](size_t size);
static void operator delete[](void* deadObject, size_t size);
static void* operator new(size_t size, void* loc); //placement new
Foo() : _id(0) { cout << "default ctor. this=" << this << " id=" << _id << endl; }
Foo(int i) : _id(i) { cout << "ctor. this=" << this << " id=" << _id << endl; }
~Foo() { cout << "dtor. this=" << this << " id=" << _id << endl; }
};
void* Foo::operator new(size_t size)
{
Foo* p = (Foo*)malloc(size);
cout << "Foo::operator new(), size=" << size << "t return: " << p << endl;
return p;
}
void Foo::operator delete(void* pdead, size_t size)
{
cout << "Foo::operator delete(), pdead= " << pdead << " size= " << size << endl;
free(pdead);
}
void* Foo::operator new[](size_t size)
{
Foo* p = (Foo*)malloc(size);
cout << "Foo::operator new[](), size=" << size << "t return: " << p << endl;
return p;
}
void Foo::operator delete[](void* pdead, size_t size)
{
cout << "Foo::operator delete[](), pdead= " << pdead << " size= " << size << endl;
free(pdead);
}
void* Foo::operator new(size_t size, void* loc)
{
cout << "Foo::placement new(), p= " << loc << " size= " << size << endl;
return loc;
}
void test_overload_operator_new_and_array_new()
{
cout << "ntest_overload_operator_new_and_array_new().......... n";
cout << "sizeof(Foo)= " << sizeof(Foo) << endl;
{
Foo *p = new Foo(7); //Foo::operator new
delete p; //Foo::operator delete
Foo *pArray = new Foo[3]; //Foo::operator new[], 无法给elements 以 initializer
delete[] pArray; //Foo::operator delete[]
}
{
cout << "ntest_overload_placement_new().......... n";
Foo* p1 = new Foo; //Foo::operator new
new(p1) Foo(7); //Foo replacement new
delete p1; //Foo::operator delete
}
{
cout << "ntesting global expression ::new and ::new[] n";
// 这会绕过 overloaded new(), delete(), new[](), delete[]()
Foo* p = ::new Foo(7);
::delete p;
Foo* pArray = ::new Foo[3];
::delete [] pArray;
}
}
} //namespace
int main(int argc, char** argv)
{
jj06::test_overload_operator_new_and_array_new();
return 0;
}
运行结果如下:
代码语言:javascript复制test_overload_operator_new_and_array_new()..........
sizeof(Foo)= 8
Foo::operator new(), size=8 return: 0x1b1250
ctor. this=0x1b1250 id=7
dtor. this=0x1b1250 id=7
Foo::operator delete(), pdead= 0x1b1250 size= 8
Foo::operator new[](), size=32 return: 0x1b6fa0
default ctor. this=0x1b6fa8 id=0
default ctor. this=0x1b6fb0 id=0
default ctor. this=0x1b6fb8 id=0
dtor. this=0x1b6fb8 id=0
dtor. this=0x1b6fb0 id=0
dtor. this=0x1b6fa8 id=0
Foo::operator delete[](), pdead= 0x1b6fa0 size= 32
test_overload_placement_new()..........
Foo::operator new(), size=8 return: 0x1b1250
default ctor. this=0x1b1250 id=0
Foo::replacement new(), p= 0x1b1250 size= 8
ctor. this=0x1b1250 id=7
dtor. this=0x1b1250 id=7
Foo::operator delete(), pdead= 0x1b1250 size= 8
testing global expression ::new and ::new[]
ctor. this=0x1b1250 id=7
dtor. this=0x1b1250 id=7
default ctor. this=0x1b6fa8 id=0
default ctor. this=0x1b6fb0 id=0
default ctor. this=0x1b6fb8 id=0
dtor. this=0x1b6fb8 id=0
dtor. this=0x1b6fb0 id=0
dtor. this=0x1b6fa8 id=0
placement new可以有很多个版本,前提是每个版本的声明都必须有独特的参数列,且其中第一参数必须是size_t, 其余参数以new所指定的placement arguments为初值,出现于new(…..)小括号内的便是placement arguments。就像上面的new(p1), p1为重载placement new的第二参数,需要注意的是,上面重载的是标准库placement new,并不是新的placement new。
下面的是一个placement new和placement delete的事例:
代码语言:javascript复制#include <iostream>
using namespace std;
namespace jj07
{
class Foo
{
public:
Foo() { cout << "Foo::Foo()" << endl; }
Foo(int) {
cout << "Foo::Foo(int)" << endl;
}
//(1) 这个就是一般的 operator new() 的重载
void* operator new(size_t size) {
cout << "operator new(size_t size), size= " << size << endl;
return malloc(size);
}
//(2) 这个就是标准库已经提供的 placement new()
void* operator new(size_t size, void* start) {
cout << "operator new(size_t size, void* start), size= " << size << " start= " << start << endl;
return start;
}
//(3) 这个才是崭新的 placement new
void* operator new(size_t size, long extra) {
cout << "operator new(size_t size, long extra) " << size << ' ' << extra << endl;
return malloc(size extra);
}
//(4) 这又是一个 placement new
void* operator new(size_t size, long extra, char init) {
cout << "operator new(size_t size, long extra, char init) " << size << ' ' << extra << ' ' << init << endl;
return malloc(size extra);
}
//以下是搭配上述 placement new 的各个placement delete.
//当 ctor 发出异常,这儿对应的 placement delete 就会被唤起.
//应该是要负责释放其搭档兄弟 (placement new) 分配所得的 memory.
//但其实新版本并没有调用placement delete
//(1) 这个就是一般的 operator delete() 的重载
void operator delete(void*,size_t)
{ cout << "operator delete(void*,size_t) " << endl; }
//(2) 这是对应上述的 (2)
void operator delete(void*,void*)
{ cout << "operator delete(void*,void*) " << endl; }
//(3) 这是对应上述的 (3)
void operator delete(void*,long)
{ cout << "operator delete(void*,long) " << endl; }
//(4) 这是对应上述的 (4)
//如果没有一一对应, 也不会有任何编译报错
void operator delete(void*,long,char)
{ cout << "operator delete(void*,long,char) " << endl; }
private:
int m_i;
};
void test_overload_placement_new()
{
cout << "ntest_overload_placement_new().......... n";
Foo start; //Foo::Foo
Foo* p1 = new Foo; //op-new(size_t)
Foo* p2 = new (&start) Foo; //op-new(size_t,void*), 在栈内存构造对象
Foo* p3 = new (100) Foo; //op-new(size_t,long)
Foo* p4 = new (100,'a') Foo; //op-new(size_t,long,char)
Foo* p5 = new (100) Foo(1); //op-new(size_t,long) op-del(void*,long)
Foo* p6 = new (100,'a') Foo(1);
Foo* p7 = new (&start) Foo(1);
Foo* p8 = new Foo(1);
}
}
int main(int argc, char** argv)
{
jj07::test_overload_placement_new();
return 0;
}
运行结果如下:
代码语言:javascript复制test_overload_placement_new()..........
Foo::Foo()
operator new(size_t size), size= 4
Foo::Foo()
operator new(size_t size, void* start), size= 4 start= 0x61fd8c
Foo::Foo()
operator new(size_t size, long extra) 4 100
Foo::Foo()
operator new(size_t size, long extra, char init) 4 100 a
Foo::Foo()
operator new(size_t size, long extra) 4 100
Foo::Foo(int)
operator new(size_t size, long extra, char init) 4 100 a
Foo::Foo(int)
operator new(size_t size, void* start), size= 4 start= 0x61fd8c
Foo::Foo(int)
operator new(size_t size), size= 4
Foo::Foo(int)
下面讲述per-class allocator,也就是重载类的new和delete,构造内存池。
代码语言:javascript复制#include <iostream>
using namespace std;
namespace jj04
{
//ref. C Primer 3/e, p.765
//per-class allocator
class Screen {
public:
Screen(int x) : i(x) { };
int get() { return i; }
void* operator new(size_t);
void operator delete(void*, size_t);
private:
Screen* next;
static Screen* freeStore;
static const int screenChunk;
private:
int i;
};
Screen* Screen::freeStore = 0;
const int Screen::screenChunk = 24;
void* Screen::operator new(size_t size)
{
Screen *p;
if (!freeStore) {
//linked list 是空的,所以攫取一大块 memory
//以下呼叫的是 global operator new
size_t chunk = screenChunk * size;
freeStore = p =
reinterpret_cast<Screen*>(new char[chunk]);
//将分配得来的一大块 memory 当做 linked list 般小块小块串接起来
for (; p != &freeStore[screenChunk-1]; p)
p->next = p 1;
p->next = 0;
}
p = freeStore;
freeStore = freeStore->next;
return p;
}
void Screen::operator delete(void *p, size_t)
{
//将 deleted object 收回插入 free list 前端
(static_cast<Screen*>(p))->next = freeStore;
freeStore = static_cast<Screen*>(p);
}
//-------------
void test_per_class_allocator_1()
{
cout << "ntest_per_class_allocator_1().......... n";
cout << sizeof(Screen) << endl; //8
size_t const N = 100;
Screen* p[N];
for (int i=0; i< N; i)
p[i] = new Screen(i);
//输出前 10 个 pointers, 用以比较其间隔
for (int i=0; i< 10; i)
cout << p[i] << endl;
for (int i=0; i< N; i)
delete p[i];
}
} //namespace
int main(int argc, char** argv)
{
jj04::test_per_class_allocator_1();
return 0;
}
上面的类重载了new和delete,所以当使用new表达式动态分配Screen对象的时候,会调用重载的成员函数,如果freestore指针为空,说明没有多余内存给Screen使用,需要再分配一大块内存(24个Screen的大小),然后利用Screen类的next指针将内存串起来。串起来后,下一次new,freestore指针不为空,freestore所指向的位置就是对象构建的位置。依次这样,就可以达到一次分配,24次构造。
运行结果如下,可以看到对象的大小为16,而每个下面的内存间隔也是16,对象是连续的。
代码语言:javascript复制test_per_class_allocator_1()..........
16
0xef1590
0xef15a0
0xef15b0
0xef15c0
0xef15d0
0xef15e0
0xef15f0
0xef1600
0xef1610
0xef1620
下面的是不重载new和delete。
代码语言:javascript复制#include <iostream>
using namespace std;
namespace jj04
{
class Screen {
public:
Screen(int x) : i(x) { };
int get() { return i; }
private:
Screen* next;
private:
int i;
};
void test_per_class_allocator_1()
{
cout << "ntest_per_class_allocator_1().......... n";
cout << sizeof(Screen) << endl; //8
size_t const N = 100;
Screen* p[N];
for (int i=0; i< N; i)
p[i] = new Screen(i);
//输出前 10 个 pointers, 用以比较其间隔
for (int i=0; i< 10; i)
cout << p[i] << endl;
for (int i=0; i< N; i)
delete p[i];
}
} //namespace
int main(int argc, char** argv)
{
jj04::test_per_class_allocator_1();
return 0;
}
运行结果如下,没有重载new和delete,调用的就是全局的new和delete,可以看到有些对象的内存是不连续的,例如第一个和第二个,后面几个的内存是连续的,但可以看到,间隔不是16,而是32,这是因为cookie导致的。
代码语言:javascript复制test_per_class_allocator_1()..........
16
0xe61250
0xe66fa0
0xe61590
0xe615b0
0xe615d0
0xe615f0
0xe61610
0xe61630
0xe61650
0xe61670