在Windows平台进行C/C++编程,你可能写过或见过N多进行内存分配的方法,常见的如malloc和new,或Windows API中的GlobalAlloc、LocalAlloc、HeapAlloc等等。

那它们各自的含义以及它们之间的区别是什么呢?

总览

  • GlobalAlloc、LocalAlloc、HeapAlloc、VirtualAlloc都是Windows API,都只能在Windows平台使用
  • malloc是C标准库的一部分,依赖于不同平台的libc实现
  • new是运算符,依赖于编译器的实现
  • 他们之前的关系引用MSDN的一张图说明:

​ new调用malloc, malloc调用Heap Memory API(HeapAlloc), 堆函数调用Virtual Memory API(VirtualAlloc)。

GlobalAlloc & LocalAlloc

1
2
3
4
5
6
7
8
DECLSPEC_ALLOCATOR HGLOBAL GlobalAlloc(
[in] UINT uFlags,
[in] SIZE_T dwBytes
);
DECLSPEC_ALLOCATOR HLOCAL LocalAlloc(
[in] UINT uFlags,
[in] SIZE_T uBytes
);

16-bit

GlobalAlloc和LocalAlloc是16位Windows操作系统时代的产物。现在已不推荐使用。

Intel 8086时代,内存采用段式访问,访问本段的数据或指令时,不需要变更段地址(意味着段地址寄存器不需修改)。而访问本段范围以外的数据或指令时,则需要变更段地址(意味着段地址寄存器需要修改)。

因此,在16位处理器环境下,如果访问本段内(64K范围内)地址的值,用一个16位的指针(表示段内偏移)就可以访问到;而要访问本段以外地址的值,则需要用16位的段内偏移+16位的段地址,总共32位的指针。

也就是我们所说的近指针(near pointer)和远指针(far pointer)。

举个例子,如果你有一个近指针p,它的值是0x1234,然后数据段的值为0x012F,则当你写入数据到*p时,你即将访问的地址为012F:1234。

当我们定义指针时,默认是近指针,如果需要定义远指针,需要特别标识。

1
2
char near *p; // default
char far *p;

GlobalAlloc这个函数会分配一个段选择子(selectors),进而通过它来访问你请求的内存地址。你可以使用这个段选择子来声明一个远指针。一个远指针包括一个段选择子和一个近指针。

在16位Windows上,LocalAlloc和GlobalAlloc有着完全不同的行为。LocalAlloc会返回一个近指针,而GlobalAlloc会返回一个段选择子。

32-bit ~

32位Windows操作系统之后,线性内存模型(linear memory model)更广泛的被应用。在这种模式下,应用程序看到的内存是一个单独的连续地址空间。CPU可以直接(且线性)寻址所有可利用的内存位置,无需诉诸任何内存分段或分页机制。

从32位 Windows 开始, GlobalAlloc和LocalAlloc的实现都为HeapAlloc。

1
::HeapAlloc(::GetProcessHeap(), ...);

所以在32位操作系统之后,GlobalAlloc和LocalAlloc几乎没有区别。

HeapAlloc

1
2
3
4
5
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
[in] HANDLE hHeap,
[in] DWORD dwFlags,
[in] SIZE_T dwBytes
);

上面提到的GlobalAlloc和LocalAlloc在32位系统之后都是使用HeapAlloc实现的,且第一个参数都是GetProcessHeap的结果,即进程的缺省堆。

一般情况下,你并不需要使用HeapAlloc,除非你需要自己管理堆,或者避免使用CRT(c-runtime-library)。

VirtualAlloc

1
2
3
4
5
6
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);

这是一个极其底层的Windows API。通常用来分配大块的内存。例如如果想在进程A和进程B之间通过共享内存的方式实现通信,可以使用该函数(这也是较常用的情况)。不要用该函数实现通常情况的内存分配。

它在虚拟地址空间中预定一块内存区域,然后调拨物理存储器。

malloc & new

1
void* malloc( size_t size );

malloc是运行时依赖的,是C标准库的一部分。

1
2
3
4
::(optional) new ( type ) initializer(optional)	(1)	
::(optional) new new-type initializer(optional) (2)
::(optional) new (placement-params) ( type ) initializer(optional) (3)
::(optional) new (placement-params) new-type initializer(optional) (4)

new是编译器依赖和语言依赖的。也就是说,它需要一个编译C++的编译器。通常情况下,它是由malloc实现的,所以也可以说new是运行时依赖的。

上面讲的malloc和new本质上的区别。下面是一些应用场景的区别:

(0)new/delete是运算符,malloc/free是库函数。

(1)申请的内存所在位置

  • new 操作符是从自由存储区(free store)上为对象动态分配内存空间。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。
  • malloc函数是从堆上动态分配内存。堆是操作系统中的术语。

(2)是否需要指定内存大小

  • new不需要,编译器会自行计算。
  • malloc需要。

(3)返回类型

  • new内存分配成功时,返回的是对象类型的指针。
  • malloc返回的是void *。

(4)分配失败

  • new分配失败会抛出bad_alloc异常。
  • malloc分配内存失败返回NULL。

(5)重载

  • new/delete操作符可以重载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Foo {
    public:
    void* operator new(size_t size)
    {
    std::cout << "Overloading new operator" << std::endl;
    void* p = ::operator new(size);
    return p;
    }
    void operator delete(void* p)
    {
    std::cout << "Overloading delete operator" << std::endl;
    }
    };
    int main()
    {
    Foo* foo = new Foo;
    delete foo;
    }
  • malloc/free不允许。

Reference