本文主要是讲解一下C和C++在堆上开辟空间, malloc realloc calloc new delete …如何使用,如何避免内存泄漏
我们来看看C/C++中程序的内存划分
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
我们平时在C语言学习的过程中定义变量,定义数组都是在栈上开辟,有很明显的缺陷,比如想开辟的空间较大,在栈上就非常的不合适,所以我们来看看C语言动态内存开辟和C++动态内存开辟的共同点和区别
C语言提供了好几个动态内存开辟的函数,我们来看看
这个函数就是在堆上申请空间, 参数size指的是开辟空间的字节大小
如果申请成功,返回申请到的空间的首地址
如果申请失败, 返回NULL
返回的参数类型是void* ,因为它并不知道你要如何使用, 所以要强转成你想使用的指针类型
可以看到这样就申请一个int类型的空间
我们在使用的时候,一般都是使用sizeof配合使用
DataType* ptr = (DataType*) malloc (sizeof(DataType) * n );
例如我们想要开一个10个整型的空间
这里要注意的是,malloc开辟的空间是一块连续的空间, 所以可以当做数组使用
在很多编译器中,都需要检查malloc是否开辟成功,否则编译不通过,
总结一下malloc
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针
我们使用malloc在堆上开辟空间了之后,如果我们不使用这块空间, 就要手动释放掉, 这就涉及到了c/c++很难避免的问题叫内存泄漏,那我们如何手动释放一块空间呢, C语言提供了free这个函数来释放
参数就是堆上开辟的地址,free对这块空间进行处理,但是free并不会把这个指针置空,所以我们一般都需要手动置空
我们来使用一下这个函数
这里已经释放完了, 但是arr还是没有被置空, 但我们继续对arr解引用呢??
大家可以试试, 当然这是学习指针的时候就应该注意的野指针问题了,所以一定要置空哦
int main(void)
{
int* arr = (int*)malloc(sizeof(int) * 10);
if (NULL == arr)
{
exit(-1); //表示退出程序
}
//使用空间
free(arr);
arr = NULL;
return 0;
}
大家学习了malloc的使用之后,calloc就比较容易了
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
我们来试一下
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整
void* realloc (void* ptr, size_t size);
realloc开辟空间有两种情况
一种是在原地进行扩容,另一种是原地空间不够了,在异地进行扩容
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:**在堆空间上另找一个合适大小
的连续空间来使用,会把原来空间的内容拷贝过去。这样函数返回的是一个新的内存地址 **
如果realloc第一个参数传入NULL,那么realloc的作用就相当于malloc
对NULL指针进行解引用,读写操作都会造成空指针异常,
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(-1);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
}
在这里我们在执行赋值之后并不会发生错误
可以看到这里没执行free之前是正常的,包括值都正常赋进去了, 为什么可以正常赋值呢?
编译器并不会进行检查下标越界,不管是哪个C编译器一般都不会进行检查越界,因为下标可以作用于任意的指针,不止止是数组名,
C的下标检查需要的开销太大了,编译器必须在程序中插入指令,证实下标表达式的结果所引用的元素和指针属于同一个数组,还需要存储数组的长度和位置…以及自动扩容和动态分配的又该如何, 会非常麻烦,时间和空间会有很多浪费,而且会影响指针的灵活性, 所以更多要求程序员本身注意不要越界
所以这里可以正常赋值, 但是在free的时候就会程序奔溃,因为你想要释放本不属于你的空间
这就本身属于滥用了,但是并不缺乏很多人去这样做,
free函数本身功能就是释放动态开辟的内存, 你如果使用free释放在栈上的空间,就一定会出错
void test()
{
int a = 10;
int* p = &a;
free(p);
}//大家可以自行尝试一下
假如我们开辟了10个整型的空间,那我们想要变成5个,是否可以把后五个释放掉呢?
当然是不可以的,要使用realloc
void test()
{
int* p = (int*)malloc(sizeof(int) * 10);
int* tmp = p;
tmp += 5;
free(tmp);
}
还有两个常见的错误是 对动态开辟的空间进行多次释放
忘记释放,造成内存泄漏 , 这两个大家可以自行尝试一下
malloc 进行空间开辟
calloc = malloc + memset
realloc 动态增容
一般我们需要使用初始化好的空间就用calloc
我们需要使用动态增容, 就使用realloc
一定要注意常见的内存错误, 申请的空间不用了一定要释放, 并且把指针置空,避免野指针
C++是面向对象的语言, 引入了类和对象,所以在C++中我们用对象来说,
首先动态开辟的对象还是在堆上,C++是兼容C的,C语言的内存管理在C++中还可以使用,但是使用起来总是有些不便,而且有的问题解决不了, 所以C++通过new 和delete 进行动态内存管理
这个语法使用还是比较简单的
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3]; //[]中是对象个数
//单个对象使用delete
delete ptr4;
delete ptr5;
//多个对象使用delete[]
delete[] ptr6;
}
在C++中new开辟的数组也可以初始化
操作起来比较简单, 需要注意的就是new 和 delete需要配对使用, new[] 和 delete[]需要配对使用
malloc和free也需要配对使用, 不要混着用, 可能会造成位置错误
有些朋友就会问了,那么new和delete 到底和malloc和free有啥区别吗?
可以说对于 int /double …这种内置类型是一模一样的,但是对于自定义类型来说就不同了
C++创建对象会调用默认构造函数, 对象生命周期结束时也会调用析构函数
那么new和delete有没有调用构造函数和析构函数呢 ,我们来看看
首先定义一个空类,只要构造和析构函数
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
所以new/delete 和 malloc/free最大的区别就是 new和delete 会调用对象的构造函数和析构函数
operator new 和 operator delete 是系统提供的全局函数
new和delete在底层调用的就是operator new 和 operator delete
那么具体是怎么实现的呢,我们来瞅瞅
首先来看一下反汇编
可以看到确实调用了opertor new 和 构造函数, 那么operator new 函数是怎么实现的呢?
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void *p;
//使用malloc开辟空间
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
//抛出异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
既然operator new 调用的也是malloc,为什么还要封装成 operator new 呢?
因为C++是面向对象的语言, C是面向过程的语言, 面向过程的语言返回错误是以错误码的形式来返回,面向对象的语言返回错误通过异常和错误来返回, 所以 使用了operator new ,当然 只是原因之一吧,
operator delete 同理,
我们是可以手动调用operator new 的
可以看到,不会调用构造函数,也不会调用析构函数
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
可以看到是使用_free_dbg来释放空间的
#define free§ _free_dbg(p, _NORMAL_BLOCK)
所以是使用free来释放空间的,
但是delete 是先调用析构函数, 再去 调用operator delete
对于内置类型来说,new delete 与 new[] delete [] 的区别就是new delete 开辟和释放的是单个对象,而
new[] delete []是多个对象 ,与malloc 和free 的差别并不大, 只不过是简化了使用
new在申请失败时会直接抛出异常,而malloc返回nullptr
定位new : 在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
这个一般是配合内存池使用,因为内存池分配出的内存没有初始化,如果是自定义类型的对象,需要调用构造函数
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main(void)
{
A* pa = (A*)malloc(sizeof(A));
//调用定位new,1是初始化列表, 构造函数中的参数
new(pa)A(1);
//我们可以手动调用析构函数
pa->~A();
operator delete(pa);
return 0;
}
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
什么是内存泄漏:
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。
C/C++程序中一般我们关心两种方面的内存泄漏:
内存泄漏会造成非常严重的事故,你想想,服务器程序,一天少一点点,然后程序崩溃宕机, 对于已经上线的项目来说是非常严重的事故
我们随便写一个代码虽然内存泄漏了,但是不会造成事故的原因是: 进程结束后, 系统会自动回收该进程的空间, 而对于长期运行的程序来说,就会造成很大的错误
那么如何避免内存泄漏呢 ?
当然是靠我们自身的设计规范,写代码规范,C++11引入了智能指针,可以极大程度的避免内存泄漏,我们后边再聊
那么文章就到这里结束了,希望大家在C/C++学习过程中,能更好的使用动态内存分配,有想要交流的小伙伴,可以私信我哦