C++内存管理学习笔记
一.C/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";
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);
}
//选择题
选项: 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_
//填空题
sizeof(num1) = _40_;//sizeof数组名是求数组的大小
sizeof(char2) = _5__; strlen(char2) = __4_;
sizeof(pChar3) = _4|8_; strlen(pChar3) = _4_;
sizeof(ptr1) = _4|8_; sizeof(ptr2) = __4|8_;
//4代表在32位的进程空间中,8代表在64位的进程空间中
各段内存说明:
非静态局部变量/函数参数/返回值
等存储在栈中,栈是向下增长
的- 内存映射段是
高效的I/O映射方式
,用于装载一个共享的动态内存库
。用户可使用系统接口创建共享共享内存
,做进程间通信
- 堆用于程序运行时
动态内存分配
,堆是向上增长
的 - 数据段存储
全局数据和静态数据
- 代码段存储
可执行的代码和只读常量
二.回顾C语言中的动态内存管理
动态内存管理就是在堆上动态的分配和释放空间。C语言中动态分配的方式是malloc、realloc、calloc、free
四个函数,前三个函数负责分配和调整空间,而free
则负责释放前三个函数分配的空间。
1.面试题1:malloc/calloc/realloc的区别是什么?
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);
malloc:
malloc函数用来分配堆上的空间,按照字节为单位calloc:
calloc函数也是用来开辟堆上的空间,但和malloc的区别是它把这块空间初始化为0,也是按照字节为单位。nmemb指的是分配的字节数,size指的是每个成员占的字节数realloc:
realloc函数是用来调整已开辟的空间,调整方法为:如果该空间后边的空间足够扩大,则直接把后边的空间续上,源指针不变;如果该空间后边的空间不够扩大,则在内存中另外找一块空间,源指针改变指向新的空间,并且自动的将源空间freefree:
free函数用来释放前三个函数开辟的空间
2.面试题2:32位平台指针为什么是4个字节?
在32位的机器上一个进程的地址空间也是32位的,那么一个虚拟地址就是由32个0、1串构成的,一个字节是8个比特位,那么一个地址就得用4个字节表示(32/8=4)。64位平台也是一样的道理,指针大小为8个字节。
3.面试题3:如何malloc一个大于3G的空间?
在一个32位的进程地址空间下,我们最多可以在堆上开辟的空间小于3G,因为有1G是操作系统的。我们要想在堆上申请到大于3G的空间可以考虑把运行这个进程的平台换成64位的,在64位的地址空间就可以申请到大于3G的堆空间。
三.C++中的内存管理
C++的语法是兼容C语言
的语法,所以C语言中的malloc、realloc、calloc、free
在C++中都适用。但是C++中由于内存分配失败的处理方法和C语言不一样
,C++处理失败的方法是抛异常
,而C语言处理失败的方法是返回NULL
,所以C++
有自己独特的内存管理
方法,既new、delete
。
1.new/delete操作内置类型用法
int main()
{
int* ptr1 = new int;//动态申请一个int类型的空间
int* ptr2 = new int(2);//动态申请一个int类型的空间并初始化为2
int* ptr3 = new int[3];//动态申请3个int类型的空间
int* ptr4 = new int[3]();//动态申请3个int类型的空间并初始化为0
//int* ptr5 = new int[3](1);不能这么初始化
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr4;
return 0;
}
注::申请和释放单个元素的空间,使用new和delete操作符
,申请和释放连续的空间,使用new[]和 delete[]
2.new/delete操作自定义类型用法
class Date
{
public:
Date()
{}
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "date" << this << endl;
}
~Date()
{
cout << "~date" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//C++中new/delete操作自定义类型
Date* d1 = new Date(2018, 11, 5);
Date* d2 = new Date[10]();//或者Date* d2 = new Date[10]
delete d1;
delete[] d2;
//C语言中malloc/delete操作自定义类型
cout << "------------------------" << endl;
Date* d3 = (Date*)malloc(sizeof(Date));
Date* d4 = (Date*)malloc(sizeof(Date)* 10);
free(d3);
free(d4);
return 0;
}
运行上边的程序,我们可以根据结果发现:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
四.operator new和operator delete函数
new和delete
是用户进行动态内存申请和释放的操作符
,operator new 和operator delete
是系统提供的全局函数
,new
在底层调用operator new
全局函数来申请空间
,delete
在底层通过operator delete
全局函数来释放空间
new(操作符)--->调用operator new(函数)--->调用malloc(函数)--->调用构造
//new失败抛异常(符合C++规范)
//malloc失败返回NULL
delete(操作符)-->调用析构--->调用operator delete(函数)--->free(函数)
operator new
实际也是通过malloc
来申请空间,如果malloc申请空间成功
就直接返回,否则执行用户提供的空间不足应对措施
,如果用户提供该措施就继续申请,否则就抛异常
。operator delete
最终是通过free
来释放空间的。operator new
和operator delete
用户也可以自己实现,用户实现时即可实现成全局函数
,也可实现成类的成员函数
,但是一般情况下不需要实现,除非有特殊需求。
五.定位new表达式(placement-new)
定位new表达式
是在已分配的原始内存空间
中调用构造函数初始化一个对象
。
使用格式:new (place_address) type或者new (place_address) type(initializer-list)。place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:定位new表达式
在实际中一般是配合内存池使用
。因为内存池分配出的内存没有初始化
,所以如果是自定义类型的对象
,需要使用new的定义表达式进行显示调构造函数进行初始化
。
class Date
{
public:
Date()
{}
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "date" << this << endl;
}
~Date()
{
cout << "~date" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//d1和d2现在还不是一个对象,因为没有调用构造函数初始化,只能说大小和对象的大小相同
Date* d1 = (Date*)malloc(sizeof(Date));
Date* d2 = (Date*)malloc(sizeof(Date));
//调用new的定位表达式初始化
new(d1)Date(2018, 11, 5);//调用有参构造
new(d2)Date();//调用无参构造
}
六.常见面试题总结
1.malloc/free和new/delete的区别?
共同点:它们都是在堆上开辟空间
,而且都需要手动释放
。
不同点:
malloc/free
是函数
,而new/delete
是操作符
,new/delete
在底层调用了malloc/free
malloc
申请的空间不能直接初始化
,而new申请的空间会调用构造函数,可以直接传参初始化
malloc
申请空间时,需要手动计算空间大小并传递
,new只需在其后跟上空间的类型
即可malloc
的返回值为void*
, 在使用时必须强转
,new不需要
,因为new后跟的是空间的类型
malloc申请空间失败
时,返回的是NULL
,因此使用时必须判空
,new
不需要,但是new需要捕获异常
malloc/free
只能申请内置类型
的空间,不能申请自定义类型
的空间,因为其不会调用构造与析构函数
, 而new可以
,new在申请空间后会调用构造函数完成对象的构造
,delete在释放空间前会调用析构函数 完成空间中资源的清理
malloc
申请的空间一定在堆上
,new不一定
,因为operator new函数
可以重新实现
new/delete
比malloc/free
的效率稍微低点
,因为new/delete
的底层封装了malloc/free
,存在函数调用的开销
2.请设计一个类,该类只能在堆上创建对象
- 将类的
构造函数私有
,拷贝构造声明成私有
。防止别人调用拷贝在栈上生成对象
。 - 提供一个
静态的成员函数
,在该静态成员函数中完成堆对象的创建
#include<iostream>
using namespace std;
class HeapType
{
public:
static HeapType* CreateObj()
{
return new HeapType;
}
void Print()
{
cout << "HeapType:" << this << endl;
}
private:
HeapType()//构造函数私有化
{}
//防拷贝
HeapType(HeapType const&);
// HeapType& operator=(HeapType const&);
};
int main()
{
HeapType::CreateObj()->Print();
HeapType::CreateObj()->Print();
//HeapType ht; 这样不行,因为构造器是私有的,所以满足条件,不在栈上
//HeapType ht1(HeapType::CreateObj());//拷贝构造也不行
return 0;
}
由上述运行结果可以看出这是两个不同的对象
。
3.请设计一个类,该类只能在栈上创建对象
class StackType
{
public:
StackType()
{}
void Print()
{
cout << "StackType:" << this << endl;
}
private:
//不能再堆上创建,我们可以考虑直接把operator new和new的定位表达式声明为私有的
//将operator new声明为私有的,就把new的定位表达式也声明为私有的了
void* operator new(size_t size);
void operator delete(void* p);
};
int main()
{
StackType st1;//ok
st1.Print();
//StackType st2 = new StackType();//不行,因为operator new被屏蔽了
//StackType* st3 = (StackType*)malloc(sizeof(StackType));
//new(st3)StackType();//不行,因为new的定位表达式也被屏蔽
return 0;
}
七.单例模式
有关单例模式的实现单独总结于我的另一篇博客:
https://blog.csdn.net/hansionz/article/details/83752531