C++动态内存

C++动态内存

C++编译器维护三种内存空间用于保存不同种类的数据。

  • 静态内存
    • 保存局部static对象、类static数据成员和定义在任何函数之外的变量
    • 静态内存对象由编译器自动创建和销毁。静态内存对象在使用之前创建,在程序结束时销毁
  • 栈内存
    • 保存定义在函数内的非static对象
    • 栈对象由编译器自动创建和销毁。栈对象仅在其定义的程序块运行时才存在
  • 堆/自由空间
    • 程序用堆来存储动态分配的对象,即程序运行时分配的对象
    • 动态对象的生命周期由程序控制,需要显式的创建和销毁

动态内存可以直接使用newdelete运算符进行管理,但是直接管理容易出现问题,所以标准库提供了智能指针

智能指针用于管理动态对象,行为类似常规指针,主要负责自动释放所指向的对象。

标准库在memory头文件中提供了两种智能指针和一个伴随类

  • shared_ptr指针,允许多个指针指向同一个对象
  • unique_ptr指针,独占所指向的对象
  • weak_ptr指针,是伴随类,指向share_ptr所管理的对象

1. 直接管理

使用new运算符分配内存,使用delete运算符释放new分配的内存

1.1 new运算符

new动态分配和初始化
普通对象

new运算符使用下方所示格式,分配内存并返回一个指向该动态内存的指针

new表达式执行两个动作:创建并初始化对象、分配内存

1
2
int *p1 = new int;
int *p3 = new Test;

使用new创建的对象的初始化有以下几种方式

  • 默认初始化

    默认情况下new T语句会对T类型执行默认初始化

    如果T为内置类型和复合类型,则为未定义;如果T为类类型,则调用默认构造函数

    1
    2
    3
    4
    5
    double*p = new double;					//默认初始化
    int main(){
    cout << *p << endl; //输出不为0,取决于编译器
    return 0;
    }
  • 值初始化

    当使用new T()类型的语句时,会对T执行值初始化

    如果T为内置类型和复合类型,则初始化为零;如果T为类类型,则调用默认构造函数

    1
    2
    3
    4
    5
    double *p = new double();				//值初始化
    int main(){
    cout << *p << endl; //输出为0
    return 0;
    }
  • 直接初始化

    不使用等号,而是直接在类型右侧紧跟()或者{}来进行初始化。注意要和构造函数相匹配

    1
    2
    3
    int *a = new int(1);
    string *b = new string(7,'b');
    vector<int>*c = new vector<int>{1,2,3,4,5,6};

初始化过程中的,当(){}中只有单一初始化器时(一个元素),可以使用auto自动推断要分配的对象的类型

1
2
3
auto p1 = new auto(1);					//p1为 int*类型
auto p1 = new auto{1}; //和上一个相同
auto p2 = new auto{1,2,3}; //出错,有多个初始化器
const对象

new运算符还可用于动态分配const对象。语法格式类似普通对象,且new操作返回的是指向const的指针

1
2
//语法格式
const int* p= new const int();

类似其他的const对象,动态分配的const对象在分配内存时必须进行初始化

动态分配的const对象的初始化有如下情况

  • 如果是一个有默认构造函数的类类型,可以使用上述三种方式的任意一种
  • 其他类型的对象必须使用值初始化或者直接初始化
定位new

当内存耗尽时,new表达式会返回一个空指针并抛出**bad_alloc异常**

1.2 delete运算符

在动态内存使用完毕之后,需要使用delete表达式来将动态内存归还给系统

delete表达式接收一个指向将要释放的对象的指针

1
2
int *p = new int();
delete p;

delete表达式执行两个动作:销毁对象、释放内存

delete运算符必须接收指针,并且最好是使用new运算符动态分配的内存的指针。如果将非new分配的指针交给delete运算符,或者是一个指针释放多次,其行为都是未定义的,可能产生破坏效果

动态分配的const对象的内存释放格式相同,没有特别之处

1
2
const int *a = new const int();
delete a; //不需要const等说明符

对于基本数据类型,delete运算符可以直接释放内存,对于自定义数据类型,delete运算符会调用其析构函数

1.3 相关问题

忘记释放内存
1
2
3
4
5
6
int *test(int a){
return new int(a);
}
void use_test(){ //此时的调用者必须负责内存的释放,或者将指针返回
int *p = test(1); //将释放内存的责任传递,否则调用者执行完毕后
} //此内存将无法释放
使用已经被释放的对象
1
2
3
4
5
int *a = new int();
int *b = a;
delete a;
a = nullptr;
cout << *b << endl; //此时b的内容不确定,可能引发错误
对同一内存释放多次
1
2
3
4
5
6
int *a = new int();
int *b = a;
delete a; //a已经释放了内存,之后b指针指向的内容不确定
a = nullptr;
delete b; //b又释放了一次,可能引发错误
b = nullptr;

2. 智能指针

2.1 共同特性

定义和初始化

智能指针也是模板,创建智能指针时,必须给出指针所指向的类型

1
2
3
//智能指针的声明和定义
shared_ptr<string> p1;
unique_ptr<int> u1;

智能指针在没有显式初始化时采用默认初始化,此时智能指针被初始化为一个空指针(nullptr)

智能指针可以使用new的返回指针来初始化,但是由于接受指针参数的智能指针构造函数是explicit修饰的,因此此种初始化方式只能使用直接初始化,而不能使用拷贝初始化

1
2
3
4
5
6
7
8
9
10
11
12
shared_ptr<int>p1(new int(1));			//正确,使用直接初始化
unique_ptr<int>p2(new int(1)); //正确

shared_ptr<int>p3 = new int(1); //错误,使用拷贝初始化
unqiue_ptr<int>p4 = new int(1); //错误

shared_ptr<int> clone(int p){ //错误,隐式的使用拷贝初始化
return new int(p);
}
shared_ptr<int> clone(int p){ //正确,显式的类型转换
return shared_ptr<int>(new int(p));
}

智能指针默认使用delete释放它所关联的对象,可以通过修改delete操作来修改智能指针的释放操作

除了修改delete操作,还可以重载智能指针的默认删除器,但是不同的智能指针格式不同

相关操作

shared_ptrunique_ptr都支持的操作

操作 说明
shared_ptr<T> sp
unique_ptr<T> up
空智能指针,可以指向类型为T的对象
p 将智能指针p作为条件判断,如果p指向一个对象,则为true
*p 解引用智能指针,获取指向的对象
p->mem 访问对象元素mem,等价于(*p).mem
p.get() 返回指向智能指针p所指对象的普通指针,类型为T*
swap(p,q)
p.swap(q)
交换pq中的指针

2.2 shared_ptr类

定义和初始化

除了shared_ptrunique_ptr的相同的定义和初始化操作,shared_ptr还有如下方式可以进行初始化

操作 说明
shared_ptr<T> p(q); 使用p管理指针q指向的对象
q必须指向使用new分配的内存且能够转换为T*类型
shared_ptr<T>p(q);
shared_ptr<T>p = q;
pshared_ptr q的拷贝,此操作会递增q中的计数
q中的指针必须能够转换为T*
shared_ptr<T> p(q,d); 使用p管理指针q指向的对象
q可以是普通指针或者是shared_ptr指针
p使用对象d代替delete
相关操作
操作概述
操作 说明
make_shared<T>(args); 创建一个类型为T的动态分配的对象,使用args初始化
返回指向此对象的shared_ptr指针
p = q; 二者都是shared_ptr,所保存的指针必须可以相互转换
此操作会递减p的引用计数,递增q的引用计数
p.unique(); p.use_count()为1,返回true,否则返回false
p.use_count(); 返回与p共享对象的智能指针数量,速度可能比较慢
p.reset();
p.reset(q);
p.reset(q,d);
如果p的引用计数为1,则释放此对象
如果提供了指针q,则p指向q,否则置空
如果提供了参数d,则调用d而不是delete释放q
make_shared函数

定义在头文件memory中。此函数在内存中分配一个对象并初始化它,然后返回指向此对象的shared_ptr指针

1
2
3
shared_ptr<int> p1 = make_shared<int>(1);
shared_ptr<string> p2 = make_shared<string>(10,'9');
shared_ptr<int> p3 = make_shared<int>(); //值初始化为0

make_shared实际上调用指定类型的构造函数来创建对象,因此传递的参数args必须与T的某个构造函数相匹配。不传递参数时,则进行值初始化

通常用auto定义变量来保存make_shared的结果

1
auto p = make_shared<int>(1);
重载删除器

shared_ptr默认使用delete运算符释放对象,对于自定义类型,delete运算符默认会调用对象的析构函数

当对象没有析构函数时,容易忘记显式释放内存;或者默认的析构函数无法满足要求时,可以通过shared_ptr的重载删除器的操作,修改释放对象内存时的操作,并且实现对象的自动管理

1
2
3
4
5
6
7
/*
* T:指针管理的对象的类型
* p:指针名称
* q:指向对象的普通指针或者另一个智能指针
* d:用于在销毁对象时执行的函数,函数必须接收一个T*的参数,且只能有此参数
*/
shared_ptr<T>p(q,d);
1
2
3
4
5
6
7
8
9
10
11
12
class A{
public:
int a = 1;
};
void freeA(A*a){
delete a;
}

int main(){
shared_ptr<A>p(new A(),freeA);
return 0;
}
管理动态内存
引用计数

每个shared_ptr内部都保存一个计数器,称为引用计数,用于保存指向指针当前所指对象的shared_ptr的个数

当拷贝一个shared_ptr时,引用计数会增加。比如,用一个shared_ptr初始化另一个shared_ptr、将shared_ptr作为函数参数、将shared_ptr作为函数返回值。

当给shared_ptr赋一个新值,或者销毁shared_ptr(如离开作用域)时,引用计数会减少

1
2
3
4
shared_ptr<int> p1 = make_shared<int>(1);
shared_ptr<int>p2(p1); //引用计数+1
cout << p1.use_count() << endl; //引用计数为2
cout << p2.use_count() << endl; //引用计数为2
自动销毁

当一个对象的引用计数为0时,shared_ptr类会自动销毁此对象

每次销毁shared_ptr时,调用shared_ptr的析构函数。此析构函数会将引用计数减1,如果引用计数为0,则调用对象的析构函数,销毁对象

1
2
3
4
5
6
7
shared_ptr<int> factory(int a){
return make_shared<int>(a);
}
void use_factory(int a){
auto p = factory(a);
}
//当use_factory执行完毕,离开作用域之后,p所指向的动态对象就会被自动销毁

不要将shared_ptr和普通指针混用,因为shared_ptr随时可能释放内存,造成普通指针悬置

1
2
3
4
5
6
7
8
9
void test(shared_ptr<int>p){
cout << *p << endl;
}
int main(){
int *p = new int();
test(shared_ptr<int>(p)); //临时的shared_ptr在test内引用计数为1
cout << *p << endl; //test执行完毕,则释放内存,此时p悬置
return 0;
}

不要使用get方法的返回指针初始化或者赋值给另一个智能指针,容易造成引用计数混乱,从而导致内存被意外释放

可以使用get方法返回的指针初始化或赋值给一个普通指针,但是注意此指针及其衍生出的指针不能使用delete或者让其引用计数归零,否则会导致原有智能指针悬置

1
2
3
4
5
6
shared_ptr<int>p(new int(1));
int *q = p.get();
{
shared_ptr<int>q1(q); //q1和p独立,二者的引用计数独立,但是指向相同的内存
}
int a = *p; //q1释放了内存,导致此处p指针悬置
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//通过拷贝构造的对象和原对象共享相同的容器,且使用智能指针管理
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;

StrBlob();
StrBlob(std::initializer_list<std::string> il);

size_type size() const{return data->size();}
bool empty() const{return data->empty();}

void push_back(const std::string &t){data->push_back(t);}
void pop_back();

std::string& front(); //访问首元素
std::string& back(); //访问尾元素

private:
//使用智能指针管理共享数据
std::shared_ptr<std::vector<std::string>> data;
//检查index为i的元素是否存在
void check(size_type i,const std::string &msg)const;
};
//构造函数
StrBlob::StrBlob():data(std::make_shared<std::vector<std::string>>()){}
StrBlob::StrBlob(std::initializer_list<std::string> il):data(std::make_shared<std::vector<std::string>>(il)){}

void StrBlob::check(size_type i,const std::string &msg)const{
if(i >= data->size())
//抛出异常
throw std::out_of_range(msg);
}

std::string& StrBlob::front(){
check(0,"StrBlob为空");
return data->front();
}
std::string& StrBlob::back(){
check(0,"StrBlob为空");
return data->back();
}

void StrBlob::pop_back(){
check(0,"StrBlob为空");
data->pop_back();
}

2.3 unique_ptr类

特点

unique_ptr独占它所指向的对象,不能将两个unique_ptr指向同一个对象

1
2
3
4
int *p = new int(1);
unique_ptr<int>u1(p);
unique_ptr<int>u2(p);
//两个unique_ptr指向同一个对象,会报错

unique_ptr被销毁时,它所指向的对象也被销毁

由于unique_ptr独占它所指向的对象,因此不允许进行拷贝和赋值操作

1
2
3
4
unique_ptr<int>u1(new int(1));
unique_ptr<int>u2(u1); //不支持拷贝操作
unique_ptr<int>u3;
u3 = u1; //不支持赋值操作

不能对unique_ptr进行拷贝和赋值有一个例外,即可以对将要销毁的unique_ptr进行拷贝和赋值

常用于函数返回

1
2
3
4
unique_ptr<int> test(unique_ptr<int> p){	//出错,参数传递执行了一次拷贝,但原指针不会销毁
unique_ptr<int>p1(new int(1));
return p1; //正确,函数返回执行了一次拷贝,因为原指针即将被销毁
}
相关操作
操作 说明
unqiue_ptr<T> u1; unqiue_ptr,指向类型为T的对象
u = nullptr; 使用u指向的对象并将u置空
u.release(); u放弃指针的控制权,返回指向对象的普通指针,并将u置空
u.reset();
u.reset(q);
u.reset(nullptr);
释放u所指向的对象
如果提供了指针q,则令u重新指向q所指对象
没有提供指针q,则将u置空
unique_ptr<T,D*> u(p,d); 重载删除器的unique_ptr指针的声明、定义和初始化操作
1
2
3
4
5
6
7
8
int *p = new int(1);
int *p2 = new int(2);
unique_ptr<int>u;
u.reset(p); //u指向p
u.reset(p2); //将p释放,u指向p2
cout << *p << endl; //原对象已经被释放,此处输出为不确定的值
int *p3 = u.release(); //返回指向p2所指对象的指针,并将u置空
cout << *p3 << endl; //输出2
重载删除器

类似shared_ptr对删除器的重载,unique_ptr也可以对删除器进行重载,只是格式略有不同,必须显式给出删除器的类型,且删除器的类型是unique_ptr类型的一部分

1
2
3
4
5
6
7
8
9
/*
* T:指针管理的对象的类型
* u:指针名称
* p:指向对象的普通指针
* D*:指向d函数的指针
* d:用于在销毁对象时执行的函数,函数必须接收一个T*的参数,且只能有此参数
*/
unique_ptr<T,D*>u(p,d);
//对于unique_ptr来说,删除器是其类型的一部分,因此要注意相关操作中类型的匹配
1
2
3
4
5
6
7
8
9
10
11
12
class A{
public:
int a = 1;
};
void freeA(A*a){
delete a;
}
int main(){
unique_ptr<A,decltype(freeA)*>p(new A(),freeA);
p.reset(new A()); //reset操作不会修改p的类型,因此删除器保持不变
return 0;
}
1
2
3
4
5
//注意此时类型的匹配
unique_ptr<A,decltype(freeA)*> test(){
unique_ptr<A,decltype(freeA)*>p(new A(),freeA);
return p;
}

2.4 weak_ptr类

weak_ptr是一种不控制所指向对象的生命周期的智能指针,指向一个由shared_ptr管理的对象,但是不会影响shared_ptr的引用计数,更不会影响到对象的销毁。weak_ptr类似python中的弱引用

定义和初始化

weak_ptr可以使用默认初始化,同shared_ptrunique_ptr的默认初始化一样,初始化为nullptr

1
weak_ptr<T> w;

不同于shared_ptrunique_ptr可以使用new进行直接初始化,weak_ptr指向的是shared_ptr管理的对象,可以使用shared_ptr或者weak_ptr进行直接初始化和拷贝初始化

1
2
3
4
5
shared_ptr<int>p(new int(1));
weak_ptr<int>w1(p); //正确,使用shared_ptr的直接初始化
weak_ptr<int>w1 = p; //正确,使用shared_ptr的拷贝初始化
weak_ptr<int>w2(w1); //正确,使用weak_ptr的直接初始化
weak_ptr<int>w2 = w1; //正确,使用weak_ptr的拷贝初始化
操作总结
操作 说明
weak_ptr<T> w;
weak_ptr<T> w(p);
weak_ptr<T> w = p;
weak_ptr指针的定义和初始化
包括默认初始化,直接初始化和拷贝初始化
w = p; p为一个shared_ptr或者weak_ptr指针
赋值后wp共享对象,但是不影响原有的引用计数
w.reset(); w置空,不会影响对象的引用计数和释放
w.use_count(); w指向的对象上的shared_ptr的个数
w.expired(); w.use_count()0,返回true,否则为false
w.lock(); w.expired()=true,返回空的shared_ptr
w.expired()=false,返回指向w所指对象的shared_ptr
用于检查weak_ptr指向的对象是否还存在
1
2
3
4
if(shared_ptr<int>p = w.lock()){
//只有当p不为空,即为w所指向的对象仍然存在时,才会进入此语句块
//在此块内访问对象是安全的
}

3. 动态数组

某些应用场景下,需要一次性为很多对象分配连续内存,此时传统的new操作无法满足要求

标准库提供了两种一次性分配一个对象数组的方法

  • 新的new表达式语法
  • 标准库allocator

使用上述方式分配内存构造的对象数组称为动态数组,大部情况下最好使用标准库容器以获取更好的体验和性能,而不是手动分配动态数组

3.1 new和智能指针

定义、初始化及释放
定义和释放

使用新的newdelete语法来进行动态数组的定义和释放

1
2
3
4
5
T *p = new T[size];			//T为数组内对象的类型

typedef T a[size]; //使用数组的类型别名
using a = T[size];
T *p = new a;

定义动态数组时,其中的size为整型,但不一定是常量,表示动态数组中的对象的个数。p为指向动态数组中第一个T类型对象的指针。也可以使用第二种累哦行别名的方式定义动态数组,实际执行过程和第一种完全相同

对于new表达式的返回结果pp是一个T*类型的指针,指向数组中的第一个对象。p不是指向数组类型的指针更不是数组类型的对象,不能将其用于数组的beginend操作,也不能使用范围for语句直接处理动态数组。动态数组不是数组类型

1
delete [] p;

释放动态数组时,不论使用何种方式定义,delete后必须加上[],否则执行的结果不确定,可能引发错误

初始化

默认情况下使用默认初始化

1
2
int *p = new int[10];
string *s = new string[10];

可以在[]后紧跟一个()来进行值初始化

1
2
int *p = new int[10]();
string *s = new string[10]();

还可以使用列表初始化{}的方式显式的初始化动态数组,类似普通数组的列表初始化,但是之只能进行直接初始化

初始化器的数目少于元素的数目,剩下的元素进行值初始化;初始化器的数目大于元素的数目,报错,不会分配内存

1
2
int *p = new int[4]{0,1,2,3};
int *p = new int[]{0,1,2,3}; //错误,不能像普通数组一样省略大小
智能指针管理动态数组

使用unique_ptr管理通过new运算符创建的动态数组时,需要在对象类型后跟[]

1
2
unique_ptr<T[]>p(new T[size]);
unique_ptr<int[]>p(new int[3]{1,2,3});

由于此时unique_ptr指向的是动态数组,而不是单个对象,因此智能指针p不能直接使用*->运算符。此时销毁智能指针所指对象时自动调用delete[]而不是delete,并且可以直接使用下标运算符来访问数组中的元素

1
p[0];	p[1];

除了上述特殊之外,unique_ptr的其他操作都可用于管理动态数组

shared_ptr不支持直接管理动态数组,需要在初始化时显式提供适用于动态数组的自定义删除器。只要提供了自定义的适用于动态数组的删除器,就可以使用shared_ptr管理动态数组

1
2
3
shared_ptr<T>p(new T[size],删除器);
//使用lambda作为删除器
shared_ptr<int>p(new int[3]{1,2,3},[](int*p){delete [] p;});

指向动态数组的shared_ptr未定义下标运算符,也不支持指针算数运算。但不同于unique_ptr,此处的shared_ptr支持*->运算符,也支持其他的shared_ptr操作。此时对shared_ptr执行*或者->运算时,shared_ptr相当于指向动态数组的第一个对象的指针,只能直接访问第一个对象

1
2
3
shared_ptr<Test>p(new Test[3]{Test(2),Test(3),Test(4)},[](Test*p){delete []p;});
cout << (*p).a <<endl; //输出2
cout << p->a << endl; //输出2

3.2 allocator类

new操作将内存分配和对象构造组合在一起,delete操作将对象析构和内存释放组合在一起。当分配一大块内存时,通常需要按需构造对象,此时如果使用newdelete则会造成大量不必要的对象构造和赋值操作,从而导致资源的浪费

当分配一大块内存时,通常需要将内存分配和对象构造相分离,待需要的时候按需构造对象

标准库memory提供了allocator类实现内存分配和对象构造的分离

定义和内存分配

allocator类是一个模板类,定义时必须指定要分配内存的对象类型

1
2
allocator<T>alloc;			//定义allocator对象
T* p = alloc.allocate(n); //分配n个T对象占用的内存,并返回指向T类型的指针

allocator类根据对象类型和数量来确定内存大小和对齐位置,并且使用allocator类分配的内存是未构造的

allocator类使用construct成员函数在给定的内存位置构造特定的对象。

construct函数接收一个指针(类型为T*,指向将要构造的对象的位置)和零个或多个额外参数

  • 没有额外参数,进行默认初始化
  • 有额外参数,将额外参数传入相匹配的构造函数来构造对象
1
2
3
4
5
6
allocator<string>alloc;
string*p = alloc.allocate(10);
auto q = p;
alloc.construct(q++); //空字符串
alloc.construct(q++,10,'c'); //10个'c'构成的字符串
alloc.construct(q++,"aa"); //字符串"aa"
销毁和内存释放

构造出来的对象可以使用destory成员函数来销毁。destory函数接收一个指向要销毁对象的指针,并调用此对象的析构函数来销毁对象。不能将destory用于未构造的内存

1
2
3
4
allocator<int>alloc;
auto p = alloc.allocate(10);
alloc.construct(p,1);
alloc.destory(p);

对象或元素被销毁后,释放出来的内存空间可以重复利用,再次进行构造,当然也可以使用deallocate释放

deallocate函数接收两个参数pn

  • p为指向要释放的内存的起始位置的指针
  • n为要释放的内存块的个数,要和分配内存时的一致,否则会出现异常

注意可以直接使用deallocate回收内存,而不用提前销毁对象,但是这样可能会导致对象销毁异常

并且使用deallocate时,n的值必须和内存分配时给出的值一致,即整块内存必须同时分配和释放

伴随算法

标准库为allocator类定义了两种伴随算法(不是成员函数),用于快速拷贝和填充未初始化的内存

算法 说明
uninitialized_copy(b,e,b2); 从迭代器be指定的范围内拷贝元素到迭代器b2指定的内存中
b2指定的内存必须足够容纳所有要拷贝的元素,否则可能出错
算法返回指向最后一个构造元素之后位置的指针
uninitialized_copy_n(b,n,b2); 从迭代器b开始拷贝n个元素到迭代器b2指定的内存中
b2指定的内存必须足够大
算法返回指向最后一个构造元素之后位置的指针
uninitialized_fill(b,e,t); 在迭代器be指定的范围内构造对象
对象的值均为t的拷贝
uninitialized_fill_n(b,n,t); 从迭代器b指向的内存地址开始构造n个对象
对象的值均为t的拷贝,且内存必须足够大

上述伴随算法构造对象的类型根据迭代器的类型确定。此处的迭代器可以是容器的迭代器类型,也可以是指针

1
2
3
4
5
allocator<int>alloc;
int*p = alloc.allocate(10);
uninitialized_fill_n(p,10,1);
int *q = alloc.allocate(10);
uninitialized_copy_n(p,10,q);