博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

值语义 引用语义(对象语义)

Posted on 2014-09-22 16:29  bw_0927  阅读(1847)  评论(1)    收藏  举报

https://www.zybuluo.com/guochy2012/note/14265

http://ewangplay.appspot.com/?p=27001

 

 

定义

值语义和引用语义的定义是这样的:复制后与以前的对象无关的对象叫做值语义,无法复制或者复制后与原来的对象存在关联的对象称为引用语义。

那么该如何理解上面的定义呢? 我的看法是:对象看起来表现的像一个值,就叫做值语义。

 

Java

我们先从Java中的相关语法看起:

 
  1. int a = 8;
  2. int b = a;

这里的a b 都是int,属于java中原生数据类型,他们的特点是复制后二者再无关联。它们表现出来是两个value,所以int类型为值语义(value semantic)。 
与他们形成鲜明对比的是用户自定义的class:

 
  1. Employee a1 = new Employee("zhangsan", 23);
  2. Employee a2 = a1;
  3. a2.setAge(30);
  4. System.out.println(a1.getAge());

这段代码输出显然是30,第二行语句,导致a和b指向了同一个实际的对象。在这里,java的对象表现的行为不是一种value的行为,而是一种reference,所以这里称java中的对象为引用语义(reference semantic)。

 

C++

回到C++中,再看这个问题,以C++中一个普通的类为例:

 
  1. class ItemBook{
  2. private:
  3. string isbn_;
  4. double price_;
  5. int amount_;
  6. };

如果我们写出以下的代码:

 
  1. ItemBook a1;
  2. // set member of a1;
  3. ItemBook a2 = a1;

那么a1和a2复制后没有任何关联,他们的表现和java中的原生数据类型一样,属于value语义。事实上,C++内置的标准库类型例如string、vector、list等都是值语义,凡是可以放入容器的元素都必须具备value语义,即必须具备拷贝的能力,放入标准容器的元素和之前的元素没有任何的关联。

那么C++中的引用语义看起来有些奇怪,可以这样理解:正是因为类不可复制,所以我们可以采用智能指针,来把它们转化为reference语义。 
典型的例子是TcpConnection,它显然是不可复制的(因为牵扯到系统的资源),那么我们在程序中该如何把它作为参数传递呢?我们定义下面的类型:

 
  1. typedef boost::shared_ptr<TcpConnection> TcpConnectionPtr;

这样我们在使用TcpConnection的场合一概采用TcpConnectionPtr,它表现的像一个引用,这就实现了引用语义。 
对于那些对象复制后存在关联的对象,很多时候我们干脆禁用掉copy能力。

 

Java和C++的本质区别

我们可以得出这样一个结论,C++和java的本质区别在于语义不同!

 

总结

在设计C++的类时,严格的区分开对象是否可复制,也就是严格区分value语义和reference语义,这样可以避免很多潜在的BUG。

=========

(1)容器元素满足的基本条件

STL容器中的元素必须满足下面的几点基本需求:

  • 容器中的元素必须是可复制的,而且复制实例跟原实例要保持一致。(通过拷贝构造函数)
  • 容器中的元素必须是可赋值的。(通过赋值操作符)
  • 容器中的元素必须是可销毁的。(通过析构函数)
  • 对于序列容器中元素,默认构造函数必须是可用的。
  • 对于关联容器中元素,诸如<,>等比较操作符必须提供,因为关联容器中的排序规则需要用到这些操作符。
  • 一般情况下,==操作符也要提供,通常情况下查找元素操作需要用到。

(2)容器的值语义和引用语义

值语义是指当一个元素放入容器中时,在容器内部创建一个该元素的复制品,返回的就是这个复制品,而不是元素本身。如果你修改了容器中元素的值,实际上是修改了这个复制品,而原先的元素并没有被修改。
引用语义是指当一个元素放入容器中时,存储的就是这个元素本身。如果你修改了容器中元素的值,也即是修改了这个元素本身。
STL的容器采用的是值语义。这样的选择有优点也有缺点,优点是:
  • 复制元素是简单容易操作的。
  • 引用语义是容器出错的。你必须时刻保证所引用的对象没有被销毁,而且你还要提防循环引用问题。
缺点是:
  • 复制复杂的对象将导致性能的下降。
  • 在不同的容器中引用管理一个对象实例变得不可能。
实际情况下我们需要两种语义:我们有时候偏爱值语义,另一个时候我们又偏爱引用语义。不幸的是,STL中的容器只支持值语义。而引用语义需要我们自己去模拟。很自然地,我们想到了指针,因为指针本身实现的就是引用语义。我们可以把指向对象的指针放入容器中,不就解决了容器引用语义的问题?但我们知道,c和c++中的普通指针虽然是一个利器,却需要高超的剑士来驱驾,不然就会自断经脉。用普通指针作为容器元素带了的问题也不容小觑:比如它指向的对象是否已经被他人给干掉了?对于容器中元素的一些常见的操作比如查找、排序等也会因为指针而变得复杂。一个更加友好的方式是使用智能指针,比如前面我们介绍的auto_ptr,但我们前面已经介绍过了,auto_ptr智能指针决定不能用于容器元素,因为auto_ptr得语义跟容器元素的基本需求不符。不幸的是,c++标准库中没有提供可用于容器元素的智能指针,需要我们自己去编写。
 
===http://zh.highscore.de/cpp/boost/

智能指针 boost::shared_ptr 基本上类似于 boost::scoped_ptr。 关键不同之处在于 boost::shared_ptr 不一定要独占一个对象。 它可以和其他boost::shared_ptr 类型的智能指针共享所有权。 在这种情况下,当引用对象的最后一个智能指针销毁后,对象才会被释放。

因为所有权可以在 boost::shared_ptr 之间共享,任何一个共享指针都可以被复制,这跟 boost::scoped_ptr 是不同的。 这样就可以在标准容器里存储智能指针了——你不能在标准容器中存储 std::auto_ptr,因为它们在拷贝的时候传递了所有权。

#include <boost/shared_ptr.hpp> 
#include <vector> 

int main() 
{ 
  std::vector<boost::shared_ptr<int> > v; 
  v.push_back(boost::shared_ptr<int>(new int(1))); 
  v.push_back(boost::shared_ptr<int>(new int(2))); 
} 

多亏了有 boost::shared_ptr,我们才能像上例中展示的那样,在标准容器中安全的使用动态分配的对象。 因为 boost::shared_ptr 能够共享它所含对象的所有权,所以保存在容器中的拷贝(包括容器在需要时额外创建的拷贝)都是和原件相同的。如前所述,std::auto_ptr做不到这一点,所以绝对不应该在容器中保存它们。