`
aspnetwinform
  • 浏览: 84513 次
  • 性别: Icon_minigender_2
  • 来自: 武汉
社区版块
存档分类
最新评论

从零开始学C++之对象语义与值语义、资源管理(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、基于对象与面向对象的区别(from 这里

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

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

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

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

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

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



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



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

(一):禁止拷贝。

比如

AddNode ad1(left, right);

AddNode ad2(ad1);

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


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

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


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

//抽象类
classNode
{
public:
Node(){}
virtualdoubleCalc()const=0;
virtual~Node(void){}
private:
Node(constNode&);
constNode&operator=(constNode&);
};

//抽象类
classBinaryNode:publicNode
{
public:
BinaryNode(Node*left,Node*right)
:left_(left),right_(right){}
~BinaryNode()
{
deleteleft_;
deleteright_;
}
protected:
Node*constleft_;
Node*constright_;
};

classAddNode:publicBinaryNode
{
public:
AddNode(Node*left,Node*right)
:BinaryNode(left,right){}
doubleCalc()const
{
returnleft_->Calc()+right_->Calc();
}
};

classNumberNode:publicNode
{
public:
NumberNode(doublenumber):number_(number)
{

}
doubleCalc()const
{
returnnumber_;
}

private:
constdoublenumber_;
};


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

C++ Code
<nobr>1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br></nobr>
NumberNode*left=newNumberNode(3);
NumberNode*right=newNumberNode(4);

AddNodead1(left,right);

AddNodead2(ad1);

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

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


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


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

//抽象类,对象语义,禁止拷贝(首先需要拷贝NonCopyable)
classNode:privateNonCopyable
{
public:
virtualdoubleCalc()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
<nobr>1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br> 10<br> 11<br> 12<br> 13<br> 14<br> 15<br> 16<br> 17<br> 18<br> 19<br> 20<br> 21<br> 22<br> 23<br> 24<br> 25<br> 26<br> 27<br> 28<br> 29<br> 30<br> 31<br> 32<br> 33<br> 34<br> 35<br> 36<br> 37<br> 38<br> 39<br> 40<br> 41<br> 42<br> 43<br> 44<br> 45<br> 46<br> 47<br> 48<br> 49<br> 50<br> 51<br> 52<br> 53<br> 54<br> 55<br> 56<br> 57<br> 58<br></nobr>
#ifndef_NODE_H_
#define_NODE_H_

classNode
{
public:
Node();
~Node();
voidCalc()const;
};

classNodePtr
{
public:
explicitNodePtr(Node*ptr=0)
:ptr_(ptr){}

NodePtr(NodePtr&other)
:ptr_(other.Release()){}

NodePtr&operator=(NodePtr&other)
{
Reset(other.Release());
return*this;
}

~NodePtr()
{
if(ptr_!=0)
deleteptr_;
}

Node&operator*()const{return*Get();}

Node*operator->()const{returnGet();}

Node*Get()const{returnptr_;}

Node*Release()
{
Node*tmp=ptr_;
ptr_=0;
returntmp;
}
voidReset(Node*ptr=0)
{
if(ptr_!=ptr)
{
deleteptr_;
}
ptr_=ptr;
}
private:
Node*ptr_;
};

#endif//_NODE_H_

Node.cpp:


C++ Code
<nobr>1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br> 10<br> 11<br> 12<br> 13<br> 14<br> 15<br> 16<br> 17<br></nobr>
#include<iostream>
#include"Node.h"

Node::Node()
{
std::cout<<"Node..."<<std::endl;
}

Node::~Node()
{
std::cout<<"~Node..."<<std::endl;
}

voidNode::Calc()const
{
std::cout<<"Node::Calc..."<<std::endl;
}

main.cpp:


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

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

intmain(void)
{
Node*p1=newNode;
NodePtrnp(p1);
np->Calc();

NodePtrnp2(np);

Node*p2=newNode;
NodePtrnp3(p2);
np3=np2;//np3先deletep2,接着接管p1;

return0;
}


从输出可以看出,通过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
<nobr>1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br> 10<br> 11<br> 12<br> 13<br> 14<br> 15<br> 16<br> 17<br> 18<br> 19<br> 20<br> 21<br> 22<br> 23<br> 24<br> 25<br> 26<br> 27<br> 28<br> 29<br> 30<br> 31<br> 32<br> 33<br> 34<br> 35<br> 36<br> 37<br> 38<br> 39<br> 40<br> 41<br> 42<br> 43<br> 44<br> 45<br> 46<br> 47<br> 48<br> 49<br> 50<br> 51<br> 52<br> 53<br> 54<br> 55<br></nobr>
//抽象类
classNode
{
public:
Node(){}
virtualdoubleCalc()const=0;
virtual~Node(void){}
private:
Node(constNode&);
constNode&operator=(constNode&);
};

//抽象类
classBinaryNode:publicNode
{
public:
BinaryNode(std::auto_ptr<Node>&left,std::auto_ptr<Node>&right)
:left_(left),right_(right){}
~BinaryNode()
{
//deleteleft_;
//deleteright_;
}
protected:
std::auto_ptr<Node>left_;
std::auto_ptr<Node>right_;
};

classAddNode:publicBinaryNode
{
public:
AddNode(std::auto_ptr<Node>&left,std::auto_ptr<Node>&right)
:BinaryNode(left,right){}
doubleCalc()const
{
returnleft_->Calc()+right_->Calc();
}
};

classNumberNode:publicNode
{
public:
NumberNode(doublenumber):number_(number)
{

}
doubleCalc()const
{
returnnumber_;
}

private:
constdoublenumber_;
};

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


对auto_ptr 做一点小结:

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

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


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

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

void push_back(const _Ty& _Val);

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

其实可以这样来使用:


C++ Code
<nobr>1<br> 2<br> 3<br></nobr>
std::auto_ptrnode(newNode);
vector<Node*>vec;
vec.push_back(node.release());

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


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


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


vec.push_back(node.release()); 当node.release() 调用完毕,进而调用push_back 时,由这里知道,push_back 会先调用operater


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


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


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


Ptr_vector.h:

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

#include<vector>
#include<memory>

template<typenameT>
classptr_vector:publicstd::vector<T*>
{
public:
~ptr_vector()
{
clear();
}

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

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

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

voidpush_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 裸指针。


此时,我们就可以如下地使用Ptr_vector:

C++ Code
<nobr>1<br> 2<br> 3<br> 4<br></nobr>
std::auto_ptrnode(newNode);
Ptr_vector<Node>vec;
vec.push_back(node.release());
//vec.push_back(node);

这样就确保一定不会发生内存泄漏,即使push_back 失败也不会。


参考:

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



分享到:
评论

相关推荐

    C++智能指针的原理和实现.pdf

    2.1 auto_ptr auto_ptr是通过由 new 表达式获得的对象,并在auto_ptr⾃⾝被销毁时删除该对象的智能指针,它可⽤于为动态分配的对象提供异常安 全、传递动态分配对象的所有权给函数和从函数返回动态分配的对象,是⼀...

    C++智能指针(1).pdf

    std::auto_ptr 真正让⼈容易误⽤的地⽅是其不常⽤的复制语义,即当复制⼀个 std::auto_ptr 对象时(拷贝复制或 operator= 复制),原对象 所持有的堆内存对象也会转移给复制出来的对象。⽰例代码如下: #include ...

    C++基于消息队列的多线程实现示例代码

    lock_guard 对象通常用于管理某个锁(Lock)对象,因此与 Mutex RAII 相关,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,...

    unique_resource:unique_resource,用于独家所有权资源管理的通用 RAII 包装器

    unique_resource,用于独家所有权资源管理的通用 RAII 包装器。 这是使用 Boost 软件许可证 1.0 的 unique_resource 实现。 此实现基于 C++ 标准委员会论文中的示例实现。 什么是 unique_resource unique_resource...

    Vulkan包装器

    它添加了诸如RAII之类的C ++概念,并移动了语义以简化Vulkan对象的管理。 当VKW对象超出范围时,它的析构函数将破坏基础的Vulkan对象。 例如: { vk::Instance instance = createInstance (...); // instance ...

    C++程序的设计机制3 RAII机制

    C++程序的设计机制3 RAII机制

    c++11/14/17

    c++11/14/17新特性讲解及c++20展望,lambda,raii,,share_ptr

    基于C++实现的轻量级Web服务器源码+项目说明.zip

    使用RAII手法封装互斥器(pthrea_mutex_t)、 条件变量(pthread_cond_t)等线程同步互斥机制,使用RAII管理文件描述符等资源 使用shared_ptr、weak_ptr管理指针,防止内存泄漏 下一步开发计划 添加异步日志系统,...

    C++内存管理.doc

    内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++...

    C++中的RAII机制详解

    主要介绍了C++中的RAII机制详解,RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法,需要的朋友可以参考下

    学习C++,学习windows/linux编程,学习网络编程,学习reactor模型,自己实现一遍练手;

    学习C++,学习windows/linux编程,学习网络编程,学习reactor模型,自己实现一遍练手; 包含(以下实现是Windows/Linux平台通用的,美名其曰跨平台) 基本工具库: 日志工具(日志优先级、同步日志、异步日志、带...

    my_simple_RAII

    文章my_simple_RAII对应的code 环境vs2008

    C_C++问题总结

    C/C++问题总结 1. 关键字 1.1 const 1.1.1 常量 1.1.2 修饰指针 1.1.3 修饰函数参数与返回值 1.1.4 类中的应用 1.2 static 1.3 volatile 1.4 extern 2. 函数 2.1 sizeof与strlen区别 2.2 strcpy、sprintf与memcpy ...

    Google C++ Style Guide(Google C++编程规范)高清PDF

    The format of the symbol name should be &lt;PROJECT&gt;_&lt;PATH&gt;_&lt;FILE&gt;_H_. To guarantee uniqueness, they should be based on the full path in a project's source tree. For example, the file foo/src/bar/baz.h...

    raii.js:基于ES6 Promise的RAII堆栈工具

    RAII.js介绍RAII.js是基于ES6 Promise的RAII(资源获取即初始化)实现。 RAII.js确保您的资源按顺序进行了初始化和销毁​​,并且可以随时取消(销毁)整个堆栈。安装只需通过npm在项目中安装raii.js即可: npm ...

    nanoalarm:C++ 的简单 RAII 警报

    纳米警报 C++ 的简单 RAII 警报概要 int main() { nanoalarm::Alarm a(1); pause(); ok(1, "passed"); done_testing();}执照 The MIT License (MIT)Copyright (C) 2015 Tokuhiro Matsuno, http://64p.org/ ...

    关于c++ 智能指针及 循环引用的问题

    如此c++引入 智能指针 ,智能指针即是C++ RAII的一种应用,可用于动态资源管理,资源即对象的管理策略。 智能指针在 &lt;memory&gt;标头文件的 std 命名空间中定义。 它们对 RAII 或 获取资源即初始化 编程惯用法至关重要...

    fixed_storage_arrays:一组具有各种功能的固定大小的C ++存储阵列

    nonstd::push_array&lt;T&gt; 精简的“数组和计数器”固定大小的数据结构,用于将数据写入一次然后进行复制。 可复制的。 没什么特别的。 nonstd::raw_buffer&lt;T&gt; 针对给定类型,围绕std::aligned_storage构建的数组。 ...

    C++编程规范101条规则、准则与最佳实践PDF.rar

    第13条 确保资源为对象所拥有。使用显式的RAII和智能指针 24 编程风格 27 第14条 宁要编译时和连接时错误,也不要运行时错误 28 第15条 积极使用const 30 第16条 避免使用宏 32 第17条 避免使用...

Global site tag (gtag.js) - Google Analytics