C/C++内存管理实例代码详解:掌握内存分布与优化技巧
详解C/C++内存管理

2021 年 06 月 11 日 15 时 19 分 32 秒进行了更新,更新者为 2021dragon。
内存管理在 C++中是一个令人极为不满和痛恨的问题,同时也是极具争议的问题。C++的高手们通过它获得了更优的性能以及更大的自由。今天,将为大家分享 C/C++内存管理的实例代码,有需要的朋友可以参考一下。
目录
C/C++给予了程序员管理内存的权利。这是 C/C++的语言特色。它虽然带来了复杂度与危险性,但从另一方面来看,又增加了控制力与灵活性。这是 C/C++的独特之处,也是其强大之处。
C/C++内存分布
让我们先来看看下面这段代码:
int globalVar = 1;
定义了一个静态全局变量 staticGlobalVar 并初始化为 1 。 这个静态全局变量属于静态存储类别 ,在整个程序的生命周期内都存在 。 它在程序开始执行时被初始化 ,其值为 1 。 并且在程序的其他部分可以访问和使用这个变量 。
void Test()
{
static int staticVar = 1;
int localVar = 1;
定义了一个包含 10 个元素的整型数组 num1,其初始值分别为 1、2、3、4。
char char2[] = "abcd";
char* pChar3 = "abcd";
申请了 4 个整型大小的内存空间,并将其地址赋值给指针 ptr1 。
定义了一个整型指针 ptr2 ,通过 calloc 函数分配了 4 个整型大小的内存空间,并将其强制转换为整型指针类型。
一个名为 ptr3 的整型指针被重新分配内存,它是通过将 ptr2 所指向的内存区域重新分配为 sizeof(int) * 4 大小的内存空间得到的,并且重新分配后的内存被强制转换为整型指针类型。
free(ptr1);
free(ptr3);
}
你知道代码中的各个部分分别存储在内存中的哪一个区域吗?
【说明】
栈被称作堆栈,它的作用是存储非静态局部变量、函数参数以及返回值等。栈的增长方向是向下的。
内存映射段是一种高效的 I/O 映射方式。它可以用来装载一个共享的动态内存库。用户能够使用系统接口去创建共享内存,从而实现进程间的通信。
3、堆用于存储运行时动态内存分配,堆是向上增长的。
4、数据段又叫静态区,用于存储全局数据和静态数据。
5、代码段又叫常量区,用于存放可执行的代码和只读常量。
顺便提一下:为什么说栈是向下增长的,而堆是向上增长的?
简单来说,一般情况下,在栈区开辟空间时,先开辟的空间地址比较高;在堆区开辟空间时,先开辟的空间地址比较低。
在下面的代码里,变量 a 以及变量 b 是存储在栈区的,而指针 c 和指针 d 则指向堆区的内存空间。
#includeusing namespace std; int main() { //栈区开辟空间,先开辟的空间地址高 int a = 10; int b = 20; cout << &a << endl; cout << &b << endl; //堆区开辟空间,先开辟的空间地址低 int* c = (int*)malloc(sizeof(int)* 10); int* d = (int*)malloc(sizeof(int)* 10); cout << c << endl; cout << d << endl; return 0; }
栈区开辟空间时,先开辟的空间地址较高,所以打印出的 a 的地址比 b 的地址大;堆区开辟空间时,先开辟的空间地址较低,所以 c 所指向的空间地址小于 d 所指向的空间地址。
在堆区开辟空间时,后开辟的空间地址不一定比先开辟的空间地址高。因为在堆区,后开辟的空间有可能处于前面某一已被释放的空间位置。
C语言中动态内存管理方式
malloc、calloc、realloc和free
一、malloc
malloc 函数的功能在于开辟特定字节大小的内存空间。若开辟成功,它会返回该空间的首地址;若开辟失败,就会返回一个 NULL。在传参时,只需传入需要开辟的字节个数即可。
二、calloc
calloc 函数能够开辟指定大小的内存空间。若开辟成功,它会返回该空间的首地址;若开辟失败,就会返回一个 NULL。在传参时,calloc 函数需要传入开辟的内存用于存放的元素个数以及每个元素的大小。并且,calloc 函数在开辟好内存后,会将空间内容里的每一个字节都初始化为 0。
三、realloc
realloc 函数能够对已开辟的动态内存的大小进行调整。其第一个参数是需要调整大小的动态内存的起始地址。第二个参数则是动态内存调整后的新大小。realloc 函数如同上面提到的两个函数一样,倘若开辟成功,就会返回开辟好的内存的起始地址;倘若开辟失败,就会返回 NULL。
realloc函数调整动态内存大小的时候会有三种情况:
原地扩。如果需扩展的空间后方有足够的空间,那么 realloc 函数会直接在原空间后方进行扩展,并且会返回该内存空间的首地址,这个首地址也就是原来的首地址。
异地扩。若需扩展的空间后方没有足够空间来扩展,那么 realloc 函数会在堆区找寻一块能满足要求的新内存空间,接着把原空间内的数据复制到新空间里,并且会主动把原空间的内存释放掉(也就是归还给操作系统),最后返回新内存空间的首地址。
扩充失败了。因为需扩展的空间后方没有足够的空间,而且堆区中也没有符合需要开辟的内存大小的空间,所以开辟内存失败,最终返回了一个 NULL。
四、free
free 函数的作用在于释放由 malloc、calloc 以及 realloc 函数所申请的动态内存空间。其释放空间的大小是依据之前申请的内存空间的大小来确定的。
若想要进一步知晓 malloc、calloc、realloc 以及 free 的相关内容,那就请阅读 C 语言的动态内存管理。
C++中动态内存管理方式
C 语言的内存管理方式在 C++中是可以继续被使用的。然而,在某些方面它却无法发挥作用,并且使用起来也较为繁琐。正因如此,C++提出了它自己的内存管理方式,即通过 new 和 delete 操作符来进行动态内存管理。
new和delete操作内置类型
一、动态申请单个某类型的空间
//动态申请单个int类型的空间 int* p1 = new int; //申请 delete p1; //销毁
其作用等价于:
//动态申请单个int类型的空间 使用 malloc 函数申请一个 int 类型大小的内存空间,并将其地址赋值给指针 p2 。 free(p2); //销毁
二、动态申请多个某类型的空间
//动态申请10个int类型的空间 int* p3 = new int[10]; //申请 delete[] p3; //销毁
其作用等价于:
//动态申请10个int类型的空间 使用 malloc 函数申请了一块能存储 10 个 int 类型数据大小的内存空间,并将其地址赋值给指针 p4 。 free(p4); //销毁
三、动态申请单个某类型的空间并初始化
//动态申请单个int类型的空间并初始化为10 申请并创建一个整型指针 p5,然后将其初始化为 10 。 即通过 new 操作符为整型分配内存空间,并将值 10 赋给该指针所指向的位置。 这样就得到了一个指向值为 10 的整型的指针 p5 。 delete p5; //销毁
其作用等价于:
//动态申请一个int类型的空间并初始化为10 申请了一个整型指针 p6,通过 malloc 函数分配了 sizeof(int) 大小的内存空间,并将其强制转换为整型指针类型。 *p6 = 10; //赋值 free(p6); //销毁
四、动态申请多个某类型的空间并初始化
//动态申请10个int类型的空间并初始化为0到9 创建一个整型指针 p7,通过 new 操作符申请了一个包含 10 个整型元素的数组,并将这些元素初始化为 0、1、2、3、4、5、6、7、8、9。 delete[] p7; //销毁
其作用等价于:
//动态申请10个int类型的空间并初始化为0到9
申请了一个整型指针 p8,通过 malloc 函数分配了可以存储 10 个整型大小的内存空间,即 sizeof(int)*10 。
for (int i = 0; i < 10; i++) //赋值
{
p8[i] = i;
}
free(p8); //销毁
申请单个元素的空间时使用 new 操作符,释放单个元素的空间时使用 delete 操作符;申请连续的空间时使用 new[ ]操作符,释放连续的空间时使用 delete[ ]操作符。
new和delete操作自定义类型
对于以下自定义类型:
class Test
{
public:
Test() //构造函数
:_a(0)
{
cout << "构造函数" << endl;
}
~Test() //析构函数
{
cout << "析构函数" << endl;
}
private:
int _a;
};
一、动态申请单个类的空间
用new和delete操作符:
Test* p1 = new Test; //申请 delete p1; //销毁
用malloc和free函数:
申请一个 Test 类型的指针 p2,通过 malloc 函数分配 sizeof(Test) 大小的内存空间,将分配的内存地址强制转换为 Test* 类型并赋值给 p2 。 free(p2); //销毁
二、动态申请多个类的空间
用new和delete操作符:
Test* p3 = new Test[10]; //申请 delete[] p3; //销毁
用malloc和free函数:
申请了一个 Test 类型的指针 p4,通过 malloc 函数分配了 sizeof(Test) * 10 大小的内存空间,用于存储 10 个 Test 类型的元素。 free(p4); //销毁
在申请自定义类型的空间时,new 会去执行构造函数的操作,delete 会去执行析构函数的操作,然而 malloc 和 free 则不会进行这样的操作。
总结一下:
C++中对于申请内置类型的对象或者数组这一情况而言,使用 new/delete 与使用 malloc/free 不存在明显差别。
如果是自定义类型,区别较为显著。new 操作符用于开辟空间并调用构造函数进行对象的初始化,delete 操作符则先调用析构函数清理对象,然后释放空间。而 malloc 函数只是单纯地开辟空间,free 函数只是单纯地释放空间。
建议在 C++ 中,对于内置类型以及自定义类型的申请操作和释放操作,都尽量去使用 new 这个操作符以及 delete 这个操作符。
operator new 函数以及 operator delete 函数
在底层,new 和 delete 是通过调用全局函数 operator new 和 operator delete 来实现申请和释放空间的操作。
operator new 的用法与 malloc 的用法完全一样,其功能是在堆上申请空间;operator delete 的用法与 free 的用法完全一样,其功能是在堆上释放空间。
使用 operator new 函数申请 10 个 int 类型大小的内存空间,并将其地址赋值给指针 p1 ,p1 为 int 类型指针,即 int* p1 = (int*)operator new(sizeof(int)* 10); operator delete(p1); //销毁
其作用等价于:
使用 operator new 函数申请了可以存储 10 个 int 类型数据的空间,并将其地址赋值给指针 p2 。 free(p2); //销毁
而 operator delete 的底层是通过调用 free 函数来释放空间。
系统提供了全局函数 operator new 和 operator delete 。虽然如此,我们依然可以针对某个类,对其专属的 operator new 和 operator delete 函数进行重载,以此来提高效率。
new和delete的实现原理内置类型
如果申请的是内置类型的空间,new/delete 与 malloc/free 基本相似。不同之处在于,new/delete 申请和释放的是单个元素的空间,new[ ]/delete [ ]申请和释放的是连续的空间。另外,malloc 申请失败时会返回 NULL,而 new 申请失败时会抛出异常。
自定义类型
new的原理
1、调用operator new函数申请空间。
2、在申请的空间上执行构造函数,完成对象的构造。
delete的原理
1、在空间上执行析构函数,完成对象中资源的清理工作。
2、调用operator delete函数释放对象的空间。
new T[N]的原理
调用 operator new[ ]函数,在这个函数里实际调用 operator new 函数来完成 N 个对象空间的申请。
2、在申请的空间上执行N次构造函数。
delete[ ] 的原理
1、在空间上执行N次析构函数,完成N个对象中资源的清理。
调用 operator delete[ ]函数,在这个函数内部实际会调用 operator delete 函数,以此来完成 N 个对象空间的释放。
定位new和表达式(placement-new)
定位 new 表达式是在已分配的原始内存空间里进行操作。它会调用构造函数来初始化一个对象。
使用格式:
new(place_address)属于某种类型或者 new(place_address)属于某种类型且带有初始化列表
其中,place_address 必定是一个指针,而 initializer-list 则是类型的初始化列表。
使用场景:
定位 new 表达式在实际中通常是与内存池一同使用的。因为内存池分配出的内存未被初始化,所以当涉及到自定义类型的对象时,就需要借助定位 new 表达式来显式地调用构造函数进行初始化。
#includeusing namespace std; class A { public: A(int a = 0) //构造函数 :_a(a) {} ~A() //析构函数 {} private: int _a; }; int main() { //new(place_address)type 形式 A 指针 p1 分配了一块大小为 A 结构体大小的内存空间。 new(p1)A; 以 //new(place_address)type(initializer-list) 的形式 A* p2 等于通过 malloc 函数分配内存空间,该内存空间的大小为 A 类型的大小。 new(p2)A(2021); //析构函数也可以显示调用 p1->~A(); p2->~A(); return 0; }
在未通过定位 new 表达式进行显示调用构造函数来初始化之前,malloc 所申请的空间不能被视为一个对象。它仅仅是一块与 A 对象大小相同的空间,原因是构造函数尚未执行。
常见面试题
malloc/free和new/delete的区别?
共同点:
都是从堆上申请空间,并且需要用户手动释放。
不同点:
malloc 是一个函数,free 也是一个函数。new 是一个操作符,delete 也是一个操作符。
malloc 申请的空间不会进行初始化操作;而 new 申请的空间会进行初始化操作。
malloc 在申请空间时,需要人为地去计算空间的大小并且进行传递;而 new 只需要在它之后跟上空间的类型就可以了。
malloc 的返回值为 void* 。在使用 malloc 时,必须进行强转操作。而 new 则不需要强转,因为 new 后面跟的是空间的类型。
malloc申请失败时,它所返回的是 NULL。所以在使用 malloc 时,必须要对其返回值进行判空操作。而 new 则不需要进行这样的判空操作,不过 new 需要捕获异常。
申请自定义类型对象时,malloc 和 free 仅仅会开辟空间,不会去调用构造函数以及析构函数。而 new 在申请空间之后会调用构造函数,以此来完成对象的初始化。delete 在释放空间之前会调用析构函数,从而完成空间中资源的清理工作。
内存泄漏 什么是内存泄漏,内存泄漏的危害?
内存泄漏:
内存泄漏指的是由于疏忽或错误,导致程序未能释放已经不再使用的内存。内存泄漏并非是内存在物理层面上的消失,而是当应用程序分配了某段内存后,由于设计方面的错误,失去了对该段内存的掌控,从而造成了内存的浪费。
内存泄漏的危害:
长期运行的程序若出现内存泄漏,影响会很大。像操作系统、后台服务等情况,一旦出现内存泄漏,就会致使响应逐渐变慢,最终会卡死。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
分配了一个整型大小的内存空间,并将其地址赋值给指针 p1 。 首先使用 malloc 函数来申请内存空间,该函数的参数为 sizeof(int) ,即整型的大小。然后将返回的内存地址强制转换为 (int*) 类型,并赋值给指针 p1 。 这样,p1 就指向了分配的整型内存空间。
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func() 函数执行后抛出异常,这使得 delete[] p3 未被执行,从而导致 p3 未被释放。
delete[] p3;
}
内存泄漏分类?
在C/C++中我们一般关心两种方面的内存泄漏:
1、堆内存泄漏(Heap Leak)
程序执行过程中,通过 malloc、calloc、realloc、new 等操作从堆中分配出的一块内存被称为堆内存。使用完后,必须通过调用相应的 free 或者 delete 来释放它。倘若程序设计出现错误,致使这部分内容未被释放,那么之后这部分空间就无法再被使用,进而会产生 Heap。
Leak。
2、系统资源泄漏
程序会使用系统分配的资源,像套接字、文件描述符、管道等。如果没有使用对应的函数将其释放掉,就会导致系统资源被浪费。情况严重时,还可能导致系统效能降低,使系统执行变得不稳定。
如何避免内存泄漏?
工程前期的设计规范良好,能养成良好的编码规范,对于申请的内存空间,要记住匹配后去释放。
2、采用RALL思想或者智能指针来管理资源。
有些公司在内部有规范,会使用内部实现的私有内存管理库。这个库自带一种功能选项,即可以进行内存泄漏检测。
4、出问题了使用内存泄漏工具检测。
内存泄漏非常常见,解决方案分为两种:
1、事前预防型。如智能指针等。
2、事后查错型。如泄漏检测工具。
如何一次在堆上申请4G的内存?
在堆上申请4G的内存:
#includeusing namespace std; int main() { //0xffffffff转换为十进制就是4G 申请了一个大小为 0xfffffffful 的内存空间,并将其地址赋值给 void 类型的指针 p 。 cout << p << endl; return 0; }
这样就能一次性在堆上申请 4G 内存了。