目录
一.动态内存管理的前置知识
1.栈区
a.栈区的特点
b.注意事项
2.堆区
a.堆区的特点
b.注意事项
3.全局/静态区
a.作用域和生命周期
b.注意事项
4.常量区
二.C语言动态内存管理
1.malloc 函数
a.接口简介与使用实例
b.注意要点
2.calloc 函数:
3.realloc 函数
a.接口简介与使用实例
b.注意要点
4.free 函数
三.C++动态内存管理
1.new 和 delete
a.理解使用实例
b.注意事项
2.运算符重载
a.operator new 与 operator delete函数
b. 定位new 表达式
一.动态内存管理的前置知识
我们在代码中用来存储数据的空间一共有这么几类:栈区、堆区、全局/静态区和常量区。那么,它们分别有什么用?相互间又有何区别?
1.栈区
栈区由编译器自动分配和释放,主要存储局部变量、函数参数等。栈区的内存分配是自动的,且按照后进先出(后定义的变量会先被释放)的原则进行。
a.栈区的特点
①后进先出:栈区的数据结构是一个栈,即只能在一端(栈顶)进行数据的压入(push)和弹出(pop)操作。新加入的数据会被放在栈顶,而最后加入的数据会最先被弹出。
②自动管理内存:栈区的内存分配和释放是由操作系统自动完成的,无需我们手动干预。当函数调用时,系统会为其分配一块栈空间用于存储局部变量和参数等;当函数返回时,系统会自动回收这块栈空间。
③存储效率高:由于栈区的数据结构相对简单,且内存分配和释放是自动完成的,因此栈区的存储效率较高,访问速度也较快。
b.注意事项
①避免栈溢出:栈溢出是指由于栈空间不足而导致的程序崩溃或异常,这一般是由于递归调用过深、局部变量过大或数组越界等问题,所以,在编写代码时,我们应该注意这类问题~~
②注意变量的作用域和生命周期:栈区中的变量具有明确的作用域和生命周期,它们只在定义它们的函数内部可见,并在函数返回时失效。
③避免野指针和内存泄漏:虽然栈区的内存是由系统自动管理的,但我们仍需注意避免野指针和内存泄漏等问题。例如,不要将栈区中的变量地址赋给全局指针或传递给其他函数,以免在变量失效后仍然使用该地址。
2.堆区
堆区是程序运行时用于动态分配内存的区域。与栈区不同,堆区的内存分配不是由编译器自动完成的,而是由程序员根据需要在程序运行时显式地申请和释放。这种动态内存分配方式提供了更大的灵活性,允许程序在运行时根据需要调整内存使用。
a.堆区的特点
堆区的内存通常是不连续的,因为系统使用链表等数据结构来管理空闲内存块。这种不连续性使得堆区的内存分配和释放相对复杂,但也提供了更大的灵活性。
b.注意事项
避免内存泄漏:内存泄漏是指程序在运行时未能正确释放已分配的内存,导致内存占用不断增加。为了避免内存泄漏,我们应确保在不再需要使用堆区上的内存时及时释放它。
避免野指针:野指针是指访问指向无效内存地址的指针。这通常发生在释放内存后未将指针置NULL,然后再次使用该指针。所以,我们应在释放内存后将指针置为NULL。
3.全局/静态区
全局/静态区是内存中用于存储全局变量和静态变量的区域。全局变量是在所有函数外部定义的变量,而静态变量则可以通过在变量前加上 static 关键字来声明,无论是在函数内部还是外部。
a.作用域和生命周期
作用域:全局变量的作用域是整个程序,即它们可以在程序的任何位置被访问和修改。而静态变量仅在定义它们的函数内部可见。
生命周期:全局变量的生命周期从程序开始执行到程序结束。它们在程序启动时被分配内存,并在程序结束时被释放。
b.注意事项
命名冲突:由于全局变量的作用域是整个程序,因此在使用全局变量时需要特别小心,以避免命名冲突。静态全局变量可以帮助缓解这个问题,因为它们的作用域被限制在定义它们的文件内部。
内存管理:虽然全局/静态区的内存管理相对简单(由编译器自动处理),但也需要注意避免内存泄漏和野指针等问题。特别是当使用指向全局/静态区内存的指针时,要确保在不再需要时正确释放内存(如果适用)。然而,对于全局/静态区本身来说,由于它们的生命周期与程序相同,因此通常不需要手动释放内存。
多线程环境:在多线程环境中,对全局/静态区的访问需要特别小心,以避免竞态条件和数据不一致等问题。通常需要使用同步机制(如互斥锁)来保护对全局/静态区变量的访问。
4.常量区
常量存储区用于存储常量值,这些值在程序编译时就已经确定,并且在程序的整个执行期间都不会改变。常量存储区通常位于代码段中,与程序代码一起存储。
常量区存储的是程序中定义的常量值,如整数常量、字符常量、字符串常量等。对于字符串常量,常量区通常只存储一个副本,即使它们在程序中多次出现。这有助于节省内存空间。
常量区中的常量值在程序的整个执行期间都存在,直到程序终止。
常量区中的内存通常是只读的,这意味着程序不能修改这些常量值。尝试修改常量存储区中的值可能会导致程序崩溃或未定义行为。
所以,所谓“动态内存管理”其实就是对堆上的空间做管理,不同语言对堆的操作接口不同,本文主要讲述C语言和C++对堆的操作接口~~
二.C语言动态内存管理
在C语言中,动态内存管理是一种在程序运行时根据需要分配和释放内存(堆上的内存)的机制。与静态内存分配(其大小是固定的,在编译时确定)不同,动态内存管理允许程序在运行时灵活地调整其内存需求。C语言提供了几个函数来实现动态内存管理,主要包括 malloc、calloc、realloc和 free。
1.malloc 函数
a.接口简介与使用实例
void* malloc ( size_t size );
功能:malloc 函数用于分配指定大小的内存块,并返回一个指向该内存块的指针。
参数:size 是要开辟的空间大小,其单位是字节。
返回值:如果分配成功,返回指向分配的内存块的指针;如果分配失败,返回 NULL。
b.注意要点
①类型转换:malloc 返回的是一个 void* 类型的指针,因此在使用之前通常需要进行类型转换。
②内存初始化:malloc 分配的内存块是未初始化的,其内容是随机的。如果需要初始化内存,可以使用 calloc 函数,它会自动将分配的内存初始化为零。
③内存泄漏:使用 malloc 分配的内存必须在使用完毕后通过 free 函数释放,否则会导致内存泄漏。
④检查返回值:始终检查 malloc 的返回值是否为 NULL,以确保内存分配成功。
2.
calloc 函数:
void* calloc (size_t num, size_t size);
功能:calloc 函数用于分配内存,并将分配的内存初始化为零。它通常用于分配数组。
参数:num 是要分配的元素个数,size 是每个元素的大小(以字节为单位)。
返回值:如果分配成功,返回指向分配的内存块的指针;如果分配失败,返回 NULL 。
realloc 的注意要点与 malloc 类似,除了元素初始化~~
3.realloc 函数
a.接口简介与使用实例
void* realloc(void* ptr, size_t size);
功能:将 ptr 原先指向的堆空间大小调修改 size。
参数:ptr 是指向之前分配的内存块的指针,size 是新的大小(以字节为单位)。
返回值:如果调整成功,返回指向新分配(或调整大小)的内存块的指针;如果调整失败,返回 NULL(并且原来的内存块保持不变)。
b.注意要点
内存泄漏:如果 realloc 失败并返回 NULL,而原来的指针 str 不再被保存(例如,如果直接 str = realloc (str, 20*sizeof(char)); 而没有检查返回值),则会导致内存泄漏,因为原来的内存块无法再被访问以进行 free 操作。
数据丢失:如果 realloc 成功但新大小小于旧大小,则超出新大小范围的数据会丢失。
性能:realloc 可能会移动内存块到新的位置,因此在处理大型数据结构或频繁的内存重新分配时,需要注意性能影响。
4.free 函数
void free(void* ptr);
功能:free 函数用于释放之前通过 malloc、calloc 和 realloc 分配的内存。
参数:ptr 是指向要释放的内存块的指针。
注意:在释放内存后,应将指针设置为 NULL,以避免使用已释放的内存(即野指针问题)。将指针设置为 NULL 只是一个良好的编程习惯,并不是强制性的,因为一旦内存被释放,指针的值就不再有意义。
三.C++动态内存管理
与C语言类似,C++也提供了malloc、calloc、realloc和free等函数来进行动态内存管理。然而,C++引入了更高级、更面向对象的内存管理机制,即 new 和 delete 操作符,以及智能指针,如unique_ptr 和 shared_ptr。
1.new 和 delete
a.理解使用实例
1.动态申请内置类型数据空间时,new/calloc除了操作上有点不同,实质并没有太大区别;
*2.动态申请自定义类型数据空间时,new实例化出来的对象会调用类的构造函数,而malloc不会;delete释放空间时会调用类的析构函数,而free不会!!
如:A* p=new A[10]; 会调用10次A的构造函数,delete[] p;会调用10次A的析构函数
malloc申请空间失败,会返回NULL,同时记录错误码;new申请空间失败,会直接抛异常!
用例助理解:
int* p1= new int; 开辟出大小是4个字节的堆空间,*p1默认为1。 delete p1;释放它
int* p2=new int(10); 开辟出大小是4个字节的堆空间,并给*P2赋值10。delete p2;资源释放
int* p3=new int[10]; 开辟大小是40字节的堆空间,每个元素初始值都是0。delete[] p2;资源释放。
int* p4=new int[10]{1,2,3}; //动态开辟一个大小是40字节的堆空间,其被初始化成{1,2,3,0,0,0...}
b.注意事项
①匹配使用:确保使用 new 分配的内存通过delete释放,使用new[]
分配的内存通过delete[]
释放。
②避免重复释放:不能对同一块内存使用多次delete或delete[]
。
③异常处理:如果new运算符在分配内存时失败(例如,由于堆内存不足),它会抛出一个异常。因此,在使用 new 时,可能需要考虑异常处理。
④操作符:new 和 delete是操作符,不是函数,而 malloc 和 calloc 是函数!!
2.运算符重载
a.operator new 与 operator delete函数
new 和 delete 是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过调用operator new全局函数来释放空间。
在C++中,new 和 delete 运算符本身是不能被直接重载的,就像其他基本的运算符(如+
、-
、*
等)一样。然而,我们可以通过定义特殊的函数来间接地“重载”这些运算符的行为。
如类内重载:
b. 定位new 表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象