• 对象语义与值语义、资源管理(RAII、资源所有权)、模拟实现auto_ptr<class>、实现Ptr_vector


    一、对象语义与值语义

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


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

    对象拷贝是禁止的(Noncopyable)

    OR

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


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


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


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


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


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


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

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

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

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

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

    性。所以当你判断一个新的技术是否是面向对象的时候,通常可以使用后两个特性来加以判断。“面向对象”和“基于对象”都实现了“封

    装”的概念,但是面向对象实现了“继承和多态”,而“基于对象”没有实现这些。


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


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

    (一):禁止拷贝。

    比如

    AddNode ad1(left, right);

    AddNode ad2(ad1);

    假设允许拷贝且没有自己实现拷贝构造函数(默认为浅拷贝),则会有两个指针同时指向一个Node对象,容易发生析构两次的运行时错误。

    下面看如何禁止拷贝的两种方法:

    方法一:将Node 的拷贝构造函数和赋值运算符声明为私有,并不提供实现

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
     

    //抽象类
    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(Node *left, Node *right)
            : left_(left), right_(right) {}
        ~BinaryNode()
        {
            delete left_;
            delete right_;
        }
    protected:
        Node *const left_;
        Node *const right_;
    };

    class AddNode: public BinaryNode
    {
    public:
        AddNode(Node *left, 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_;
    };

    此时如下的最后一行就会编译出错了:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
     
    NumberNode *left = new NumberNode(3);
    NumberNode *right = new NumberNode(4);

    AddNode ad1(left, right);

    AddNode ad2(ad1);

    即要拷贝构造一个AddNode 对象,最远也得从调用Node类的拷贝构造函数开始(默认拷贝构造函数会调用基类的拷贝构造函数,如果是自己实现的而且没有显式调用,将不会调用基类的拷贝构造函数),因为私有,故不能访问。

    需要注意的是,因为声明了Node类的拷贝构造函数,故必须实现一个构造函数,否则没有默认构造函数可用。


    方法二:Node类继承自一个不能拷贝的类,如果有很多类似Node类的其他类,此方法比较合适

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
     
    class NonCopyable
    {
    protected: //构造函数可以被派生类调用,但不能直接构造对象
        NonCopyable() {}
        ~NonCopyable() {}
    private:
        NonCopyable(const NonCopyable &);
        const NonCopyable &operator=(const NonCopyable &);
    };

    //抽象类,对象语义,禁止拷贝(首先需要拷贝NonCopyable)
    class Node : private NonCopyable
    {
    public:
        virtual double Calc() const = 0;
        virtual ~Node(void) {}
    };

    注意NonCopyable 类的构造函数声明为protected,则不能直接构造对象,如NonCopyable nc; // error 

    但在构造派生类,如最底层的AddNode类时,可以被间接调用。

    同样地,NonCopyable类的拷贝构造函数和赋值运算符为私有,故如 AddNode ad2(ad1); 编译出错。


    二、资源管理

    (一)、资源所有权

    1、局部对象

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

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

    如 void fun()

    {

    Test t; //局部对象

    }

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

    class A

    {

    private:

    B b; //先析构A,再析构b 

    };

    2、动态对象(new 分配内存)

    (1)、对于动态分配对象就不是这样了,它总是通过指针访问。在它们的生存期内,指针可以指向一个资源序列,若干指针可以指向相同的资源。动态分配资源的释放不是自动完成的,需要手动释放,如delete 指针。
    (2)、如果对象从一个指针传递到另一个指针,所有权关系就不容易跟踪。容易出现空悬指针、内存泄漏、重复删除等错误。


    (二)、RAII 与 auto_ptr

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

    对应于智能指针auto_ptr,可以理解为一个auto_ptr对象拥有资源的裸指针,并负责资源的释放。

    下面先来看auto_ptr 的定义:

    // TEMPLATE CLASS auto_ptr
    template<class _Ty>
    class auto_ptr

    {

    ....

    private:

    _Ty *_Myptr; // the wrapped object pointer

    }

    实际上auto_ptr 是以模板方式实现的,内部成员变量只有一个,就是具体类的指针,即将这个裸指针包装起来。auto_ptr 的实现里面还封装了很多关于裸指针的操作,这样就能像使用裸指针一样使用智能指针,如->和* 操作;负责裸指针的初始化,以及管理裸指针指向的内存释放。

    这样说还是比较难理解,可以自己实现一个模拟 auto_ptr<Node> 类的NodePtr 类,从中体会智能指针是如何管理资源的:

    Node.h:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
     
    #ifndef _NODE_H_
    #define _NODE_H_

    class Node
    {
    public:
        Node();
        ~Node();
        void Calc() const;
    };

    class NodePtr
    {
    public:
        explicit NodePtr(Node* ptr = 0)
            : 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;
            return tmp;
        }
        void Reset(Node* ptr = 0)
        {
            if (ptr_ != ptr)
            {
                delete ptr_;
            }
            ptr_ = ptr;
        }
    private:
        Node* ptr_;
    };

    #endif // _NODE_H_

    Node.cpp:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     
    #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:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     
    #include <iostream>
    using namespace std;

    #include "DebugNew.h"
    #include "Node.h"

    int main(void)
    {
        Node *p1 = new Node;
        NodePtr np(p1);
        np->Calc();

        NodePtr np2(np);

        Node *p2 = new Node;
        NodePtr np3(p2);
        np3 = np2; //np3先delete p2,接着接管p1;

        return 0;
    }

    从输出可以看出,通过NodePtr 智能指针对象包装了裸指针,NodePtr类通过重载-> 和 * 运算符实现如同裸指针一样的操作,如

    np->Calc();  程序中通过智能指针对象的一次拷贝构造和赋值操作之后,现在共有3个局部智能指针对象,但np 和 np2 的成员ptr_ 已经被设置为0;第二次new 的Node对象已经被释放,现在np3.ptr_ 指向第一次new 的Node对象,程序结束,np3局部对象析构,delete ptr_,析构Node对象。

    从程序实现可以看出,Node 类是可以拷贝,而且是默认浅拷贝,故是对象语义对象,现在使用智能指针来管理了它的生存期,不容易发生内存泄漏问题。(程序中编译时使用了这里的内存泄漏跟踪器,现在new 没有匹配delete 但没有输出信息,说明没有发生内存泄漏)。


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

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

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
     
    //抽象类
    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:
        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_;
    };

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


    对auto_ptr 做一点小结:

    1、auto_ptr不能作为STL容器的元素
    2、STL容器要求存放在容器中的元素是值语义,要求元素能够被拷贝。
    3、auto_ptr的拷贝构造或者赋值操作会改变右操作数,因为右操作数的所有权要发生转移。

    实际上auto_ptr 是值语义(将对象语义转换为值语义),auto_ptr 之所以不能作为STL容器的元素,关键在于第3点,即
    auto_ptr的拷贝构造或者赋值操作会改变右操作数,如下的代码:

     C++ Code 
    1
    2
    3
     
    std::auto_ptr<Node> node(new Node);
    vector<std::auto_ptr<Node> > vec;
    vec.push_back(node);

    在编译到push_back 的时候就出错了,查看push_back 的声明:

    void push_back(const _Ty& _Val);

    即参数是const 引用,在函数内部拷贝时不能对右操作数进行更改,与第3点冲突,所以编译出错。

    其实可以这样来使用:

     C++ Code 
    1
    2
    3
     
    std::auto_ptr node(new Node);
    vector<Node *> vec;
    vec.push_back(node.release());

    也就是先释放所有权成为裸指针,再插入容器,在这里再提一点,就是vector 只负责裸指针本身的内存的释放,并不负责指针指向内存的释放,假设一

    MultipleNode 类有成员vector<Node*> vec_; 那么在类的析构函数中需要遍历容器,逐个delete 指针; 才不会造成内存泄漏。


    更谨慎地说,如上面的用法还是存在内存泄漏的 可能性。考虑这样一种情形:

    这里知道,push_back 会先调用operater

     new 分配指针本身的内存,如果此时内存耗尽,operator new 失败,push_back 抛出异常,此时裸指针既没有被智能指针接管,也

    没有插入vector(不能在类的析构函数中遍历vector 进行delete 操作),那么就会造成内存泄漏。

    为了解决这个潜在的风险,可以实现一个Ptr_vector 模板类,负责指针指向内存的释放:

    Ptr_vector.h:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
     
    #ifndef _PTR_VECTOR_H_
    #define _PTR_VECTOR_H_

    #include <vector>
    #include <memory>

    template <typename T>
    class ptr_vector : public std::vector<T *>
    {
    public:
        ~ptr_vector()
        {
            clear();
        }

        void clear()
        {
            std::vector<T *>::iterator it;
            for (it = begin(); it != end(); ++it)
                delete *it; //释放指针指向的内存

            std::vector<T *>::clear(); //释放指针本身
        }

        void push_back(T *const &val)
        {
            std::auto_ptr<T> ptr(val);              // 用auto_ptr接管val所有权
            std::vector<T *>::push_back(val);       // operator new
            ptr.release();
        }

        void push_back(std::auto_ptr<T> &val)
        {
            std::vector<T *>::push_back(val.get());
            val.release();
        }
    };

    #endif // _PTR_VECTOR_H_

    Ptr_vector 继承自vector 类,重新实现push_back 函数,插入裸指针时,先用局部智能指针对象接管裸指针所有权,如果

    std::vector<T *>::push_back(val);  成功(operator new 成功),那么局部智能指针对象释放裸指针的所有权;如果

    std::vector<T *>::push_back(val);  失败(operator new 失败),抛出异常,栈展开的时候要析构局部对象,此时局部智能指针对象的析构函数内会

    delete 裸指针。


    此外,在Ptr_vector 类中还重载了push_back,能够直接将智能指针作为参数传递,在内部插入裸指针成功后,释放所有权。

    当Ptr_vector 对象销毁时调用析构函数,析构函数调用clear(),遍历vector<T*>,delete 裸指针。

     C++ Code 
    1
    2
    3
    4
     
    std::auto_ptr node(new Node);
    Ptr_vector<Node> vec;
    vec.push_back(node.release());
    // vec.push_back(node);


    参考:

    C++ primer 第四版
    Effective C++ 3rd
    C++编程规范

  • 相关阅读:
    linux 彻底删除文件及 find命令permission refused问题解决
    ubuntu系统中dpkg lock问题分析及解决
    ubuntu server18.04 更换默认源为阿里源-加速
    docker安装与卸载( liunx )
    ubuntu下dpkg lock问题
    docker pull报x509问题及docker启动失败问题解决
    windows 常用命令行操作
    uwsgi运行django应用是报错no app loaded. going in full dynamic mode
    internal server error原因及解决
    docker-compose启动容器后执行脚本或命令不退出 | 运行内部程序
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8471148.html
Copyright © 2020-2023  润新知