1. C/C++内存分布
我们来看下面的一段代码和相关问题
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
在哪里?__C__ ????????
staticGlobalVar在哪里?
__C__
staticVar
在哪里?__C__? ? ? ? ??
localVar在哪里?
__A__
num1
在哪里?
__A__
char2
在哪里?__A__? ? ? ? ? ??
*
char2在哪里?__A__
pChar3
在哪里?__A__ ????????
*
pChar3
在哪里?
__D__
ptr1
在哪里?__A__ ????????? ? ??
*
ptr1
在哪里?
__B__
2.
填空题:
sizeof
(
num1
)
=
__40__
;
sizeof
(
char2
)
=
__5__
;? ? ? ? ? ??
strlen
(
char2
)
=
__4__
;
sizeof
(
pChar3
)
=
__4/8__
;????????
strlen
(
pChar3
)
=
__4__
;
sizeof
(
ptr1
)
=
__4/8__
;
注:
1.下面是c/c++中程序内存区域划分
2.代码char char2[] = "abcd"是在静态区创建字符串"abcd"并在栈区创建数组char2,将静态区的"abcd"字符串拷贝到栈区的数组char2的空间中。所以*char2也是在栈区的
2.C语言中动态内存管理方式
malloc/calloc/realloc
和
free
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);
}
注:这里不用free(p2)了,因为后面realloc函数:如果p2地址后面空间足够,就在后面拓展空间,并将空间首地址赋值给p3,此时p2和p3指向同一地址,free(p3)就是把p2指向的空间释放了;如果p2地址后面空间不够,那么realloc函数会找一块空间足够的内存空间,将空间首地址赋值给p3,将之前p2指向空间的数据拷贝过来,并自动释放之前p2指向空间。
综上,不用free(p2)
【思考题】
malloc/calloc/realloc的区别?
3.C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:
通过new和delete操作符进行动态内存管理
3.1.new/delete操作内置类型?
如果不看返回值的话,对于内置类型而言,用malloc和new,free和delete除了用法不同,没有什么区别
int main()
{
int* p1 = new int; // new1个int对象
int* p2 = new int[10]; // new10个int对象
int* p3 = new int(10); // new一个int对象,初始化成10
int* p4 = new int[10]{ 10, 1, 2, 3 }; // new10个int对象,初始化成{}中的值
//c++98不支持这种方式,c++11才开始支持这种方式
// 不匹配,可能会报错,可能不会报错。建议一定要匹配
delete p1;
delete[] p2;
delete p3;
delete[] p4;
return 0;
}
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]
如果不看返回值的话,对于内置类型而言,用malloc和new,free和delete,除了用法不同,没有什么区别,他们区别在于自定义类型
3.2.new和delete操作自定义类型
对于自定义类型, malloc只开空间,new开空间+调用构造函数初始化,free只释放空间,delete调用析构函数清理资源 + 释放空间
场景一:
c语言使用malloc函数创建一个链表结点的代码:
c++使用new创建一个链表结点的代码:
注:如果在很多地方我们都需要访问一个类的成员时(例如上面的ListNode),我们可以使用struct创建类,里面成员变量都是公有,类外面可以使用,如下图所示
场景二:
c++建立带头双向链表:
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
ListNode(int val = 0)
:_next(nullptr)
, _prev(nullptr)
, _val(val)
{}
};
class List
{
public:
List()
{
_head = new ListNode;
_head->_next = _head;
_head->_prev = _head;
}
ListNode* Find(int x);
private:
ListNode* _head;
};
int main()
{
List lt;
return 0;
}
注:
1.这样写创建lt对象的时候就自动完成了申请空间和所有的初始化工作
建立对象lt,首先调用List的默认构造函数,_head = new ListNode首先创建一块ListNode类大小的空间给_head,并且调用ListNode的默认构造函数对这块空间初始化,即_next和_prev设置为空,_val设置为0,然后_head->_next = _head和_head->_prev = _head,完成了建立双向带头链表的所有初始化工作
场景三:
注:
1.上面的代码Stack* ps2 = new Stack,首先开辟Stack类大小的空间,然后调用stack的构造函数进行初始化,构造函数初始化的代码_a = new int[capacity],又会new开辟空间给成员变量_a;delete ps2首先调用stack的析构函数,析构函数中代码delete[] _a;释放_a指向的空间,析构函数执行完后,再释放ps2指向的空间
2.malloc的功能是开辟空间,new的功能是开空间+调用构造函数初始化,free的功能是释放空间,delete的功能是调用析构函数清理资源 + 释放空间
场景四:
class Stack
{
public:
Stack(int capacity = 10)
{
_a = new int[capacity];
_capacity = capacity;
_top = 0;
}
~Stack()
{
delete[] _a;
_capacity = 0;
_top = 0;
}
void Push(int x)
{}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue {
public:
MyQueue()
: _pushST(4)
, _popST(4)
{}
private:
Stack _pushST;
Stack _popST;
};
int main()
{
MyQueue* obj = new MyQueue;
delete obj;
return 0;
}
注:
1.上面代码的关系图如下所示
2.代码MyQueue* obj = new MyQueue创建一个MyQueue大小的内存空间,将空间首地址给到obj指针,并且调用MyQueue的构造函数初始化内存空间,MyQueue里面有显式的默认构造函数,系统调用MyQueue的默认构造函数,_pushST(4)和_popST(4)分别调用Stack的默认构造函数,并将实参4传给Stack的默认构造函数的形参,Stack的默认构造函数_a = new int[capacity](其中形参capacity为传过来的4),又会去创建一块内存空间,将内存空间的首地址给_a指针,Stack的默认构造函数执行完后,MyQueue* obj = new MyQueue语句执行完毕
代码delete obj首先会去调用MyQueue的析构函数,MyQueue里面没有显式的析构函数,系统调用MyQueue自动生成的默认析构函数,MyQueue自动生成的默认析构函数对于自定义类型Stack _pushST和Stack _popST去调用其自己的默认析构函数,因此分别去调用Stack的默认析构函数,Stack的默认构造函数delete[] _a将_a指向的内存空间释放,Stack的默认构造函数执行完后,释放obj指针指向的内存空间,delete obj语句执行完毕
class Stack
{
public:
Stack(int capacity = 10)
{
_a = new int[capacity];
_capacity = capacity;
_top = 0;
}
~Stack()
{
delete[] _a;
_capacity = 0;
_top = 0;
}
void Push(int x)
{}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue {
private:
Stack _pushST;
Stack _popST;
};
int main()
{
MyQueue* obj = new MyQueue;
delete obj;
return 0;
}
注:
1.上面代码的关系图如下所示
2.代码MyQueue* obj = new MyQueue创建一个MyQueue大小的内存空间,将空间首地址给到obj指针,并且调用MyQueue的构造函数初始化内存空间,MyQueue里面没有显式的默认构造函数,系统调用MyQueue自动生成的默认构造函数,MyQueue自动生成的默认构造函数对于自定义类型Stack _pushST和Stack _popST去调用其自己的默认构造函数,因此分别去调用Stack的默认构造函数,Stack的默认构造函数_a = new int[capacity]又会去创建一块内存空间,将内存空间的首地址给_a指针,Stack的默认构造函数执行完后,MyQueue* obj = new MyQueue语句执行完毕
代码delete obj首先会去调用MyQueue的析构函数,MyQueue里面没有显式的析构函数,系统调用MyQueue自动生成的默认析构函数,MyQueue自动生成的默认析构函数对于自定义类型Stack _pushST和Stack _popST去调用其自己的默认析构函数,因此分别去调用Stack的默认析构函数,Stack的默认构造函数delete[] _a将_a指向的内存空间释放,Stack的默认构造函数执行完后,释放obj指针指向的内存空间,delete obj语句执行完毕
new和malloc返回值:
malloc失败,返回空指针,new失败,抛异常,如下图所示
注:其实new失败抛了异常,我们应该去捕获,捕获的方式如下图所示,这里后面会讲,现在知道即可
4.operator new与operator delete函数
4.1.operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator 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来释放空间的
注:
1.operator new封装了malloc,如果malloc失败则抛异常
2.operator delete封装了free。上面的代码我们可以看到operator delete里面调用的其实是_free_dbg,我们看后面的free的实现可以看到,free就是_free_dbg,因此可以说operator delete封装了free
其实从用法和功能的角度来讲,operator new函数、operator delete函数和malloc函数、free函数基本没有区别,有一点不同的就是malloc需要检查失败而operator new不用检查失败,如下图所示
其实operator new函数与operator delete函数c++定义出来不是给我们用的,他们没有直接价值,但是他们有间接价值,他们是new和delete的底层原理。new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间
如下图所示当编译器遇到Stack* ps3 = new Stack代码,?会翻译成两步:
1.call operator new 调用operator delete函数
2.call Stack 调用类里面的默认构造函数
因此new、operator new、malloc的关系和delete、operator delete、free的大致关系如下图所示
4.2.operator new与operator delete的类专属重载
有些情况下,需要不停的向堆申请空间释放空间,如下图所示,如果输入一个很大的n值,那么不停的链表插入,向堆区new申请空间,然后Clear释放空间,再如果输入一个很大的n值继续不停的插入向堆区申请空间......
这样一次申请空间,new会调用operator new和构造函数,operator new调用malloc,如果不停的向堆区申请和释放空间,执行效率不高
池化技术:
我们可以申请内存池,在内存池中申请和释放空间,内存池可以认为比堆更近,申请释放空间更快,可以提高效率
如下图所示,在new的类中显式的写operator new和operator delete函数(new ListNode创建ListNode类大小的空间,就在ListNode类里面写operator new和operator delete函数),那么new不再去调用全局的operator new函数而是优先调用new的类中的operator new函数,delete该类的时候同理不再去调用全局的operator delete函数而是优先调用类中的operator delete函数
下面代码new的类中的operator new函数里面写的代码就是申请内存池空间,operator delete函数里面写的代码就是释放内存池空间(内存池相关内容后面会讲,我们只需要理解这两句代码是在内存池申请和释放空间即可)
5.new和delete的实现原理
5.1.内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
5.2.自定义类型
new的原理
1. 调用operator new函数申请空间
2. 在申请的空间上执行构造函数,完成对象的构造
delete的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用operator delete函数释放对象的空间
new T[N]的原理
1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
2. 在申请的空间上执行N次构造函数
delete[]的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
如下图所示代码,如果new和delete的形式不匹配,对于int类型没有报错,而对于Stack类型会报错,因此在写的时候,new和delete的形式一定要匹配上
6. 定位new表达式(placement-new)
定位new表达式是在
已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者
new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表的参数
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化
如下图所示,下图一是初始化前,下图二是初始化后,这样下图的代码Stack* obj = (Stack*)operator new(sizeof(Stack))和new(obj)Stack(4)就等价于Stack* obj = new Stack(4)
7.常见面试题
7.1.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需要捕获异常
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
7.2.内存泄漏
7.2.1.什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏其实是指针丢了。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
7.2.2.内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏:
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
7.2.3.如何检测内存泄漏
7.2.4.如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
7.3.如何一次在堆上申请4G的内存
// 将程序编译成x64的进程,运行下面的程序试试?
#include <iostream>
using namespace std;
int main()
{
void* p = new char[0xfffffffful];
cout << "new:" << p << endl;
return 0;
}
|