欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > C/C++内存管理

C/C++内存管理

2024/10/24 5:20:50 来源:https://blog.csdn.net/2301_79178723/article/details/140217034  浏览:    关键词:C/C++内存管理

目录

  • C/C++内存分布
    • 例题
    • 答案选择题
      • 解析
    • 答案填空题
      • 解析
    • 答案简答题
  • C语言中动态内存管理方式
  • C++中动态内存管理
    • new/delete操作内置类型
    • new和delete操作自定义类型
      • C和C++动态内存管理区别
  • operator new与operator delete函数
    • operator new与operator delete函数
  • new和delete的实现原理
    • 内置类型
    • 自定义类型
    • 拓展(只需要了解一下)
      • 额外内存空间
      • new和delete更深入的问题
  • 定位new表达式(placement-new)
    • 定位new的用法
  • 常见的面试问题
    • malloc/free和new/delete的区别
      • 用法区别
      • 底层区别

感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接
🐒🐒🐒 个人主页
🥸🥸🥸 C语言
🐿️🐿️🐿️ C语言例题
🐣🐣🐣 python
🐓🐓🐓 数据结构C语言
🐔🐔🐔 C++
🐿️🐿️🐿️ 文章链接目录

C/C++内存分布

下面是C/C++内存划分的区域
在这里插入图片描述
现在有两个问题
1:为什么要划分这些区域,这些区域有什么作用
答案:划分这些区域是为了方便内存管理,比如全局变量,他的生命周期是全局的,因为比较特殊,所以存储的位置也就不同
2:在这些区域中,哪个区域我们应该重点关注
答案:堆需要我们重点关注,因为堆相对于其他区域来说,堆需要自己来释放空间,而其他区域可以自动销毁

说明
1. 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
创建共享共享内存,做进程间通信。
3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段–存储全局数据和静态数据。
5. 代码段–可执行的代码/只读常量。

例题

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";const 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);
}选择题:选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)globalVar在哪里?____   staticGlobalVar在哪里?____staticVar在哪里?____   localVar在哪里?____num1 在哪里?____char2在哪里?____   *char2在哪里?___pChar3在哪里?____      *pChar3在哪里?____ptr1在哪里?____        *ptr1在哪里?____填空题:sizeof(num1) = ____; sizeof(char2) = ____;      strlen(char2) = ____;sizeof(pChar3) = ____;     strlen(pChar3) = ____;sizeof(ptr1) = ____;简答题sizeof 和 strlen 区别?

答案选择题

  选择题:选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)globalVar在哪里?__C__   staticGlobalVar在哪里?__C__staticVar在哪里?__C__   localVar在哪里?__A__num1 在哪里?__A__char2在哪里?__A__       *char2在哪里?_A__pChar3在哪里?__A__      *pChar3在哪里?__D__ptr1在哪里?__A__        *ptr1在哪里?__B__

解析

globalVar是全局变量 staticGlobalVar 和staticVar一个是全局静态,另一个是局部静态(因为有static)

localVar因为是非静态的局部变量所以是在栈里,而num1代表整个数组,而这个数组也是在栈里存储的

char2在栈上,是因为char2中的字符串abcd以及\0是存储在常量区中的,然后将存储在常量区里的abcd以及\0拷贝给char2,然后存储在栈上,也就是说char2和num1是类似的,但是要注意的是num1是直接初始化,而char2是拷贝后初始化的
在这里插入图片描述
char2是在栈上,char2是数组名表示的是数组首元素的地址,所以char2是对数组首元素地址的解引用表示的是a,而这里的a是拷贝后的a,因此也是在栈上

pChar3被const修饰,但是被const修饰并不代表他就在常量区,我们可以验证一下

int main()
{int a = 0;const int b = 1;static int c = 2;cout << &a << "\n" << &b << "\n" <<&c<< endl;return 0;
}

如果存储的区域不同,那么地址也会相差的非常大,而从结果可以发现a和b地址是非常相近的,但是c和ab地址一眼看去就知道存储的不在同一块区域
在这里插入图片描述
所以const修饰的是叫做常变量,而不是常量
pchar3是一个存储在栈是的指针,这个指针指向字符串abcd,因为这个字符串存储在常量区,所以当*pchar3解引用时表示的就是在常量区中的a

在这里插入图片描述
ptr1和上面相同,是在栈上的一个指针变量指向堆上的一块空间(malloc开辟),而*ptr就是对指针解引用,在堆上
为了更好理解常量区,我们来验证一下
对下面的代码进行调试

int main()
{char char2[] = "abcd";char2[0]++;return 0;
}

在这里插入图片描述

在这里插入图片描述
经过调试可以看出char2存储的字符串可以被修改,所以char2并没有在常量区

再来看看下面的代码

int main()
{const char* char2 = "abcd";char2[0]++;return 0;
}

这里的"abcd"是在常量区的,所以不能被修改
在这里插入图片描述

当我们强制类型转化后运行发现会出问题
在这里插入图片描述
在这里插入图片描述
所以常量区里的东西是不可以被修改的

答案填空题

    填空题:sizeof(num1) = __40__; sizeof(char2) = __5__;      strlen(char2) = __4__;sizeof(pChar3) = __4或8__;     strlen(pChar3) = __4__;sizeof(ptr1) = __4或8__;

解析

num1是一个数组,数组里面有1,2,3,4,0,0,0,0,0,0,所以sizeof(num1)最后的结果是410=40
char2是一个字符数组,存储的是一个字符串,字符数组是将字符串的每个字符分别作为一个元素存入,所以字符数组中的元素有a,b,c,d,\0这5个元素,sizeof(char2)的结果就是5
1=5,而strlen(char2)是不计算\0,所以结果就是4*1=4
pchar3是一个指针,指针大小要看电脑是32位还是64位,所以sizeof(pchar3)结果是4或者8,而strlen(pchar3)还是遇到\0就结束,所以结果是4 *1=4
ptr1也是指针,所以sizeof(ptr1)结果是4或者8

答案简答题

功能不同:
sizeof 用于获取数据类型或变量所占的内存大小(字节数)。
strlen 用于计算字符串的长度。
作用对象不同:
sizeof 可以作用于任何数据类型。
strlen 只能用于字符串(以 \0 结尾的 char*)。
计算时机不同:
sizeof 在编译时计算。
strlen 在运行时计算。
结果不同:
sizeof 返回的是字节数,表示变量或类型占用的内存大小。
strlen 返回的是字符数,表示字符串的长度(不包括 \0)

C语言中动态内存管理方式

void Test ()
{int* p1 = (int*) malloc(sizeof(int));free(p1);int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);free(p3 );
}

malloc/calloc/realloc的区别是什么?
答案:calloc会在malloc基础是进行初始化,realloc就是扩容机制不同
需要free(p2)吗
答案: int* p3 = (int*)realloc(p2, sizeof(int)*10)导致p3和p2的地址是相同的,所以释放p3也就是释放了p2

C++中动态内存管理

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

new/delete操作内置类型

void Test()
{// 动态申请一个int类型的空间int* ptr1 = new int;// 动态申请一个int类型的空间并初始化为10int* ptr2 = new int(10);// 动态申请10个int类型的空间int* ptr3 = new int[3];//动态申请10个int类型的空间并且初始化,后面没初始化的默认为0int*ptr4-new int[10]{1,2,3,4,5}delete ptr1;delete ptr2;delete[] ptr3;//new[]就需要delete[]delete[]ptr4
}

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用
new[]和delete[]

new和delete操作自定义类型

下面是C语言对自定义类型的一些写法

struct ListNode
{ListNode* _next;ListNode* _prev;int val;
};
struct ListNode* CreateListNode(int val)
{struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->_next = NULL;newnode->_prev = NULL;newnode->val = val;return newnode;
}

下面是C++的写法

struct ListNode
{ListNode* _next;ListNode* _prev;int _val;ListNode(int val):_next(nullptr), _prev(nullptr), _val(val){}
};
int main()
{ListNode* node1 = new ListNode(1);ListNode* node2 = new ListNode(2);ListNode* node3 = new ListNode(3);return 0;
}

在这里插入图片描述

C和C++动态内存管理区别

1:C++中的new和delete使用方法上变的更简洁了
2:C++中的new可以控制初始化了
3:C++中在申请自定义类型的空间时,new会开空间并且调用构造函数,delete会调用析构函数,而malloc与free不会
4:C++中new失败后会报错,不需要自己去检查

operator new与operator delete函数

operator new与operator delete函数

new和delete是用户进行动态内存申请和释放的操作符

operator new 和operator delete是系统提供的全局函数

new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间

operator new是对malloc封装,operator delete是对free封装

下面是用operator new的一个例子

int main()
{int* p1 = (int*)operator new(10 * 4);
}

但是不能这样写
在这里插入图片描述

new和delete的实现原理

内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

自定义类型

class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a;
};
int main()
{int* p1 = (int*)operator new(10 * 4);A* ptr = new A;return 0;
}

new开空间是会调用malloc,但是直接去调malloc开辟失败了会返回空,所以用operator new去封装malloc
下面是调用的一些过程
在这里插入图片描述
根据上面的过程,可以得出new反馈异常的环节是在operator new(这里只需要了解就行了,不用深究)

delete和上面类似,但是需要注意的是delete先调用析构函数,然后再调用operator delete,因为如果先调用operator delete就会先调用free,当空间释放后,再调用析构函数就没有意义了

在这里插入图片描述
总结
new的原理
调用operator new函数申请空间
在申请的空间上执行构造函数,完成对象的构造

delete的原理
在空间上执行析构函数,完成对象中资源的清理工作
调用operator delete函数释放对象的空间

new T[N]的原理
调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对
象空间的申请在申请的空间上执行N次构造函数

delete[]的原理
在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理,调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

拓展(只需要了解一下)

额外内存空间

来看看下面这个代码

class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a;
};
int main()
{A* ptr1 = new A;A* ptr2 = new A[10];delete ptr1;delete[]ptr2;return 0;
}

在这里插入图片描述
现在有一个疑问,delete[]ptr2为什么知道需要调用10次析构函数呢
其实是因为在new的时候会多开4个字节的空间去存储需要调用析构函数的次数,然后返回地址的时候直接返回40个字节的首地址
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
但是有时也不会多开4个字节出来

当我们把析构函数屏蔽掉后会发现他不会再多开4个字节,因为编译器会自动生成析构函数,但是编译器生成的习惯函数什么事都不干,所以就没必要多开4个字节,这只是其中的一种情况,还有一种是内置类型不会多开4个字节

new和delete更深入的问题

int main()
{int* p1 = new int[10];delete p1;return 0;
}

这段代码不会报错,并且没有内存泄漏,这是因为new[]底层是operator new[],再往深一层就是malloc,同理,delete的底层是operator delete,再往深一层就是free,既然malloc空间之后进行free掉了,那当然是没有问题的
在这里插入图片描述

class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a;
};
int main()
{A* p2 = new A[10];delete p2;return 0;
}

这段代码会崩溃
在这里插入图片描述
当我们多加一个[]后却可以正常运行
在这里插入图片描述

或者我们屏蔽掉析构函数也可以正常运行
在这里插入图片描述
这个问题其实也是额外空间的问题,A*p2=new A[10]是通过malloc开辟出来的空间,但是malloc开辟空间后返回的地址却不是这块空间开始的位置
在这里插入图片描述
用delete去释放内存的时候,我们发现当实现析构函数的时候,delete是从p2位置开始释放,但是p2指向的位置又不是malloc开辟空间最开始的位置,也就是说delete是从中间部分开始释放内存,而用来存储空间用的那4个字节是没有释放掉的,并且也不允许从中间位置开始去释放内存,所以这里崩溃的原因就是因为释放空间位置出错了

当在delete p2中间假设[]后就是告诉他p2前面还有4个字节是用来存储析构次数的,要从p2前面4个字节的位置开始去释放内存

析构函数屏蔽掉也可以运行的原因是因为当析构函数屏蔽后就没有个4个字节的空间去记录需要调用多少次析构函数,所以p2指向的位置就是最开始的位置

在这里插入图片描述

定位new表达式(placement-new)

定位new只需要简单了解一下就可以了
对于构造函数他并不像析构函数可以直接显示调用,因为构造函数一般都是可以自动调用,但是要注意析构函数最好不要显示调用,对于栈如果显示调用析构函数会涉及到析构两次的问题

下面的A a1直接就自动调用了构造函数,a1.~A()是显示调用析构函数
在这里插入图片描述
在这里插入图片描述

定位new的用法

new(指针)A(初始化值)
定位new主要用于一块已经开辟出来的空间但是没有初始化的情况,比如内存池…

常见的面试问题

malloc/free和new/delete的区别

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放

用法区别

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,
    如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需
    要捕获异常

底层区别

申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new
在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成
空间中资源的清理

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com