众所周知
c
使用malloc
和calloc
函数在运行时动态分配内存,并使用free()
释放动态分配的内存。c++
支持这些函数,并且还有两个运算符new
和delete
,他们以更好、更简单的方式执行分配和释放内存。- 对于
int a
、char str[10]
等普通变量, 在栈上分配,内存会自动分配和释放。 对于像int *p = new int[10]
这样的动态分配内存,程序员有责任在不再需要时释放内存。 如果程序员不释放内存,则会导致 内存泄漏 (直到程序终止时才会释放内存)。
我们再来看下int *p = new int[10]
这段代码的分配过程,个人觉得可以加深理解
再分析下如下代码
int *p ;
p = new int[5];
在这里,我们首先声明了一个指针p
,然后这个p
也被创建在
栈中。new
再堆中分配内存,返回一个指针赋值给p
; 下面是内存的结构图
每个
new
获取的对象,必须用delete
析构并释放内存,以免内存泄漏。
new
运算符请求堆中的内存分配。 如果有足够的内存可用,它将内存初始化为指针变量并返回其地址。new
运算符的语法:
pointer_variable = new datatype;
这里,pointer-variable
是data-type
类型的指针。数据类型可以是包括数组在内的任何内置数据类型,也可以是包括结构和类在内的任何用户定义的数据类型。
- 这是初始化内存的语法
pointer_variable = new datatype(value);
- 这是分配内存块的语法
pointer_variable = new datatype[size];
示例:
#include <iostream>
using namespace std;
int main () {
int *ptr1 = NULL;
ptr1 = new int;
float *ptr2 = new float(223.324);
int *ptr3 = new int[28];
*ptr1 = 28;
cout << "指针变量值 1 : " << *ptr1 << endl;
cout << "指针变量值 2 : " << *ptr2 << endl;
if (!ptr3)
cout << "内存分配失败\n";
else {
for (int i = 10; i < 15; i++)
ptr3[i] = i+1;
cout << "存储在内存块中的值: ";
for (int i = 10; i < 15; i++)
cout << ptr3[i] << " ";
}
return 0;
}
输出
指针变量值 1 : 28
指针变量值 2 : 223.324
存储在内存块中的值: 11 12 13 14 15
5. delete
运算符
delete
运算符用于释放内存。 用户有权通过此删除运算符释放创建的指针变量。
删除运算符的语法,
delete pointer_variable;
删除已分配内存块的语法,
delete[ ] pointer_variable;
示例:
#include <iostream>
using namespace std;
int main () {
int *ptr1 = NULL;
ptr1 = new int;
float *ptr2 = new float(299.121);
int *ptr3 = new int[28];
*ptr1 = 28;
cout << "指针变量值 1 : " << *ptr1 << endl;
cout << "指针变量值 2 : " << *ptr2 << endl;
if (!ptr3)
cout << "分配失败\n";
else {
for (int i = 10; i < 15; i++)
ptr3[i] = i+1;
cout << "存储在内存块中的值: ";
for (int i = 10; i < 15; i++)
cout << ptr3[i] << " ";
}
delete ptr1;
delete ptr2;
delete[] ptr3;
return 0;
}
输出:
指针变量值 1 : 28
指针变量值 2 : 299.121
存储在内存块中的值: 11 12 13 14 15
new
工作机制(分配的过程):
- 调用一个名为
operator new
(或者operator new[]
)的标准库函数,分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或对象的数组)- 编译器运行相应的构造函数以构造这些对象,并为其传入初始值
- 对象被分配了空间并构造完成,返回一个指向该对象的指针
- 调用相应的
operator new()
函数,动态分配内存。如果operator new()
不能成功获得内存,则调用new_handler
函数。 如果没有设置new_handler函数或者new_handler未能分配足够内存,则抛出std::bad_alloc
异常。“new
运算符表达式”所调用的operator new()
函数,按照C++
的名字查找规则,首先做依赖于实参的名字查找(即ADL
规则),在要申请内存的数据类型T的内部、数据类型T定义处的名字空间查找;如果没有查找到,则直接调用全局的::operator new()
函数。- 在分配到的动态内存块上初始化相应类型的对象并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用
operator delete()
函数释放已经分配到的内存。
delete
工作机制(释放过程)
delete sp;delete[] arr;
- 调用
sp
所指对象的或者arr
所指数组的元素执行相应的析构函数- 调用
operator delete
或者operator delete[]
标准库函数释放内存空间
注:删除一个动态分配的内存块后,确保不要再次删除相同的块。防止这种情况发生的一种方法是立即将指针设置为nullptr。删除nullptr无效。
- C++ 提供了两种标准来检查分配是否成功:抛出异常;不抛异常返回空。
第一种:new
失败会抛出std::bad_alloc
异常,如果抛出这个异常并且它没有被特定的处理程序处理,那么程序执行就会终止。
第二种:当内存分配失败时,不抛出一个bad_alloc
异常或终止程序,返回的指针是一个 空指针 ,程序继续正常执行。
- 抛异常使用
#include <iostream>
#include <new>
int main()
{
try {
while (true) {
new int[10000990000ul];
}
} catch (const std::bad_alloc& e) {
std::cout <<"e.what() -->"<<e.what() << '\n';
}
}
运行结果
e.what() -->std::bad_alloc
new-handler
函数是分配函数在内存分配尝试失败时调用的函数。
默认实现抛出std::bad_alloc
。用户可以安装自己的new-handler
,它可能提供不同于默认处理程序的行为。
如果new-handler
返回,分配函数将重复之前失败的分配尝试,并在分配再次失败时再次调用new-handler
。为了结束循环,new-handler
可以调用std::set_new_handler(nullptr):
如果在分配失败后,分配函数发现std::get_new_handler
返回空指针值,它将抛出std::bad_alloc
。
#include <iostream>
#include <new>
static int i = 0;
void handler(){
std::cout << "内存申请失败, 程序终止 \n handler 被调次数 "<<i<<std::endl;
i++;
if(i>5){
std::set_new_handler(nullptr);
}
}
int main(){
std::set_new_handler(handler);
try {
while (true) {
new int[10000990000ul];
}
} catch (const std::bad_alloc& e) {
std::cout <<"e.what() -->"<<e.what() << '\n';
}
}
运行结果
内存申请失败, 程序终止
handler 被调次数 0
内存申请失败, 程序终止
handler 被调次数 1
内存申请失败, 程序终止
handler 被调次数 2
内存申请失败, 程序终止
handler 被调次数 3
内存申请失败, 程序终止
handler 被调次数 4
内存申请失败, 程序终止
handler 被调次数 5
e.what() -->std::bad_alloc
- 不抛异常使用
int *p = new(nothrow) int;
if (!p){
cout << "内存分配失败\n";
}
struct MyClass {
int data[100];
MyClass() {std::cout << "constructed [" << this << "]\n";}
};
MyClass * p2 = new (std::nothrow) MyClass;
if(p2!){
cout << "内存分配失败\n";
}
new
运算符表达式不可重载。但可重载operator new()
函数。
new operator
程序运行时可以使用
new
操作符动态分配(即预留)执行时保存对象或内置数组所需的确切内存量。对象或内置数组是在空闲存储区(也称为堆)中创建的,空闲存储区是分配给每个程序用于存储动态分配的对象的内存区域。一旦内存被分配到空闲存储区中,你就可以通过new
返回的指针来访问它。当你不再需要这些内存时,你可以通过使用delete
操作符来释放(即释放)这些内存,从而将其返回到自由存储区,这些内存可以被以后的新操作重用。
new操作符,就象sizeof一样是语言内置的。它是可被重载的
- 使用示例1:
//将新创建的double对象初始化为3.14159,并将结果指针赋值给ptr
double *ptr = new double( 3.14159 );
//初始化一个新的Time对象到12:45 PM,并将结果指针赋值给timePtr。
Time *timePtr = new Time( 12, 45, 0 );
//声明了一个int指针gradesArray,并将指向动态分配的10个int数组的第一个元素的指针赋给它.new int[10]值后面的圆括号初始化数组的元素——基本数值类型设为0,bool设为false,指针设为nullptr,类对象由它们的默认构造函数初始化。
int *gradesArray = new int[ 10 ]();
//使用列表初始化器来初始化动态分配的内置数组的元素
int *gradesArray = new int[ 10 ]{};
delete [] gradesArray;
- 使用示例2
#include <iostream>
#include <new>
#include <memory>
using namespace std;
class Integer
{
public:
Integer( int i = 0 );
~Integer();
void setInteger( int i );
int getInteger() const;
private:
int value;
};
Integer::Integer( int i ): value( i ){
cout << "构造 " <<value << endl;
}
Integer::~Integer(){
cout << "析构" <<value << endl;
}
void Integer::setInteger( int i ){
value = i;
}
int Integer::getInteger() const{
return value;
}
int main()
{
cout << "创建一个指向Integer的unique_ptr对象\n";
unique_ptr< Integer > ptrToInteger( new Integer( 7 ) );
ptrToInteger->setInteger( 99 );
cout << "setInteger后的值 "<< ( *ptrToInteger ).getInteger()
<< "\n\n程序结束" <<endl;
return 0;
}
运行结果
创建一个指向Integer的unique_ptr对象
构造 7
setInteger后的值 99
程序结束
析构99
- 使用示例3:
当管理数组的unique_ptr超出作用域时,它使用delete[]删除内存,以便数组的每个元素都接收到析构函数调用。
unique_ptr< string[] > ptr( new string[ 10 ]);
ptr[ 2 ] = "hello";
cout << ptr[ 2 ] << endl;
operator new
- throwing (默认抛异常版本)
void* operator new (std::size_t size);- nothrow (不抛异常版本)
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;- placement
void* operator new (std::size_t size, void* ptr) noexcept;
- 全局 new和delete自定义
include <cstdio>
#include <cstdlib>
#include <new>
// 最小函数集的替换:
// 无 inline ,由 [replacement.functions]/3 要求
void* operator new(std::size_t sz)
{
std::printf("已调用全局 new 运算符,大小为 %zu\n", sz);
if (sz == 0)
++sz; // 避免 std::malloc(0),它可能会在成功时返回 nullptr
if (void *ptr = std::malloc(sz))
return ptr;
throw std::bad_alloc{}; // 由 [new.delete.single]/3 要求
}
void operator delete(void* ptr) noexcept
{
std::puts("已调用全局 delete 运算符");
std::free(ptr);
}
int main()
{
int* p1 = new int;
delete p1;
int* p2 = new int[10]; // C++11 中保证调用替换
delete[] p2;
}
运行结果
已调用全局 new 运算符,大小为 4
已调用全局 delete 运算符
已调用全局 new 运算符,大小为 40
已调用全局 delete 运算符
- 类中的operator new
#include <iostream>
#include <new>
#include <memory>
using namespace std;
class A
{
public:
A(){
cout<<"A constructor"<<endl;
}
~A(){
cout<<"A destructor"<<endl;
}
void * operator new (size_t size){
cout<<"new "<<size<<endl;
void *p = malloc(size); // ((A*)p)->a = 100;
return p;
}
void operator delete(void *p){
cout<<"delete"<<endl;
free(p);
}
void * operator new[] (size_t size){
cout<<"new[] "<<size<<endl;
return malloc(size);
}
void operator delete[](void *p){
cout<<"delete[] "<<endl;
free(p);
}
private:
int a;
};
int main()
{
A * cp = new A;
delete cp;
A * cpa = new A[20];
delete []cpa;
return 0;
}
运行结果
标准输出:
new 4
A constructor
A destructor
delete
new[] 88
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A constructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
A destructor
delete[]
- 重载placement new
#include <cstdlib>
#include <iostream>
char buf[100];
struct A {} ;
struct E {} ;
class T {
public:
T() { throw E() ; }
void * operator new(std::size_t,const A &){
std::cout << " 调用类T Placement new " << std::endl;
return buf;
}
void operator delete(void*, const A &){
std::cout << " 调用类T Placement delete " << std::endl;
}
} ;
void * operator new ( std::size_t, const A & ){
std::cout << " 调用 Placement new " << std::endl; return buf;
}
void operator delete ( void *, const A & ){
std::cout << " 调用 Placement delete " << std::endl;
}
int main ()
{
A a ;
try {
T * p = new (a) T ;
/* ... */
} catch (E exp) {std::cout << " 异常捕获 " << std::endl;}
return 0 ;
}
运行结果
调用类T Placement new
调用类T Placement delete
异常捕获
new 表达式和operator new函数
二者区别
- 操作符vs函数:
new
既是一个操作符也是一个关键字,而operator new
只是一个函数。- "
new
"调用“operator new
”:
“new operator
” 调用“operator new()
” , 就像+ operator
调用operator +()
- “
Operator new
” 可以被重写:operator new
可以重载,就像允许执行定制任务的函数一样- 内存分配: ‘
new
表达式’ 调用‘operator new’ 分配原始内存,然后调用构造函数
ADL规则
本节我们只做简要的了解,目的是为了知道全局作用域的operator new
和类中的operator new
是如何被查找调用的。
定义
实参依赖查找(argument-dependent lookup),又称 ADL 或 Koenig 查找 ,是一组对函数调用表达式(包括对重载运算符的隐式函数调用)中的无限定的函数名进行查找的规则。在通常无限定名字查找所考虑的作用域和命名空间之外,还会在它的各个实参的命名空间中查找这些函数。
标准库示例
#include <vector>
#include <algorithm>
#include <iostream>
int main () {
std::vector<int> v {3,2,1,4};
sort(begin(v), end(v)); // compiles!
for (auto x : v) std::cout << x << ' ';
std::cout << '\n';
}
查找路径如下:
v
的类型是std::vector
ADL
首先在std
的命名空间中查找begin
和end
函数- 找见了
std::begin
和std::end
函数,并且调用它begin/end
的返回类型是std::vector<int>::iterator
ADL
在命名空间std
中查找sort
- 找见
std::sort
并调用它
delete operator
和 operator delete
类似,这里不做详述
new和delete的查找过程
当编译器发现一条new
表达式或者delete
表达式之后,查找过程如下:
- 首先在类及其基类的作用域中查找,若该类包含
operator new
成员或者operator delete
成员,则相应的表达式将调用这些成员 - 若不满足【1】,编译器在全局作用域查找匹配函数,此时若编译器找到了用于自定义的版本,则使用该版本执行
new
表达式或者delete
表达式; - 若不满足【2】,则使用标准库定义的版本
- 需注意:若直接使用
::new
或者::delete
,则直接在全局作用域查找匹配的operator new
函数。
术语说明
- 内存泄漏
当程序员在堆中创建内存并忘记删除它时,就会发生内存泄漏。
内存泄漏的后果是它通过减少可用内存量来降低计算机的性能。 最终,在最坏的情况下,可能会分配过多的可用内存,并且系统或设备的全部或部分停止正常工作,应用程序失败,或者系统速度大大降低。
对于像守护进程和服务器这样的程序来说,内存泄漏是特别严重的问题,这些程序根据定义永远不会终止。
- 栈展开
栈展开(unwinding)是指当前的
try...catch...
块匹配成功或者匹配不成功异常对象后,从try
块内异常对象的抛出位置,到try
块的开始处的所有已经执行了各自构造函数的局部变量,按照构造生成顺序的逆序,依次被析构。如果当前函数内对抛出的异常对象匹配不成功,则从最外层的try
语句到当前函数体的起始位置处的局部变量也依次被逆序析构,实现栈展开,然后再回退到调用栈的上一层函数内从函数调用点开始继续处理该异常。
catch
语句如果匹配异常对象成功,在完成了对catch
语句的参数的初始化(对传值参数完成了参数对象的copy构造)之后,对同层级的try块执行栈展开。
由于线程执行时,被调用的函数的参数、返回地址、局部变量等都是依函数调用次序保存在函数调用栈(即线程运行时栈)上。当前被调用函数的参数、局部变量名字可以覆盖掉早前调用函数的同名变量,看起来就是只有当前函数内的名字可以访问,早前调用的函数内部的名字都不可访问,就像磁带被“卷起”。异常处理时按照函数调用顺序的逆序析构,依次析构各个被调函数的局部变量,就类似把已经卷起的“磁带”再展开,抹去上面记录的数据,故此“栈展开”得名。unwinding在物理学、电工学上也翻译做“退绕”、“退卷”。