欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++智能指针简单剖析

程序员文章站 2022-04-17 08:53:46
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。 1. 智能指针背后的设计思想...

智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。
1. 智能指针背后的设计思想
auto_ptr、unique_ptr和shared_ptr这几个智能指针背后的设计思想。我简单的总结下就是:将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
因此,要转换remodel()函数,应按下面3个步骤进行:

包含头义件memory(智能指针所在的头文件); 将指向string的指针替换为指向string的智能指针对象;

删除delete语句。

C++智能指针简单介绍
STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr。
使用注意点
全部三种智能指针都应避免的一点:
string vacation("I wandered lonely as a cloud.");
shared_ptr pvac(&vacation);   // No

pvac过期时,程序将把delete运算符用于非堆内存,这是错误的。

为什么摒弃auto_ptr?
先来看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr vocation; 
vocaticn = ps;

上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次。要避免这种问题,方法有多种:

定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr 的策略,但unique_ptr的策略更严格。 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。 shared_ptr内部实现上使用的是引用计数

当然,同样的策略也适用于复制构造函数。
每种方法都有其用途,但为何说要摒弃auto_ptr呢?
下面举个例子来说明。

#include
#include
#include
using namespace std;

int main() {
  auto_ptr films[5] =
 {
  auto_ptr (new string("Fowl Balls")),
  auto_ptr (new string("Duck Walks")),
  auto_ptr (new string("Chicken Runs")),
  auto_ptr (new string("Turkey Errors")),
  auto_ptr (new string("Goose Eggs"))
 };
 auto_ptr pwin;
 pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针
 cout << "The nominees for best avian baseballl film are\n";
 for(int i = 0; i < 5; ++i)
    cout << *films[i] << endl;
 cout << "The winner is " << *pwin << endl;
 cin.get();
  return 0;
}

运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:

使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。 使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误

这就是为何要摒弃auto_ptr的原因,一句话总结就是:避免潜在的内存崩溃问题。

unique_ptr为何优于auto_ptr?
可能大家认为前面的例子已经说明了unique_ptr为何优。于auto_ptr,也就是安全问题,下面再叙述的清晰一点。 与auto_ptr不一样的是,unique_ptr是没有复制构造函数的,这就防止了一些“悄悄地”丢失所有权的问题发生
请看下面的语句:
auto_ptr p1(new string ("auto") ; //#1
auto_ptr p2;                       //#2
p2 = p1;                                   //#3       

在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象;
但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。
下面来看使用unique_ptr的情况:

unique_ptr p3 (new string ("auto");   //#4
unique_ptr p4;                       //#5
p4 = p3;                                      //#6    

编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
但unique_ptr还有更聪明的地方。
有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

 unique_ptr demo(const  * s)
{
    unique_ptr temp (new string (s));
    return temp;
}

并假设编写了如下代码:

unique_ptr ps;
ps = demo(“Uniquely special”);

demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值,这正是unique_ptr更聪明的地方。
总之,当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

unique_ptr pu1( string (hello world));
unique_ptr pu2;
pu2 = pu1;              //not allowed
unique_ptr pu3;
pu3 = unique_ptr( string ());   //allowed

其中前一个留下悬挂的unique_ptr(pu1),这可能导致危害。而后面不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
当然,您可能确实想执行类似于前面的操作,仅当以非智能的方式使用摒弃的智能指针时(如解除引用时),这种赋值才不安全。要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。下面是一个使用前述demo()函数的例子,该函数返回一个unique_ptr对象:
使用move后,原来的指针仍转让所有权变成空指针,可以对其重新赋值。

unique_ptr ps1, ps2;
ps1 = demo(hello);
ps2 = move(ps1);
ps1 = demo(alexia);
cout << *ps2 << *ps1 << endl;

智能指针的实现:
用智能指针来创建一个动态分配的字符串对象:
shared_ptr pstr(new string(“abc”));//新创建一个对象,引用计数器为1
一个智能指针对当前智能指针进行拷贝时,引用计数器加1:
shared_ptr pstr(new string(“abc”)); //pstr指向的对象只有一个引用者
shared_ptr pstr2(pstr); //pstr跟pstr2指向相同的对象,此对象有两个引用者
当两个智能指针进行赋值操作时,左边的指针指向的对象引用计数减1,右边的加1。

shared_ptr pstr(new string("abc"));
shared_ptr pstr2(new string("hello"));

pstr2 = pstr; //给pstr2赋值,令他指向另一个地址,递增pstr指向的对象的引用计数,递减pstr2原来指向的对象引用计数
指针离开作用域范围时,同样引用计数减1。当引用计数为0时,对象被回收。
根据以上的分析,我们对它做一个简单的实现:

#include 
using namespace std;

template 
class mySmartPointer {

public:
    mySmartPointer(T*);             //构造函数
    mySmartPointer(mySmartPointer&);    //拷贝构造函数
    T* operator->();            //重载->操作符
    T& operator*();             //重载*操作符
    mySmartPointer& operator= (mySmartPointer&);  //赋值运算符
    ~mySmartPointer(); //析构函数

private:
    int *num;    //引用计数
    T *p;        //智能指针底层保管的指针
};


template 
mySmartPointer::mySmartPointer(T *p): num(new int(1)), p(p)
{
}

//对普通指针进行拷贝,同时引用计数器加1
template 
mySmartPointer::mySmartPointer(mySmartPointer &sp): num(&(++*sp.num)), p(sp.p)
{
}

template 
T* mySmartPointer::operator->()
{
    return p;
}

template 
T& mySmartPointer::operator*()
{
    return *p;
}

//定义赋值运算符,左边的指针计数减1,右边指针计数加1,当左边指针计数为0时,释放内存
template 
mySmartPointer& mySmartPointer::operator= (mySmartPointer& sp)
{
    ++*sp.num;
    if (--*num == 0) { //自我赋值同样能保持正确
        delete num;
        delete p;
    }
    this->p = sp.p;
    this->num = sp.num;
    return *this;
}

template 
mySmartPointer::~mySmartPointer()
{
    if (--*num == 0) {
        delete num;
        delete p;
    }
}

int main()
{
    mySmartPointer pstr1(new string("abc"));
    mySmartPointer pstr2(pstr1);     //拷贝
    mySmartPointer pstr3(new string("bcd"));
    pstr3 = pstr2;                           //赋值

    cout << *pstr1 << " " << *pstr2 << " " << *pstr3 << endl;

    return 0;
}