C++ 工程实践(8):值语义

来源:互联网 发布:为什么程序员工资40万 编辑:程序博客网 时间:2024/06/01 22:51

陈硕 (giantchen_AT_gmail)
http://blog.csdn.net/Solstice  http://weibo.com/giantchen
陈硕关于 C++ 工程实践的系列文章: http://blog.csdn.net/Solstice/category/802325.aspx
排版正常的版本: http://www.cnblogs.com/Solstice/category/287661.html
陈硕博客文章合集下载: http://blog.csdn.net/Solstice/archive/2011/02/24/6206154.aspx
本作品采用“Creative Commons 署名-非商业性使用-禁止演绎 3.0 Unported 许可协议(cc by-nc-nd)”进行许可。http://creativecommons.org/licenses/by-nc-nd/3.0/

本文是前一篇《C++ 工程实践(7):iostream 的用途与局限》的后续,在这篇文章的“iostream 与标准库其他组件的交互”一节,我简单地提到iostream的对象和C++标准库中的其他对象(主要是容器和string)具有不同的语义,主要体现在iostream不能拷贝或赋值。今天全面谈一谈我对这个问题的理解。

本文的“对象”定义较为宽泛,a region of memory that has a type,在这个定义下,int、double、bool 变量都是对象。

如果出现排版错乱,请移步: http://www.cnblogs.com/Solstice/archive/2011/08/16/2141515.html

什么是值语义

值语义(value sematics)指的是对象的拷贝与原对象无关,就像拷贝 int 一样。C++ 的内置类型(bool/int/double/char)都是值语义,标准库里的 complex<> 、pair<>、vector<>、map<>、string 等等类型也都是值语意,拷贝之后就与原对象脱离关系。Java 语言的 primitive types 也是值语义。

与值语义对应的是“对象语义/object sematics”,或者叫做引用语义(reference sematics),由于“引用”一词在 C++ 里有特殊含义,所以我在本文中使用“对象语义”这个术语。对象语义指的是面向对象意义下的对象,对象拷贝是禁止的。例如 muduo 里的 Thread 是对象语义,拷贝 Thread 是无意义的,也是被禁止的:因为 Thread 代表线程,拷贝一个 Thread 对象并不能让系统增加一个一模一样的线程。

同样的道理,拷贝一个 Employee 对象是没有意义的,一个雇员不会变成两个雇员,他也不会领两份薪水。拷贝 TcpConnection 对象也没有意义,系统里边只有一个 TCP 连接,拷贝 TcpConnection  对象不会让我们拥有两个连接。Printer 也是不能拷贝的,系统只连接了一个打印机,拷贝 Printer 并不能凭空增加打印机。凡此总总,面向对象意义下的“对象”是 non-copyable。

Java 里边的 class 对象都是对象语义/引用语义。ArrayList<Integer> a = new ArrayList<Integer>(); ArrayList<Integer> b = a; 那么 a 和 b 指向的是同一个ArrayList 对象,修改 a 同时也会影响 b。

值语义与 immutable 无关。Java 有 value object 一说,按(PoEAA 486)的定义,它实际上是 immutable object,例如 String、Integer、BigInteger、joda.time.DateTime 等等(因为 Java 没有办法实现真正的值语义 class,只好用 immutable object 来模拟)。尽管 immutable object 有其自身的用处,但不是本文的主题。muduo 中的 Date、Timestamp 也都是 immutable 的。

C++中的值语义对象也可以是 mutable,比如 complex<>、pair<>、vector<>、map<>、string 都是可以修改的。muduo 的 InetAddress 和 Buffer 都具有值语义,它们都是可以修改的。

值语义的对象不一定是 POD,例如 string 就不是 POD,但它是值语义的。

值语义的对象不一定小,例如 vector<int> 的元素可多可少,但它始终是值语义的。当然,很多值语义的对象都是小的,例如complex<>、muduo::Date、muduo::Timestamp。

值语义与生命期

值语义的一个巨大好处是生命期管理很简单,就跟 int 一样——你不需要操心 int 的生命期。值语义的对象要么是 stack object,或者直接作为其他 object 的成员,因此我们不用担心它的生命期(一个函数使用自己stack上的对象,一个成员函数使用自己的数据成员对象)。相反,对象语义的 object 由于不能拷贝,我们只能通过指针或引用来使用它。

一旦使用指针和引用来操作对象,那么就要担心所指的对象是否已被释放,这一度是 C++ 程序 bug 的一大来源。此外,由于 C++ 只能通过指针或引用来获得多态性,那么在C++里从事基于继承和多态的面向对象编程有其本质的困难——资源管理。

考虑一个简单的对象建模——家长与子女:a Parent has a Child, a Child knows his/her Parent。在 Java 里边很好写,不用担心内存泄漏,也不用担心空悬指针:

public class Parent{    private Child myChild;}    public class Child{    private Parent myParent;}

只要正确初始化 myChild 和 myParent,那么 Java 程序员就不用担心出现访问错误。一个 handle 是否有效,只需要判断其是否 non null。

在 C++ 里边就要为资源管理费一番脑筋:Parent 和 Child 都代表的是真人,肯定是不能拷贝的,因此具有对象语义。Parent 是直接持有 Child 吗?抑或 Parent 和 Child 通过指针互指?Child 的生命期由 Parent 控制吗?如果还有 ParentClub 和 School 两个 class,分别代表家长俱乐部和学校:ParentClub has many Parent(s),School has many Child(ren),那么如何保证它们始终持有有效的 Parent 对象和 Child 对象?何时才能安全地释放 Parent 和 Child ?

直接但是易错的写法:

class Child;class Parent : boost::noncopyable{ private:  Child* myChild;};class Child : boost::noncopyable{ private:  Parent* myParent;};

如果直接使用指针作为成员,那么如何确保指针的有效性?如何防止出现空悬指针?Child 和 Parent 由谁负责释放?在释放某个 Parent 对象的时候,如何确保程序中没有指向它的指针?在释放某个 Child 对象的时候,如何确保程序中没有指向它的指针?

这一系列问题一度是C++面向对象编程头疼的问题,不过现在有了 smart pointer,我们可以借助 smart pointer 把对象语义转换为值语义,从而轻松解决对象生命期:让 Parent 持有 Child 的 smart pointer,同时让 Child 持有 Parent 的 smart pointer,这样始终引用对方的时候就不用担心出现空悬指针。当然,其中一个 smart pointer 应该是 weak reference,否则会出现循环引用,导致内存泄漏。到底哪一个是 weak reference,则取决于具体应用场景。

如果 Parent 拥有 Child,Child 的生命期由其 Parent 控制,Child 的生命期小于 Parent,那么代码就比较简单:

class Parent;class Child : boost::noncopyable{ public:  explicit Child(Parent* myParent_)    : myParent(myParent_)  {  } private:  Parent* myParent;};class Parent : boost::noncopyable{ public:  Parent()    : myChild(new Child(this))  {  } private:  boost::scoped_ptr<Child> myChild;};

在上面这个设计中,Child 的指针不能泄露给外界,否则仍然有可能出现空悬指针。

如果 Parent 与 Child 的生命期相互独立,就要麻烦一些:

class Parent;typedef boost::shared_ptr<Parent> ParentPtr;class Child : boost::noncopyable{ public:  explicit Child(const ParentPtr& myParent_)    : myParent(myParent_)  {  } private:  boost::weak_ptr<Parent> myParent;};typedef boost::shared_ptr<Child> ChildPtr;class Parent : public boost::enable_shared_from_this<Parent>,               private boost::noncopyable{ public:  Parent()  {  }  void addChild()  {    myChild.reset(new Child(shared_from_this()));  } private:  ChildPtr myChild;};int main(){  ParentPtr p(new Parent);  p->addChild();}

上面这个 shared_ptr+weak_ptr 的做法似乎有点小题大做。

考虑一个稍微复杂一点的对象模型:a Child has parents: mom and dad; a Parent has one or more Child(ren); a Parent knows his/her spouser. 这个对象模型用 Java 表述一点都不复杂,垃圾收集会帮我们搞定对象生命期。

public class Parent{    private Parent mySpouser;    private ArrayList<Child> myChildren;}public class Child{    private Parent myMom;    private Parent myDad;}

如果用 C++ 来实现,如何才能避免出现空悬指针,同时避免出现内存泄漏呢?借助 shared_ptr 把裸指针转换为值语义,我们就不用担心这两个问题了:

class Parent;typedef boost::shared_ptr<Parent> ParentPtr;class Child : boost::noncopyable{ public:  explicit Child(const ParentPtr& myMom_,                 const ParentPtr& myDad_)    : myMom(myMom_),      myDad(myDad_)  {  } private:  boost::weak_ptr<Parent> myMom;  boost::weak_ptr<Parent> myDad;};typedef boost::shared_ptr<Child> ChildPtr;class Parent : boost::noncopyable{ public:  Parent()  {  }  void setSpouser(const ParentPtr& spouser)  {    mySpouser = spouser;  }  void addChild(const ChildPtr& child)  {    myChildren.push_back(child);  } private:  boost::weak_ptr<Parent> mySpouser;  std::vector<ChildPtr> myChildren;};int main(){  ParentPtr mom(new Parent);  ParentPtr dad(new Parent);  mom->setSpouser(dad);  dad->setSpouser(mom);  {    ChildPtr child(new Child(mom, dad));    mom->addChild(child);    dad->addChild(child);  }  {    ChildPtr child(new Child(mom, dad));    mom->addChild(child);    dad->addChild(child);  }}

如果不使用 smart pointer,用 C++ 做面向对象编程将会困难重重。

值语义与标准库

C++ 要求凡是能放入标准容器的类型必须具有值语义。准确地说:type 必须是 SGIAssignable concept 的 model。但是,由 于C++ 编译器会为 class 默认提供 copy constructor 和 assignment operator,因此除非明确禁止,否则 class 总是可以作为标准库的元素类型——尽管程序可以编译通过,但是隐藏了资源管理方面的 bug。

因此,在写一个 class 的时候,先让它继承 boost::noncopyable,几乎总是正确的。

在现代 C++ 中,一般不需要自己编写 copy constructor 或 assignment operator,因为只要每个数据成员都具有值语义的话,编译器自动生成的 member-wise copying&assigning 就能正常工作;如果以 smart ptr 为成员来持有其他对象,那么就能自动启用或禁用 copying&assigning。例外:编写 HashMap 这类底层库时还是需要自己实现 copy control。

值语义与C++语言

C++ 的 class 本质上是值语义的,这才会出现 object slicing 这种语言独有的问题,也才会需要程序员注意 pass-by-value 和 pass-by-const-reference 的取舍。在其他面向对象编程语言中,这都不需要费脑筋。

值语义是C++语言的三大约束之一,C++ 的设计初衷是让用户定义的类型(class)能像内置类型(int)一样工作,具有同等的地位。为此C++做了以下设计(妥协):

  • class 的 layout 与 C struct 一样,没有额外的开销。定义一个“只包含一个 int 成员的 class ”的对象开销和定义一个 int 一样。
  • 甚至 class data member 都默认是 uninitialized,因为函数局部的 int 是 uninitialized。
  • class 可以在 stack 上创建,也可以在 heap 上创建。因为 int 可以是 stack variable。
  • class 的数组就是一个个 class 对象挨着,没有额外的 indirection。因为 int 数组就是这样。
  • 编译器会为 class 默认生成 copy constructor 和 assignment operator。其他语言没有 copy constructor 一说,也不允许重载 assignment operator。C++ 的对象默认是可以拷贝的,这是一个尴尬的特性。
  • 当 class type 传入函数时,默认是 make a copy (除非参数声明为 reference)。因为把 int 传入函数时是 make a copy。
  • 当函数返回一个 class type 时,只能通过 make a copy(C++ 不得不定义 RVO 来解决性能问题)。因为函数返回 int 时是 make a copy。
  • 以 class type 为成员时,数据成员是嵌入的。例如 pair<complex<double>, size_t> 的 layout 就是 complex<double> 挨着 size_t。

这些设计带来了性能上的好处,原因是 memory locality。比方说我们在 C++ 里定义 complex<double> class,array of complex<double>, vector<complex<double> >,它们的 layout 分别是:(re 和 im 分别是复数的实部和虚部。)

value1

而如果我们在 Java 里干同样的事情,layout 大不一样,memory locality 也差很多:

value2

Java 里边每个 object 都有 header,至少有两个 word 的开销。对比 Java 和 C++,可见 C++ 的对象模型要紧凑得多。

待续

下一篇文章我会谈与值语义紧密相关的数据抽象(data abstraction),解释为什么它是与面向对象并列的一种编程范式,为什么支持面向对象的编程语言不一定支持数据抽象。C++在最初的时候是以 data abstraction 为卖点,不过随着时间的流逝,现在似乎很多人只知 Object-Oriented,不知 data abstraction 了。C++ 的强大之处在于“抽象”不以性能损失为代价,下一篇文章我们将看到具体例子。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 开发商暗埋水管热熔接头漏水怎么办 钻墙把水管钻破了怎么办 钻墙时不小心把水管钻破了怎么办 孩子把同桌的铅笔盒弄坏了怎么办 水管伐坏了物业不给修怎么办 卫生间埋在地下的水管漏水怎么办 埋在地下的水管冻住了怎么办 洗脸盆下水管有眉笔掉堵了怎么办 楼上打空调眼导致墙面潮湿怎么办 阳台做防水了又贴砖还漏水怎么办 洗菜盆里的盖子吸住拔不起来怎么办 掉小区阴井盖里物业不管怎么办 卫生间下水道被头发堵了怎么办妙招 久量时尚夜光台灯不亮了怎么办 我想做水电暖工程没有资质怎么办 修手机被弄丢了没有开维修单怎么办 手机去维修被老板弄丢了怎么办 5年保修期过后房子楼顶漏水怎么办 墙内水管漏水怎么办3步骤巧维修 下面有地暖下面水管漏水怎么办 热水器接的塑料水管子漏水怎么办 电热水器烧热水时热水管漏水怎么办 户户通位置锁定模块异常1怎么办 户户通位置锁定模块异常3怎么办 逆战安全系统检测客户端异常怎么办 电视户户通位置信息模块异常怎么办 苹果手机wi-fi模块坏了怎么办 xp本地连接受限制或无连接怎么办 公司没帮二级建造师延续注册怎么办 考造价师考过了年限不够怎么办? 上海众非称重不显示数字怎么办 宝骏630防火墙隔热棉掉了怎么办 冷车启动水温报警灯亮了怎么办 租铺位面积比合同面积小怎么办 农村村民建房领居不肯签名怎么办 车祸死者家属不来协商赔偿怎么办 26岁想回去当老师了怎么办 面包车排量小空调带不凉快怎么办 科三包过不给退钱人跑了怎么办 护师职称考试成绩单丢了怎么办 河南二级建造师报名地址填错怎么办