博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
智能指针的原理及实现方案
阅读量:4106 次
发布时间:2019-05-25

本文共 6044 字,大约阅读时间需要 20 分钟。

 

智能指针的原理及实现方案


   本文主要讨论C++程序设计的一种常用技术——智能指针(smart pointer),主要内容包括引用计数(reference count)和句柄类(handle class)。如果文中有错误或遗漏之处,敬请指出,谢谢! 

   作者: tyc611, 2007-02-01


   当类中有指针成员时,一般有两种方式来管理指针成员:一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;另一种更优雅的方式是使用智能指针,从而实现指针指向的对象的共享。

 

   智能指针(smart pointer)的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。

 

   每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

 

   实现引用计数有两种经典策略:一是引入辅助类,二是使用句柄类。下面分别介绍这些内容。

 


问题描述

 

   假设有一个名为TestPtr的类,里面有一个指针成员,简化为如下代码:

class TestPtr

{
public:
    TestPtr(int*p): ptr(p){
}
    ~TestPtr( ) { delete ptr; }
    // other operations
private:
    int*ptr;
    // other data
};

 

在这种情况下,类TestPtr对象的任何拷贝、赋值操作都会使多个TestPtr对象共享相同的指针。但在一个对象发生析构时,指针指向的对象将被释放,从而可能引起悬垂指针。

 

   现在我们使用引用计数来解决这个问题,一个新的问题是引用计数放在哪里。显然,不能放在TestPtr类中,因为多个对象共享指针时无法同步更新引用计数。

 


方案一

 

   这里给出的解决方案是,定义一个单独的具体类(RefPtr)来封装指针和相应的引用计数。由于这个类只是用于对类TestPtr中的成员指针ptr进行了封装,无其它用途,所以把引用计数类RefPtr的所有成员均定义为private,并把类TestPtr声明为它的友元类,使TestPtr类可以访问RefPtr类。示例代码如下:

 

class RefPtr

{
    friendclass TestPtr;
    int*ptr;
    size_tcount;
    RefPtr (int*p): ptr(p),count(1){}
    ~RefPtr (){
        delete ptr;
    }
};
class TestPtr
{
public:
    TestPtr(int*p): ptr(new RefPtr(p)){
}
    TestPtr(const TestPtr& src): ptr(src.ptr){
        ++ptr->count;
    }
    TestPtr&operator=(const TestPtr& rhs){
        // self-assigning is also right
        ++rhs.ptr->count;
        if(--ptr->count== 0)
            delete ptr;
        ptr = rhs.ptr;
        return*this;
    }
    ~TestPtr(){
        if(--ptr->count== 0)
            delete ptr;
    }
private:
    RefPtr *ptr;
};

 

   当希望每个TestPtr对象中的指针所指向的内容改变而不影响其它对象的指针所指向的内容时,可以在发生修改时,创建新的对象,并修改相应的引用计数。这种技术的一个实例就是写时拷贝(Copy-On-Write)。

 

   这种方案的缺点是每个含有指针的类的实现代码中都要自己控制引用计数,比较繁琐。特别是当有多个这类指针时,维护引用计数比较困难。

 


方案二

 

   为了避免上面方案中每个使用指针的类自己去控制引用计数,可以用一个类把指针封装起来。封装好后,这个类对象可以出现在用户类使用指针的任何地方,表现为一个指针的行为。我们可以像指针一样使用它,而不用担心普通成员指针所带来的问题,我们把这样的类叫句柄类。在封装句柄类时,需要申请一个动态分配的引用计数空间,指针与引用计数分开存储。实现示例如下:

 

#include<iostream>

#include<stdexcept>
usingnamespacestd;
#define TEST_SMARTPTR
class Stub
{
public:
    void print(){
        cout<<"Stub: print"<<endl;
    }
    ~Stub(){
        cout<<"Stub: Destructor"<<endl;
    }
};
template<typename T>
class SmartPtr
{
public:
    SmartPtr(T *p = 0): ptr(p), pUse(newsize_t(1)){
}
    SmartPtr(const SmartPtr& src): ptr(src.ptr), pUse(src.pUse){
        ++*pUse;
    }
    SmartPtr&operator=(const SmartPtr& rhs){
        // self-assigning is also right
        ++*rhs.pUse;
        decrUse();
        ptr = rhs.ptr;
        pUse = rhs.pUse;
        return*this;
    }
    T *operator->(){
        if(ptr)
            return ptr;
        throwstd::runtime_error("access through NULL pointer");
    }
    const T *operator->()const{
        if(ptr)
            return ptr;
        throwstd::runtime_error("access through NULL pointer");
    }
    T &operator*(){
        if(ptr)
            return*ptr;
        throwstd::runtime_error("dereference of NULL pointer");
    }
    const T &operator*()const{
        if(ptr)
            return*ptr;
        throwstd::runtime_error("dereference of NULL pointer");
    }
    ~SmartPtr(){
        decrUse();
#ifdef TEST_SMARTPTR
        std::cout<<"SmartPtr: Destructor"<<std::endl;// for testing
#endif
    }
    
private:
    void decrUse(){
        if(--*pUse == 0){
            delete ptr;
            delete pUse;
        }
    }
    T *ptr;
    size_t*pUse;
};
int main()
{
    try{
        SmartPtr<Stub> t;
        t->print();
    }catch(constexception& err){
        cout<<err.what()<<endl;
    }
    SmartPtr<Stub> t1(new Stub);
    SmartPtr<Stub> t2(t1);
    SmartPtr<Stub> t3(new Stub);
    t3 = t2;
    t1->print();
    (*t3).print();
    
    return 0;
}

 


   如果文中有错误或遗漏之处,敬请指出,谢谢!


 


参考文献:

[1] C++ Primer(Edition 4)

[2] Thinking in C++(Volume Two, Edition 2)


 TAG

发表于: 2007-02-01 ,修改于: 2007-02-02 10:22,已浏览2021次,有评论7

 

 

 

网友评论

 

内容:

方案一

TestPtr& operator= (const TestPtr& rhs) {

        // self-assigning is also right

        ++rhs.ptr->count;

        if (--ptr->count == 0) 

/*为什么要在赋值构造函数中减,一加一减不是相当与于没加?

  如执行  HasPtr p1(&i, 42);

               HasPtr p2 = p1;

   这个时候count还是2么?

            delete ptr;

        ptr = rhs.ptr;//是否在执行这句后count才为2

        return *this;

    }

有点问题能解释一下么,谢谢

Blog作者的回复:

因为是右操作数赋值给左操作数,所以右操作数的引用计数需要增加一(共享该指针的对象又多了一个),而左操作数的引用计数需要减小一。你举的例子“HasPtr p2 = p1;”是调用拷贝构造函数,不是赋值。

C++评论于:2007-03-02 16:15:18 218.108.29.

 

内容:

不是么?

那C++ Primer 4 中文版第411页讲赋值操作符举的例子: 

Sales_item trans, accum;

trans = accum;

和 HasPtr p2 = p1 有什么区别?

Blog作者的回复:

注意,你有个基本的概念没有弄清楚。对于你举的例子,是先定义了变量(这里是类变量,将调用默认构造函数构造对象),再对变量进行赋值。而HasPtr p2 = p1;是直接调用拷贝构造函数来构造p2对象,也就是构造一个p1的副本。建议你去参考一下拷贝构造函数。
这点区别在最简单的内置类型变量声明时也一样,比如:
int a;
a = 5;      // assignment
int b = a;  // is not assignment, but initialization
这儿一个是赋值,而另一个是初始化(在类类型对象的构造时就是调用构造函数或者拷贝构造函数进行初始化对象)

 

C++评论于:2007-03-05 20:58:56 218.108.29.

 

 

内容:

噢。理解了。非常谢谢你啊!

 

C++评论于:2007-03-06 13:00:32 218.108.29.

 

 

内容:

SmartPtr& operator= (const SmartPtr& rhs) {

        // self-assigning is also right

        ++*rhs.pUse;

        decrUse();

        ptr = rhs.ptr;

        pUse = rhs.pUse;

        return *this;

这儿的rhs不是const 引用,为什么还能修改的成员pUse

Blog作者的回复:

注意,这儿的:++*rhs.pUse;
改变的是pUse所指对象(一个int对象)的值,这个int对象不属于Smart类(虽然pUse指针属于Smart类),所以这里并没有修改Smart对象(它的两个数据成员ptr和pUse都没有改变)

 

评论于:2007-04-07 21:54:45 84.13.156.

 

 

内容:

我是不是可以这样理解,两个方案仅仅是形式上的简便,(即使用上的方便)而没什么特殊的不同.还有在方案一中如果指向对象中的一个想要改变值时,也就是要新建一个对象才能完成是吧.那具体的代码要怎么写呢?

比如说有object_1,object_2,object_3指向了同一地址,这时object_1想改变对象的内容了,要怎么写.是object_1=object_changes;吗?

那object_changes又是要怎样去初始化才能满足要求呢,(比如说对象中有很多个成员,而我只想改少数几个,我是不是对object_changes一一赋值后,再赋给object_1) 

Blog作者的回复:

>> 我是不是可以这样理解,两个方案仅仅是形式上的简便,(即使用上的方便)而没什么特殊的不同
----------------
它们的共同点都需要一个计数器,而区别在于:在使用第一种方案时,使用者必须自己控制引用计数,使用不太方面;而第二种方案在使用时,就没有这些麻烦,更“智能”些,也是更可取的方式。
>> 还有在方案一中如果指向对象中的一个想要改变值时,也就是要新建一个对象才能完成是吧.那具体的代码要怎么写呢?
比如说有object_1,object_2,object_3指向了同一地址,这时object_1想改变对象的内容了,要怎么写.是object_1=object_changes;吗?
那object_changes又是要怎样去初始化才能满足要求呢,(比如说对象中有很多个成员,而我只想改少数几个,我是不是对object_changes一一赋值后,再赋给object_1) 
--------------
我不是很理解你的意思,你的意思是不是当引用计数大于一(也就是有多个指针指向同一共享对象)时修改该共享对象的问题?如果是这样的话,看你的需求了。如果不希望改变共享的对象,而是改变自己的“拥有”的那份,那么此时相当于所谓的“写时拷贝”技术,即对需要修改的对象,将它参与共享的引用计数减一(即断开与该共享对象的关系),然后自己申请一段空间,用新值初始化该空间。
也不知道我是否理解了你的问题,如果我理解错了,请说得更明白些(用代码比较容易点)

 

评论于:2007-04-17 16:01:39 222.92.42.

 

 

内容:

>>我不是很理解你的意思,你的意思是不是当引用计数大于一(也就是有多个指针指向同一共享对象)时修改该共享对象的问题?如果是这样的话,看你的需求了。如果不希望改变共享的对象,而是改变自己的“拥有”的那份,那么此时相当于所谓的“写时拷贝”技术,即对需要修改的对象,将它参与共享的引用计数减一(即断开与该共享对象的关系),然后自己申请一段空间,用新值初始化该空间。

也不知道我是否理解了你的问题,如果我理解错了,请说得更明白些(用代码比较容易点)

>>就是你说的意思.谢谢了.希望能给我些完成端口的介绍.

我的邮件yiyucyp@yahoo.com.cn

Blog作者的回复:

这个我一时也找不到例子,你google一下,应该有所收获

 

评论于:2007-04-23 15:43:05 222.92.42.

 

 

内容:

uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu

 

 

 

 

转载地址:http://mhjsi.baihongyu.com/

你可能感兴趣的文章
如何在 Linux 中将文件编码转换为 UTF-8
查看>>
Android应用内存泄漏的定位、分析与解决策略
查看>>
Android适配难题全面总结
查看>>
Linux下莱特币Litecoin挖矿教程
查看>>
安装Mod_Pagespeed 使 Apache和Nginx性能加速10倍
查看>>
Win10下Hyper-V虚拟机不能联网的解决办法
查看>>
ThinkPad T460S 拆解图 拆解图
查看>>
Chrome 错误代码:ERR_UNSAFE_PORT
查看>>
如何在 Linux 中找出最近或今天被修改的文件
查看>>
CentOS 上的 FirewallD 简明指南
查看>>
ecshop适应PHP7的修改
查看>>
Qt最新的教程合集
查看>>
T460/s 安装Sierra 10.12.2 成功分享……
查看>>
在 Linux 系统下使用 PhotoRec & TestDisk 工具来恢复文件
查看>>
Linux命令行下载工具 aria2 实例
查看>>
Installing Imagick for PHP 7 on Windows 10
查看>>
辩证看待 C++:后现代系统语言的选择
查看>>
Qt v5.8.0 已发布
查看>>
MySQL Group Replication调研剖析
查看>>
Laravel v5.4 已发布
查看>>