文章目录
- 前言
- 动态内存与智能指针
- shared_ptr类
- shared_ptr和unique_ptr都支持的操作
- shared_ptr独有的操作
- make_shared 函数
- shared_ptr的拷贝和赋值
- shared_ptr自动销毁所管理的对象
- shared_ptr还会自动释放相关联的内存
- 程序使用动态内存出于以下原因
- 直接管理内存
- 使用new动态分配和初始化对象
- 动态分配的const对象
- 内存耗尽 定位new
- 释放动态内存
- 使用new和delete管理动态内存存在三个常见问题
- delete之后重置指针值
- 智能指针和new对比
- shared_ptr和new结合使用
- 定义和改变shared_ptr的其他方法
- 不要混合使用智能指针和普通指针
- 不要使用get初始化另一个智能指针或为智能指针赋值
- 智能指针和异常
- 使用自己定义的释放操作
- 使用智能指针基本规范
- unique_ptr
- unique_ptr特有的操作
- release返回指针
- 传递unique_ptr参数和返回unique_ptr
- 向unique_ptr传递删除器
- weak_ptr
- 动态数组
- new和数组
- 释放动态数组
- 智能指针和动态数组
- 指向数组的unique_ptr
- 连接两个字符串
- allocator类
- 标准库allocator类及其算法
- allocator分配未构造的内存
- allocator算法
前言
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称为自由空间或堆。程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。
动态内存与智能指针
动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是及其困难的。忘记释放内存,会产生内存泄漏;在尚有指针引用内存的情况下我们就释放了内存,会产生引用非法内存的指针。
新标准库提供了两种智能指针类型来管理动态对象,它可以自动释放所指向的对象。shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。weak_ptr是一种弱引用,指向shared_ptr所管理的对象,这三种类型都定义在memory头文件中。
shared_ptr类
当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:
shared_ptr<string>p1; shared_ptr,可以指向string
shared_ptr<list<int>>p2; shared_ptr,可以指向list<int>
默认初始化的智能指针中保存着一个空指针。
shared_ptr和unique_ptr都支持的操作
shared_ptr独有的操作
make_shared 函数
p3指向一个值为42的int的智能指针
shared_ptr<int>p3 = make_shared<int>(42);p4指向一个值为"88888"的string的智能指针
shared_ptr<string>p4 = make_shared<string>(5,'8');p5指向一个值为0的int的智能指针(int的初始化值为0)
shared_ptr<int>p5 = make_shared<int>();
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。离开作用域计数器会递减,赋值操作,原指针计算器会递减,赋值指向的指针计数器会递增。示例代码如下:
auto p = make_shared<int>(42);//p指向的对象只有p一个引用者auto r = make_shared<int>(4);//p指向的对象只有p一个引用者auto r2(r);//p指向的对象只有p一个引用者cout << "p引用数量:" << p.use_count()<<endl;{auto q(p);//p和q指向相同对象,此对象有两个引用者cout << "p引用数量:" << p.use_count() << endl;cout << "p指向:" << *p << endl;*q = 10;cout << "p指向:" << *p << endl;}cout << "在赋值之前,p引用数量:" << p.use_count() << endl;cout << "在赋值之前,原r引用数量:" << r2.use_count() << endl;r = p;cout << "在赋值之后,原r引用数量:" << r2.use_count() << endl;cout << "在赋值之后,p引用数量:" << p.use_count() << endl;cout << "p指向:" << *p << endl;
输出结果:
p引用数量:1
p引用数量:2
p指向:42
p指向:10
在赋值之前,p引用数量:1
在赋值之前,原r引用数量:2
在赋值之后,原r引用数量:1
在赋值之后,p引用数量:2
p指向:10
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会通过析构函数自动销毁此对象。shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr还会自动释放相关联的内存
当动态对象不再被使用时,shared_ptr会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。
假设现有一个Foo类,示例代码:
//factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg){return make_shared<Foo>(arg);
}void use_factory(T arg){shared_ptr<Foo>p=factory(arg);//使用p
}//p离开作用域,它指向的内存会被自动释放//但如果有其他shared_ptr也指向这块内存,则它就不会被释放:
shared_ptr<Foo> use_factory2(T arg){shared_ptr<Foo>p=factory(arg);//使用preturn p;//当我们返回p时,引用计数进行了递增
}//p离开作用域,它指向的内存不会被释放
因此,如果将shared_ptr存放于一个容器中,而后不再需要全部元素而只使用其中一部分,要记得用erase删除不再需要的那些元素。
程序使用动态内存出于以下原因
- 程序不知道自己需要使用多少对象 例如:容器类
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
我们定义一个类,它使用动态内存是为了让多个对象共享相同的底层数据。我们定义一个名为Blob的类,保存一组元素。我们希望Blob对象的不同拷贝之间共享相同的元素,即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素。
Blob类的定义如下:
#ifndef __BLOB__
#define __BLOB__#include<string>
# include<iostream>
# include<vector>
# include<memory>
using namespace std;class Blob {
public:typedef vector<string>::size_type size_type;Blob();Blob(initializer_list<string>il);size_type size()const { return data->size(); }bool empty()const{return data->empty();}//添加和删除元素void push_back(const string &t) { data->push_back(t); }void pop_back();//元素访问string & front();string & back();//获取元素shared_ptr<vector<string>> getData() { return data; }//修改元素void changeData(size_type i,string str) {(*data)[i] = str;}private:shared_ptr<vector<string>>data;void check(size_type i,const string &msg)const;
};Blob::Blob() :data(make_shared<vector<string>>()){}
Blob::Blob(initializer_list<string>il) : data(make_shared<vector<string>>(il)) {}
void Blob::check(size_type i, const string &msg)const {if (i >= data->size())throw out_of_range(msg);
}
string& Blob::front() {check(0,"front on empty Blob");return data->front();
}
string& Blob::back() {check(0, "back on empty Blob");return data->back();
}
void Blob::pop_back() {check(0, "pop_back on empty Blob");data->pop_back();
}
void print(ostream& os,Blob b) {for (auto a : *(b.getData())) {os << a << " ";}os << endl;
}#endif
对Blob进行测试代码如下:
Blob b1{ "a","an","the" };cout << "b1的元素为:";print(cout,b1);Blob b2(b1);cout << "b2的元素为:";print(cout, b2);b2.changeData(0,"hello");cout << "对b2中的元素进行修改后:" << endl;cout << "b1的元素为:";print(cout, b1);cout << "b2的元素为:";print(cout, b2);
输出结果:
b1的元素为:a an the
b2的元素为:a an the
对b2中的元素进行修改后:
b1的元素为:hello an the
b2的元素为:hello an the
从输出结果我们可以看出,Blob在多个对象间共享数据。当我们拷贝、赋值或销毁一个Blob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。
我们定 义的Blob与vector的区别:
vector<string>v1;
{vector<string>v2={"a","an","the"};v1=v2;//从v2拷贝元素到v1中
}//v2被销毁,其中的元素也被销毁
//v1有三个元素,是原来v2元素的拷贝
Blob<string>b1;
{Blob<string>b2={"a","an","the"};b1=b2;//b1和b2共享相同的元素
}//b2被销毁,其中的元素不能销毁
//b1指向最初由b2创建的元素
直接管理内存
C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。
例如:
int *pi = new int(1024);
vector<int> *pv = new vector<int>{1,2,3,4,5};
如果我们提供了一个括号包围的初始化器,就可以使用auto,从此初始化器来推断我们想要分配的对象的类型。只有当括号中仅有单一初始化器时才可以使用auto:
auto p1 = new auto(obj); //p1指向一个与obj类型相同的对象
auto p2 = new auto(a,b,c); //错误:括号中只能有单个初始化器
动态分配的const对象
类似其他任何const对象,一个动态分配的const对象必须进行初始化。
const int *pci = new const int(1024);
内存耗尽 定位new
虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常。我们称这种形式的new为定位new。
int *p1 = new int;//如果分配失败,new抛出std::bad_alloc
int *p1 = new (nothrow) int;//如果分配失败,new返回一个空指针
释放动态内存
我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:
delete p;
与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。
使用new和delete管理动态内存存在三个常见问题
- 忘记delete内存,会导致“内存泄漏”问题,因为这种内存永远不可能归还给自由空间了。
- 使用已经释放掉的对象。
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。
delete之后重置指针值
当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的,以下代码重置p对q没有任何作用。
int *p(new int(42));
auto q=p; //p和q指向相同的内存
delete p;//p和q均变为无效
p=nullptr;//指出p不再绑定到任何对象
智能指针和new对比
int *q=new int(42),*r=new int(100);
r=q;
auto q2=make_shared<int>(42),r2=make_shared<int>(100);
r2=q2;
以上代码new指针的两个问题:
- 内存泄漏问题,r和q都指向42的内存地址,而r原来保存的地址——100的内存再无指针管理,不能进行释放,造成内存泄漏。
- 空悬指针问题,r和q指向同一个动态对象,如果程序编写不当,很容易产生释放了其中一个指针,而继续使用另一个指针的问题。
智能指针的优势:
- 将q2赋予r2,赋值操作会将q2指向的对象地址赋予r2,并将r2原来指向的对象的引用计数减1,将q2指向的对象的引用计数加1,这样前者的引用计数变为0,其占用的内存空间会被释放,不会造成内存泄漏。
- 而后者的引用计数变为2,也不会因为r2和q2之一的销毁而释放它的内存空间,因此也不会造成空悬指针的问题。
shared_ptr和new结合使用
如果我们不出事后一个智能指针,它就会被初始化为一个空指针。我们还可以用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数是explicit,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。
shared_ptr<int>p1=new int(1024);//错误
shared_ptr<int>p2(new int(1024));//正确:使用了直接初始化形式
定义和改变shared_ptr的其他方法
不要混合使用智能指针和普通指针
void process(shared_ptr<int>ptr){}shared_ptr<int> x(new int(1024));//引用计数为1
process(x); //正确,拷贝x会递增它的引用计数,在process中引用计数为2
int j=*x;//正确:引用计数为1
void process(shared_ptr<int>ptr){}int *x(new int(1024));
process(x); //错误,不能将int*转换为shared_ptr<int>
process(shared_ptr<int>(x));//正确,但内存会被释放
int j=*x;//未定义的,x是一个空悬指针
在该调用中,我们将一个临时的shared_ptr传递给process,当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数为0,因此,当临时变量被销毁时,它所指向的内存会被释放,但x继续指向(已经释放的)内存,从而变成一个空悬指针,如果试图使用x的值,其行为是未定义的。
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。
不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。当我们需要向不能使用智能指针的代码传递一个内置指针时可用get。使用get返回的指针的代码不能delete此指针。
shared_ptr<int>p(new int(42));//p的引用计数为1int *q = p.get();//正确,但使用q时要注意,不要让它管理的指针被释放,p的引用计数仍然为1{process(shared_ptr<int>(q));}int foo = *p;//错误,此时p的引用计数为1,但p指向的内存已经被释放了,p成为类似空悬指针的shared_ptr
在本例中,p和q指向相同的内存,由于它们是相互独立创建的,因此各自的引用计数都是1。当q所在的程序块结束时,q被销毁,这会导致q所指向的内存被释放。从而q变成一个空悬指针,当我们试图使用p时,将发生未定义的行为。而且当p被销毁时,这块内存会被第二次delete。
智能指针和异常
使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f(){shared_ptr<int>sp(new int(42));//分配一个新对象//这段代码抛出一个异常,且在f中未被捕获
}//在函数结束时shared_ptr自动释放内存
与之相对的,如果使用内置指针管理内存,且在new之后在对应的delete之前发生异常,则内存不会被释放:
void f(){int *ip=new int(42);//动态分配一个新对象//这段代码抛出一个异常,且在f中未被捕获,则内存不会被释放delete ip;//释放内存的代码
}
使用自己定义的释放操作
现有以下类和函数:
struct destination;//表示我们正在连接什么
struct connection;//使用连接所需的信息
connection connect(destination*);//打开连接的函数
void disconnect(connection);//关闭给定的连接
代码一:
void f(destination &d){//获得一个连接connection c = connect(&d);//使用连接//如果我们在f退出前忘记调用disconnect,就无法关闭c了
}
代码二,使用shared_ptr:
//定义删除器:
void end_connection(connection *p){disconnect(*p);}
//创建shared_ptr:
void f(destination &d){//获得一个连接connection c = connect(&d);shared_ptr<connection>p(&c,end_connection);//使用连接//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection,接下来end_connection会调用disconnect,从而确保连接被正确关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接被正确关闭。
使用智能指针基本规范
- 不适用相同的内置指针值初始化(或reset)多个智能指针
- 不delete get()返回的指针
- 不适用get()初始化或reset另一个智能指针
- 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
unique_ptr
与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象,与shared相同的操作在表中
unique_ptr特有的操作
由于一个 unique_ptr拥有它所指向的对象,因此 unique_ptr不支持普通的拷贝或赋值操作:
unique_ptr<string>p1(new string("hello"));
unique_ptr<string>p2(p1);//错误,不支持拷贝
unique_ptr<string>p3;
p3=p1;//错误,不支持赋值
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr:
release返回指针
如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p2.release();//错误,p2不会释放内存,而且我们丢失了指针
auto p=p2.release();//正确,但我们必须记得 delete p
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p){return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p){unique_ptr<int>ret(new int(p));//...return ret;
}
向unique_ptr传递删除器
类似shared_ptr,unique_ptr默认情况下用delete释放它所指向的对象。与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器。
对比shared_ptr的连接程序,我们重写连接程序如下:
//定义删除器:
void end_connection(connection *p){disconnect(*p);}
//创建shared_ptr:
void f(destination &d){//获得一个连接connection c = connect(&d);unique_ptr<connection,decltype(end_connection)*>p(&c,end_connection);//使用连接//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
示例代码:
auto p = make_shared<int>(42);auto q(p);auto p2(p);cout << "p.use_count():" << p.use_count() <<endl;weak_ptr<int>wp(p);cout << "p.use_count():" << p.use_count() << endl;cout << "wp.use_count():" << wp.use_count() << endl;
输出结果:
p.use_count():3
p.use_count():3
wp.use_count():3
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock。次函数检查weak_ptr指向的对象是否仍存在。
if(shared_ptr<int>np=wp.lock()){ //如果np不为空则条件成立
//在if中,np与p共享对象
}
动态数组
new和数组
int *pia = new int[10];//分配10个int,pia指向第一个int
分配一个数组会得到一个元素类型的指针。
我们还可以提供一个元素初始化器的花括号列表:
释放动态数组
delete p;//p必须指向一个动态分配的对象或为空
delete [] p;//p必须指向一个动态分配的数组或为空,
//数组中的元素按逆序进行销毁,即,最后一个元素首先被销毁,
//然后是倒数第二个,以此类推
智能指针和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本。
unique_ptr<int[]>up(new int[10]);
up.release();//自动用delete[]销毁其指针
指向数组的unique_ptr
与unique_ptr不同,shared_ptr不直接支持管理动态数组,如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:
shared_ptr<int>sp(new int[10],[](int *p){delete[]p;});
sp.reset();//使用我们提供的lambda释放数组,它使用delete[]释放数组
如果未提供删除器,这段代码将是未定义的。默认情况下,shared_ptr使用delete销毁它指向的对象。
连接两个字符串
代码:
const char *c1 = "hello";const char *c2 = "world";char *r = new char[strlen(c1)+ strlen(c2)+1];strcpy(r,c1);strcat(r, c2);cout << r << endl;string s1 = "hello";string s2 = "world";strcpy(r,(s1+s2).c_str());cout << r << endl;
输出结果:
helloworld
helloworld
allocator类
new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起,这可能会导致不必要的浪费。
标准库allocator类定义在头文件memory中。它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
标准库allocator类及其算法
allocator分配未构造的内存
还未构造对象的情况下就使用原始内存是错误的。
为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。
当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数。
allocator算法
类似copy,uninitialized_copy返回(递增后的)目的位置迭代器,因此,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。