参考资料:《C++ Primer中文版 第五版》
我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。
在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。
动态内存管理经常会出现两种问题:
为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。
理解智能指针需要从下面三个层次:
Animal a = new Animal();
Animal b = a;
你当然知道,这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,
Animal a;
Animal b = a;
这里却是就是生成了两个对象。
C++ 标准库(STL)中
头文件:#include < memory >
C++ 98
std::auto_ptrstd::string ps (new std::string(str));
C++ 11
Class shared_ptr 实现共享式拥有(shared ownership)概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。为了在结构较复杂的情景中执行上述工作,标准库提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等辅助类。
Class unique_ptr 实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。你可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。
多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。
创建智能指针时必须提供额外的信息,指针可以指向的类型:
shared_ptr<string> p1;
shared_ptr<list<int>> p2;
默认初始化的智能指针中保存着一个空指针。
智能指针的使用方式和普通指针类似,解引用一个智能指针返回它指向的对象,在一个条件判断中使用智能指针就是检测它是不是空。
if(p1 && p1->empty())
*p1 = "hi";
如下表所示是shared_ptr和unique_ptr都支持的操作:
如下表所示是shared_ptr特有的操作:
make_shared函数:
最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。头文件和share_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>();
make_shared用其参数来构造给定类型的对象,如果我们不传递任何参数,对象就会进行值初始化
shared_ptr的拷贝和赋值
当进行拷贝和赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。
auto p = make_shared<int>(42);
auto q(p);
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
auto r = make_shared<int>(42);//r指向的int只有一个引用者
r=q;//给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//r原来指向的对象已没有引用者,会自动释放
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象,它是通过另一个特殊的成员函数-析构函数完成销毁工作的,类似于构造函数,每个类都有一个析构函数。析构函数控制对象销毁时做什么操作。析构函数一般用来释放对象所分配的资源。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。
shared_ptr还会自动释放相关联的内存
当动态对象不再被使用时,shared_ptr类还会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
使用了动态生存期的资源的类:
程序使用动态内存的原因:
(1)程序不知道自己需要使用多少对象
(2)程序不知道所需对象的准确类型
(3)程序需要在多个对象间共享数据
在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的地址
有一种方法可以避免悬空指针的问题:在指针即将要离开其作用于之前释放掉它所关联的内存,如果我们需要保留指针可以在delete之后将nullptr赋予指针,这样就清楚的指出指针不指向任何对象。动态内存的一个基本问题是可能多个指针指向相同的内存
shared_ptr和new结合使用
如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的职能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针
shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式
下表为定义和改变shared_ptr的其他方法:
不要混合使用普通指针和智能指针
如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
也不要使用get初始化另一个智能指针或为智能指针赋值
shared_ptr<int> p(new int(42));//引用计数为1
int *q = p.get();//正确:但使用q时要注意,不要让它管理的指针被释放
{
//新程序块
//未定义:两个独立的share_ptr指向相同的内存
shared_ptr(q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p;//未定义,p指向的内存已经被释放了
p和q指向相同的一块内部才能,由于是相互独立创建,因此各自的引用计数都是1,当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放,p这时候就变成一个空悬指针,再次使用时,将发生未定义的行为,当p被销毁时,这块空间会被二次delete
其他shared_ptr操作
可以使用reset来将一个新的指针赋予一个shared_ptr:
p = new int(1024);//错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024));//正确。p指向一个新对象
与赋值类似,reset会更新引用计数,如果需要的话,会释放p的对象。reset成员经常和unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:
if(!p.unique())
p.reset(new string(*p));//我们不是唯一用户,分配新的拷贝
*p+=newVal;//现在我们知道自己是唯一的用户,可以改变对象的值
智能指针和异常
如果使用智能指针,即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放,sp是一个shared_ptr,因此sp销毁时会检测引用计数,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。
使用我们自己的释放操作
默认情况下,shared_ptr假定他们指向的是动态内存,因此当一个shared_ptr被销毁时,会自动执行delete操作,为了用shared_ptr来管理一个connection,我们必须首先必须定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。
智能指针陷阱:
(1)不使用相同的内置指针值初始化(或reset)多个智能指针。
(2)不delete get()返回的指针
(3)不使用get()初始化或reset另一个智能指针
(4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
(5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。
weak_ptr的操作
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针
unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
下表是unique的操作:
虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique
//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1.release());//release将p1置为空
unique_ptr<string>p3(new string("Trex"));
//将所有权从p3转移到p2
p2.reset(p3.release());//reset释放了p2原来指向的内存
调用release会切断unique_ptr和它原来管理的的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr.
unique_ptr<int> clone(int p)
{
//正确:从int*创建一个unique_ptr
return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int(p));
return ret;
}
用unique_ptr传递删除器
被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。
auto_ptr 与 unique_ptr 比较
建议:
1-每种指针都有不同的使用范围,unique_ptr指针优于其它两种类型,除非对象需要共享时用shared_ptr。
2- 建议– 如果你没有打算在多个线程之间来共享资源的话,那么就请使用unique_ptr。
3 -建议- 使用make_shared而不是裸指针来初始化共享指针。
4 -建议 – 在设计类的时候,当不需要资源的所有权,而且你不想指定这个对象的生命周期时,可以考虑使用weak_ptr代替shared_ptr。
c++智能指针——原理与实现
static_cast
向上转换是一种隐式转换。
dynamic_cast
用于多态类型的转换,使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。
B中需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。
其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时类型检查
只适用于指针或引用,不能用于内置的基本数据类型的强制转换。
对不明确的指针的转换将失败(返回 nullptr),但不引发异常
在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
向上转换,即为子类指针指向父类指针(一般不会出问题);向下转换,即将父类指针转化子类指针。
向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。
#include
using namespace std;
class base
{
public :
void m(){cout<<"m"<<endl;}
};
class derived : public base
{
public:
void f(){cout<<"f"<<endl;}
};
int main()
{
derived * p;
p = new base;
p = static_cast<derived *>(new base);
p->m();
p->f();
return 0;
}
本例中定义了两个类:base类和derived类,这两个类构成继承关系。在base类中定义了m函数,derived类中定义了f函数。在前面介绍多态时,我们一直是用基类指针指向派生类或基类对象,而本例则不同了。
本例主函数中定义的是一个派生类指针,当我们将其指向一个基类对象时,这是错误的,会导致编译错误。
但是通过强制类型转换我们可以将派生类指针指向一个基类对象,p = static_cast
在程序中p是一个派生类对象,我们将其强制指向一个基类对象,首先通过p指针调用m函数,因为基类中包含有m函数,这一句没有问题,之后通过p指针调用f函数。一般来讲,因为p指针是一个派生类类型的指针,而派生类中拥有f函数,因此p->f();这一语句不会有问题,但是本例中p指针指向的确实基类的对象,而基类中并没有声明f函数,虽然p->f();这一语句虽然仍没有语法错误,但是它却产生了一个运行时的错误。换言之,p指针是派生类指针,这表明程序设计人员可以通过p指针调用派生类的成员函数f,但是在实际的程序设计过程中却误将p指针指向了一个基类对象,这就导致了一个运行期错误。
产生这种运行期的错误原因在于static_cast强制类型转换时并不具有保证类型安全的功能,而C++提供的dynamic_cast却能解决这一问题,dynamic_cast可以在程序运行时检测类型转换是否类型安全。
当然dynamic_cast使用起来也是有条件的,它要求所转换的操作数必须包含多态类类型(即至少包含一个虚函数的类)。
#include
using namespace std;
class base
{
public :
void m(){cout<<"m"<<endl;}
};
class derived : public base
{
public:
void f(){cout<<"f"<<endl;}
};
int main()
{
derived * p;
p = new base;
p = dynamic_cast<derived *>(new base);
p->m();
p->f();
return 0;
}
在本例中利用dynamic_cast进行强制类型转换,但是因为base类中并不存在虚函数,因此p = dynamic_cast
为了解决本例中的语法错误,我们可以将base类中的函数m声明为虚函数,virtual void m(){cout<<“m”< dynamic_cast还要求<>内部所描述的目标类型必须为指针或引用。 const_cast reinterpret_cast 用于位的简单重新解释 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。 允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全) 也允许将任何整数类型转换为任何指针类型以及反向转换。 reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。 reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。 改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。 bad_cast bad_cast 使用#include
#include
try {
Circle& ref_circle =
dynamic_cast<Circle&>(ref_shape);
}
catch (bad_cast b) {
cout << "Caught: " << b.what();
}