C++ Primer 学习笔记_42_面向对象编程(9)--虚函数与多态(六):auto_ptr与shared_ptr、对象/值语义、资源管理(RAII)、实现auto_ptr、Ptr_vector


一、auto_ptr与shared_ptr

1、auto_ptr

    auto_ptr是当前C++标准库中提供的一种智能指针。

   auto_ptr类是接受一个类型形参的模板,它为动态分配的对象提供异常安全,auto_ptr类在头文件memory中定义。

auto_ptr

auto_ptr<T>ap;

创建名为ap未绑定的auto_ptr对象

auto_ptr<T>ap(p);

创建名为apauto_ptr对象,ap拥有指针p指向的对象。该构造函数为explicit

auto_ptr<T>ap1(ap2);

创建名为ap1auto_ptr对象,ap1保存原来存储在ap2中的指针。将所有权转给ap1ap2成为未绑定的auto_ptr对象

ap1= ap2

将所有权ap2转给ap1删除ap1指向的对象并且使ap1指向ap2指向的对象,使ap2成为未绑定的

~ap

析构函数。删除ap指向的对象

*ap

返回对ap所绑定的对象的引用

ap->

返回ap保存的指针

ap.reset(p)

如果pap的值不同,则删除ap指向的对象并且将ap绑定到p

ap.release()

返回ap所保存的指针并且使ap成为未绑定的

ap.get()

返回ap保存的指针


【小心地雷】

auto_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组[会导致未定义的运行时行为]

auto_ptr被复制或复制时,有不寻常的行为,因此,不能将auto_ptr存储在标准库容器类型中

每个auto_ptr对象绑定到一个对象或者指向一个对象。当auto_ptr对象指向一个对象的时候,可以说它“拥有”该对象。当auto_ptr对象超出作用域或者另外撤销的时候,就自动回收auto_ptr所指向的动态分配对象


    我们可以这样使用auto_ptr来提高地吗安全性

int* p = new int(0);
auto_ptr<int> ap(p);

    从此我们不必关系何时释放p,也不用担心发生异常会有内存泄漏,这是因为auto_ptr的析构函数会执行指针的释放,而析构函数会在ap出了作用域以后执行。

注意:

(1)因为auto_ptr析构的时候肯定会删除它所拥有的哪个对象,因此,两个auto_ptr不能同时拥有同一个对象。比如:

int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p); //error

(2)因为auto_ptr的析构函数中删除指针用的是delete,而不是delete[],所以auto_ptr不应该用来管理一个数组指针。比如:

int* p = new int[10];
auto_ptr<int> ap(p);

    与引用计数型智能指针不同的,auto_ptr要求其对“裸”指针的完全占有性。也就是说一个“裸”指针不能同时被两个以上的auto_ptr所拥有。所以,拷贝或赋值的目标对象将先释放其原来所拥有的对象。

(3)因为一个auto_ptr被拷贝或赋值后,其已经失去对原对象的所有权,这时候,对这个auto_ptr进行解引用操作是不安全的。如下

int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2 = ap1;
cout << *ap1;  //Error,此时ap1只剩一个NULL指针

——较为隐蔽的情形是将auto_ptr作为函数参数按值传递,在函数调用过程中在函数的作用域中会产生一个局部对象来接收传入的auto_ptr(拷贝构造)。此时,传入的实参auto_ptr就失去了其对原对象的所有权。如下

void f(auto_ptr<int> ap) {cout << *ap;}
auto_ptr<int> ap1(new int(0));
f(ap1);
cout << *ap1; //Error,经过f(ap)函数调用,ap1已经不再拥有任何对象了

(4)因为auto_ptr不具有值语义,所以auto_ptr不能被用在STL标准容器中。值语义下面详细讲。


回顾博文《C++ Primer 学习笔记_24_类与数据抽象(10)--static 与单例模式、auto_ptr与单例模式、const成员函数、const 对象、mutable修饰符》中关于auto_ptr的内容.


2、shared_ptr

    auto_ptr由于它的破坏性复制语义,无法满足标准容器对元素的要求,因而不能放在标准容器中。boost库中提供了一种新型的智能指针shared_ptr,它解决了在多个指针间共享对象所有权的问题,同时也满足容器对元素的要求,因此可以安全放入容器中

    shared_ptr的作用同指针,但会记录有多少个shared_ptr共同指向一个对象。这便是所谓的引用计数。一旦最后一个这样的指针被销毁,也就是某个对象的引用计数变为0,这个对象会被自动删除。这在非环形数据结构中防止资源泄漏很有帮助。

【例】关于C++标准模板库,下列说法错误的有哪些()。(多选)

A、std::auto_ptr<class A>类型的对象,可以放到std::vector<std::auto_ptr<class A>>容器中

B、std::shared_ptr<class A>类型的对象,可以放到std::vector<std::shared_ptr<class A>>容器中

C、对于复杂类型T的对象tobj,++tobj和tobj++的执行效率相比,前者更高

D、采用new操作符创建对象时,如果没有足够内存空间而导致创建失败,则new操作符会返回NULL

解答:AD。

C选项,即便是基础类型++x的效率也比x++高,因为++x是对x加1然后返回x本身,而x++则要构建一个临时变量用来保存x的值,然后x本身++,最后返回的是临时变量,所以一般情况下在程序中如果只是简单要求对对象加1,推荐使用++x。

D选项,new在失败后,抛出标准异常std::bad_alloc而不是返回NULL。



二、对象语义与值语义 

1、值语义是指对象的拷贝与原对象无关。拷贝之后就与原对象脱离关系,彼此独立互不影响(深拷贝)。比如说int,C++中的内置类型都是值语义,前面学过的三个标准库类型string,vector,map也是值语义。默认的拷贝构造函数是浅拷贝。


2、对象语义指的是面向对象意义下的对象

(1)对象拷贝是禁止的(Noncopyable)

(2)一个对象被系统标准的复制方式复制后,与被复制的对象之间依然共享底层资源,对任何一个的改变都将改变另一个(浅拷贝)


3、值语义对象生命期容易控制


4、对象语义对象生命期不容易控制(通过智能指针来解决,见本文下半部分)。智能指针实际上是将对象语义转化为值语义,利用局部对象(智能指针)的确定性析构,包括auto_ptr, shared_ptr, weak_ptr,  scoped_ptr。

auto_ptr      所有权独占,不能共享,但是可以转移
shared_ptr    所有权共享,内部维护了一个引用计数,+1,-1

weak_ptr      弱指针,它要与shared_ptr配合使用,循环引用

scoped_ptr    与auto_ptr类似,所有权独占,不能共享,但是不可以转移


5、值语义与对象语义是分析模型决定的,语言的语法技巧用来匹配模型。


6、值语义对象通常以类对象的方式来使用,对象语义对象通常以指针或引用方式来使用


7、一般将只使用到值语义对象的编程称为基于对象编程,如果使用到了对象意义对象,可以看作是面向对象编程。


8、基于对象与面向对象的区别

      很多人没有区分“面向对象”和“基于对象”两个不同的概念。面向对象的三大特点(封装,继承,多态)缺一不可。通常“基于对

象”是使用对象,但是无法利用现有的对象模板产生新的对象类型,继而产生新的对象,也就是说“基于对象”没有继承的特点。而“多

态”表示为父类类型的子类对象实例,没有了继承的概念也就无从谈论“多态”。现在的很多流行技术都是基于对象的,它们使用一些

封装好的对象,调用对象的方法,设置对象的属性。但是它们无法让程序员派生新对象类型。他们只能使用现有对象的方法和属

性。所以当你判断一个新的技术是否是面向对象的时候,通常可以使用后两个特性来加以判断。“面向对象”和“基于对象”都实现了“封装”的概念,但是面向对象实现了“继承和多态”,而“基于对象”没有实现这些。


假设现在有这样一个继承体系:

C++ Primer 学习笔记_42_面向对象编程(9)--虚函数与多态(六):auto_ptr与shared_ptr、对象/值语义、资源管理(RAII)、实现auto_ptr、Ptr_vector_第1张图片

其中Node,BinaryNode 都是抽象类,AddNode 有两个Node* 成员,Node应该实现为对象语义:



三、资源管理

1、资源所有权

局部对象

资源的生存期为嵌入实体的生存期。

(1)一个代码块拥有在其作用域内定义的所有自动对象(栈分配的对象,局部对象)释放这些资源的任务是完全自动的(调用析构函数)。如:

void fun()
{
Test t; //局部对象
}

(2)所有权的另一种形式是嵌入。一个对象拥有所有嵌入其中的对象。释放这些资源的任务也是自动完成(外部对象的析构函数调用内部对象的析构函数)。如

class A
{
    private:
    B b; //先析构A,再析构b 
};


【示例:析构顺序可以用下列代码测试】

#include <iostream>
using namespace std;

class Test
{
public:
    Test()
    {
        cout<<"Test constructor"<<endl;
    }
    ~Test()
    {
        cout<<"Test destructor"<<endl;
    }
};
 
class Test1
{
public:
    Test1()
    {
        cout<<"Test1 constructor"<<endl;
    }
    ~Test1()
    {
        cout<<"Test1 destructor"<<endl;
    }
};
 
class Test2
{
public:
    Test2()
    {
        cout<<"Test2 constructor"<<endl;
    }
    ~Test2()
    {
        cout<<"Test2 destructor"<<endl;
    }
private:
    Test a;
    Test1 b;
};
 
int main()
{
    Test2 *c=new Test2;
    delete c;
    return 0;
}

运行结果:

Test constructor
Test1 constructor
Test2 constructor
Test2 destructor
Test1 destructor
Test destructor



动态对象(new 分配内存)

(3)对于动态分配对象就不是这样了,它总是通过指针访问。在它们的生存期内,指针可以指向一个资源序列,若干指针可以指向相同的资源。动态分配资源的释放不是自动完成的,需要手动释放,如delete 指针


(4)如果对象从一个指针传递到另一个指针,所有权关系就不容易跟踪。容易出现空悬指针、内存泄漏、重复删除等错误。

int* p = new int;  //获取四个字节的内存

int* p2 = p;  //这四个字节的所有权则不明确

int* p3 = p;  //这四个字节的所有权则不明确



2、RAII 与 auto_ptr

(1)RAII

    一个对象可以拥有资源。在对象的构造函数中执行资源的获取(指针的初始化),在析构函数中释放(delete 指针)。这种技法把它称之为RAII(Resource Acquisition Is Initialization:资源获取即初始化)。如前所述的资源指的是内存,实际上还可以扩展为文件句柄,套接字,互斥量,信号量等资源。


(2)示例

    实现一个模拟 auto_ptr<Node> 类的NodePtr 类,从中体会智能指针是如何管理资源的

Node.h:

#ifndef _NODE_H_
#define _NODE_H_
class Node
{
public:
    Node();
    ~Node();
    void Calc() const;
};
class NodePtr
{
public:
    explicit NodePtr(Node* ptr = 0)  //构造函数,将传递进来的指针转移到ptr_
        : ptr_(ptr) {}
    NodePtr(NodePtr& other)
        : ptr_(other.Release()) {}
    NodePtr& operator=(NodePtr& other)  //=运算符不同于拷贝构造函数的是,需要先释放再获取所有权
    {
        Reset(other.Release());
        return *this;
    }
    ~NodePtr() //在析构函数中释放资源
    {
        if (ptr_ != 0)
            delete ptr_;
    }
    Node& operator*() const { return *Get(); }
    Node* operator->() const { return Get(); }
    Node* Get() const { return ptr_; }
    Node* Release()
    {
        Node* tmp = ptr_;
        ptr_ = 0;  //释放所有权,则是将ptr_置空
        return tmp;
    }
    void Reset(Node* ptr = 0)
    {
        if (ptr_ != ptr)  //如果相等,则不需要释放
        {
            delete ptr_;  //先释放
        }
        ptr_ = ptr;  //再获取所有权
    }
private:
    Node* ptr_;
};
#endif // _NODE_H_


Node.cpp:

#include <iostream>
#include "Node.h"
Node::Node()
{
    std::cout << "Node ..." << std::endl;
}
Node::~Node()
{
    std::cout << "~Node ..." << std::endl;
}
void Node::Calc() const
{
    std::cout << "Node::Calc ..." << std::endl;
}


main.cpp:

#include <iostream>
using namespace std;
#include "DebugNew.h"
#include "Node.h"
int main(void)
{
    Node *p1 = new Node;
    NodePtr np(p1);  //1、需要实现构造函数和析构函数,否则会出现裸指针现象
    np->Calc();  // 希望np可以类似p1->Calc(),则重载->运算符
    NodePtr np2(np);  //2、拷贝的时候需要释放np的所有权,否则会出现重复释放现象
    Node *p2 = new Node;
    NodePtr np3(p2);
    np3 = np2; //np3先delete p2,接着接管p1;
    return 0;
}
运行结果:

C++ Primer 学习笔记_42_面向对象编程(9)--虚函数与多态(六):auto_ptr与shared_ptr、对象/值语义、资源管理(RAII)、实现auto_ptr、Ptr_vector_第2张图片

第一、需要实现构造函数和析构函数,否则会出现裸指针现象
希望np可以类似p1->Calc(),则重载->运算符
希望np可以类似*p1,则重载*运算符
第二、重载拷贝构造函数,拷贝的时候需要释放np的所有权,否则会出现重复释放现象
第三、重载=运算符,赋值的时候需要释放np的所有权,否则会出现重复释放现象

总结:

    应用RAII技巧将裸指针转换为智能指针(是一个类)

    将指针转换为对象

    对象的析构函数在生存期结束的时候是确定被调用的。

    所以简单来说,智能指针的本质思想就是:用栈上对象(智能指针对象)来管理堆上对象的生存期。


3、auto_ptr示例

在本文最前面的程序中,虽然实现了禁止拷贝,但如上所述,对象语义对象的生存期仍然是不容易控制的,下面将通过智能指针auto_ptr<Node>  来解决这个问题,通过类比上面NodePtr 类的实现可以比较容易地理解auto_ptr<Node>的作用:

#include <iostream>
#include <memory>
using namespace std;
//抽象类
class Node
{
public:
    Node() { }
    virtual double Calc() const = 0;
    virtual ~Node(void) {}
private:
    Node(const Node &);
    const Node &operator=(const Node &);
};
//抽象类
class BinaryNode : public Node
{
public:
    BinaryNode(std::auto_ptr<Node> left, std::auto_ptr<Node> right) : left_(left), right_(right) {}
    ~BinaryNode()
    {
//        delete left_;
//        delete right_;
    }
protected:
    //Node* const left_;
    //Node* const right_;
    std::auto_ptr<Node> left_;  //全都改为智能指针管理
    std::auto_ptr<Node> right_;
};
class AddNode: public BinaryNode
{
public:
    AddNode(std::auto_ptr<Node> left, std::auto_ptr<Node> right) : BinaryNode(left, right) { }
    double Calc() const
    {
        return left_->Calc() + right_->Calc();
    }
};
class NumberNode: public Node
{
public:
    NumberNode(double number): number_(number)
    {
    }
    double Calc() const
    {
        return number_;
    }
private:
    const double number_;
};
int main()
{
    AddNode ad1(std::auto_ptr<Node>(new NumberNode(3)), std::auto_ptr<Node>(new NumberNode(4)));
    cout << ad1.Calc() << endl;
    //AddNode ad2(ad1); //Error
    return 0;
}

运行结果:

7

需要注意的是,在BinaryNode 中现在裸指针的所有权已经归智能指针所有,由智能指针来管理Node 对象的生存期,故在析构函数中不再需要delete 指针的操作


我们知道,vector保存的是指针本身,并没有保存指针所指向的内存,vector不负责指针所指向的内存的管理

因此不能使用vector<std::auto_ptr>,因为vector析构的时候,它只负责释放指针变量本身,不会释放指针所指向的内存。




参考:

C++ primer 第四版

C++ primer 第五版

你可能感兴趣的:(C++ Primer 学习笔记_42_面向对象编程(9)--虚函数与多态(六):auto_ptr与shared_ptr、对象/值语义、资源管理(RAII)、实现auto_ptr、Ptr_vector)