文章目录
1 C语言中的动态内存分配
1.1 动态内存分配的意义
- C语言中的一切操作都是基于内存的。
- 变量和数组都是内存的别名:
- 内存分配由编译器在编译期间决定;
- 定义数组的时候必须指定数组长度;
- 数组长度是在编译期就必须确定的。
我们在程序运行的过程中,可能需要使用一些额外的内存空间。因此就出现了动态内存分配。
1.2 malloc和free
malloc和free用于执行动态内存分配和释放。
注意:malloc和free是库函数,而不是系统调用。 其函数原型如下:
void* malloc(size_t size);
void free(void* pointer);
关于malloc有以下几点需要注意:
- malloc所分配的是一块连续的内存(不会对内存进行初始化)。
- malloc以字节为单位,并且不带任何的类型信息。
- malloc实际分配的内存可能会比请求的多,不能依赖于不同平台下的malloc行为。
- 当请求的动态内存无法满足时(可能内存已耗尽或者依次申请的内存字节数太大)malloc返回NULL。
关于free有如下几点需要注意:
- free用于将动态内存归还系统。
- 当free的参数为NULL时,函数直接返回。
思考:
malloc(0);将返回什么?
分析:malloc(0)是成功的,理论上来说内存是有起始地址和长度这两种属性的。成功的返回值了起始地址,但是长度为0,因此不能使用。但是实际,可能会返回的比申请的多,所以成功分配并且能够使用。如果程序中一直malloc(0)而不free是有可能导致内存泄漏的。
内存泄漏检测模块:
mleak.h:
#ifndef _MLEAK_H_
#define _MLEAK_H_
#include <malloc.h>
#define MALLOC(n) mallocEx(n, __FILE__, __LINE__)
#define FREE(p) freeEx(p)
void* mallocEx(size_t n, const char* file, const line);
void freeEx(void* p);
void PRINT_LEAK_INFO();
#endif
mleak.c:
#include "mleak.h"
#define SIZE 256
/* 动态内存申请参数结构体 */
typedef struct
{
void* pointer;
int size;
const char* file;
int line;
} MItem;
static MItem g_record[SIZE]; /* 记录动态内存申请的操作 */
void* mallocEx(size_t n, const char* file, const line)
{
void* ret = malloc(n); /* 动态内存申请 */
if( ret != NULL )
{
int i = 0;
/* 遍历全局数组,记录此次操作 */
for(i=0; i<SIZE; i++)
{
/* 查找位置 */
if( g_record[i].pointer == NULL )
{
g_record[i].pointer = ret;
g_record[i].size = n;
g_record[i].file = file;
g_record[i].line = line;
break;
}
}
}
return ret;
}
void freeEx(void* p)
{
if( p != NULL )
{
int i = 0;
/* 遍历全局数组,释放内存空间,并清除操作记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer == p )
{
g_record[i].pointer = NULL;
g_record[i].size = 0;
g_record[i].file = NULL;
g_record[i].line = 0;
free(p);
break;
}
}
}
}
void PRINT_LEAK_INFO()
{
int i = 0;
printf("Potential Memory Leak Info:\n");
/* 遍历全局数组,打印未释放的空间记录 */
for(i=0; i<SIZE; i++)
{
if( g_record[i].pointer != NULL )
{
printf("Address: %p, size:%d, Location: %s:%d\n", g_record[i].pointer, g_record[i].size, g_record[i].file, g_record[i].line);
}
}
}
main.c:
#include <stdio.h>
#include "mleak.h"
void f()
{
MALLOC(100);
}
int main()
{
int* p = (int*)MALLOC(3 * sizeof(int));
f();
p[0] = 1;
p[1] = 2;
p[2] = 3;
FREE(p);
PRINT_LEAK_INFO();
return 0;
}
1.3 calloc和realloc
- malloc的同胞兄弟:
- void* calloc(size_t num, size_t size);
- void* realloc(void* pointer, size_t new_size);
- calloc的参数代表所返回内存的内存信息:
- calloc会将返回的内存初始化为0。
- realloc用于修改一个原先已经分配的内存块大小(不会对扩大的内存进行初始化),也就是重置内存大小:
- 在使用realloc之后应使用其返回值;
- 当pointer的第一个参数为NULL,等价于malloc。
2 C++中的动态内存分配
2.1 new和delete
C++中的动态内存分配:
- C++中通过new关键字进行动态内存申请。
- C++中的动态内存申请是基于类型进行的。
- delete关键字用于内存释放。
如:
变量申请:
Type *pointer = new Type;
//……
delete pointer;
数组申请:
Type *pointer = new Type[N];
//……
delete[] pointer; // 如果使用delete会导致内存泄漏
实例分析:C++中的动态内存分配
#include <stdio.h>
int main()
{
int* p = new int;
*p = 5;
*p = *p + 10;
printf("p = %p\n", p);
printf("*p = %d\n", *p);
delete p;
p = new int[10];
for(int i=0; i<10; i++)
{
p[i] = i + 1;
printf("p[%d] = %d\n", i, p[i]);
}
delete[] p;
return 0;
}
new关键字的初始化:
int *pi = new int(1);
float *pf = new float(2.0f);
char *pc = new char(‘c’);
编程实验:初始化动态内存
#include <stdio.h>
int main()
{
int* pi = new int(1);
// int* pa = new int[1];
float* pf = new float(2.0f);
char* pc = new char('c');
printf("*pi = %d\n", *pi);
printf("*pf = %f\n", *pf);
printf("*pc = %c\n", *pc);
delete pi;
delete pf;
delete pc;
return 0;
}
2.2 new、malloc和delete、free的区别
new关键字与malloc函数的区别:
new | malloc |
---|---|
new关键字是C++的一部分,在所有的C++编译器中都被支持 | malloc是由C库提供的函数,在某些系统开发中是不能调用的 |
new以具体的类型为单位进行内存分配 | malloc以字节为单位进行内存分配 |
new在申请单个类型变量时可以进行初始化 | malloc仅根据需要审定定量的内存空间不具备内存初始化的特性 |
new能够触发构造函数的调用 | malloc仅分配需要的内存空间 |
对象的创建只能使用new | malloc不适合面向对象开发 |
delete关键与free函数的区别:
delete | free |
---|---|
delete在所有C++编译器中都被支持 | free在某些系统开发中是不能调用的 |
delete能够触发析构函数的调用 | free仅归还之前分配的空间 |
对象的销毁只能使用delete | free不适合面向对象开发 |
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Test
{
int* mp;
public:
Test()
{
cout << "Test::Test()" << endl;
mp = new int(100);
cout << *mp << endl;
}
~Test()
{
delete mp;
cout << "~Test::Test()" << endl;
}
};
int main()
{
Test* pn = new Test; // 调用构造函数
Test* pm = (Test*)malloc(sizeof(Test)); // 仅分配内存空间,对象未初始化
delete pn; // 调用析构函数
free(pm); // 仅释放所分配的内存空间
return 0;
}
3 动态内存申请的结果
3.1 编译器默认new实现
问题:动态内存申请一定成功吗?
常见的动态内存分配代码:
我们必须知道如下事实:
- malloc函数申请失败时返回NULL值。
- new关键字申请失败时(根据编译器的实现不同而不同):
- 返回NULL值(古老的编译器)。
- 抛出std::bad_allic异常。
问题:new语句中的异常是怎么抛出来的?
我们先来看一下new关键字在C++规范中的标准行为:
在堆空间申请足够大的内存:
- 成功:
- 在获取的空间中调用构造函数创建对象。
- 返回对象的地址。
- 失败:
- 抛出std::bad_alloc异常。
new在分配内存时:
- 如果空间不足,会调用全局的new_handler()函数。
- new_handler()函数中抛出std::bad_alloc异常。
可以自定义new_handler()函数:
- 处理默认的new内存分配失败的情况。
new_handler()的定义和使用:
我们需要知道并不是所有的编译器都有new_handler()函数,比如vc、g++中没有new_handler函数,bcc中是有的。以VC中为例,来分析一下异常是如何抛出的,new操作符的源码如下:
/***
*new.cxx - defines C++ new routine
*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
*Purpose:
* Defines C++ new routine.
*
*******************************************************************************/
#ifdef _SYSCRT
#include <cruntime.h>
#include <crtdbg.h>
#include <malloc.h>
#include <new.h>
#include <stdlib.h>
#include <winheap.h>
#include <rtcsup.h>
#include <internal.h>
void * operator new( size_t cb )
{
void *res;
for (;;) {
// allocate memory block
res = _heap_alloc(cb);
// if successful allocation, return pointer to memory
if (res)
break;
// call installed new handler
if (!_callnewh(cb))
break;
// new handler was successful -- try to allocate again
}
RTCCALLBACK(_RTC_Allocate_hook, (res, cb, 0));
return res;
}
#else /* _SYSCRT */
#include <cstdlib>
#include <new>
_C_LIB_DECL
int __cdecl _callnewh(size_t size) _THROW1(_STD bad_alloc);
_END_C_LIB_DECL
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{ // try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{ // report no memory
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
/*
* Copyright (c) 1992-2002 by P.J. Plauger. ALL RIGHTS RESERVED.
* Consult your license regarding permissions and restrictions.
V3.13:0009 */
#endif /* _SYSCRT */
可以看到两种版本都会调用_callnewh函数,这个函数中会判断new_handler是否可用,可用的话就不会抛出异常,否则会直接抛出异常。
问题:如何跨编译器统一new的行为,提高代码的可移植性?
解决方案如下:
全局范围(不推荐):
- 重新定义new/delete的实现,不抛出任何异常。
- 自定义new_handler()函数,不抛出任何异常。
类层次范围:
- 重载new/delete,不抛出任何异常。
单次使用内存分配:
- 使用notrhow参数,指名new不抛出任何异常。
结论:
- 古老的编译器new失败时才会返回NULL,现代编译器则会抛出bad_alloc异常,只是实现细节不同。
- 不是所有的编译器都遵循C++的标准规范。
- 编译器可能重定义new的实现,并在实现中抛出bad_alloc异常。
- 编译器的默认实现中,可能没有设置全局的new_handler()函数。
- 对于一致性要求较高的代码,需要考虑new的具体细节。
3.2 重载new操作符直接返回NULL分析
g++中如果new返回NULL会继续调用构造函数,可能会导致段错误的发生;bcc和vc中如果new返回NULL不会继续调用构造函数。new后面如果加上了throw(),会告诉编译器如果内存申请失败直接返回NULL,并且不能进行构造函数的调用。
注意:重载返回NULL和编译器默认的new实现返回NULL是两种没有任何关联的行为。
编程实验:动态内存申请
#include <iostream>
#include <new>
#include <cstdlib>
#include <exception>
using namespace std;
class Test
{
int m_value;
public:
Test()
{
cout << "Test()" << endl;
m_value = 0;
}
~Test()
{
cout << "~Test()" << endl;
}
void* operator new (unsigned int size) throw()
{
cout << "operator new: " << size << endl;
// return malloc(size);
return NULL;
}
void operator delete (void* p)
{
cout << "operator delete: " << p << endl;
free(p);
}
void* operator new[] (unsigned int size) throw()
{
cout << "operator new[]: " << size << endl;
// return malloc(size);
return NULL;
}
void operator delete[] (void* p)
{
cout << "operator delete[]: " << p << endl;
free(p);
}
};
void my_new_handler()
{
cout << "void my_new_handler()" << endl;
}
void ex_func_1()
{
new_handler func = set_new_handler(my_new_handler);
try
{
cout << "func = " << func << endl;
if( func )
{
func();
}
}
catch(const bad_alloc&)
{
cout << "catch(const bad_alloc&)" << endl;
}
}
void ex_func_2()
{
Test* pt = new Test();
cout << "pt = " << pt << endl;
delete pt;
pt = new Test[5];
cout << "pt = " << pt << endl;
delete[] pt;
}
void ex_func_3()
{
int* p = new(nothrow) int[10];
// ... ...
delete[] p;
int bb[2] = {0};
struct ST
{
int x;
int y;
};
ST* pt = new(bb) ST();
pt->x = 1;
pt->y = 2;
cout << bb[0] << endl;
cout << bb[1] << endl;
pt->~ST(); // 如果显示的指定了创建对象的内存,就需要显示的调用析构函数
}
int main(int argc, char *argv[])
{
// ex_func_1();
// ex_func_2();
// ex_func_3();
return 0;
}
参考资料: