C++内存管理¶
约 3846 个字 591 行代码 3 张图片 预计阅读时间 20 分钟
C/C++内存分配¶
在C语言动态内存管理章节已经了解到内存的分类,包括下面四个区域:
- 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段--高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
- 堆--用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段--存储全局数据和静态数据。
- 代码段--可执行的代码/只读常量
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在上面代码中,globalVar
为全局变量,与静态变量staticGlobalVar
和staticGlobalVar
,存放在数据段(静态区),num1
和char2
均为局部数组,所以均放在内存的栈区。对于指针类型pChar3
,ptr1
,ptr2
和ptr3
均为局部变量,所以均存放在栈区,但是pChar3
指向的内容是字符串的第一个元素的地址,该地址在内存的代码段(常量区),其余三个指针均指向在内存堆区开辟的空间
内部链接属性和外部链接属性¶
在C++和C语言中都存在外部链接属性和内部属性,这两个属性用于判断某一个变量或者函数是否在多文件情况下可以在其他文件被访问
内部链接属性:简单理解就是这个变量只在当前文件是可见的,如果其他文件想使用定义在某一个源文件中具有内部链接属性的变量,此时在定义内部链接属性的变量时需要保证该对应变量被extern
修饰,再在需要使用到该变量的其他文件中使用extern
关键字引入
外部链接属性:简单理解就是这个变量在所有文件中是可见的,如果其他文件想使用定义在某一个源文件中具有外部链接属性的变量,只需要使用extern
关键字引入具有外部链接属性的变量到需要使用的文件中即可
C语言中的const
的全局变量和普通的全局变量都具有外部链接属性,而C++中,被const
修饰的全局变量是具有内部链接属性,而普通的全局变量则是具有内部链接属性,利用这两个特点就可以分别演示具有外部链接属性的变量和内部链接属性的变量如何引入
C | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
在实际的项目管理中,一般更喜欢将常量等定义在头文件中,此时C++中具有内部链接属性的const
全局变量就可以发挥其内部链接属性的作用,因为具有内部链接属性,所以当头文件在被包含的源文件中展开时可以保证const
全局变量在当前所处的源文件中是唯一的,但是C语言中的const
修饰的全局变量具有外部链接属性,所以C语言要是定义常量更多还是使用宏,但是宏进行替换还是有一些细节问题,这也是为什么C++希望将const
修饰的全局变量更改为具有内部链接属性而不是具有外部链接属性的一大原因,所以C++就可以通过const
定义一个真正意义上的常量,并且可以显式指定类型,例如下面的代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
const
全局变量叫常量,但是C语言很少这么说? 其实在C语言中,如果声明了一个const
变量,并且这个变量有外部链接(external linkage),那么它会在链接阶段进行符号解析。这意味着,如果你在一个源文件中声明了一个const
变量,并在另一个源文件中引用它,那么编译器会在每个源文件中分别处理这些声明,然后链接器会确保它们指向相同的内存位置。因此,虽然它的值在编译期间是已知的,但是它的地址可能不是,直到链接期间才能确定。而在C++中,声明具有内部链接(internal linkage)的const
变量,这种情况下,变量通常是在编译期就确定的,而且不会出现在符号表中供链接器处理。这使得这样的const
变量更像一个真正的常量,因为它不需要在多个源文件之间共享
所以,C语言的const
修饰的全局变量严格来说不能算编译时期可确定的变量,但是C++中的const
修饰的全局变量如果在其值为字面量或者在编译期可以确定的值可以算
C++内存管理¶
C++内存管理介绍¶
在C语言中,使用malloc/calloc/realloc
进行动态内存空间的开辟,但是这三种方式中只有calloc
会对数据区域进行初始化,并且初始化值为0,并且使用free
宏对动态分配的空间进行释放
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
在C++中,仍然可以使用malloc/calloc/realloc
进行动态内存分配和free
进行空间释放,但是更推荐使用new
进行动态内存分配以及delete
进行空间释放,所以上面的代码可以转化为下面的代码
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
C++内存管理使用¶
C++内存管理基本语法¶
在C++中,使用new
关键字进行内存空间的申请
C++ | |
---|---|
1 2 3 4 5 6 |
|
在C++中,使用delete
关键字进行内存空间的释放
C++ | |
---|---|
1 2 3 4 |
|
Note
注意,new
和delete
一定要匹配使用,单个空间开辟就使用单个空间的释放,连续空间的开辟就使用连续空间的释放,更不能new
和free
等交叉使用
对于内置类型来说,如下面代码
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
对于内置类型来说,如果使用free
进行释放时,并不会出现报错
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
尽管也正常输出,但是依旧不建议使用free
对new
开辟的空间进行释放
对于自定义类型来说,分两种情况:
- 自定义类型中的成员变量仅为内置类型
- 自定义类型中的成员变量有指针指向的动态内存分配的空间
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
在上面的代码中,对于单个对象空间的分配时,会调用一次构造函数,而对连续空间的对象空间分配时,会调用对象个数次构造函数。对于空间释放,当只有一个对象时,只调用一次析构函数,当有连续空间的对象时,则会调用对象个数次析构函数
如果将上面代码中的delete
改为free
,则不会调用析构函数
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
在上面的代码中,使用new
为自定义类型的对象开辟空间时会调用构造函数,但是释放空间时使用free
编译器会给出警告,如果没有删除析构函数,那么程序将运行终止,所以不要使用free
和delete
交叉使用
另外对于下面的代码来说
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
因为ptr2
指向的空间是连续的对象空间,此时使用对单个空间释放形式的delete
时,程序也会终止运行,所以单个空间开辟使用单个空间释放,多个空间开辟使用多个空间释放
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
在上面的代码中,类成员变量的类型为指针类型,在构造函数中,为该指针分配了5个连续的int
类型空间,在析构函数中,释放这5个连续的空间。当类实例化对象时,对于单个空间的开辟和释放会调用一次构造函数和一次析构函数,而对于连续空间的开辟和释放会调用连续空间个数次的构造函数和析构函数
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
对于单个空间的释放来说,不会出现程序崩溃,尽管没有调用析构函数释放类对象成员变量指向的内存空间,但是当程序结束时,该空间会得到释放,此时也即内存泄漏
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
对于多个连续空间的释放来说,因为free
只能释放一个空间,如果直接理解会认为释放掉了连续空间中的第一个空间而并未释放后面的空间导致程序内存泄漏错误。但是实际上并不是,因为内存泄漏只有在没有足够内存时才会导致程序崩溃,编译器是不会检查内存泄露的。而真正导致程序崩溃的原因是VS在此处做出的优化
而之所以free
释放空间时会报错,就是因为返回的起始位置并不是开辟的空间的真正起始位置从而导致程序崩溃,free
释放空间时必须从开辟的空间的实际起始位置开始,而不能释放部分空间,而delete
(不是delete[]
)的底层设计也是free
(只是做了一些异常检查),所以调用delete
而不是delete[]
也会导致程序崩溃,而delete[]
不崩溃是因为在释放连续空间时会先向前移动找到存储空间个数的值,再通过该值确定调用析构函数的次数,最后销毁指向连续空间的指针
总结:
对于内置类型来说,因为内置类型开辟的空间如果使用free
也是直接释放指针指向的空间,只是不会调用析构函数,一般情况下不会出现问题,但是不推荐使用free
。建议对单个空间用delete
,连续空间用delete[]
对于自定义类型来说,使用free
比较容易产生内存泄漏,所以不论是成员变量是内置类型,还是成员变量是涉及资源申请的,都建议使用delete
和delete[]
operator new
和 operator delete
函数¶
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数,new
在底层调用operator new
全局函数来申请空间,delete
在底层通过operator delete
全局函数来释放空间
operator new
实际也是通过malloc
来申请空间,operator delete
最终是通过free
来释放空间的
Note
注意operator new
和 operator delete
函数不是运算符重载
内置类型:
对于operator new
函数来说
如果是单个空间,那么在申请空间时使用new
,底层会调用malloc
函数开辟单个空间,但是不同于malloc
函数,operator new
如果空间申请失败会抛出异常,而不是返回空指针
如果是连续空间,则在申请空间时new[]
,底层会调用malloc
函数开辟连续的空间
对于operator delete
函数来说
如果是单个空间,那么在释放空间时使用delete
,底层会调用free
函数释放单个空间,但是不同于free
宏,operator delete
会在释放时检查一些可能存在的问题
如果是连续空间,则在申请空间时delete[]
,底层会调用free
函数开辟连续的空间
自定义类型:
对于operator new
函数来说
如果是单个空间,则在申请空间时使用new
,会调用operator new
申请空间,再在申请的空间上调用构造函数创建对象
如果是多个连续空间,则在申请空间时使用new[]
,会调用operator new
申请连续的空间,再在申请的连续空间上调用申请空间个数次的构造函数创建对象
对于operator delete
函数来说
如果是单个空间,则在释放空间时使用delete
,释放空间时先调用类的析构函数,再调用operator delete
函数释放指向单个存储对象空间的指针
如果是连续空间,则在释放空间时使用delete[]
,释放空间时先调用类的析构函数,调用次数为创建的对象的个数,最后调用operator delete
函数释放指向存储对象的连续空间的指针
定位new
表达式(placement-new
)¶
定位new
表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
基本语法¶
C++ | |
---|---|
1 |
|
定位new
最大的特点就是将开辟位置交给了程序员自己,例如下面的代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
new
在数组空间中开辟空间,此时只要定位new
起始地址是数组的起始地址,就会出现地址一直与数组的起始地址相同,而普通的new
开辟的空间起始地址则有操作系统分配 Note
需要注意,上面的代码中,由于定位new
开辟的空间是在栈数组,该数组开辟在栈上,出了main
函数作用域就会销毁,所以此时定位new
在此基础上开辟的空间就不需要单独使用deleter[]
释放
使用场景¶
定位new
表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new
的定义表达式进行显式调构造函数进行初始化
Note
内存池的内存申请本质上还是在堆上,但是不同于向操作系统申请,内存池申请的空间一般会大于用户需要的空间,从而减少申请的次数
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
malloc
/free
和new
/delete
¶
相同点¶
malloc/free
和new/delete
的共同点是:都是从堆上申请空间,并且需要用户手动释放
不同点¶
malloc
和free
是函数,new
和delete
是操作符malloc
申请的空间不会初始化,new
可以初始化malloc
申请空间时,需要手动计算空间大小并传递,new
只需在其后跟上空间的类型即可, 如果是多个对象,[]
中指定对象个数即可malloc
的返回值为void*
, 在使用时必须强转,new
不需要,因为new
后跟的是空间的类型malloc
申请空间失败时,返回的是NULL
,因此使用时必须判空,new
不需要,但是new
需要捕获异常- 申请自定义类型对象时,
malloc/free
只会开辟空间,不会调用构造函数与析构函数,而new
在申请空间后会调用构造函数完成对象的初始化,delete
在释放空间前会调用析构函数完成空间中资源的清理