内存管理
# C/C++内存分布
先看代码:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof (int)* 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int)* 4);
free(ptr1);
free(ptr3);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面代码中各个部分在内存中的哪些区域呢?
C/C++ 中常见的内存区域及其用途:
- 代码段
代码段也称为文本段或指令段,主要用于存储程序的机器代码(即编译后的二进制指令)。这部分内存是只读的,不允许修改 - 数据段
- 只读数据段,用于存储程序中的常量数据,如字符串字面量、常量变量等。这部分内存也是只读的,不允许修改
- 读写数据段,用于存储程序中的全局变量和静态变量。这部分内存是可以读写的,可以在程序运行过程中修改其内容
- 堆
堆是动态分配的内存区域,通常用于动态分配的对象和数据结构。在 C++ 中,通过new
和delete
操作符来分配和释放堆内存。堆内存的生命周期由程序员控制。 - 栈
栈是用于存储局部变量的内存区域。在函数调用时,局部变量和函数调用的栈帧都会被压入栈中。栈的特点是先进后出(First In Last Out, FILO),并且由操作系统OS自动管理。当函数调用结束时,相应的栈帧会被弹出
# C++动态内存管理
首先,C语言内存管理的方式(malloc \calloc\realloc\free )在C++中可以继续使用。但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理
int* a = new int(10); // 开辟4个字节空间,并且初始化10
int* b = new int[10]; // 开辟10*4个字节空间的数组
delete a; // 释放空间
delete[] b; // 释放空间
2
3
4
5
# operator new和operator delete
new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new和delete在底层是通过调用全局函数operator new和operator delete来申请和释放空间的。
operator new和operator delete的用法和malloc和free的用法完全一样,其功能都是在堆上申请和释放空间。
int* p1 = (int*)operator new(sizeof(int)* 10); //申请
operator delete(p1); //销毁
2
实际上,operator new的底层是通过调用malloc函数来申请空间的,当malloc申请空间成功时直接返回;若申请空间失败,则尝试执行空间不足的应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。而operator delete的底层是通过调用free函数来释放空间的。
区分:
- malloc
- operator new -> malloc + 失败抛异常机制
- new -> operator new + 调用构造函数
注意:
虽然说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]的原理
1、调用operator new[ ]函数,在operator new[ ]函数中实际调用operator new函数完成N个对象空间的申请。
2、在申请的空间上执行N次构造函数。
delete[ ] 的原理
1、在空间上执行N次析构函数,完成N个对象中资源的清理。
2、调用operator delete[ ]函数,在operator delete[ ]函数中实际调用operator delete函数完成N个对象空间的释放。
# 指针
毫无疑问,在整个C/C++的学习过程中,使用指针对计算机底层内存的操作是一种奇妙的魅力
在C语言中我们学过操作符(&)取地址操作符和叫解引用操作符(*)
int a = 10;
int* p = &a;
*p = 200;
cout << a << endl;
2
3
4
指针变量存储地址,
32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节
const可以用来修饰指针变量:
- const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本身的内容可变。
- const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。
# 指针运算
指针 +- 整数 / 指针 +- 指针
int main() {
int arr[] = { 1,2,3,4,5 };
int* p = arr;
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
cout << *p << endl;
p++;
}
return 0;
}
2
3
4
5
6
7
8
9
int main() {
string str = "abcdef";
char* p = &str[0];
char* start = p;
while (*p != '\0') {
p++;
}
cout << p - start << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
# 指针与数组
一般情况下:数组名就是首元素的地址
但有两种特殊情况:
sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)
# 野指针
指针未初始化
数组访问越界
指针指向空间释放,不要返回局部变量的地址
int* test() {
int n = 100;
return &n;
}
int main() {
int* p = test();
printf("%d\n", *p);
return 0;
}
2
3
4
5
6
7
8
9
# 特殊的指针
- 数组指针
存放的应该是数组的地址,能够指向数组的指针变量
int arr[] = { 0 };
int(*p)[10] = &arr
2
- 函数指针
以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名的方式获得函数
int (*pf3) (int x, int y) = 函数名
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型
2
3
4
5
6
- 函数指针数组
把函数的地址存到一个数组中,那这个数组就叫函数指针数组
int (*parr1[3])();
# 引用
# 引用的概念
引用不是定义一个变量,而是已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
其使用的基本形式为:类型& 引用变量名(对象名) = 引用实体
注意点:
- 一个变量可以有多个引用
- 引用必须初始化
int a = 100;
int& b;//未初始化
比如:int& b = a <==> int* const b = &a
2
3
- 常引用
引用类型必须和引用实体是同种类型的。但是仅仅是同种类型,还不能保证能够引用成功,我们若用一个普通引用类型去引用其对应的类型,但该类型被const所修饰,那么引用将不会成功
int main() {
const int a = 10;
//int& ra = a; //该语句编译时会出错,a为常量
const int& ra = a;//正确
//int& b = 10; //该语句编译时会出错,10为常量
const int& b = 10;//正确
return 0;
}
2
3
4
5
6
7
8
9
# 引用的使用
# 作为参数
我们知道,函数有传址和传值两种调用方式
要让形参改变实参,可以使用传值调用,同样地,我们用引用作为函数参数,来达到同样的目的
如下:交换两个值
void swap02(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 100; int b = 200;
swap02(a, b);
cout << a << endl<<b<<endl;
system("pause");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# 作为返回值
当然引用也能做返回值,但是要特别注意,我们返回的数据不能是函数内部创建的普通局部变量,因为在函数内部定义的普通的局部变量会随着函数调用的结束而被销毁。我们返回的数据必须是被static修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。
注意:如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;如果已经还给系统了,则必须使用传值返回
# 指针和引用
引用和指针的区别:
引用在定义时必须初始化,指针没有要求。
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
没有NULL引用,但有NULL指针。
在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
引用进行自增操作就相当于实体增加1,而指针进行自增操作是指针向后偏移一个类型的大小。
有多级指针,但是没有多级引用。
访问实体的方式不同,指针需要显示解引用,而引用是编译器自己处理。
引用比指针使用起来相对更安全。
# 内存泄漏
内存泄漏:
内存泄漏是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏的危害:
长期运行的程序出现内存泄漏,如操作系统、后台服务等,出现内存泄漏会导致响应越来越慢,最终卡死。有效措施:
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记住匹配的去释放。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库,该库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。
# from cen