一、C/C++内存分布
从用途和存储的角度来看,在C/C++程序中有局部数据、静态数据、全局数据、常量数据、动态申请的数据五种主要的数据,各种数据的特点如下:
局部数据:随用随创建,存储在栈区,作用域只在局部,生命周期在局部,出了作用域就销毁。
静态数据:存储在数据段,作用域只在局部或当前文件,生命周期伴随程序“一生”。
全局数据:存储在数据段,作用域在全局,生命周期伴随程序“一生”。
常量数据:存储在代码段。
动态申请的数据:存储在堆区。
小Tips:这里需要注意char char2[] = "abcd";,"abcd"是常量字符串,存储在代码段,但是char2只是一个局部域的数组,它会把这个常量字符串拷贝一份,存储在栈区,然后char2是数组名,表示数组首元素的地址,这里表示的就是拷贝过来存储在栈区的字符串的首元素地址,并不是表示代码段中常量字符串的首元素地址。其次,const并不会改变变量的存储区域,因为pChar3是一个局部变量,所以它还是存储在栈区,前面加const修饰,是因为它存的是代码段中常量字符串的首地址。可见只要是在局部创建的变量,只要前面没有加static修饰,那这个变量一定是存储在栈区。
📖总结:
栈:又叫堆栈,用于存放非静态局部变量、函数参数、返回值等等,栈是向下生长的。
内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
堆:用于程序运行时动态内存分配,堆是向上增长的。
数据段:用于存储全局数据和静态数据。
代码段:用于存储可执行的代码/只读的常量。
小Tips:数据段也可以叫做静态区,代码段也可以叫做常量区。其中数据段和代码段是站在操作系统的角度进行划分的,而静态区和常量区是站在语言的角度进行划分的。
二、C++内存管理方式
在学习C语言时,提到的4个内存管理函数malloc、realloc、calloc、free大家还记得嘛👀?忘了的话可以走传送门,回去看看。在C++中我们任然可以使用C语言中的内存管理方式,但有些地方就无能为力了,而且使用起来比较麻烦,因此C++中提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
2.1 new和delete操作内置类型
void Test() { // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请10个int类型的空间,不初始化 int* ptr6 = new int[10]; // 动态申请10个int类型的空间,并初始化前三个 int* ptr7 = new int[10]{1,2,3}; delete ptr4; delete ptr5; delete[] ptr6; delete[] ptr7; }
小Tips:new对内置类型来说,和C语言一样,只是纯粹的开空间,对于动态申请到的空间,如果不进行人为初始化的话,里面存的都是随机值。动态申请并初始化,类型后面跟的是圆括号(),动态申请多个连续空间,类型后面跟的是方括号[],如果要对这多个连续的空间初始化,可以在[]的后面跟{},里面是初始化的数据,可以只初始化一部分,后面默认补0。申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间使用new[]和delete[]
2.2 new和delete操作自定义类型
📖C语言的玩法
先来回顾一下,C语言中针对自定义类型是怎么玩的,以动态申请一个链表节点为例:
//节点类型 struct List_Node { int _val; struct List_Node* _next; }; //申请节点的函数 List_Node* BuyListNode(int x) { List_Node* tmp = (List_Node*)malloc(sizeof(List_Node)); if (nullptr == tmp) { perror("malloc"); exit(-1); } tmp->_val = x; tmp->_next = nullptr; } int main() { //动态申请三个节点 List_Node* p1 = BuyListNode(1); List_Node* p2 = BuyListNode(2); List_Node* p3 = BuyListNode(3); return 0; }
如上面的代码所示,在C语言中,要想为自定义类型动态申请一块空间,我们单独封装了一个函数BuyListNode,这个函数完成了空间申请、合理性检查、初始化等工作,然后通过调用这个函数实现自定义类型的动态内存申请。
📖C++的玩法
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { //通过new动态申请一块空间,存储A类型变量 A* p2 = new A(1); delete p2; //通过new动态申请3个连续空间,存储3个A类型的数据 A* p6 = new A[3]; delete[] p6; return 0; }
通过打印结果可以看出,new/delete对于自定义类型出了开空间,还会调用构造函数和析构函数。
小Tips:上面代码中A* p2 = new A(1);,我们进行了人为的初始化,即用圆括号传了一个1,由于new会去调用构造函数,所以这个1最终会被A类构造函数的形参接受,用于初始化对象中的_a成员变量;而A* p6 = new A[3];申请的3个连续空间(数组),我们并没有进行人为的初始化,这在当前环境下也是可以的,因为构造函数的形参有缺省值,属于默认构造函数,new去调用构造函数的时候,可以不需要传实参直接使用缺省值,但是如果A类没有默认构造函数,那我们在使用new动态申请空间的时候,一定要进行人为的传值初始化。
如果A类没有默认构造函数,我们在new的时候就需要人为的传参进行初始化,像下面这样。
//通过new动态申请一块空间,存储A类型变量 A* p2 = new A(1); //通过new动态申请10个连续空间,存储10个A类型的数据 A* p6 = new A[3]{ 1, 2, 3 };//通过隐式类型转换去初始化 A* p6 = new A[3]{ A(1), A(2), A(3) };//通过匿名对象去初始化 //A* p6 = new A[3]{ A(1), A(2)};//申请了三块空间,只初始化了两个,这是不被允许的
小Tips:对于动态申请的自定义类型数组,如果该自定义类型没有默认构造,则需要进行人为的初始化,并且申请了几块空间就要初始化几个,不像动态申请的内置类型数组,可以只初始化前面一部分,后面默认是0。此外,如果该类的构造函数有多个形参(超过一个),就不支持隐式类型转换去初始化了,可以继续使用匿名对象去初始化。
📖一些建议
这里建议大家malooc、free、new、delete、new[]、delete[]不要混用,严格按照下面的对应关系去使用:
malloc——free
new——delete
new[]——delete[]
对于内置类型,混用了可能没什么,但是对于自定义类型可就不一定了。
三、operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过调用operator delete全局函数来释放空间。
小Tips:operator new和operator delete并不是简单的运算符重载哦。
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void *p; while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // report no memory // 如果申请内存失败了,这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); } /* operator delete: 该函数最终是通过free来释放空间的 */ void operator delete(void *pUserData) { _CrtMemBlockHeader * pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData == NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead = pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); _free_dbg( pUserData, pHead->nBlockUse ); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; } /* free的实现 */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)
上面就是operator new和operator delete的实现,从中我们可以发现,operator new实际上也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete最终是通过free来释放空间的。
小Tips:operator new、operator delete和malloc、free在功能上相似,都是到堆上申请空间,然后释放空间,它们的区别在于,前者失败了会抛异常,而后者失败了是返回错误码,通过返回值来表示这一块有问题。抛异常是面向对象语言的一大特色,关于异常春人会在后面的文章中给大家分享,感兴趣的友友可以关注一下春人哟。