第2章    构造函数语意学 (The Semantics of Constructor)

关于C++,最常听到的一个抱怨就是,编译器背着程序猿做了太多事情.Conversion运算符就是最常被引用的一个样例.

2.1    Default Constructor的建构操作

C++ Annotated Reference Manual(ARM)指出"default constructors ...在须要的时候被编译器产生出来".keyword眼是"在须要的时候".被谁须要?做什么事情?

看看以下这段程序代码:

class Foo {
public:
int val;
Foo *pnext;
}; void foo_bar() {
Foo bar;
if (bar.val || bar.pnext)
// ... do something
}

在这个样例中,正确的程序语意是要求Foo有一个default constructor,能够将它的两个members初始化为0,上面这段代码不符合ARM所述的"在须要的时候".其间的区别在于一个是程序的须要,一个是编译器的须要.上述代码不会合成出一个default constructor.

    那么什么时候才会合成出一个default constructor呢?

当编译器须要它的时候!此外,被合成出来的constructor仅仅运行编译器所需的行动,也就是说,即使有须要为class Foo合成一个default constructor,那个constructor也不会将两个data members val和pnext初始化为0.为了让上一段代码正确运行,class Foo的设计者必须提供一个显式的default
constructor,将两个members适当地初始化.

    C++ Standard已经改动了ARM的说法,尽管其行为其实仍然同样.C++ Standard指出"对于class X,假设没有不论什么user-declared constructor,那么会有一个default constructor被隐式声明出来....一个被隐式声明的default constructor将是一个trivial constructor..."

"带有 Default constructor"的Member Class Object

假设一个class没有不论什么constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是"nontrivial",编译器须要为此class合成出一个default constructor,只是这个合成操作仅仅有在constructor真正须要被调用时才会发生.

    于是出现一个有趣的问题:在C++各个不同的编译模块中,编译器怎样避免合成出多个default constructor(譬如说一个是为A.C档合成,还有一个为B.C档合成)?解决的办法是把合成的default constructor,copy constructor,destructor,assignment copy operator都以inline方式完毕.一个inline函数有静态链接(static linkage),不会被档案以外者看到.假设函数太复杂,不适合做成inline,就会合成出一个explicit
non-inline static实体.

    比如,以下的程序片段中,编译器为class Bar合成一个default constructor:

class Foo {
public:
Foo();
Foo(int)
...
};
class Bar {
public:
Foo foo;
char *str;
};
void foo_bar() {
Bar bar; //Bar::foo必须在此处初始化
//Bar::foo是一个member object.而其class Foo拥有defautl constructor
if (str) {
...
}
};

被合成的Bar default constructor内含必要的代码,可以调用class Foo的default constructor来处理member object Bar::foo,但它并不产生不论什么代码来初始化Bar::str.将Bar::foo初始化是编译器的责任,将Bar::str初始化是程序猿的责任,被合成的default constructor可能例如以下:

// Bar的default constructor可能被这样合成
// 被member foo调用class Foo的default constructor
inline Bar::Bar() {
// C++伪代码
foo.Foo::Foo();
}

注意,被合成的default constructor仅仅满足编译器的须要,而不是程序的须要,为了让这个程序片段可以正确运行,字符指针str也须要被初始化.如果程序猿经由以下的default constructor提供了str的初始化操作:

// 程序猿定义的default constructor
Bar::Bar() { str = 0; }

如今程序的需求获得满足了,可是编译器还须要初始化member object foo.因为default constructor已经被显式定义,编译器没有办法合成第二个.

    编译器採取行动:假设 class A内含一个或一个以上的member class objects,那么 class A的每个constructor必须调用每个member classes的default constructor,编译器会扩张已存在的constructors,在当中安插一些码,使得user code在被运行之前,先调用必要的default constructors.沿续前一个样例,扩张后的constructors可能像这样:

// 扩张后的default constructor
// C++伪代码
Bar::Bar() {
foo.Foo::Foo(); // 附加上complier code
str = 0; // 显式user code
}

假设有多个class member objects都要求constructor初始化操作,将怎样呢?C++语言要求以"member objects在class中的声明次序"来调用各个constructors.这一点由编译器完毕,它为每个constructor安插程序代码,以"member声明次序"调用每个member所关联的default
constructor.这些码被安插在显式user code之前.假设有例如以下所看到的三个classes:

class Dopey {
public:
Dopey();
};
class Sneezy {
public:
Sneezy(int);
Sneezy();
};
class Bashful {
public:
Bashful();
};
以及一个 class Snow_White:
class Snow_White {
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
private:
int number;
};

假设Snow_White未定义default constructor。就会有一个nontrivial constructor被合成出来,依次序调用Dopey、Sneezy、Bashful的default constructor。然而假设Snow_White定义以下这种default constructor:

// 程序猿所写的default constructor
Snow_White::Snow_White() : Sneezy(1024) {
member = 2048;
}
它会被扩张为:
// 编译器扩张后的default constructor
// C++伪代码
Snow_White::Snow_White() : Sneezy(1024) {
// 插入member class object
// 调用其constructor
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy(1024);
bashful.Bashful::Bashful();
// 显式user mode
member = 2048;
}

顺序依次为类的成员对象,类的构造函数。

"带有Default Constructor"的Base Class

假设一个没有不论什么的constructors的class派生自一个"带有default constructor"的base class ,那么这个derived class 的default constructor会被视为nontrivial。并因此须要被合成出来,它将调用上一层的base classes的default constructor(依据它们的声明次序),对一个后继派生的 class 而言,这个合成的constructor和一个"被明白提供的default constructor"没有差异。

假设设计者提供多个constructor,但当中都没有default constructor呢?编译器会扩张现有的每个constructor。将"用以调用全部必要的default constructors"的程序代码加进去,它不会合成一个新的default constructor,这是由于其它"由user所提供的constructor"存在的缘故。

假设同一时候存在"带有default constructors"的member class objects,那些default constructor也会被调用在——在全部base
class constructor都被调用之后。

能够看出,构造是从类层次的最根处開始的。在每一层。首先调用基类构造函数。然后调用成员对象的构造函数

"带有一个Virtual Function"的Class

另有两种情况,也须要合成出default constructor:

    1.    class 声明(或继承)一个virtual function

    2.    class 派生自一个继承串链,当中有一个或很多其它的virtual base classes.

    无论哪一种情况,因为缺乏由user声明的constructors,编译器会具体记录合成一个default constructor的必须信息,以以下这个程序片段为例:

class Widget {
public:
virtual void flip() = 0;
// ...
};
void flip(const Widget &widget) {
widget.flip();
}
// 如果Bell和Whistle都派生自Widget
void foo() {
Bell b;
Whistle w;
flip(b);
flip(w);
}

以下两个扩张会在编译期间发生:

    1.    一个virtual function table(在cfront中被称为vtbl)会被编译器产生出来,内放 class 的 virtual functions 地址。

    2.    在每个 class object中。一个额外的pointer member(也就是vptr)会被编译器合成出来,内含相关的 class vtbl的地址。

    此外。widget.flip()的虚拟引发操作(virtual invocation)会被又一次改写。以使用widget的vptr和vtbl中的flip()条目:

// widget.flip()的虚拟引发操作(virtual invocation)的转变
(*widget.vptr[1])(&widget)

当中:

    1表示flip()在virtual table中的固定索引

    &widget代表要交给"被调用的某个flip()函数实体"的this指针。

    为了让这机制发挥功效。编译器必须为每个Widget(或其派生类的)object的vptr设定初始值,放置适当的virtual table地址。对于 class 所定义的每个constructor。编译器会安插一些代码做这种事情(看5.2节)。

对于那些未声明不论什么 constructor的classes。编译器会为它们合成一个default constructor。以便正确地初始化每个 class object的vptr.

"带有一个Virtual Base Class"的Class

Virtual base class 的实现法在不同的编译器之间有极大的差异。然而,每一种实现法的共通点在于必须使virtual base class 在其每个derived class object 中的位置,可以于运行期准备妥当。比如以下这段代码中:

class X {
public:
int i;
};
class A : public virtual X {
public:
int j;
};
class B : public virtual X {
public:
double d;
};
class C : public A, public B {
public:
int k;
};
// 无法在编译时期决定(resolve) pa->X::i的位置
void foo(const A *pa) {
pa->i = 1024;
}
main()
{
foo(new A);
foo(new C);
// ...
}

编译器无法确定foo()中"经由pa而存取的X::i"的实际偏移位置,由于pa的真正类型能够改变。编译器必须改变"运行存取操作"的那些码。使X::i能够延迟至运行期才决定。原先cfront的做法是靠"在derived class object的每个virtual base classes中安插一个指针"完毕。

全部"经由reference或pointer来存取一个virtual
base class"的操作都能够通过相关指针完毕。foo能够被改写例如以下。以符合这种实现策略:

// 可能的编译器转换操作
void foo(const A *pa) {
pa->_vbcX->i = 1024;
}

当中。_vbcX表示编译器所产生的指针。指向virtual base class X.

    _vbcX(或编译器所做出的某个东西)是在 class object 建构期间被完毕的。对于 class 所定义的每个constructor。编译器会安插那些"同意每个virtual base class的运行期存取操作"的码。

假设 class 没有声明不论什么constructor,编译器必须为它合成一个default constructor.

总结

有四种情况。会导致"编译器必须为未声明constructor的classes合成一个default constructor‘",C++ Stardard把那些合成物称为implicit nontrivial default constructor。被合成出来的constructor仅仅能满足编译器(而非程序)的须要。它之所以可以完毕任务。是借着"调用member object或者base class的default constructor"或者"为每个object初始化其virtual function机制或virtual
base class机制"而完毕。至于没有存在那四种情况而又没有声明不论什么constructor的classes,它们拥有的是implicit trivial default constructor,它们实际上并不会被合成出来.

    在合成的default constructor中,仅仅有base class subjects和member class objects会被初始化,全部其他的nonstatic data member,如整数,整数指针,整数数据等都不会被初始化,这些初始化操作对程序而言也许须要,但对编译器则并不是必要.假设程序须要一个"把某指针设为0"的default constructor,那么提供它的应该是程序猿.

    C++新手一般有两个常见的误解:

    1.    不论什么class假设未定义default constructor,就会被合成出来

    2.    编译器合成出来的default constructor会明白设定"class 内有每个data member的默认值"

    这两个都是错误的.

C++对象模型——Default Constructor的建构操作(第二章)的更多相关文章

  1. Default Constructor的构造操作

    Default Constructor的构造操作 C++ Annotated Reference Manual书中的Section 12.1说过:default constructor 只有在编译器需 ...

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

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

  3. 构造函数语义学之Default Constructor构建操作

    一.Default Constructor的构建操作 首先大家要走出两个误区: 1).任何class如果没有定义default constructor,就会被合成一个来. 2).便以其合成出来的def ...

  4. C++对象模型(一):The Semantics of Constructors The Default Constructor (默认构造函数什么时候会被创建出来)

    本文是 Inside The C++ Object Model, Chapter 2的部分读书笔记. C++ Annotated Reference Manual中明确告诉我们: default co ...

  5. Copy Constructor的构造操作

    Copy Constructor的构造操作 有三种情况,会以一个object的内容作为另一个class object的初值: 1.  对一个object做显式的初始化操作 class X{…}; X ...

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

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

  7. Eclipse错误:Implicit super constructor ClassName is undefined for default constructor. Must define an explicit constructor

    public class Test01 { private String name; private int age; public Test01(String name){ this.name = ...

  8. 构造函数语义学——Default Constructor篇

    构造函数语义学--Default Constructor 篇 这一章原书主要分析了:编译器关于对象构造过程的干涉,即在对象构造这个过程中,编译器到底在背后做了什么 这一章的重点在于 default c ...

  9. The Semantics of Constructors: The Default Constructor (默认构造函数什么时候会被创建出来)

    本文是 Inside The C++ Object Model, Chapter 2的部分读书笔记. C++ Annotated Reference Manual中明确告诉我们: default co ...

随机推荐

  1. 剑指Offer(书):链表的倒数第K个节点

    题目:输入一个链表,输出该链表中倒数第k个结点. 分析:要注意三点:链表为空:链表个数小于k:k的值<=0; public ListNode FindKthToTail(ListNode hea ...

  2. linux文本界面../和./的区别

    linux文本界面../和./的区别 ../代表的是上一个目录 ./代表的当前目录

  3. DocView mode 1 -- 手册翻译

    文档原文在线地址 * 35 Document Viewing** DocView mode is a major mode for viewing DVI, PostScript (PS), PDF, ...

  4. 使用Unity做2.5D游戏教程(一)

    最近在研究Unity 3D,看了老外Marin Todorov写的教程很详细,就翻译过来以便自己参考,翻译不好的地方请多包涵. 如果你不了解2.5D游戏是什么,它基本上是个3D游戏而你可以想象是压扁的 ...

  5. BZOJ 4822 [Cqoi2017]老C的任务 ——树状数组

    直接离散化之后用树状数组扫一遍. 把每一个询问拆成四个就可以做了. %Silvernebula 怒写KD-Tree #include <map> #include <cmath> ...

  6. SPOJ GSS2 Can you answer these queries II ——线段树

    [题目分析] 线段树,好强! 首先从左往右依次扫描,线段树维护一下f[].f[i]表示从i到当前位置的和的值. 然后询问按照右端点排序,扫到一个位置,就相当于查询区间历史最值. 关于历史最值问题: 标 ...

  7. POJ 3469 Dual Core CPU ——网络流

    [题目分析] 构造一个最小割的模型. S向每一个点连Ai,每一个点向T连Bi. 对于每一个限制条件,在i和j之间连一条Cij的双向边即可. 然后求出最小割就是最少的花费. 验证最小割的合理性很容易. ...

  8. Snmp的学习总结——Snmp的基本概念

    摘自:http://www.cnblogs.com/xdp-gacl/p/3978825.html 一.SNMP简单概述 1.1.什么是Snmp SNMP是英文"Simple Network ...

  9. codeblocks 中文编码问题

    参考文章: code::blocks 初使用遇到的问题记录 codeblocks 中文编码问题 string var="汉"; cout<<var<<end ...

  10. 家用电脑架服务器提供web

    要搞一个可以对外的web服务,需要服务器,域名.这些都需要money,但有时,我们只是想自己可以在外面访问,或是提供给朋友看自己的网站有多牛.这时使用家用电脑配置一个可以提供web的服务器,就显得很必 ...