一、对象语义与值语义
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++编程规范
分享到:
相关推荐
2.1 auto_ptr auto_ptr是通过由 new 表达式获得的对象,并在auto_ptr⾃⾝被销毁时删除该对象的智能指针,它可⽤于为动态分配的对象提供异常安 全、传递动态分配对象的所有权给函数和从函数返回动态分配的对象,是⼀...
std::auto_ptr 真正让⼈容易误⽤的地⽅是其不常⽤的复制语义,即当复制⼀个 std::auto_ptr 对象时(拷贝复制或 operator= 复制),原对象 所持有的堆内存对象也会转移给复制出来的对象。⽰例代码如下: #include ...
lock_guard 对象通常用于管理某个锁(Lock)对象,因此与 Mutex RAII 相关,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,...
unique_resource,用于独家所有权资源管理的通用 RAII 包装器。 这是使用 Boost 软件许可证 1.0 的 unique_resource 实现。 此实现基于 C++ 标准委员会论文中的示例实现。 什么是 unique_resource unique_resource...
它添加了诸如RAII之类的C ++概念,并移动了语义以简化Vulkan对象的管理。 当VKW对象超出范围时,它的析构函数将破坏基础的Vulkan对象。 例如: { vk::Instance instance = createInstance (...); // instance ...
C++程序的设计机制3 RAII机制
c++11/14/17新特性讲解及c++20展望,lambda,raii,,share_ptr
使用RAII手法封装互斥器(pthrea_mutex_t)、 条件变量(pthread_cond_t)等线程同步互斥机制,使用RAII管理文件描述符等资源 使用shared_ptr、weak_ptr管理指针,防止内存泄漏 下一步开发计划 添加异步日志系统,...
内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++...
主要介绍了C++中的RAII机制详解,RAII是Resource Acquisition Is Initialization的简称,是C++语言的一种管理资源、避免泄漏的惯用法,需要的朋友可以参考下
学习C++,学习windows/linux编程,学习网络编程,学习reactor模型,自己实现一遍练手; 包含(以下实现是Windows/Linux平台通用的,美名其曰跨平台) 基本工具库: 日志工具(日志优先级、同步日志、异步日志、带...
文章my_simple_RAII对应的code 环境vs2008
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 ...
The format of the symbol name should be <PROJECT>_<PATH>_<FILE>_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介绍RAII.js是基于ES6 Promise的RAII(资源获取即初始化)实现。 RAII.js确保您的资源按顺序进行了初始化和销毁,并且可以随时取消(销毁)整个堆栈。安装只需通过npm在项目中安装raii.js即可: npm ...
纳米警报 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++ RAII的一种应用,可用于动态资源管理,资源即对象的管理策略。 智能指针在 <memory>标头文件的 std 命名空间中定义。 它们对 RAII 或 获取资源即初始化 编程惯用法至关重要...
nonstd::push_array<T> 精简的“数组和计数器”固定大小的数据结构,用于将数据写入一次然后进行复制。 可复制的。 没什么特别的。 nonstd::raw_buffer<T> 针对给定类型,围绕std::aligned_storage构建的数组。 ...
第13条 确保资源为对象所拥有。使用显式的RAII和智能指针 24 编程风格 27 第14条 宁要编译时和连接时错误,也不要运行时错误 28 第15条 积极使用const 30 第16条 避免使用宏 32 第17条 避免使用...