C/C++内存管理详解
Posted 小赵小赵福星高照~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++内存管理详解相关的知识,希望对你有一定的参考价值。
C/C++内存管理
文章目录
内存的静态分配和动态分配的区别主要是 两个:
一是时间不同。静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。
二是空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由函数malloc进行分配。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现。
C/C++内存分布
栈区:就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。函数调用建立栈帧,参数、函数中局部变量都存在栈帧中,栈是向下增长的,向下增长的意思是:从栈申请的内存地址会越来越小,在栈区当中是先使用高地址再使用低地址:
void f()
{
int b = 0;
cout << &b << endl;
}
int main( )
{
int a = 0;
cout << &a << endl;
f();
return 0;
}
可以看到先使用高地址再使用低地址。
堆区:
一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。理论上而言,后malloc的内存地址比先malloc的要大,但是也不一定,但是不一定,因为有可能下一次申请的是之前其他空间释放回来的。
数据段(全局数据,静态数据):
全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的(DATA段)和未初始化的(BSS段),在C++里面没有这个区分了,它们共同占用同一块内存区。
代码段:
可执行代码其实就是二进制指令,这里是存放二进制代码的。通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
内核空间:
是给操作系统用的
这几个区域划分的特点:
1、这几个区域中堆是很大的,比如有32位-4G空间,内核空间占用1G,而堆差不多就占用了快3G
2、实际上,栈是很小的,Linux下一般只有8M,所以递归调用深度太深会导致栈溢出
3、数据段和代码段也不是很大,因为没有多少数据
下面我们来看一下下面代码的数据是分别存储在哪里的?
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);
}
数据段 数据段 数据段 栈区 栈区 栈区 栈区 栈区 代码段 栈区 堆区
1、选择题
选项: A.栈 B.堆 C.数据段 D.代码段
globalVar在哪里?____ staticGlobalVar在哪里?____
staticVar在哪里?____ localVar在哪里?____
num1 在哪里?____
char2在哪里?____ *char2在哪里?___
pChar3在哪里?____ *pChar3在哪里?____
ptr1在哪里?____ *ptr1在哪里?____
globalVar在哪里?
globalVar是全局变量所以它在数据段
staticGlobalVar在哪里?
staticGlobalVar是静态变量所以它在数据段
staticVar在哪里?
staticVar是静态变量所以它在数据段
localVar在哪里?
localVar创建在test函数栈帧中,它是局部变量,在栈区
num1 在哪里?
num1是数组,在函数栈帧中创建,存储在栈区
char2在哪里?
char2是数组,在函数栈帧中创建,存储在栈区
*char2在哪里?
*char2拿到的是字符a的地址,因为char2是数组,里面的元素也是存储在栈帧里面的,所以*char2也在栈区
pchar3在哪里?
pchar3是指针变量,存储在栈区,它指向字符串的首字符地址
*pchar3在哪里?
*pchar3是常量字符a,常量字符串是存储在常量区的,就是在代码段
ptr1在哪里?
ptr1是指针变量,他存储在栈区
*ptr1在哪里?
ptr1指向的内存空间是malloc出来的,所以*ptr1是在堆区
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);
}
calloc等价于malloc+memest(0),开空间+初始化为0
realloc是对malloc或calloc的空间进行扩容
C++兼容C,c语言这一套动态内存申请与释放在C++中都可以用
C++中动态内存管理
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
注意:
malloc/free是库函数,而new/delete是操作符
void Test()
{
//C语言 malloc等是库函数
int* p1 = (int*)malloc(sizeof(int));
free(p1);
//C++ new/delete是操作符
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
delete ptr4;
delete ptr5;
}
这里new/delete和malloc/free有没有什么区别呢?
如果动态申请的对象是内置类型,用malloc和new没有区别,如果动态申请的对象是自定义类型,有区别,例如:
class A
{
public:
A(int a=0)
:_a(a)
{
cout<<"A()"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
}
private:
int _a;
};
int main()
{
//C语言自定义类型开辟空间
A* p3 = (A*)malloc(sizeof(A));//没有初始化
free(p3);
//C++自定义类型开辟空间
A* p4 = new A;//调用构造函数初始化
delete p4;//调用析构函数
}
new和delete不仅仅会开空间/释放空间,还会调用构造函数和析构函数
使用malloc没有初始化:
使用new进行了初始化:
这里其实是调用默认的构造函数进行了初始化,并且delete时调用了析构函数:
那我们想给new的对象传参怎么写呢?这样写:
A* p4 = new A(10);//调用构造函数初始化
我们还可以new数组:
int *ptr = new int[10];
delete[] ptr;
A *ptr1 = new A[10];//new了10个对象,调用了10次构造函数
delete[] ptr1;
注意在new数组时,delete时需要加[]
对于内置类型用malloc和new没什么区别,但是自定义类型,用malloc和new有区别,new和delete还会调用构造函数和析构函数,一定要匹配使用,否则可能会崩溃,在C++中,建议使用new和delete,malloc和free能做到的,new和delete都能做到,new和delete能做到的,malloc和free不一定能做到,并且new和delete更方便一些,为什么使用new和delete更方便呢?例如我们在写链表时:
struct ListNode
{
int _val;
ListNode* _next;
ListNode(int val):_val(val),_next(nullptr)
{}
};
用C创建节点并初始化它:
//C
ListNode* n1 = (ListNode*)malloc(sizeof(ListNode));
n1->val = 1;
n1->next = nullptr;
用C++创建节点并初始化它:
//C++
ListNode* n2 = new ListNode(1);//一次性完成了开辟空间和初始化
C++一次性就完成了开辟空间和初始化,所以new和delete更方便。
接下来我们来看一下new和delete到底时怎么样的原理:
operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
注意:
它们两个只是全局的库函数,它们并不是new和delete的重载
我们在new一个对象T时,其实编译器会这样做:
1、申请内存,调用operator new(底层其实是将malloc的封装实现)
2、调用构造函数
在delete时,编译器会这么做:
1、调用T的析构函数
2、调用operator delete(底层其实是将free的封装实现)
我们之前在使用malloc时,malloc失败会返回NULL:
int main()
{
//malloc失败,返回NULL
char* p1 = (char*)malloc((size_t)2*1024*1024*1024);
if(p1==NULL)
{
printf("malloc fail\\n");
}
else
{
printf("malloc success\\n");
}
return 0;
}
那么new操作符在new失败时会做什么处理呢?
int main()
{
char* p2 = new char[0x7fffffff];
//并没有执行
if(p2==NULL)
{
printf("new fail\\n");
}
else
{
printf("new success\\n");
}
return 0;
}
我们可以看到new失败时,并没有执行if语句就出错了,为什么呢?是因为new和malloc不一样,申请空间失败了,它会抛异常,下面我们用一段捕获异常的代码来说明:
int main()
{
//new和malloc不一样,申请空间失败了,会抛异常
try
{
char* p2 = new char[0x7fffffff];//如果出错抛异常,跳到捕获异常的位置,没有捕获会报错
}
catch(const exception& e)
{
cout<<e.what()<<endl;
}
}
operator new其实就是对malloc的封装,如果申请内存失败了,抛异常,封装malloc+抛异常,我们进行调试反汇编可以看到执行new时,他要调用operator new函数和A的构造函数:
有operator new,那么也有operator delete,operator deltete也是对free进行封装。
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
我们也可以直接用operator new和operator delete函数:
int* p3 = (int*)operator new(sizeof(int)*10);
operator delete(p3);
operator new与operator delete的类专属重载
假设我们写了一个链表的程序,怎么检测一下有没有ListNode的节点没有释放?
我们可以重载一个类ListNode专属的operator new和operator delete成员函数
我们在new一个ListNode时,那么申请空间就会调用专属的operator new,delete时,释放空间调用专属的operator delete,我们可以定义一个静态变量_count来记录,申请时_count++,释放_count–:
struct ListNode
{
int val;
struct ListNode* next;
static int _count;
ListNode(int x)
:val(x)
, next(nullptr)
{
cout << "ListNode()" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
void* operator new(size_t n)
{
++_count;
return ::operator new(n);
// 去内存池
}
void operator delete(void* p)
{
--_count;
return ::operator delete(p);
// 释放去内存池
}
};
int ListNode::_count = 0;
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* prev = NULL, *cur = head;
while (cur)
{
if (cur->val == val)
{
// 1、头删
// 2、中间删除
prev->next = cur->next;
delete cur;
cur = prev->next;
}
else
{
// 迭代往后走
prev = cur;
cur = cur->next;
}
}
return head;
}
// 检测一下有没有ListNode的节点没有释放
int main()
{
ListNode* n1 = new ListNode(1);
ListNode* n2 = new ListNode(2);
ListNode* n3 = new ListNode(2);
ListNode* n4 = new ListNode(3);
ListNode* n5 = new ListNode(4);
ListNode* n6 = new ListNode(2);
n1->next = n2;
n2->next = n3;
n3->next = n4;
n4->next = n5;
n5->next = n6;
n6->next = nullptr;
ListNode* list = removeElements(n1, 2);
cout <<"没有释放节点数量:" <<ListNode::_count << endl;
return 0;
}
这个在实际中并不常用,了解一下即可。
new和delete的实现原理
内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
自定义类型
- 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来释放空间
int main()
{
Stack st;
Stack* ps = new Stack;
delete ps;
retunr 0;
}
直接创建对象和new对象有什么区别呢?下面我们画图讲解:
定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
构造函数调用什么时候自动调用呢?
1、创建对象时
2、new一个对象时
//显式对一块空间调用构造函数初始化
class Test
{
public:
Test()
: _data(0)
{
cout<<"Test():"<<this<<endl;
}
~Test()
{
cout<<"~Test():"<<this<<endl;
}
private:
int _data;
};
void Test()
{
// pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
Test* pt = (Test*)malloc(sizeof(Test));
new(pt) Test; // 注意:如果Test类的构造函数有参数时,此处需要传参
}
pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行,显式对一块空间调用构造函数初始化,new(pt) Test;
如果我们复制一份a数组到另外一块空间b数组,怎么复制呢?
也许你想的是这样,通过一个循环来拷贝:
int main()
{
A a[5];
//复制一份a数组到另外一块空间b
A* pb1 = new A[5];
for(int i =0;i<5;++i)
{
b[i]=a[i];
}
cout<<endl;
//代价大 构造+赋值
return 0;
}
但是这样代价大,经过了构造+赋值重载,那么能不能直接构造呢?通过定位new表达式就可以直接构造:
int main()
{
A a[5];
//能不能直接构造?
A *pb = (A*)malloc(sizeof(A)*5);
for(int i =0;i<5;++i)
{
new(pb+i)A(a[i]);
}//只有构造
return 0;
}
常见面试题
malloc/free和new/delete的区别
1、特点和用法
malloc和free是函数,new和delete是操作符;malloc申请空间时,需要手动计算空间大小并传递,new只需在后面跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可;malloc的返回值是void*,在使用时需要强转,new不需要,因为new后面跟的是空间的类型
2、底层原理区别
申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数和析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
3、处理错误的方式
malloc申请空间失败时,返回NULL,因为使用时需要判空,new不需要,但是new需要捕获异常
内存泄漏
什么是内存泄漏呢?
在堆上申请了的空间,在我们不用了以后也没有释放,就存在内存泄漏,因为你不用了,也没有还给系统,别人也用不了。俗话说:占着茅坑不拉屎
int main()
{
char* p = new char[1024*1024*1024];
return 0;
}
运行结束后:
上面程序存在内存泄漏,一次泄漏1G,但是程序运行结束后,对我们系统也好像没有影响,好像会将我们申请的内存自动还回去,实际上一个进程正常结束后,会把映射的内存都会释放掉,所以上面的程序,我们没有主动释放,但是进程结束也释放,那么内存泄漏好像也没啥事,因为进程正常结束都会释放。
其实并不是,你在看看下面的场景:
1、要是进程没有正常结束呢!僵尸进程,就可能存在一些资源没有释放
2、长期运行的服务器程序。比如王者荣耀后台服务,长期运行,只有维护升级的时候才会停,内存泄漏会导致可用内存越来越少,程序越来越慢,甚至挂掉。事故
3、物联网设备:扫地机器人、冰箱等等,内存很小,也会经不起内存泄漏折腾
在C++中我们需要主动释放内存,而Java不需要主动释放内存,Java后台有垃圾回收器,接管了内存释放
那么我们如何预防内存泄漏呢?
1、智能指针
2、内存泄漏的检测工具
如何申请4G的内存呢?将操作系统换为64位就可以了:
int main()
{
try
{
char* p = new char[0x7fffffff];以上是关于C/C++内存管理详解的主要内容,如果未能解决你的问题,请参考以下文章