C/C++内存分布
我们先来看下面的一段代码 :
int g_val=100; int g_unval; int main(int argc,char* argv[],char* env[]) { printf("code addr :%p\n",main); const char* p="hello"; printf("read only :%p\n",p); printf("global val :%p\n",&g_val); printf("global uninit val:%p\n",&g_unval); char* q1=(char*)malloc(10); char* q2=(char*)malloc(10); char* q3=(char*)malloc(10); char* q4=(char*)malloc(10); printf("heap addr :%p\n",q1); printf("heap addr :%p\n",q2); printf("heap addr :%p\n",q3); printf("heap addr :%p\n",q4); printf("stack addr :%p\n",&q1); printf("stack addr :%p\n",&q2); printf("stack addr :%p\n",&q3); printf("stack addr :%p\n",&q4); static int i=0; printf("static addr :%p\n",&i); printf("args addr :%p\n",argv[0]); printf("env addr :%p\n",env[0]); return 0; }
实际输出和对应的内存分布
我在Linux进程一文当中对内存分布和虚拟内存有详细讲解,不了解的小伙伴可以去看看这篇文章:
说明:
1.栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2.内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3.堆用于程序运行时动态内存分配,堆是可以上增长的。
4.数据段(初始化数据和未初始化数据区)–存储全局数据和静态数据。
5.代码段–可执行的代码/只读常量。
C语言中动态内存管理方式:malloc/calloc/realloc/free
1.malloc和free
C语言提供了一个动态内存开辟的函数:
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
1.如果开辟成功,则返回一个指向开辟好空间的指针。
2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
4.如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
free函数用来释放动态开辟的内存。
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2.如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。
2.calloc
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
1.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
2.与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
int main() { int* p = (int*)calloc(10, sizeof(int)); if (NULL != p) { } free(p); p = NULL; return 0; }
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
3.realloc
1.realloc函数的出现让动态内存管理更加灵活。
2.有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);
1.ptr 是要调整的内存地址
2.size 调整之后新大小
3.返回值为调整之后的内存起始位置。
4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
5.realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况,realloc函数的使用就要注意一些。
4.常见的动态内存错误
对NULL指针的解引用操作
void test() { int* p = (int*)malloc(INT_MAX / 4); *p = 20;//如果p的值是NULL,就会有问题 free(p); }
对动态开辟空间的越界访问
void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { exit(EXIT_FAILURE); } for (i = 0; i <= 10; i++) { *(p + i) = i;//当i是10的时候越界访问 } free(p); }
对非动态开辟内存使用free释放
void test() { int a = 10; int *p = &a; free(p); }
使用free释放一块动态开辟内存的一部分
void test() { int* p = (int*)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 }
对同一块动态内存多次释放
void test() { int* p = (int*)malloc(100); free(p); free(p);//重复释放 }
动态开辟内存忘记释放(内存泄漏)
void test() { int* p = (int*)malloc(100); if (NULL != p) { *p = 20; } } int main() { test(); while (1); }
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放。
C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
1.new/delete操作内置类型
void Test() { // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请10个int类型的空间 int* ptr6 = new int[3]; delete ptr4; delete ptr5; delete[] ptr6; // C++11支持 A* ptr7 = new A[2]{1,2}; A* ptr8 = new A[2]{ A(1), A(2) }; }
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],匹配起来使用。
2.new和delete操作自定义类型
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间 //还会调用构造函数和析构函数 A* p1 = (A*)malloc(sizeof(A)); A* p2 = new A(1); free(p1); delete p2; // 内置类型是几乎是一样的 int* p3 = (int*)malloc(sizeof(int)); // C int* p4 = new int; free(p3); delete p4; A* p5 = (A*)malloc(sizeof(A) * 10); A* p6 = new A[10]; free(p5); delete[] p6; return 0; }
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
3.new和malloc使用上的区别
以下代码在32位环境下实验,在64位环境下需开辟更大空间
malloc
#include <iostream> using namespace std; int main() { // 失败返回NULL char* p1 = (char*)malloc(1024u*1024u*1024u*2 - 1); printf("%p\n", p1); return 0; }
new
#include <iostream> using namespace std; int main() { char* p2 = new char[1024u * 1024u * 1024u * 2 - 1]; printf("%p\n", p2); delete(p2); return 0; }
通过两段代码我们不难发现,两者最大的区别就是在开辟空间失败后,malloc只能返回NULL,而new抛异常,这样我们更容易发现问题存在,当然这里还不算最大的区别和其最大作用体现,他真正的作用体现在类中,下面我们会提到。
operator new与operator delete函数
1.operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。(和运算符重载没关系)
我们可以看看上一段函数new和delete函数在调用时的反汇编代码
operator new与operator delete函数定义部分源码
/* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间 失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否 则抛异常。 */ 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 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。==operator delete 最终是通过free来释放空间的。 ==
2.重载operator new与operator delete函数
没错,operator new与operator delete函数还可以自己进行重载
#include <iostream> using namespace std; class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; //重载operator new,在申请空间时:打印在哪个文件、哪个函数、第多少行,申请了多少个字节 void* operator new(size_t size, const char* fileName, const char* funcName, size_t lineNo) { void* p = ::operator new(size); cout <<"new:"<< fileName << "||" << funcName << "||" << lineNo << "||" << p << "||" << size << endl; return p; } // 重载operator delete,在释放空间时:打印再那个文件、哪个函数、第多少行释放 void operator delete(void* p, const char* fileName, const char* funcName, size_t lineNo) { cout <<"delete:"<<fileName << "||" << funcName << "||" << lineNo << "||" << p << endl; ::operator delete(p); } //#ifdef _DEBUG #define new new(__FILE__, __FUNCTION__, __LINE__) #define delete(p) operator delete(p, __FILE__, __FUNCTION__, __LINE__) //#endif int main() { A* p1 = new A; delete(p1); return 0; }
可以看到我们重载的new是会自动调用构造函数的,但是delete函数并没有调用析构函数,这是因为我们在重载的时候,宏定义改变了其调用形式,导致编译时其不认为还有delete函数的机制。
3.实现一个类专属的operator new和operator delete
#include <iostream> using namespace std; struct ListNode { int _val; ListNode* _next; // 内存池 static allocator<ListNode> alloc; void* operator new(size_t n) { cout << "operator new -> STL内存池allocator申请" << endl; void* obj = alloc.allocate(1); return obj; } void operator delete(void* ptr) { cout << "operator delete -> STL内存池allocator申请" << endl; alloc.deallocate((ListNode*)ptr, 1); } struct ListNode(int val) :_val(val) , _next(nullptr) {} }; //allocator在STL后才会提到,这里我们先不探讨它 allocator<ListNode> ListNode::alloc; int main() { // 频繁申请ListNode. 想提高效率 -- 申请ListNode时,不去malloc,而是自己定制内存池 ListNode* node1 = new ListNode(1); delete node1; return 0; }
这里用到了allocator,提到了内存池的概念,这个要到后面的STL才会学习,这里我们只是用这个做一个演示。利用operator new和operator delete的这种特性,我们可以为每一个类打造属于自己专属的operator new和operator delete,彼此互不干扰,且使用方式也是正常调用,如果出现内存泄漏我们也可以很好的观察到。