C++入门(三)之内存管理
C/C++内存分布C/C++中程序内存分布ps: y以下内存是以32位程序下,linux系统讨论这块叫进程虚拟地址空间(大约4G),每个程序都有这个空间,哪块要存数据,就要跟物理内存(电脑的实际内存)进行建立映射。平时在程序里看到的地址都是这块空间的地址,程序用那块,他就会建立那块的映射栈,静态区,常量区,这块的空间的生命周期都是自动控制的堆:程序运行过程中按需求,申请和释放空间,比如我们实现链表
目录
四、operator new与operator delete函数
operator new与operator delete函数的底层实现(了解)
operator new与operator delete的类专属重载(了解)
六、定位new表达式(placement-new) (了解)
一、C/C++中程序内存分布
ps: y以下内存是以32位程序下,linux系统讨论
这块叫进程虚拟地址空间(大约4G),每个程序都有这个空间,哪块要存数据,就要跟物理内存(电脑的实际内存)进行建立映射。平时在程序里看到的地址都是这块空间的地址,程序用那块,他就会建立那块的映射
栈,静态区,常量区,这块的空间的生命周期都是自动控制的
堆:程序运行过程中按需求,申请和释放空间,比如我们实现链表,数组栈等等,都是在堆开空间。堆这块空间的生命周期是手动控制的。
ps:栈可以通过函数_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 };
int num2[] = { 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 . 代码段(常量区)(1)globalVar 在哪里? ____ (2) staticGlobalVar 在哪里? ____(3)staticVar 在哪里? ____ (4) localVar 在哪里? ____(5)num1 在哪里? ____答案:C,C,C,A,A;解析:globalVar是全局变量,在静态区;staticGlobalVar,staticVar都有static修饰,在静态区;localVar,num1 都是局部变量,在栈上。(1)char2 在哪里? ____(2) * char2 在哪里? ___(3)pChar3 在哪里? ____ (4) * pChar3 在哪里? ____(5)ptr1 在哪里? ____ (6) * ptr1 在哪里? ____答案:A,A,A,D,A,B
解析:
在栈帧上定义了pchar3,ptr1,他俩是指针, pchar3存的是“abcd”这块常量的地址。ptr1是在堆上开了块空间,存的是这块空间首元素的地址。char2在栈开了块空间,因为它是个数组,然后把“abcd”这块常量,拷贝到了空间中去。所以解引用char2还是在栈上,解引用pchar3就在常量区,解引用ptr1就在堆上。
ps:指针在全局定义的它就在静态区,在局部定义的就在栈上,看在哪定义。局部变量与类型是没有关系的。
2.填空题(1)sizeof ( num1 ) = ____ ;(2) sizeof ( num2 ) = ____ ;(3)sizeof ( char2 ) = ____ ; (4) strlen ( char2 ) = ____ ;(5)sizeof ( pChar3 ) = ____ ;(6) strlen ( pChar3 ) = ____ ;(7)sizeof ( ptr1 ) = ____ ;答案:40,16,5,4,4/8,4,4/8
解析:num1 int类型开了10个空间,一共40个字节; num2 int类型开了4个空间;
char2,"abcd\0",\0,占空间但是不算长度,sizeof计算空间,strlen计算长度;
32位下指针4字节,64位下,指针8个字节
二、C语言中动态内存管理方式
malloc/calloc/realloc和free
malloc
函数原型:void* malloc ( size_t size );这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
free
函数原型:void free ( void* ptr );free 函数用来释放动态开辟的内存。
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
calloc
函数原型:void* calloc ( size_t num , size_t size );
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
realloc
函数原型:void* realloc ( void* ptr , size_t size );
- ptr 是要调整的内存地址
- size 调整之后新大小
- realloc函数的出现让动态内存管理更加灵活。
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存, 我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
三、C++内存管理方式
delete的用法:
另外两种相匹配对应的:
new/delete操作内置类型
动态申请int和5个int数组,
int main()
{
//动态申请int和5个int数组 函数和关键字
//总结 malloc/free 和 new/delete 对于内置类型来讲没有本质区别,只有用法上的区别
int* p1 = (int*)malloc(sizeof(int));
int* p2 = (int*)malloc(sizeof(int) * 5);
int* p3 = new int;
int* p4 = new int[5]; //动态申请5个int空间
free(p1);
free(p2);
delete p3;
delete[]p4;
p1 = nullptr;
p2 = nullptr;
p3 = nullptr;
p4 = nullptr;
}
malloc/free 和 new/delete 对于内置类型来讲没有本质区别,只有用法上的区别
但要注意的是:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和 delete[ ]
PS:C++98不支持初始化new的数组,但是C++11支持用{}初始化
int main() { //C++98不支持初始化new的数组,C++11支持用{}初始化 int* p = new int[5]{ 1,2,3,4,5 }; }
小问题:
这两个有区别吗?
int main() { int* p1 = new int[5]; int* p2 = new int(5); }
答案是有的,第一个p1是动态申请5个int空间,第二个p2是动态申请一个int空间,并将这块空间初始化成5。
对于自定义类型
对于这样一个类A
class A
{
public:
A(int a=0):_a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
如果我们想要去申请自定义类型的空间时,我们可以使用malloc或者new,那么他俩有啥区别呢?
同样是动态申请单个A对象,和5个A对象数组
int main() { //动态申请单个A对象,和5个A对象数组 A* p1 = (A*)malloc(sizeof(A)); A* p2 = (A*)malloc(sizeof(A) * 5); //在堆上申请空间+调用构造函数初始化对象 A* p3 = new A; A* p4 = new A[5]; }
通过执行结果监视我们发现malloc出来的啥都没干,new出来的调用了构造函数去初始化了对象,一共调用了6次构造函数。
同样对于free与delete
int main() { //动态申请单个A对象,和5个A对象数组 A* p1 = (A*)malloc(sizeof(A)); A* p2 = (A*)malloc(sizeof(A) * 5); //在堆上申请空间+调用构造函数初始化对象 A* p3 = new A; A* p4 = new A[5]; free(p1); free(p2); delete p3; delete[]p4; }
free就是释放掉了空间,delete除了释放空间,而且还调用了析构函数
还需要注意的是:malloc和free ,new和delete 要匹配使用,不要混合使用,否则可能会崩溃。
同样new 和delete 匹配 ,new[ ] 和delete [ ] 匹配
eg:
如果类中没有默认构造函数,申请空间时也会报错。
解决方法:
1.0 提供默认构造函数
2.0 单个用括号初始化,数组用{ }初始化
A* p3 = new A(100); A* p4 = new A[5]{1,2,3,4,5};
关于调用自定义类型的实例
对于这样一个栈类
class Stack
{
public:
Stack(int capacity =4)
:_top(0)
,_capacity(capacity)
{
_a = new int[capacity];
}
~Stack()
{
delete[] _a;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1; //st1在栈上
//搞一个自己可以控制生明周期的
Stack* pst2 = new Stack;//开空间+构造函数初始化
//对象的指针
delete pst2; //析构函数(清理对象中的资源)+释放空间
return 0;
}
pst2指向动态开辟的空间,这个空间上有三个数据,一个指针,两个整形,紧接着Stack的构造函数又要开空间,开capacity个空间,第一个空间是给这个栈对象开空间,第二个空间是构造函数给这个栈对象里面的资源进行开空间。
delete pst2 第一步调用它的析构函数,清理调给_a开的空间,第二步释放掉栈对象本身的空间
总结下:new 在堆上申请空间+调用构造函数初始化对象
delete 先调用指针类型的析构函数+给堆上释放空间
C++提出new和delete,主要是解决两个问题
1.0自定义类型对象自动申请的时候,初始化和清理的问题。new和delete会调用构造函数和析构函数
2.0 new失败了以后要求抛异常,这样才符合面向对象语言的出错处理机制。
ps:delete和free一般不会失败,如果失败了,都是释放空间上存在越界或者释放指针位置不对。
四、operator new与operator delete函数
对于开空间失败而言,C与C++不同之处
面向对象的语言,处理错误的方式一般是抛异常,C++中也要求出错抛异常--try catch
面向过程的语言,处理错误的方式是返回值+代码解决 (C语言)
C语言失败是将指针置空,C++是抛异常 【异常必须被捕获,不捕获就会报错。C++用tyr与catch实现该操作】
C++捕获异常操作:结合try与catch使用,会输出 bad allocation
int main() { char* p1 = (char*)malloc(0x7fffffff); //2g if (p1 == nullptr) { printf("malloc fail\n"); } try { char* p2 = new char[0x7fffffff]; } catch (const exception&e) { cout << e.what() << endl; } return 0; }
operator new
针对这个栈类实例
class Stack
{
public:
Stack(int capacity =4)
:_top(0)
,_capacity(capacity)
{
_a = new int[capacity];
}
~Stack()
{
delete[] _a;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1; //st1在栈上
//搞一个自己可以控制生明周期的
Stack* pst2 = new Stack;//开空间+构造函数初始化
//对象的指针
delete pst2; //析构函数(清理对象中的资源)+释放空间
return 0;
}
我们通过反汇编观察 new和delete的底层
通过反汇编我们发现给pst2开空间底层是调用了operator new这个函数
operator new 又是什么呢?
实际上它就是一个库函数,并不是new的重载,我们自己可以用它去开空间
它是不会去调用构造函数的
实际上它与malloc的用法是完全一样
这个函数并不是直接提供给用户用的,实际上它是要对malloc的封装。operator new中调用malloc申请内存,失败以后,改为抛异常处理错误,这样才符合C++面向对象语言处理错误的方式
如果说没有operator new ,在调用new(因为new是操作符)的时候,就会被转换成指令 call malloc +call 构造函数,调用构造函数一般而言不会失败,调用malloc假如失败,就返回0,那么new也是返回0,但是这样就不符合C++处理错误的方式,所以C++就增加了operator new ,专门给new 用。
可以简单理解为:
new=operator new +构造函数
operator new = malloc+抛异常
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与operator delete的类专属重载(了解)
针对这个list讲解
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _data;
ListNode(int val)
:_next(nullptr)
, _prev(nullptr)
, _data(val)
{}
void* operator new(size_t n) //这时候我就不在malloc申请
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1); //stl中的内存池--空间配置器
cout << "memory pool allocate" << endl;
return p;
}
void operator delete(void* p)
{
allocator<ListNode>().deallocate((ListNode*)p, 1);
cout << "memory pool deallocate" << endl;
}
};
class List
{
public:
List()
{
_head = new ListNode(-1);
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(int val)
{
ListNode* newnode = new ListNode(val); //这调用的是全局的operator new
ListNode* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
~List()
{
ListNode* cur = _head->_next;
while (cur != _head)
{
ListNode* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
private:
ListNode* _head;
};
int main()
{
List l;
l.PushBack(1);
l.PushBack(2);
l.PushBack(3);
l.PushBack(4);
l.PushBack(5);
return 0;
}
给了一个list,使用的时候会用很多的节点,都是小块小块的内存,频繁的去找系统要内存效率可能会比较低,能不能让它动态申请内存的时候不要去找系统要,而是找内存池去申请内存,进而提高效率呢?
内存池的概念:
内存池是啥?
比如说有一家人在山上住着,山顶上没有水,只有山下有一条河,想用水的时候该怎么办呢?
第一种方式就是:用的时候才去山下打水,比如要喝水,去山下喝了水以后,又回到了山上,每次用水的时候都下山去打水回来,用一点拿一点。
[平时我们使用就类似第一种方式:操作系统的堆就像这条小河,链表要插入数据就需要一个节点,就需要去找一下堆,频繁去找堆,效率就是低的。]
第二种方法就是,在山上建一个水池,提前在这个水池里储备好水,用的时候,让水池里放点水出来,这样就比第一种高效。
[内存池就类似第二种方式,不要去频繁的找堆,提前把内存储备起来,找堆,一次性申请很多,把它们放到内存池中,效率就高很多了,这种技术就叫做池化技术] .
这两个就是我们在Listnode里实现的专属operator new 与operator delete,这样的话使用new就不会去调用全局的operator new(系统库里面的),而是调用我们自己写好的,实现链表节点使用内存池申请和释放内存,提高效率
void* operator new(size_t n) //这时候我就不在malloc申请
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1); //stl中的内存池--空间配置器
cout << "memory pool allocate" << endl;
return p;
}
void operator delete(void* p)
{
allocator<ListNode>().deallocate((ListNode*)p, 1);
cout << "memory pool deallocate" << endl;
}
通过运行结果显示,确实调用了专属的operator new 与operator delete
五、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来释放空间
实际上newT[N] 与delete [ ] 就是对 operator new与operator delete的封装
六、定位new表达式(placement-new) (了解)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
使用场景:
class Test
{
public:
Test(int data=0)
: _data(data)
{
cout << "Test():" << this << endl;
}
~Test()
{
cout << "~Test():" << this << endl;
}
private:
int _data;
};
int main()
{
Test* p = (Test*)malloc(sizeof(Test));
new(p)Test(1);
//new(p)Test;
Test* p2 = new Test(2);
delete p2;
//等价于
Test* p3 = (Test*)operator new(sizeof(Test));
new(p3)Test(3);
p3->~Test();
operator delete(p3);
}
malloc出来的不会调用构造函数,用定位new去初始化malloc出来的p;
ps: p 现在指向的只不过是与 Test 对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行这种是初始化成自己想要的值。
同理对operator new也一样可以使用
七、常见面试题
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在释放空间前会调用析构函数完成空间中资源的清理
八、内存泄漏
什么是内存泄漏
内存泄漏的危害
进程的工作原理:我们启动一个程序,开始我们的任务,然后等任务结束了,我们就停止这个进程。 进程停止后, 该进程就会从进程表中移除。
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源
- 出现内存泄漏的进程正常结束,进程结束时这些内存会还给系统,不会有太大的危害
- 出现内存泄露的进程非正常结束,比如僵尸进程,危害很大,系统会越来越慢,甚至卡死宕机
- 需要长期运行的程序,出现内存泄漏,危害很大,系统会越来越慢,甚至卡死宕机--服务器程序,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
代码体现
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
内存泄漏分类(了解)
- 堆内存泄漏(Heap leak)
- 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏
- 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何避免内存泄漏
- 1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状
- 态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保
- 证。
- 2. 采用RAII思想或者智能指针来管理资源。
- 3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
九、如何一次在堆上申请4G的内存
首先改为64位
int main() { void* p = new char[0xfffffffful]; cout << "new:" << p << endl; cout << sizeof(p) << endl; return 0; }
申请成功
32位下,是不能超过2g的
更多推荐
所有评论(0)