C++的内存管理
重新校正相关内容,让语言描述更加清晰,内容更加具体——2020-0816。
一、C++内存管理详解
1、内存的分配方式
本文会用一段C语言程序说明内存在操作系统中的分布,会从内存的低地址一直延生到内存的高地址。如果有什么错误或者模糊的地方,麻烦各位读者在评论区批评指正。
#include <stdio.h>
#include <stdlib.h>
int x;
int y=15;
int main(int agrc,char* argv[]){
int *value;
int i;
value = (int*)malloc(size0f(int)*5);
for(i=0;i<5;i++)
value[i] = i;
return 0;
}
地址从低到高
(
a
)
(a)
(a)代码区(.text):代码区,主要是用来存储代码的区域,也就是计算机所要执行的指令,如上述例子中的所有代码。
(
b
)
(b)
(b)静态数据区(.data):存储静态变量和已经初始化的全局变量,如int y = 15;
。
(
c
)
(c)
(c).bss:用来存储为未初始化的全局变量,如int x;
。
(
d
)
(d)
(d)堆(.heap):使用malloc()函数分配的内存,如value = (int*)malloc(size0f(int)*5);
。
(
e
)
(e)
(e)进程间的文件共享区域:使用mmap映射的一块内存用于进程间的文件共享。
(
f
)
(f)
(f)栈(.stack):用来存储局部变量,比如函数参数或者等等,在执行完后会释放这部分内存。
mmap的解释在以下链接中:
https://www.cnblogs.com/huxiao-tee/p/4660352.html
举例:C++中的int* p = new int[5];
此语句在内存中的存储情况如下:
分析:以上C++语句实际用到了堆和栈的概念,因为指针p是局部变量,所以局部变量指针p应该在栈区,用new来分配int[5],则应该存放在堆区。所以删除内存是delete []p;
而不是delete p;
。
2、堆和栈的区别
答:
(
a
)
(a)
(a)管理方式:栈的管理是编译器来进行分配和管理,堆的管理一般是程序员通过new和delete来对内存进行分配和释放。
(
b
)
(b)
(b)空间大小:堆在系统中一般可以分配几个G大小的内存,而栈一般分配几M大小的内存。
(
c
)
(c)
(c)碎片问题:堆在不断的执行new和delete操作中,内存逐渐被碎片化,使得程序的执行效率变低;栈采用后进先出的策略,所以不会出现碎片化的问题。
(
d
)
(d)
(d)增长方向:堆的增长方向是向着地址增大的方向进行的,而栈采用的是后进先出的策略,所以元素的增长方向是朝着地址减少的方向进行的。
(
e
)
(e)
(e)分配方式:堆一般是动态分配的;而栈既有动态分配的方式也有静态分配的方式,静态分配使用alloc函数执行,也是由编译器分配,动态由编译器分配。
(
f
)
(f)
(f)分配效率:一般情况下,栈的分配效率高于堆,因为栈分配的时候由编译器和操作系统底层将临时变量和相关地址放在特定的寄存器中,读写效率高,而堆有上层复杂的数据结构会造成效率低的问题。
PS:若是需要占用大块内存还是分配给堆比较好。需要注意栈和堆的越界问题,会造成内存泄露。
3、控制C++的内存分配
答:因为C++分配内存的过程特别简单,程序员过度的使用new和delete会造成内存中堆破裂的问题。
解决方案:从不同固定大小的内存池中分配不同类型的对象。
实践方法:对每个类重载new和delete就提供了这种控制。
void *operator new(size_t size)
{
void *p=malloc(size);
return (p);
}
void operator delete(void *p)
{
free(p);
}
如果需要创建对象数组,也就是同时创建多个对象,调用系统的new[]和delete[]操作,则会调用系统的内存,同样会出现以上的问题,所以也需要重载new[]和delete[]操作符。
class TestClass{
public:
void *operator new[](size_t size);
void operator delete[](void *p);
//其他成员
};
void *Testclass::operator new[](size_t size)
{
void *p = malloc(size);
return (p);
}
void *Testclass::operator delete[](void *p)
{
free(p);
}
4、C++内存分配可能出现的问题
答:
(
a
)
(a)
(a)内存分配未成功,却使用了它。原因:系统每次分配内存不一定会成功,如果使用了它,则会造成不可预计的错误。
a.用malloc或new申请内存之后,应该立即检查指针值是否为NULL,防止使用指针值为NULL的内存。如果指针p是函数的参数,那么在函数的入口处assert(p!=NULL);
如果是使用malloc或者new来申请内存,用if(p==NULL)
。
(
b
)
(b)
(b)内存分配虽然成功,但是尚未初始化就使用了它。原因:开发人员没有初始化的观念;误认为内存的缺省时初值为0,导致引用初值错误。
b.不要忘记为数组和动态内存赋初值,防止将未被初始化的内存作为右值使用。
(
c
)
(c)
(c)内存分配成功并且已经初始化,但操作越过了内存边界,比如数组越界。
c.避免数组或者指针的下标越界。
(
d
)
(d)
(d)内存分配成功后,忘记释放了内存,造成内存泄漏。
d.动态内存的申请与释放必须配对,防止内存泄露。
(
e
)
(e)
(e)释放了内存却还使用了它,原因:一是,程序对象的调用过程过于复杂,难以搞清对象是否释放了内存;二是,返回了栈内存中的指针和引用,因为该内存在函数体结束时自动销毁;三是,使用free/delete释放内存,没有将指针设置为NULL,导致产生野指针。
防止以上问题,采用以下的解决方案:
e.用free或delete释放了内存之后,立即将指针设置为NULL,防止产生野指针。
5、指针与数组之间的对比
答:数组在静态存储区或者栈上被创建,数组名对应着一块内存,其地址与容量在生命周期内保持不变,只有数组的内容可以改变。指针可以随时指向任意类型的内存块,是可以随时改变的。指针比数组灵活,但是也更加危险。
6、数组和指针容易发生的错误
答:char *p = "world";
编译器会给出错误,const char* 实体“world”不能初始化char*。因为字符串”world"是常量,所以更不能用p[0]='x'
来修改内容。
不能用数组名进行比较和赋值,需要借助strcpy函数和strcmp函数。同时还有需要注意的一点是当数组作为函数的参数进行传递的时候,该数组自动退化为同类型的指针。实例代码如下:
#include <iostream>
using namespace std;
void Func(char a[100]);
int main()
{
char a[] = "Hello World";
char b[100]="ddddd";
cout << "通过strcpy将数组a[]的指针a赋值给数组b[]的指针" << endl;
cout<<strcpy(b, a)<<endl;//p=a
cout << "通过strcmp对a指针指向的数组与b指针指向的数组进行比较" << endl;
cout<<strcmp(b,a)<<endl;//
cout << b << endl;
char* p = (char*)malloc(sizeof(char) * (strlen(a) + 1));
cout << "将数组a的内容赋值给指针p" << endl;
cout<<strcpy(p,a)<<endl;
cout << "将指针p的内容与数组a[]进行比较" << endl;
cout << strcmp(p,a) << endl;
cout << "将数组作为参数传递给函数" << endl;
Func(a);
free(p);
p = NULL;
return 0;
}
void Func(char a[100])
{
cout << "a的内容" << endl;
cout << a << endl;
cout << "指针a的大小" << endl;
cout << sizeof(a) << endl;
cout << "指针*a所指向的内容" << endl;
cout << *a << endl;
}
7、指针参数是如何传递内存的
答:如果函数的参数是一个指针,不能指望用该指针去申请动态内存。原因:编译器会为了每个函数的参数分配一个临时副本,指针参数p的副本是_p,编译器使_p=p,当你分配内存的时候,为_p分配了一块内存,但是指针p却没有指向这块内存,所以函数不会分配到内存。
以下为错误代码:
正确代码:
8、什么是野指针,如何预防
答:野指针不是NULL指针,是指向垃圾内存的指针。而且使用if(p==NULL)
是判断不了野指针的。
原因:a.定义指针的时候没有初始化。它的缺省值会随机指向内存中的一个地址,这是相当危险的。解决办法如下:
char* p = NULL;
char* str = (char*)malloc(100);
b.指针p被free或者delete之后,没有置为NULL,让人认为还是一个合法的指针。
c.指针的操作超越了变量的作用域范围。
class A
{
public:
void Func(void) { cout << "Func of Class A" << endl; };
};
void Test(void)
{
A* p;
{
A a;
p = &a;
}//到这里指针p已经被A的默认析构函数释放了
p->Func();//p是野指针
}
9、malloc/free和new/delete的区别和联系?
答:malloc/free是系统的库函数,new/delete是操作符,其中编译器可以管理new/delete而不能直接管理malloc、free。所以一个类中的构造函数和析构函数是使用new和delete这样操作时比较方便的。
class Obj
{
public:
Obj(void) { cout << "Initialization" << endl; }
~Obj(void) { cout << "Destroy" << endl; }
void Initialize(void) { cout << "Initialization" << endl; }
void Destroy(void) { cout << "Destroy" << endl; }
};
void UseMallocFree(void)
{
Obj* a = (obj*)malloc(sizeof(obj)); // 申请动态内存
a->Initialize(); // 初始化
//…
a->Destroy(); // 清除工作
free(a); // 释放内存
}
void UseNewDelete(void)
{
Obj* a = new Obj; // 申请动态内存并且初始化
//…
delete a; // 清除并且释放内存
}
补充:malloc/free要点,malloc函数返回的是void类型指针所以需要类型转换,内存大小是sizeof(char)等等来判断大小。
new/delete要点,new和delete函数是可以初始化的,如obj o = new obj(1);
10、内存耗尽怎么办?
答:内存耗尽会返回NULL,使用if判断使用return或者exit(-1)来返回或者终止程序,推荐使用exit(-1);
后续待补充。。。。
参考文献:
[1]https://www.cnblogs.com/findumars/p/5180490.html