前言:在C++中,内存管理是一项关键的任务,因为程序需要为变量、对象和数据结构等动态分配内存。有效的内存管理是确保程序在运行期间高效使用系统资源的重要一环。此外,C++还引入了模板的概念,以提供一种通用的编程方式。模板允许编写可以处理多种类型的通用代码,并在编译时根据实际使用的类型来生成特定类型的代码。函数模板允许定义通用的函数,而类模板允许定义通用的类。模板在泛型编程中发挥了关键作用,它使得代码可以更灵活地处理不同类型的数据,提高了代码的重用性和可扩展性。那我们马上就开始了,
目录
operator new与operator delete函数
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. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段--存储全局数据和静态数据。
5. 代码段--可执行的代码/只读常量。
2.C++内存管理方式
C语言中动态内存管理方式:malloc/calloc/realloc/free,C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
malloc/calloc/realloc的异同
malloc
、calloc
和realloc
是C语言中用于动态内存分配的函数。它们的异同如下:
malloc
:
- 函数原型:
void* malloc(size_t size)
- 功能:用于在堆中分配指定大小的内存块。
- 参数:
size
表示要分配的字节数。- 返回值:返回一个指向分配内存的指针(void*)。如果分配失败,则返回
NULL
。- 注意事项:
malloc
分配的内存块的内容是未初始化的,可能包含任意的值。
calloc
:
- 函数原型:
void* calloc(size_t num, size_t size)
- 功能:用于在堆中分配指定数量和大小的连续内存块,并将其初始化为零。
- 参数:
num
表示要分配的元素数量,size
表示每个元素的大小。- 返回值:返回一个指向分配内存的指针(void*)。如果分配失败,则返回
NULL
。- 注意事项:
calloc
分配的内存块已被初始化为零,可以确保没有随机值。
realloc
:
- 函数原型:
void* realloc(void* ptr, size_t size)
- 功能:用于重新调整先前分配的内存块的大小。
- 参数:
ptr
是指向之前分配的内存块的指针,size
是希望调整为的新大小。- 返回值:返回一个指向重新调整大小后的内存块的指针(void*)。如果调整失败,则返回
NULL
。- 注意事项:
realloc
可能会将内存块的内容复制到新的大小以适应新的内存需求。如果分配失败,原始内存块的内容将保持不变。总结:
malloc
和calloc
主要的区别在于内存初始化的行为,malloc
分配的内存未初始化,而calloc
分配的内存初始化为零。realloc
用于重新调整先前分配的内存块的大小,可以将内存大小减小或增大,并保留原始内存块的数据(如果可能的话)。- 所有这些函数都返回指向分配内存的指针,如果分配失败,则返回
NULL
。
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;//注意匹配使用
return 0;
}
new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数,在C++中经常使类和对象,所以,采用new/delete来代替malloc/free与C++更具有兼容性。
new/delete的类型匹配使用-针对自定义析构函数类
我们知道,new只有一种形式使用,但是对于释放连续的空间并且其类的析构函数是自定义的,在我们使用delete时,就必须要使用delete[]的形式来释放空间,具体我们看下面的样例:
相同的两个类,只是其中一个采用的是我们自定义的析构函数,一个采用系统默认生成的析构函数(系统默认生成的析构函数一般什么也不做,只是一个空函数),但是采用自定义析构函数的对象在申请连续的空间时,在释放时编译器会自动识别为其在申请的空间前添加一个字节,这个字节存储的是需要申请的个数,代表我们要对这个连续的空间调用多少次析构函数,这也属于编译器底层优化的一部分,如果不使用delete [ ],就会导致指针指向出现问题,析构函数的调用的次数等问题,所以一定要匹配使用。
operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过 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来释放空间的。
new/delete实现原理
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
对于自定义类型:
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表达式显示调用构造函数
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式为:new (place_address) type或者new (place_address) type(参数表)
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
何为内存池?
内存池是一种用于管理和分配内存的技术。它是在程序启动时预先分配一块连续的内存空间,并将其划分为多个固定大小的块或对象。这些块或对象可以被程序动态地分配和释放,以满足程序运行时的内存需求。
内存池的主要目的是减少内存分配和释放的开销,提高程序的性能。相比于频繁地使用new
和delete
来进行内存分配和释放,内存池可以通过预先分配一块连续的内存空间来减少内存碎片和系统调用的次数,从而提高内存分配和释放的效率。
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A(1); // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
3.常见的面试知识
3.1malloc/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在释放空间前会调用析构函数完成 空间中资源的清理
3.2 内存泄漏
什么是内存泄漏,内存泄漏的危害
内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
堆内存泄漏(Heap leak)和系统资源泄漏
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
4.模板
我们都知道函数重载可以实现同一个函数名实现不同的功能的特点,但是其也有一些不好的地方:
1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
2. 代码的可维护性比较低,一个出错可能所有的重载均出错
那么,下面我们就来看一种更加通用的模子,可以让编译器根据不同的类型利用该模子来生成代码
4.1 函数模板
typename是用来定义模板参数关键字,也可以使用class
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
经典的swap函数使我们比较熟悉的,相信大多数学校开C++的应该都拿这个题当过板子题,emm~~~,那咱就整点没见过的~~
4.1.1 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
Add(a1, d1);
*/
// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
Add(a1, (int)d1);
return 0;
}
显示实例化:在函数名后的< >中指定模板参数的实际类型,对于一些编译器没办法推出类型的场景需要使用
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
4.1.2模板参数的匹配原则
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然 后产生一份专门处理double类型的代码,对于字符类型也是如此。
1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
3.模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
4.2 类模板
类模板大致和函数模板相似,但是也有些许区别:
类模板中函数放在类外进行定义时,需要加模板参数列表,比如:
template <class T>
Vector<T>::~Vector()
{
...
}
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟< >,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
请你相信,只要你还愿意为了自己努力,世界就不会吝啬给你惊喜。也请你相信,只要你不断努力着,那么,最差的结果也不过就是大器晚成。