标签:重要 释放内存 ptr mic 函数返回 let empty 添加 列表
C++ 中动态内存管理通过一对运算符来完成:
new
,在动态内存中为对象分配空间并返回一个指向该对象的指针。delete
,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。新标准提供两种智能指针类型来管理动态对象,智能指针的行为类似常规指针,重要的区别是智能指针负责自动释放所指向的对象,新标准提供的这两种智能指针的区别在于管理底层指针的方式:
shared_ptr
允许多个指针指向同一个对象。unique_ptr
则独占所指向的对象。标准库还定义了一个名为 weak_ptr
的伴随类,它是一种弱引用,指向 shared_ptr
所管理的对象。
以上三种类型都定义在 memory
头文件中。
智能指针也是模板,创建智能指针时,必须提供指针可以指向的类型:
shared_ptr<string> p1; //shared_ptr,可以指向string
shared_ptr<list<int>> p2; //shared_ptr,可以指向int的list
默认初始化的指针保存着一个空指针。
智能指针的使用方式与普通指针类似:
//如果p1不为空,并且指向的是一个空string
if(p1 && p1->empty())
*p1 = "hi"; //解引用p1,将一个新值赋予string
最安全的分配和使用动态内存的方法是调用一个名为 make_shared
的标准库函数。
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
。
shared_ptr
也定义在头文件 memory
中。
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10,'9');
shared_ptr<int> p5 = make_shared<int>(); //p5指向一个值初始化的int,即值为0
make_shared
用其参数来构造给定的对象,如果不传递任何参数,对象就会进行值初始化。
通常使用 auto
定义一个对象来保存 make_shared
的结果:
//p6指向一个动态分配的空的vector<string>
auto p6 = make_shared<vector<string>>();
当进行拷贝或赋值时,每个 shared_ptr
都会记录有多少个其它 shared_ptr
指向相同的对象。
auto p = make_shared<int> 42; //p指向的对象只有p一个引用者
auto q(p); //p和q指向相同对象,此对象有两个引用者
可以认为 每个 shared_ptr 都有一个关联的计数器,通常称为引用计数,无论何时拷贝一个 shared_ptr,计数器都会递增:
shared_ptr
初始化另一个 shared_ptr
。shared_ptr
作为参数传递给一个函数。shared_ptr
作为函数的返回值。计数器递减:
shared_ptr
赋予一个新值。shared_ptr
被销毁。shared_ptr
离开其作用域。一旦一个 shared_ptr
的计数器变为0,它就会自动释放自己所管理的对象。
auto r = make_shared<int>(42);
r = q; //给人赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//r原来指向的对象已没有引用者,会自动释放
当指向一个对象的最后一个 shared_ptr
被销毁时,shared_ptr
会自动销毁此对象。
shared_ptr
的析构函数会递减它所指向的对象的引用计数,如果引用计数变为0,shared_ptr
的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr<Foo> factory(T arg)
{
return make_shared<Foo>(arg);
}
void use_factory(T arg)
{
shared_ptr<5Foo> p = factory(arg);
}
程序使用动态内存出于以下三种原因之一:
容器类是出于第一种原因而使用动态内存的典型例子。
当拷贝一个vector
时,原 vector
和副本 vector
中的元素是相互分离的:
vector<string> v1;
{
vector<string> v2 = {"a","an","the"};
v1 = v2; //从v2拷贝元素到v1中
} //v2被销毁,其中的元素也被销毁
//v1中有三个元素,是原来v2中元素的拷贝
加入定义一个 Blob
类,保存一组元素,与容器不同,我们希望 Blob
对象的不同拷贝之间共享相同的元素。即,当我们拷贝一个 Blob
时,原 Blob
对象及其拷贝应该引用相同的底层元素。
一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,就不能单方面地销毁底层数据:
Blob<string> b1;
{
Blob<string> b2 = {"a","an","the"};
b1 = b2;
} //b2被销毁,但b2中的元素不能被销毁
//b1指向最初由b2创建的元素
定义 Blob
类:
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;
//如果data[i] 不合法,抛出一个异常
void check(size_type i,const std::string &msg) const;
};
自由对象分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回指向该对象的指针:
int *pi = new int; //pi指向一个动态分配的、未初始化的无名对象
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:
string *ps = new string; //初始化为空string
int *pi = new int; //pi指向一个未初始化的int
可以用直接初始化方式来初始化一个动态分配的对象,也可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):
int *pi = new int(1024);
string *ps = new string(10,'9');
vector<int> new vector<int>{0,1,2,3,4,5,6,7,8,9};
也可以对动态分配的对象进行值初始化,只需要在类型名之后根一个空括号即可:
string *ps1 = new string(); //默认初始化为空string
string *ps2 = new string(); //值初始化为空string
int *pi1 = new int; //默认初始化为,*pi1的值未定义
int *pi2 = new int(); //值初始化为0,*pi2为0
对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的,不管采用什么形式,对象都会通过默认构造函数来初始化。
对于内置类型,值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值是未定义的。
出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是一个好主意。
如果提供了一个括号包围的初始化器,就可以使用 auto
,从此初始化器来推断想要分配的对象的类型,但是,由于编译器要用初始化器来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用 auto
:
auto p1 = new auto(obj); //p指向一个与obj类型相同的对象,该对象用obj进行初始化
auto p2 = new auto{a,b,c}; //错误,括号中只能有一个初始化器
p1
的类型是一个指针,指向从 obj
自动自动推断出的类型,新分配的对象用 obj
的值进行初始化。
const int *pci = new const int(1024); //分配并初始化一个const int
const string *pcs = new const string; //分配并初始化一个空的 string
一个动态分配的 const
对象必须进行初始化。
对于一个定义了默认构造函数的类类型,其 const
动态对象可以隐式初始化,而其它类型的对象就必须显示初始化,由于分配的对象是 const
的,new返回的指针是一个指向 const
的指针。
一旦一个程序用光了它所有可用的内存,它会抛出一个类型为 bad_alloc
的异常,可以改变使用 new
的方式来阻止它抛出异常:
int *p1 = new int; //如果分配失败,new 抛出std::bad_alloc
int *p2 = new (nothrow) int; //如果分配失败,new 返回一个空指针
称这种形式的 new
为定位new
,定位 new
表达式允许向 new
传递额外的参数,在这个例子中传递的是由标准库定义的名为 nothrow
的对象。
bad_alloc
,nothrow
都定义在头文件 new
中。
delete
表达式执行两个动作:销毁给定的指针指向的对象;释放对应的内存。
传递给 delete
的指针必须指向动态分配的内存,或者是一个空的指针。
释放一块并非 new
分配的内存,或者将相同的指针释放多次,其行为是未定义的。
int i,*pi1 = &i,*pi2 = nullptr;
double *pd = new double(33),*pd2 = pd;
delete i; //错误,i不是一个指针
delete pi1; //未定义,pi1指向一个局部变量
delete pd; //正确
delete pd2; //未定义,pd2指向的内存已经被释放了
delete pi2; //正确,释放一个空指针总是没有错误的
释放动态对象:
const int *pci = new const int(1024);
delete pci; //正确,释放一个const对象
对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。
返回指向动态内存的指针的函数给调用者增加了一个额外的负担------调用者必须记得释放内存。
FOO* factory(T arg)
{
return new Foo(arg); //调用者负责释放此内存
}
void use_factory(T arg)
{
//使用p,但不delete它
Foo* p = factory(arg);
//pl离开它的作用域,但它所指向的内存并没有被释放
}
注意:
使用 new
和 delete
管理动态内存存在三个常见问题:
delete
内存。忘记释放动态内存会导致内存泄漏,因为这种内存永远不可能被归还给自由空间。delete
,对象的内存就被归还给自由空间了,如果随后有一次 delete
这个指针,自由空间就可能被破坏。坚持只使用智能指针,就可以避免所有的问题。
当 delete
一个指针以后,指针的值就是无效的,虽然指针已经无效了,但是在很多机器上仍然保存着(已经释放了的)动态内存的地址。在 delete
之后,指针就变成了 空悬指针。即,指向一块曾经保存过数据对象但现在已经无效的内存的指针。
避免空悬指针:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果需要保留指针,可以在 delete
之后将 nullptr
赋予指针,这样就指出不指向任何对象。
如果不初始化一个智能指针,它就会被初始化为一个空指针。可以使用 new
返回的指针来初始化智能指针:
shared_ptr<double> p1; //shared_ptr 可以指向一个double
shared_ptr<double> p2(new int(42)); //p2 指向一个值为42的int
接受指针参数的智能指针构造函数是 explicit
的,因此不能将一个内置指针隐式转换为一个智能指针,必须使用初始化形式来初始化一个智能指针:
shared_ptr<int> p1 = new int(1024); //错误,必须使用直接初始化形式
shared_ptr<int> p2 (new int (1024)); //正确,使用了直接初始化形式
p1 的初始化隐式地要求编译器将一个 new
返回的 int*
来创建一个 shared_ptr
。由于不能进行内置指针到智能智能指针的隐式转换,因此初始化语句是错误的。基于这个原因,一个返回 shared_ptr
的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int> clone(int p){
return new int(p); //错误,隐式转换成shared_ptr<int>
}
必须显式绑定到一个要返回的指针上:
shared_ptr<int> clone(int p){
return shared_ptr<int>(new int(p));
}
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为无法知道对象何时会被销毁。
void process(shared_ptr<int> ptr){
//使用ptr
} //ptr离开作用域,被销毁
使用此函数的正确方法是给它传递一个 shared_ptr
:
shared_ptr<int> p(new int(42));
process(p); //拷贝p会递增它的引用计数,在process中引用计数为2
int i = *p; //正确,引用计数值为1
如果传递给它一个临时的 shared_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
的值,其行为是未定义的。
智能指针类型定义了一个名为 get
的函数,它返回一个内置指针,指向智能指针管理的对象。此函数的设计是为了这样的情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针,使用 get
返回的指针的代码,不能 delete
此指针。
虽然编译器不会给出警告,但将一个智能指针也绑定到 get 返回的指针上是错误的:
shared_ptr<int> p (new int(42));
int *q = p.get(); //正确,但使用q时要注意,不要让它管理的指针被释放
{
//两个独立的 shared_ptr 指向相同的内存
shared_ptr<int> (q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p; //未定义,p指向的内存已经被释放了
在上面的例子中,p
和 q
指向相同的内存,由于它们是相互独立创建的,因此各自的引用计数都是1。
当 q
所在的程序块结束时,q
被销毁,这会导致 q
所指向的内存被释放。从而 p
变成一个空悬指针。意味着使用 p
时,将发生未定义的行为。并且,当 p
被销毁时,这块内存会被第二次 delete
。
get
用来将指针的访问权限传递给代码,只有在确定代码不会 delete
指针的情况下,才能使用 get
。特别地,永远不要使用 get
初始化另一个智能指针或者为另一个智能指针赋值。
可以使用 reset 来将一个新的指针赋予一个 shared_ptr:
p = new int(1024); //错误,不能将一个指针赋予 shared_ptr
p.reset(new int(1024)); //正确,p指向一个新对象
与赋值类似,reset 会更新引用计数,如果需要的话,会释放p指向的对象。
reset 成员经常与 uniqe 一起使用,来控制多个 shared_ptr 共享的对象。在改变底层对象之前,检查自己是否是当前对象仅有的用户,如果不是,在改变之前要制作一份新的拷贝:
if(!p.unique())
p.reset(new string(*p)); //不是唯一的用户,分配新的拷贝
*p += newVal; //现在知道自己是唯一的用户,可以改变对象的值
如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f()
{
shared_ptr<int> sp(new int(42)); //分配一个新对象
// 此处代码抛出一个异常,且在f中未被捕获
}// 在函数结束时 shared_ptr 自动释放内存
函数退出有两种可能:正常处理结束或发生了异常,无论哪种情况,局部对象都会被销毁,在上面的程序中, sp
是一个 shared_ptr
,因此 sp
销毁时会将内存释放掉。
与之相对应的,当发生异常时,直接管理的内存是不会自动生成释放的,如果使用内置指针管理内存,且在 new 之后,在对应的 delete 之前发生了异常,则内存不会释放:
void f()
{
int *p = new int(42);
// 此处代码抛出一个异常,且在f中未被捕获
delete ip;
}
如果在 new
和 delete
之间发生异常,且异常未在 f
中捕获,则内存就永远不会被释放,在函数 f
之外没有指针指向这块内存,因此就无法释放它了。
分配了资源,但是没有定义析构函数的释放资源的类,将会发生资源泄漏,除此之外,如果资源分配和释放之间发生了异常,程序也会发生资源泄漏。
struct destionation;
struct connection;
connection connect(destionation *);
void disconnect(connection);
void f(destionation &d)
{
connection c = connect(&d);
//使用连接
//如果在f退出之前忘记调用 disconnect,就无法关闭 c 了
}
上面的代码,如果 connection
有析构函数,就可以在 f 结束时由析构函数自动关闭连接。但是 connection
没有析构函数。解决的办法是 使用 shared_ptr
来保证 connection
能被正确关闭。
默认情况下,shared_ptr
假定它们指向的是动态内存,因此,当一个 shared_ptr
被销毁时,它默认地对它管理的指针进行 delete
操作。为了用 shared_ptr
来管理一个 connection
,我们定义一个函数来代替 delete
,这个删除器函数必须能够完成对 shared_ptr
中保存的指针进行释放。
void end_connection(connection *p){ disconnect(*p)}
当创建一个 shared_ptr 时,可以传递一个可选参数指向删除器函数的参数:
void f(destionation &d)
{
connection c = connect(&d);
shared_ptr<connection> p(&c,end_connection);
// 在 f 退出时,即使是由于发生异常而退出,connection会被正确关闭
}
当 p
被销毁时,它不会对自己保存的指针执行 delete
,而是调用 end_connection
函数,end_connection
会调用 disconnect
,从而确保连接被关闭。
注意:
智能指针可以提供对动态分配的内存安全而又方便的管理,但是使用时,必须遵循一定的规范:
reset
多个智能指针。delete
get()
返回的指针。get()
初始化或 reset
另一个智能指针。get()
返回的指针,当最后一个对应的智能指针被销毁后,智能指针就变为无效了。new
分配的内存,记住传递给它一个删除器。一个 unique_ptr
拥有它所指向的对象,与 shared_ptr
不同,某个时刻只能有一个 unique_ptr
指向一个给定的对象。
当 unique_ptr
被销毁时,它所指向的对象也被销毁。
unique_ptr
特有的操作:
unique_ptr
没有类似 make_shared
的标准函数返回一个 unique_ptr
。
定义一个 unique_ptr
时,需要将其绑定到一个 new
返回的指针。类似 shared_ptr
,初始化 unique_ptr
必须采用直接初始化的形式:
unique_ptr<double> p1; // 可以指向一个 double 的 unique_ptr
unique_ptr<int> p2(new int(42)); //p2指向一个值为42的int
因为 unique_ptr 拥有它所指向的对象,因此 unique_ptr 不支持普通的拷贝或赋值操作:
unique_ptr<string> p1 = new(string("hello"));
unique_ptr<string> p2(p1); // 错误,unique_ptr 不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误,unique_ptr 不支持赋值
可以通过调用 release
或 reset
将指针的所有权从一个非 const
unique_ptr
转移给另一个 unique
。
unique_ptr<string> p2 = (p1.release()); // 将所有权从p1转移给p2,release 将p1置为空
unique_ptr<string> p3(new string("hello"));
p2.reset(p3.release()); // 将所有权从p3 转移给p2,reset 释放了p2原来指向的内存
release
成员返回 unique_ptr
当前保存的指针并将其置为空。p2
被初始化为原来 p1
保存的指针,而 p1
被置空。
reset
成员接受一个可选的指针参数,令 unique_ptr
重新指向给定的指针,如果 unique_ptr
不为空,它原来指向的对象被释放。
调用 release
会切断 unique_ptr
和它原来管理对象之间的联系,release
返回的指针通常被用来初始化另一个指针或者给另一个智能指针赋值,但是如果不是使用另一个智能指针来管理 release
返回的指针,程序就要负责最终的资源释放:
p2.release(); // 错误,p2不会释放内存,并且丢失了指针
auto p = p2.release(); // 正确,但是必须手动 delete(p)
不能拷贝 unique_ptr
的规则有一个例外:可以拷贝或赋值一个要被销毁的 unique_ptr
,最常见的例子就是从函数返回一个 unique_ptr
:
unique_ptr<int> clone(int p)
{
//从 int* 创建一个 unique_ptr<ptr>
return unique_ptr<int> (new int (p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<ptr> clone(int p)
{
unique_ptr<int> ret(new int(p));
return ret;
}
对于上面的两端代码,编译器都知道要返回的对象将要被销毁,在此情况下,编译器执行一种特殊的拷贝。
默认情况下 unique_ptr
使用 delete
释放它所指向的对象。可以为 unique_ptr
重载一个默认的删除器。
// p 指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT
// 它会调用一个名为fcn的delT类型对象
unique_ptr<objT,delT> p(new objT,fcn);
重写连接程序:
//decltype 返回的是一个函数类型,需要天骄一个*指出使用该类型的指针
void f(destination &d)
{
connection c = connect(&d);
unique_ptr<connection,decltype(end_connection)*> p(&c,end_connection);
}
weak_ptr
是一种不控制所指向对象生存期的智能指针,它指向一个由 shared_ptr
管理的对象,将一个 weak_ptr
绑定到一个 shared_ptr
不会改变 shared_ptr
的引用计数,一旦最后一个指向对象的 shared_ptr
被销毁,对象就会被释放,即使有 weak_ptr
指向的对象,对象也还是会被释放,因此 weak_ptr
的名字抓住了这种智能指针弱共享对象的特点。
创建一个 weak_ptr
时,要用一个 shared_ptr
来初始化它:
auto p = make_shared<int> (42);
weak_ptr<int> wp(p); // wp 弱共享p,p的引用计数未改变
wp
和 p
指向相同的对象,由于是弱共享,创建 wp
不会改变 p 的引用计数;wp
指向的对象可能被释放掉。
由于对象可能不存在,我们不能使用 weak_ptr 直接访问对象,而必须调用 lock。此函数检查 weak_ptr 指向的对象是否存在,如果存在,lock返回一个指向共享对象的 shared_ptr。
if(shared_ptr<int> np = wo.lock()) //如果np不为空则条件成立
{ }
class StrBlobPtr{
public:
StrBlobPtr():curr(0){}
StrBlobPtr(StrBlob &a,size_t sz = 0):wptr(a.data),curr(sz){}
std::string& deref() const;
StrBlobPtr& incr(); // 前缀递增
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
std::shared_ptr<std::vector<std::string>> check(std::size_t,const std::string&) const;
// 保存一个 weak_ptr,意味着底层 vector 可能会被销毁
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; // 在数组中的当前位置
};
默认构造函数生成一个空的 StrBlobPtr
,其构造函数初始化列表将 curr
显示初始化为 0
,并将 wptr
隐式初始化为一个空 weak_ptr
。
第二构造函数接受一个 StrBlob
引用和一个可选的索引值,此构造函数初始化 wptr
,令其指向给定 StrBlob
对象的 shared_ptr
中的 vector
,并将 curr
初始化为 sz
的值。
值得注意的是,不能将 StrBlobPtr
绑定到一个 const StrBlob
对象,这个限制是由于构造函数接受一个非 const StrBlob
对象的引用而导致的。
StrBlobPtr
的 check
成员与 StrBlob
中的同名成员不同,它还要检查指针指向的 vector
是否还存在:
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i,const std::string &msg) const
{
auto ret = wptr.lock();
if(!ret)
throw std::runtime_error("unbound StrBlobPtr");
if(i >= ret->size())
throw std::out_of_range(msg);
return ret;
}
如果 vector
已销毁,lock
将返回一个空指针,于是抛出一个异常。否则,check
会检查给定索引,如果索引值合法,check
返回从 lock
获得的 shared_ptr
。
deref
成员调用 check
,检查使用 vector
是否安全以及 curr
是否在合法范围内:
std::string& StrBlobPtr::deref() const
{
auto p = check(curr,"dereference past end");
return (*p)[curr]; //(*p) 是对象所指向的 vector
}
incr
成员,前缀递增,返回递增后的对象的引用:
StrBlobPtr& StrBlobPtr::incr()
{
check(curr,"increment past end of StrBlobPtr");
++curr;
return *this;
}
为了访问 data
成员,指针类必须声明为 StrBlob
的 friend
。还要为 StrBlob
类定义 begin
和 end
操作,返回一个指向它自身的 StrBlobPtr
:
class StrBlobPtr;
class StrBlob{
friend class StrBlobPtr;
StrBlobPtr begin(){return StrBlobPtr(*this);}
StrBlobPtr end(){
auto ret = StrBlobPtr(*this,data->size());
return ret;
}
};
标签:重要 释放内存 ptr mic 函数返回 let empty 添加 列表
原文地址:https://www.cnblogs.com/xiaojianliu/p/12496809.html