C & C++ 动态分配内存函数的测试

测试环境

  • CPU: Intel x64 CPU
  • OS: macOS Sierra
  • Compiler: GNU GCC 6.3.0

预备知识

  1. 区别零值指针和空指针
    零值指针,是值是0的指针,可以是任何一种指针类型,可以是通用变体类型void*,也可以是char*int*等等。
    空指针,其实空指针只是一种编程概念,就如一个容器可能有空和非空两种基本状态,而在非空时可能里面存储了一个数值是0,因此空指针是人为认为的指针不提供任何地址讯息。

  2. void*类型
    void*表示未确定类型的指针,它可以指向任何类型的数据,更明确的说是指申请内存空间时还不知道用户是用这段空间来存储什么类型的数据(比如是char, int或者其他数据类型)。

  3. 由C/C++编译的程序占用的内存分为几个部分:

    • 栈区(stack sagment):由编译器自动分配释放,存放函数的参数的值,局部变量的值等。在 Windows 下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows 下,栈的大小是 1MB,如果申请的空间超过栈的剩余空间时,将提示stack overflow。因此,能从栈获得的空间较小。
    • 堆区(heap sagment) : 一般由程序员分配释放,若程序员不释放,程序结束时可能由系统回收 。它与数据结构中的堆是两回事。堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
    • 全局区(静态区)(data sagment):全局变量和静态变量的存储区域是在一起的,程序结束后由系统释放。数据区的大小由系统限定,一般很大,Windows 32 位操作系统下可以达到 2GB,因此不会溢出。32 位操作系统的地址空间为 4GB,但是留给程序的只是 2GB,因为另外的 2GB 留给操作系统自用。Windows Server 2003 可以支持的全局变量空间达到 3GB。
    • 文字常量区:常量字符串就是放在这里的, 程序结束后由系统释放。
    • 程序代码区:存放函数体的二进制代码。

利用new函数进行动态内存分配

实现动态一维数组

int main() {
int *a, n = 10;
a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = i;
cout << a[i] << ' ';
}
return 0;
}

输出:

0 1 2 3 4 5 6 7 8 9

实现动态二维数组

int main() {
int **a, n, m;
cin >> m >> n;
a = new int*[m];
for (int i = 0; i < m; i++)
a[i] = new int[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
a[i][j] = i + j;
cout << a[i][j] << ' ';
}
cout << endl;
}
return 0;
}

输入:

3 5

输出:

0 1 2 3 4
1 2 3 4 5
2 3 4 5 6

实现动态三维数组

int main() {
int ***a, x, y, z;
cin >> x >> y >> z;
a = new int**[x];
for (int i = 0; i < x; i++) {
a[i] = new int*[y];
for (int j = 0; j < y; j++)
a[i][j] = new int[z];
}
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
for (int k = 0; k < z; k++) {
a[i][j][k] = i + j + k;
cout << a[i][j][k] << ' ';
}
cout << endl;
}
cout << endl;
}
return 0;
}

输入:

3 2 4

输出:

0 1 2 3
1 2 3 4

1 2 3 4
2 3 4 5

2 3 4 5
3 4 5 6

注意事项

1:悬垂指针
执行完delete p语句后,p变成了不确定的指针,在很多机器上,尽管p值没有明确定义,但仍然存放了它之前所指对象的地址,因为p所指向的内存已经被释放了,所以p不再有效。此时,该指针变成了悬垂指针(悬垂指针指向曾经存放对象的内存,但该对象已经不存在了)。悬垂指针往往导致程序错误,而且很难检测出来。一旦删除了指针所指的对象,立即将指针置为NULL,这样就非常清楚的指明指针不再指向任何对象。

delete p;
p = NULL;

2:内存泄露
对于一个已经被分配了空间的指针p,在没有delete p时即给其赋值p = NULL,则可能会造成内存泄露。(p曾经指向的数据再也访问不到了,但依然存在于内存中)

int *p = new int;
*p = 1;
p = NULL;//p指向的1再也访问不到了

new在分配失败时的反应(利用内存泄露测试)

当内存不够会出现内存不足的情况,C++ 提供了两中报告方式:

  1. 抛出bad_alloc异常来报告分配失败
int main() {
int *a;
while (1) {
a = new int[100000];
if (a == NULL) {
cout << "Error!\n";
return 0;
} else {
cout << "Successfully allocate memory at address 0x" << a << endl;
a = NULL;
}
}
return 0;
}

注意:标准C++ p lain new失败后抛出标准异常std::bad_alloc而非返回NULL,因此检查返回值是否为NULL判断分配是否成功是徒劳的。

  1. 返回空指针,而不会抛出异常。

nothrow new/delete不抛出异常的运算符new的形式,失败时返回NULL

将上一段代码中的 a = new int[100000] 修改为 a = new(nothrow) int[100000],运行结果如下:

C++ 为什么会采用这两种方式呢?这主要是由于各大编译器公司设计 C++ 编译器公司的结果,因为标准 C++ 是提供了异常机制的。例如,VC++ 6.0 中当new分配内存失败时会返回空指针,而不会抛出异常。而 gcc 的编译器对于 C++ 标准支持比较好,所以当new分配内存失败时会抛出异常。

究竟为什么会出现这种情况呢?

首先,C++ 是在C语言的基础之上发展而来,而且 C++ 发明时是想尽可能的与 C 语言兼容。而 C 语言是一种没有异常机制的语言,所以 C++ 应该会提供一种没有异常机制的 new 分配内存失败报告机制。(确实是如此,早期的 C++ 还没有加入异常机制)
1993年前, C++ 一直要求在内存分配失败时new要返回0,现在则是要求new抛出std::bad_alloc异常。很多 C++ 程序是在编译器开始支持新规范前写的。 C++ 标准委员会不想放弃那些已有的遵循返回 0 规范的代码,所以他们提供了另外形式的 new(以及 new[])以继续提供返回 0 功能。这些形式被称为“无抛出”,因为他们没用过一个 throw ,而是在使用 new 的入口点采用了 nothrow 对象.

利用malloc函数进行动态内存分配

函数定义(需要包含malloc.h):

extern void *malloc(unsigned int num_bytes);

功能:分配长度为num_bytes字节的内存块

返回值:如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。函数返回的指针一定要适当对齐,使其可以用于任何数据对象。

在规范的程序中我们有必要按照这样的格式去使用malloc()free():

int main() {
type *p;
if(NULL == (p = (type*)malloc(sizeof(type)))) {
perror("error...");
exit(1);
}
free(p); p = NULL;
return 0;
}

以实现动态二维数组为例:

int main() {
int **a, n, m;
cin >> m >> n;
a = (int**)malloc(sizeof(int*) * m);
for (int i = 0; i < m; i++)
a[i] = (int*)malloc(sizeof(int*) * n);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
a[i][j] = i + j;
cout << a[i][j] << ' ';
}
cout << endl;
}
return 0;
}

输入:

2 4

输出:

0 1 2 3
1 2 3 4

malloc失败时,返回空指针NULL

注意事项

  1. 申请了内存空间后,必须检查是否分配成功。
  2. 当不需要再使用申请的内存时,记得释放;释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。
  3. 这两个函数应该是配对。如果申请后不释放就是内存泄露,如果无故释放那就是什么也没有做。释放只能一次,如果释放两次及两次以上会出现错误。(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)

malloc 从哪里获得了内存空间?

答案是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

mallocnew 的区别和联系

区别

  • malloc()/free()是C/C++语言的标准库函数,要库文件支持,new/delete是C++的运算符,不需要库文件支持。
  • 对于用户自定义的对象而言,用malloc()/free()无法满足动态管理对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc()/free()是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于它们。
  • new自动计算需要分配的空间,而 malloc()需要手工计算字节数。
  • new是类型安全的,而malloc()不是,比如:
int* p = new float[2]; //编译时指出错误
int* p = malloc(2 * sizeof(float)); //编译时无法指出错误
  • new由两步构成,分别是newconstructnew可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc()无能为力。

联系

既然new/delete的功能完全覆盖了malloc()/free(),为什么C++还保留它们呢?因为C++程序经常要调用C函数,而C程序只能用malloc()/free()管理动态内存。如果用free()释放 new创建的动态对象,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放 malloc()申请的动态内存,理论上讲程序不会出错,但是该程序的可读性很差,所以必须配对使用。