关于C++,最常听到的一个抱怨就是:编译器背着程序员干了太多的事情。

default constructor函数的构建

default constructors会在需要的时候被编译器创建出来,关键字是:在需要的时候?被谁需要?用来做什么事情?

例如:

class Foo{ public: int first_val; int second_val; };

void foo_bar() {
Foo foo; //foo对象中的成员变量应在此处被初始化
if (foo.first_val)
//...do samething
}

在这个例子中,正确的程序语义是要求Foo有个default constructors,可以将它的两个members初始化为0。这段代码可曾符合"在需要的时候"?答案是NO

其间的差别在于一个是程序需要,一个是编译器需要。如果是程序需要,那初始化的任务是程序员的责任,只有当编辑器需要时default constructors才会被合成。

此外,被合成出来的default constructors只执行编译器所需的行动,也就是说,即使有需要为class Foo合成一个default constructors,那个合成出来的constructors也不会将数据成员初始化为0,为了让上一段代码正确执行,类的设计者必须提供一个明显的constructors将两个成员变量初始化。

C++ Standard[ISO-C++95]的Section2.1上讲:被编译器合成的default constructors将是一个浅薄而无能,没啥用的constructors。

下面分别讨论nontrival(非平凡) default constructors的四种情况

情况1带有default constructors的成员类对象

如果一个class没有任何constructors,但它内含一个类成员对象,而后者有default constructors(如果只有带参构造函数,编译器会认为你用心设计这个类,会将初始化任务交给设计者),那么这个类的合成的default constructors就是nontrivial,不过这个合成操作只有在constructors真正需要被调用的时候才会发生。

例如:

class Foo { public: Foo() { std::cout << "l am called" << std::endl; } Foo(int){} };
class Bar { public: Foo foo; char *str; }; int main() {
Bar foo;   //Bar::foo对象必须在此处被初始化
if (foo.str) { /*........*/}
}

被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但并不产生任何代码来处理Bar::str。即被合成的default constructor只是为了满足编译器的需要(编译器需要有个地方来初始化Bar::Foo,因为它有自己的default constructor),而不是程序的需要(初始化Bar::str是程序员的任务)。再一次提醒:被合成的default constructors只满足编译器的需要,而不是程序的需要。

如果我们在Bar类中添加构造函数:

Bar::Bar() { str = ; }

现在程序的需求满足了,但是编译器还需要初始化成员对象foo,由于default constructor已经被明确定义出来,编译器没办法合成第二个。则编译器会扩张已存在的constructor,在其中安插一些代码,使得user code在被执行之前,先调用必要的default constructor 。

已被显示声明的constructor会被编译器扩展为:

Bar::Bar(){
foo.Foo::Foo(); //编译器安插的隐藏代码代码
str = ; //显式的程序员代码
}

一个类中如果有多个类成员对象都要求constructor初始化操作,C++将以成员对象在类中的声明次序来调用各个constructor,这一点有编译器完成。

情况2带有default constructor的Base Class

如果一个没有任何constructor的类派生自一个带有default  constructor的基类,那么这个子类的default constructor会被视为nontrivial,因此需要被合成出来调用基类的default constructor(根据它们的声明次序)。

如果设计者提供多个constructor,编译器不会合成新的default constructor,而是会扩展现有的每一个constructor,将用来调用所需要的default constructor的程序代码加进去。

情况3这个类带有Virtual Function

这种情况包括两个更细的情况:

1.这个类自己声明(或者继承)一个Virtual Function。

2.这个类继承自一个继承串链,其中有virtual base class。

这种情况下,编译时,要做如下工作:

1.一个virtual function table(vtbl)会被编译器生成出来,内存放类的virtual functons地址。

2.在每一个class object中,一个额外的pointer member(就是vptr,指向vtbl)会被编译器合成出来。此外虚拟调用会被替换(w.vf() => w.vprt[1])。

为了支持这种功能,编译器必须为每个w对象设置它的vptr(这是成员变量,此时需要指向合适的vtbl),因此编译器需要在default constructor中安插一些代码来完成这种工作。

情况4这个类带有Virtual Base Class。

考虑这样的代码:

classX { public: inti; };

classA : publicvirtualX   { public: intj; };

classB : publicvirtualX   { public: doubled; };

classC : publicA, publicB { public: intk; };

//无法在编译期间解析出 pa->i 的位置(给一个pa无法确定i的地址)。

void foo( constA* pa ) { pa->i = 1024; }

main() {

foo( new A );

foo( new C );

// ...

}

由于pa的真正类型不确定,所以某些编译器会记录一个指针例(如 __vbcX)来记录X,然后通过这个指针来定位pa指向的i。上述

void foo( constA* pa ) { pa->i = 1024; }

变成了:

void foo( constA* pa ) { pa-> __vbcX ->i = 1024; }

因此,__vbcX这个指针需要在object构造期间设置好。于是编译器需要一个default constructor来完成这个工作。

复制构造函数Copy constructor的构造

何时用到copy constructor:显式用t1初始化t2;传参;返回一个类对象。

如果程序员显式定义了copy constructor,则调用它。

如果没有,其内部是通过 default memberwise initialization的手法完成的(将源对象的所有member复制给目的对象,对于member class object,会递归执行memberwise initialization)。

这些操作是如何构造的:

概念上讲,这些操作是被一个copy constructor实现的。

上述强调“概念上讲”,是因为有时候copy constructor是trivial的。

copy constructor何时是nontrivial:简单的答案为当class没有展现bitwise copy semantics时,copy constructor是nontrivial的。

那么什么时候class没有展现bitwise copy semantics:答案为有四种情况。

情况1这个类的某个member object有copy constructor。(编译器要在这个类的copy constructor来调用其member object的copy constructor)。

情况2这个类继承自某个有copy constructor的base class。(编译器要在这个类的copy constructor来调用其base class的copy constructor)。

情况3这个类声明了若干个virtual function。

如下代码

void draw(const ZooAnimal& zoey) {zoey.draw();}

void foo() {

ZooAnimal franny = yogi;

draw (yogi);   //调用 Bear::draw()

draw (franny); //调用 ZooAnimal::draw()

}

如果ZooAnimal按照bitwise copy进行复制(ZooAnimal franny = yogi;),则会出现franny的vptr设置成了yogi的vptr,于是draw (franny);调用的会是Bear的draw。(事实上不是,因为franny是一个实例,不是指针也不是引用)。

因此ZooAnimal的复制构造函数需要显式设定vptr(使之指向ZooAnimal的vtbl),这个设置动作需要在合成的copy constructor中完成。

情况4这个类派生自的继承链中有virtual base class。

同构造函数的情况4。 __vbcXXX需要显式重设,这个设置动作需要在合成的copy constructor中完成。

Raccoon和RedPanda中含有指向virtual base class subobject的指针(设为__vbcZooAnml),则当用RedPanda初始化Raccoon时( Raccoon rc=rp;),将Raccoon-> __vbcZooAnml 设置为RedPanda->__vbcZooAnml是不对的。因此需要重新设置__vbcZooAnml,这个动作需要在copy constructor中完成。

总结以上4种情况,bitwise copy semantics的意思可以理解为:类的某些成员变量(包括程序员定义的成员变量和编译器所需要的变量如vptr、__vbc等)不能按位复制时,需要调用成员变量的copy constructor或者重设vptr等编译器所需类的成员变量,这些动作都需要在发生对象复制的时候完成,因此编译器会合成一个copy constructor(入股没有的话)。

程序转化语义学 Program Transformation Semantics

显式的初始化操作 Explicit Initialization

X x0;

void foo_bar(){

X x1(x0);   //定义了x1

X x2 = x0;      //定义了x2

X x3 = X(x0);   //定义了x3

}

转化的两个动作:重写每一个定义,其初始化部分被剥除;用copy constructor初始化。

即变成了

void foo_bar(){

X x1;    //定义被重写,初始化操作被剥除

X x2;    //定义被重写,初始化操作被剥除

X x3;    //定义被重写,初始化操作被剥除

//编译器安插X copy constructor。

x1.X::X( x0 );

x2.X::X( x0 );

x3.X::X( x0 );

}

其中x1.X::X( x0 );会表现为对copy constructor(即 X::X( constX& xx);)的调用。

参数初始化 Argument Initialization

如下代码的变化

void foo(X x0);

...

X xx;

foo(xx)

变成了

void foo(X& x0);

...

X __tmp;

__tmp.X::X( XX );

foo(__tmp);

其中X声明了destruconstructor,它在foo调用完成后销毁__tmp。

另一种变化是拷贝构建(copy construct),将实际参数直接建在其应该在的位置上。

返回值的初始化 Return Value Initialization

X bar(){

X xx;

...

return xx;

}

变成了

void bar(X& _result){

X xx;

...

_result.X::X(xx);

return;

}

对函数的调用

X xx=bar();

变为:

X xx;

bar(xx);

对函数的调用

bar().memfunc();

变为:

X _tmp;

(bar(_tmp),_tmp).memfunc();

在使用者层面做优化 Optimization at the User Level

在编译器层面做优化 Optimization at the Compiler Level

针对这种转化:

X bar(){

X xx;

...

return xx;

}

变成

void bar(X& _result){

X xx;

...

_result.X::X(xx);

return;

}

这一转换的一个优化为:转变成如下代码

void bar(X& _result){

_result.X::X(xx);

//原来处理xx,现在变为处理_result。

return;

}

这一优化称之为NRV(Named Return Value)。

虽如此,NRV饱受批评。主因有二:编译器实现程度不一致(有些编译器)。函数变得复杂时,优化难以实施。

Foo() { std::cout << "l am called" << std::endl; }

C++对象模型-构造函数语意学的更多相关文章

  1. 【C++对象模型】构造函数语意学之二 拷贝构造函数

    关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在[需要的时候]才去合成默认的拷贝构造函数. 在什么时候才是[需要的时候]呢? 也就是类不展现[bitwise copy semantic ...

  2. 《深度探索C++对象模型》第二章 | 构造函数语意学

    默认构造函数的构建操作 默认构造函数在需要的时候被编译器合成出来.这里"在需要的时候"指的是编译器需要的时候. 带有默认构造函数的成员对象 如果一个类没有任何构造函数,但是它包含一 ...

  3. 【C++对象模型】构造函数语意学之一 默认构造函数

    默认构造函数,如果程序员没有为类定义构造函数,那么编译器会在[需要的时候]为类合成一个构造函数,而[需要的时候]分为程序员需要的时候和编译器需要的时候,程序员需要的时候应该由程序员来做工作,编译器需要 ...

  4. 【深度探索C++对象模型 | 02】构造函数语意学

    默认构造函数的构造操作.拷贝构造函数额构造操作  注意:默认构造函数和拷贝构造函数在必要时的时候由编译器产生出来. 参考资料 关于默认构造函数的几个错误认识(四种情况下,编译器会生成默认构造函数)

  5. 【C++对象模型】第二章 构造函数语意学

    1.Default Constructor 当编译器需要的时候,default constructor会被合成出来,只执行编译器所需要的任务(将members适当初始化). 1.1  带有 Defau ...

  6. 深度探索C++对象模型之第二章:构造函数语意学之成员初始值列表

    当我们需要设置class member的初值时,要么是经过member initialization list ,要么在construcotr内. 一.先讨论必须使用member initializa ...

  7. 深度探索C++对象模型之第二章:构造函数语意学之Copy constructor的构造操作

    C++ Standard将copy constructor分为trivial 和nontrivial两种:只有nontrivial的实例才会被合成于程序之中.决定一个copy constructor是 ...

  8. 深度探索C++对象模型之第二章:构造函数语意学之Default constructor的构造操作

    C++新手一般由两个常见的误解: 如果任何class没有定义默认构造函数(default constructor),编译器就会合成一个来. 编译器合成的的default constructor会显示的 ...

  9. 《深度探索C++对象模型(Inside The C++ Object Model )》学习笔记

    转载:http://dsqiu.iteye.com/blog/1669614 第一章 关于对象 使用class封装之后的布局成本: class并没有增加成本,data members直接内含在每一个c ...

随机推荐

  1. android发送与接收超长短信

    android发送与接收超长短信 android接收发送短信,支持的最大字符数是70个,实际是67个字符,如果发送的短信超过了该数目,那就需要用到sendMultipartTextMessage()方 ...

  2. 大量原创视频教程分享(01)---XSL语法教程

    首先,感谢博客园给这个平台来发布这些教程.. 这些教程都是本人亲自录制的,时间主要是2012-2014年,大概有几十部这么多,可能有说的不对的地方,如果可以,感谢你的指正 本人也不想误人子弟,大部分教 ...

  3. Ios中checkBox

    //使用tableview来进行布局checkBox.便于全选,全不选//radiobutton 适合用RadioButton #import <UIKit/UIKit.h> @inter ...

  4. spring事务管理——编程式事务、声明式事务

    本教程将深入讲解 Spring 简单而强大的事务管理功能,包括编程式事务和声明式事务.通过对本教程的学习,您将能够理解 Spring 事务管理的本质,并灵活运用之. 先决条件 本教程假定您已经掌握了 ...

  5. java 获取当前日期和特殊日期格式转换

     1.获取当前日期: package com.infomorrow.dao; import java.sql.Timestamp; import java.util.Calendar; import ...

  6. android 安卓APP获取手机设备信息和手机号码的代码示例

    下面我从安卓开发的角度,简单写一下如何获取手机设备信息和手机号码 准备条件:一部安卓手机.手机SIM卡确保插入手机里.eclipse ADT和android-sdk开发环境 第一步:新建一个andro ...

  7. sql中union 和 union all的区别

    最近发现一个视图出奇的慢,在生产环境还好,由于服务器配置较高,没有察觉出来.但是做了一次修改后在开发版 和测试版就直接查询不出结果了.就连select count(1) from 都运行2个小时没有结 ...

  8. Easyui中 messager出现的位置

    $.messager.alert 弹出框的位置随页面的长度越大越靠下. $.messager.alert('消息','只能对单个客户进行清款!','info'); 弹出的位置 太靠下方.修改为: $. ...

  9. Android_ViewPager_实现多个图片水平滚动

    1.示意图                       2.实现分析 (1).xml配置 <!-- 配置container和pager的clipChildren=false, 并且指定margi ...

  10. [转]PostgreSQL教程:系统表详解

    这篇文章主要介绍了PostgreSQL教程(十五):系统表详解,本文讲解了pg_class.pg_attribute.pg_attrdef.pg_authid.pg_auth_members.pg_c ...