对象语义与值语义、资源管理(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 的拷贝构造函数和赋值运算符声明为私有,并不提供实现
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 AddNode: public BinaryNode class NumberNode: public Node } private: |
此时如下的最后一行就会编译出错了:
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类的其他类,此方法比较合适
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) |
注意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:
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 class NodePtr NodePtr(NodePtr& other) NodePtr& operator=(NodePtr& other) ~NodePtr() Node& operator*() const { return *Get(); } Node* operator->() const { return Get(); } Node* Get() const { return ptr_; } Node* Release() #endif // _NODE_H_ |
Node.cpp:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream>
#include "Node.h" Node::Node() Node::~Node() void Node::Calc() const |
main.cpp:
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" int main(void) NodePtr np2(np); Node *p2 = new Node; 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>的作用:
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 AddNode: public BinaryNode class NumberNode: public Node } private: |
需要注意的是,在BinaryNode 中现在裸指针的所有权已经归智能指针所有,由智能指针来管理Node 对象的生存期,故在析构函数中不再需要delete 指针; 的操作。
对auto_ptr 做一点小结:
1、auto_ptr不能作为STL容器的元素
2、STL容器要求存放在容器中的元素是值语义,要求元素能够被拷贝。
3、auto_ptr的拷贝构造或者赋值操作会改变右操作数,因为右操作数的所有权要发生转移。
实际上auto_ptr 是值语义(将对象语义转换为值语义),auto_ptr 之所以不能作为STL容器的元素,关键在于第3点,即
auto_ptr的拷贝构造或者赋值操作会改变右操作数,如下的代码:
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点冲突,所以编译出错。
其实可以这样来使用:
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:
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> template <typename T> void clear() std::vector<T *>::clear(); //释放指针本身 void push_back(T *const &val) void push_back(std::auto_ptr<T> &val) #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 裸指针。
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++编程规范
对象语义与值语义、资源管理(RAII、资源所有权)、模拟实现auto_ptr<class>、实现Ptr_vector的更多相关文章
- EntityFramework:值语义的实体如何修改?
背景 现在很流行值对象,值对象有如下特点:开发简单.使用简单和多线程安全.我试着让 EntityFramework 中的实体保持值语义,这样的话,对值语义实体的修改就应当等于“整体替换”,好像有点问题 ...
- C++ 性能剖析 (二):值语义 (value semantics)
Value Semantics (值语义) 是C++的一个有趣的话题. 什么是值语义? 简单的说,所有的原始变量(primitive variables)都具有value semantics. 也可以 ...
- go语言之进阶篇值语义和引用语义
1.值语义和引用语义 示例: package main import "fmt" type Person struct { name string //名字 sex byte // ...
- 以对象管理资源——C++智能指针auto_ptr简介
auto_ptr是C++标准库提供的类模板,它可以帮助程序员自动管理用new表达式动态分配的单个对象.auto_ptr对象被初始化为指向由new表达式创建的对象,当auto_ptr对象的生命期结束时, ...
- Cg(C for Graphic)语言语义词与语义绑定详述 (转)
摘抄“GPU Programming And Cg Language Primer 1rd Edition” 中文名“GPU编程与CG语言之阳春白雪下里巴人” 语义词( Semantic )与语义绑定 ...
- JQuery 操作对象的属性值
通过JQuery去操作前台对象(div,span...)的属性是很常见的事情,本文就简单的介绍几种操作情形. 1):通过属性值去获取对象 2):用JQuery去修改对象的属性值 3):获取并修改对象的 ...
- jquery attr()方法 添加,修改,获取对象的属性值。
jquery attr()方法 添加,修改,获取对象的属性值. jquery中用attr()方法来获取和设置元素属性,attr是attribute(属性)的缩写,在jQuery DOM操作中会经常用到 ...
- javascript 中关于对象转换数字值的一些特点
下面是摘至<Javascript 高级程序设计第三版>里的一段话 是关于对象转换数字值的一些规则 "在应用于对象时,先调用对象的valueOf()方法以取得一个可供操作的值.然后 ...
- 读匿名object对象的属性值
读匿名object对象的属性值 1.定义读object对象值的功能方法 public static class StaticClass { public static string ValueByKe ...
随机推荐
- ExtJS4.1 ExtJS TabPanel 双击标签关闭该页
/*总觉得 TabPanel 生成的那个关闭按钮太小关闭的时候不太顺手 感觉不方便 所以想双击关闭tab方便些于是在网上找到下面的代码 URL:http://atian25.iteye.com/blo ...
- php curl 抓取
<?php set_time_limit(0); function curl_multi($urls) { if (!is_array($urls) or count($urls) == 0) ...
- 开发Google Material Design风格的WPF程序
今天在网上看到了一个Material Design风格的WPF皮肤,看上去还是挺不错的 这个项目是开源的,感兴趣的朋友可以下载试下: https://github.com/ButchersBoy/Ma ...
- DC-DC converter Control techniques
As shown in figure 3.4, PWM controller contains two main parts; voltage error-amplifier and voltage ...
- 【spring data jpa】jpa中criteria拼接in查询
示例代码: public static Specification<GoodsType> where(final GoodsType goodsType){ return new Spec ...
- client_thread.c server_thread.c
client_thread.c #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> ...
- c++ 读写Excel及数据导入SQLServer
c++ 读写Excel及数据导入SQLServer C++操作Excel ,网上的资料还是比较多的,写这篇文章也是分享给初学者一些经验. 本人 觉得CSpreadShe ...
- QT在windows下的安装与配置
先了解Qt: Qt一直以来,分为商业.开源两个版本,商业版本为用户提供了二级制的动态库,直接安装既可以使用,但是需要花钱购买license,而开源版本则遵守GPL协议,提供了源码,用户需要自行编译,才 ...
- 【Python】Django CSRF问题
参考资料: Django Ajax CSRF 认证:http://www.ziqiangxuetang.com/django/django-csrf.html Python Post遇到csrftok ...
- C#基础视频教程5.1 如何编写简单的超级热键
我们上一节介绍了编写简单计算器,实际上也是出于实用角度(这个计算器只要你肯改,肯定能做的比微软自带的计算器好用).这一节介绍做简单的超级热键(所谓的超级热键是指自定义快捷键的功能) 超级热键的最关键一 ...