👉C / C++内存分布👈
从C语言的角度来看内存,可以将内存简单地划分为栈区、堆区、静态区和常量区。
而从操作系统的角度来说,将内存划分为内核空间、栈区、内存映射段、堆区、数据段(静态区)和代码段(常量区)。
那为什么操作系统要把内存划分为一个个区域呢(分段)?其实这样子划分,是为了更好地管理数据,提高效率。因为不同的数据有着不同的性质。那有什么性质呢?我们一起来看看。
栈区:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束这些内容也会自动被销毁。其特点是效率高,但空间大小有限。
堆:由 malloc 系列函数或 new 操作符分配的内存。其生命周期由 free 或delete 决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。
数据段(静态区):保存自动全局变量和 static 变量(包括 static 全局和局部变量)。静态区的内容在整个程序的生命周期内都存在,由编译器在编译的时候分配。
代码段(常量区):存储可执行代码(二进制指令代码)和只读常量,这个区域的数据是被硬件保护的,不可以修改里面的数据。
注:栈区是向下增长的,堆区是向上增长的。局部变量和堆区的数据不会提前创建,而是随着栈帧的创建而创建,只有全局数据和静态数据、常量会被提前创建。栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放。堆无法静态分配,只能动态分配。
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); } 1. 选择题: 选项 : A.栈 B.堆 C.数据段(静态区) D.代码段(常量区) globalVar在哪里?____ staticGlobalVar在哪里?____ staticVar在哪里?____ localVar在哪里?____ num1 在哪里?____ char2在哪里?____ *char2在哪里?___ pChar3在哪里?____ *pChar3在哪里?____ ptr1在哪里?____ *ptr1在哪里?____ 2. 填空题: sizeof(num1) = ____; sizeof(char2) = ____; strlen(char2) = ____; sizeof(pChar3) = ____; strlen(pChar3) = ____; sizeof(ptr1) = ____; 3. sizeof 和 strlen 区别?
大家可以想一想上面的题目的答案是什么,我把参考答案写在下面,供大家参考。
C C C A A A A A D A B
40 5 4 4/8 4 4/8
sizeof 是操作符,计算类型、变量或者表达式的大小,单位是字节;而 strlen 是一个库函数,求'\0'之前字符出现的个数,也就是求字符串的长度。
提示:sizeof(数组名),计算的是整个数组的大小。
大家可以根据下图,再想想以上的问题为什么是这个答案。在这里,我只解释一下易错的题目。为什么 *char2 在栈区,而 *pChar3 在数据段呢?字符数组 char2 是在栈区开辟空间的,那么该数组存储的数据也在栈区上。pChar3 存的是存在代码段中“abcd”的首元素地址,那么 *pChar3 就是字符 a,其是存在代码段的。
64 位操作系统虚拟进程地址空间有 2^24 TB,那我们的硬件能跟上吗?其实虚拟进程地址空间并不是硬件上真正的内存,操作系统会对虚拟进程空间进行分页。分页之后会有页表和物理内存,将虚拟内存和物理内存建立映射关系。
👉C语言中动态内存管理方式👈
void Test () { int* p1 = (int*) malloc(sizeof(int)); free(p1); // 1.malloc/calloc/realloc的区别是什么? int* p2 = (int*)calloc(4, sizeof (int)); int* p3 = (int*)realloc(p2, sizeof(int)*10); // 这里需要free(p2)吗?不需要 free(p3 ); }
【面试题】
- malloc/calloc/realloc的区别?
- malloc的实现原理? glibc中malloc实现原理
👉C++内存管理方式👈
C语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此 C++ 又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
new和delete操作内置类型
void Test() { // 动态申请一个int类型的空间,不会初始化 int* ptr4 = new int; // 释放空间 delete ptr4; ptr4 = nullptr; // 动态申请一个int类型的空间, 并初始化为10 int* ptr5 = new int(10); delete ptr5; ptr5 = nullptr; // 动态申请10个int的空间,不会初始化 int* ptr6 = new int[10]; delete[] ptr6; ptr6 = nullptr; // 动态申请10个int的空间,前四个初始化为1 2 3 4,后六个初始化为0 int* ptr7 = new int[10]{ 1,2,3,4 }; delete[] ptr7; ptr7 = nullptr; }
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],要匹配起来使用。对于内置类型,new 和 delete 相比于malloc 和 free,除了用法上的区别,没有其他区别。
new和delete操作自定义类型
注:对于自定义类型,new/delete 和 malloc/free 最大区别是 new/delete除了开空间,还会调用自定义类型的构造函数和析构函数,而 malloc/free 不会调用自定义类型的构造函数和析构函数。
有了 new 和delete,那么之前我们写的链表就可以这样子玩了。
#include <iostream> using namespace std; struct ListNode { ListNode* _next; int _val; ListNode(int val = 0) :_next(nullptr) ,_val(val) {} }; int main() { ListNode* n1 = new ListNode(1); ListNode* n2 = new ListNode(2); ListNode* n3 = new ListNode(3); ListNode* n4 = new ListNode(4); n1->_next = n2; n2->_next = n3; n3->_next = n4; ListNode* cur = n1; while (cur != nullptr) { cout << cur->_val << ' '; cur = cur->_next; } cout << endl; return 0; }
相比于C语言构建链表,C++ 构建链表是不是相当的简单,相当的好玩?
new和delete 的常见错误
new 和 delete 最常见的错误就是不匹配使用。如果不匹配使用,可能会发生意想不到的错误,也有可能不会发生错误。
不匹配使用会不会报错,这取决于 new 和 delete 的底层实现。而其底层实现取决于编译器,所以同一段代码在不同的编译器可能会报错,也可能不会报错。但是不报错并不意味着程序没有问题。
注:以下例子均在 VS2022 上运行。
下图的代码是正确的代码,调用了十次构造函数和析构函数。那如果将 delete[] 改成 delete,会发生什么呢?我们来看一下。
通过下图可以看到,程序直接崩溃挂掉了,并不是内存泄漏。因为内存泄漏不会一下子就会是程序挂掉。
申请对象数组,会调用构造函数10次,delete 由于没有使用 [],此时只会调用一次析构函数,但往往会引发程序崩溃。
我们将自定义类型 A 的析构函数屏蔽掉,再来看看程序会不会直接崩溃掉。
好像程序没有报错哦。那为什么会这个样子呢?那我们就需要了解底层机制的实现了。如果写上 [],编译器认为你申请了多个空间,它会在前面多申请一个空间来存储你申请空间的个数,释放这些内存的时候往前找出申请空间的个数,然后再来释放申请的内存空间。
所以 deletep[ ] 省略 [ ] 时,就会报错,因为释放的位置不对。而如果屏蔽掉自己写的析构函数,那么编译器会自动生成析构函数。编译器自己生成析构函数的话,就不会在前面多开一个字节来存储申请空间的个数,也不会去调用析构函数,所以程序就没有报错。
所以,下面的代码也会有问题。