C++对象静态绑定与动态绑定

 

先看一个简单的例子,该段测试代码的输出结果是:
hello
段错误

为什么呢?

 

 

 

 

 

上面这段代码,输出为2,暴露了宏函数的一个弊端。所以引入inline.

 

 

1.  编译器在初始化及指定操作之间做出了仲裁。编译器必须确保如果某个object含有一个或一个以上的vptrs,那些vptrs的内容不会被base class object初始化或改变。

 

2. 它们每个都指向Bear Object的第一个byte。其间的差别是,pb所涵盖的地址包含整个Bear object,而pz所涵盖的地址只包含Bear object的 ZooAnimal subobject。 除了 ZooAnimal subobject中出现的members,你不能够使用pz来直接处理Bear的任何members。唯一列外是通过virtual机制。

 

3. 嗯,那么,一个指向地址1000而类型为void*的指针,将涵盖怎样的地址空间呢?是的,我们不知道!这就是为什么一个类型为void*的指针只能够含有一个地址,而不能通过它操作所指之object的缘故。  所以,转型(cast)其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。

 

4.  Default Constructor 的建构操作

class AK {
public:
    AK() { cout << "AK" << endl; }
};

class AC {
public:
    AC() { cout << "AC" << endl; }
};


class BB {
public:
    BB() {
        cout << "BB" << endl;
    }
private:
    AK ak;
};

class BBB : public BB{
    AC ac;
};

int main()
{
    BBB b;
    return 0;
}

输出结果为:
BB
AK
AC

如果设计者提供多个constructors,但其中都没有default constructor呢?编译器会扩张现有的每一个constructors,将“用以调用所有必要之default constructors”的程序代码加进去。它不会合成一个新的default consturctor,这是因为其它“由user所提供的constructors”存在的缘故。如果同时亦存在着“带有default constructors”的member class objects,那些default constructor也会被调用——在所有base class constructor都被调用之后。

5. 有四种情况,会导致“编译器必须为未声明constructor之classes合成一个default constructor”。C++ Stardand把那些合成物称为implicit nontrivial default constructors。 被合成出来的constructor只能满足编译器(而非程序) 的需要。它之所以能够完成任务,是借着“调用member object或base class的 default constructor” 或是 “为每一个 object初始化其 virtual function机制 或 virtual base class机制”而完成。 至于没有存在那四种情况而有没有声明任何constructor 的classes, 我们说它们拥有的是 implicit trivial default constructors, 它们实际上并不会被合成出来。
    在合成的default constructor 中,只有 base classes subobjects 和 member class objects 会被初始化。 所有其它的 nonstatic data member, 如整数、 整数指针、 整数数组等等都不会被初始化。 这些初始化操作对程序而言或许有需要,但对编译器则并非必要。 如果程序需要一个“把某指针设为0” 的default constructor, 那么提供它的人应该是程序员。
    C++新手一般有两个常见的误解:
    1. 任何 class 如果没有定义 default constructor, 就会被合成出一个类。
    2. 编译器合成出来的 default constructor 会明确设定 “class 内每一个 data member的默认值”。
    如你所见,没有一个是真的!

 

6.  有四种情况,一个 class 不展现出 “bitwise copy semantics”。
     1) 当 class 内含一个 member object 而后者的 class 声明有一个copy constructor时(不论是被 class设计者明确地声明,就像前面的 String那样;或是被编译器合成, 像 class Word 那样)。
     2) 当 class 继承自一个 base class 而后者存在有一个 copy constructor 时(再次强调,不论是被明确声明或是被合成而得)。
     3) 当 class 声明了一个或多个  virtual function 时。
     4) 当 class 派生自一个继承串链, 其中有一个 或多个 virtual base classes时。
  前两种情况中,编译器必须将 member 或 base class 的“copy constructors 调用操作” 安插到被合成的 copy constructor中。
  第三种情况,回忆编译期间的两个程序扩张操作(只要有一个class 声明了一个或多个 virtual functions 就会如此):
     增加一个 virtual function table(vtbl), 内含每一个有作用的 virtual function 的地址。
     将一个指向 virtual function table 的指针(vptr),安插在每一个 class object内。
     很显然,如果编译器对于每一个新产生的 class object 的vptr不能成功而正确地设好其初值,将导致可怕的后果。 因此,当编译器导入一个 vptr 到 class之中时, 该 class 就不再展现 bitwise semantics了。现在,编译器需要合成出一个 copy constructor,以求将vptr 适当地初始化。

 

 

7. 处理 Virtual Base Class Subobject

   Virtual base class 的存在需要特别处理。一个 class object如果以另一个object作为初值,而后者有一个 virtual base class subobject, 那么也会使 “bitwise copy semantics” 失效。 每一个编译器对于虚拟继承的支持承诺,都表示让“derived class object 中的 virtual base class subobject 位置”在执行期就准备妥当。 维护“位置的完整性” 是编译器的责任。 “Bitwise copy semantics” 可能会破坏这个位置,所以编译器必须在它自己合成出来的 copy constructor中做出仲裁。

 

8.  Member Initialization List
 

class X{
public:
  int i;
  int j;
public:
  X(int val) : j(val), i(j){}
};

int main()
{
    X x(4);
    cout << x.i << endl;
    return 0;
}

运行结果:
-132453224

list中的项目次序是有class中的members声明次序决定,不是由 initialization list 中的排列次序决定。
initialization list代码优先于 constructor中的代码执行。

 

9. Data语意学 (The Semantics of Data)

class A {};
class B : public virtual A {};
class C : public virtual A {};
class D : public B, public C {};


int main()
{
    cout << sizeof A << endl;
    cout << sizeof B << endl;
    cout << sizeof C << endl;
    cout << sizeof D << endl;
    return 0;
}

输出结果:

1
4
4
8

class A中没有任何成员变量,编译器会安插进去一个char,这使得这个class 的 objects得以在内存中配置独一无二的地址。
class B 和 class C的大小都是4,这个大小和机器有关,也和编译器有关。事实上 B 和 C的大小受到三个因素的影响:
1. 语言本身所造成的额外负担(overhead), 当语言支持 virtual base classes时, 就会导致一些额外负担。在 derived class中,这个额外负担就反映在某种形式的指针身上,它或者指向 virtual base class subobject, 或者指向一个相关表格; 表格中存放的若不是 virtual base class subobject的地址,就是其偏移量(offset)。
2. 编译器对于特殊情况所提供的优化处理。 Virtual base class A subobject 的 1 bytes 大小也出现在 class B 和 C身上。 传统上它被放在derived class 的固定(不变动)部分的尾端。 某些编译器会对 empty virtual base class 提供特殊支持(我的机器上没有)。
3. Alignment的限制, class B 和 C的大小截至目前为 2 bytes。 在大部分机器上, 群聚的结构体大小会受到 alignment 的限制,使它们能够更有效率地在内存中被存取。 在我的机器上, alignment 是 4bytes,所以 class B 和 C必须填补 2 bytes。 最终得到的结果就是 4 bytes。

 

10.  Data Member 的绑定(The Binding of a Data Member)

typedef char length;
class Test {
public:
    void print() { cout << sizeof data << endl; }
private:
    typedef int length;
    length data;
};

int main()
{
    Test t;
    t.print();
    return 0;
}

输出结果:
4

原因: 对 member functions 本身的分析,会直到整个class 的声明都出现了才开始。因此,在一个inline member funciton 躯体之内的一个 data member 绑定操作,会在整个class 声明完成之后才发生。

然而, 这对于 member function 的 argument list 并不为真。 Argument list 中的名称还是会在它们第一次遭遇时被适当地决议完成。因此在 extern 和 nested type names 之间的非直觉绑定操作还是会发生。 例如在下面的程序片段中, length的类型在两个 member function signatures 中都决议为 global typedef, 也就是 int。 当后续再有length 的 nested typedef声明出现时, C++ Standard 就把稍早的绑定标示为非法:

typedef char length;
class Test {
public:
    void print() { cout << sizeof data << endl; }
    void setData(length l) { data = l; }
    length Data() { return data; }
private:
    typedef int length;
    length data;
};

int main()
{
    Test t;
    t.print();
    cout << sizeof t.Data() << endl;
    return 0;
}

输出结果:
4
1

  上述这种语言状况,仍然需要某种防御性程序风格: 请始终把 “nested type 声明” 放在class 的起始处。 在上述例子中, 如果length 的 nested typedef 定义于 “在class中被参考” 之前, 就可以确保非直觉绑定的正确性。

 

11. Data Member 的布局(Data Member Layout)

Nonstatic data members 在 class object 中的排列顺序将和其被声明的顺序一样, 任何中间介入的 static data members 如 freeList 和 chunkSize 都不会被放进对象布局之中。 static data members 存放在程序的 data segment 中, 和个别的 class objects无关。

C++ Standard要求,在同一个 access section (也就是 private、public、protected等区段) 中, members 的排列只需符合“较晚出现的 members 在 class objects 中有较高的地址” 这一条件即可。 也就是说,各个members 并不一定得连续排列。 什么东西可能会介于被声明的 members 之间呢? members 的边界调整(alignment) 可能就需要填补一些 bytes。 对于 C 和 C++而言,这的确是真的, 对当前的 C++编译器实现情况而言, 这也是真的。
编译器还可能会合成一些内部使用的 data members, 以支持整个对象模型, vptr 就是这样的东西, 当前所有的编译器都把它安插在每一个“内含 virtual function 之 class” 的 object 内。 vptr 会被放在什么位置呢?传统上它被放在所有明确声明的 members 的最后。 不过如今也有一些编译器把 vptr 放在一个 class object 的最前端。 C++ Standard 秉持先前所说的 “对于布局所持的放任态度”, 允许编译器把那些内部产生出来的 members自由放在任何位置上, 甚至放在那些被程序员声明出来的 members 之间。
C++ Standard 也允许编译器将多个 access sections 之中的 data members 自由排列, 不必在乎它们出现在 class 声明中的次序。
当前各家编译器都是把一个以上的 access sections 连锁在一起,依照声明的次序,成为一个连续区块。 Access sections 的多寡并不会招来额外负担。 例如在一个 section中声明8个members,或是在8个 sections 中总共声明8 个members,得到的object 大小是一样的。

 

 

12.  Data Member 的存取

    Static data members, 按其字面意义,被编译器提出于 class 之外, 一如我在1.1 节所说, 并被视为一个 global 变量(但只在 class 生命范围之内可见)。 每一个member 的存取许可(private或protected或public), 以及与 class的关联, 并不会导致任何空间上或执行时间上的额外负担——不论是在个别的class objects 或是在 static data member 本身。 每一个 static data member 只有一个实体, 存放在程序的 data segment 之中。 每次程序参阅(取用) static member, 就会被内部转化为对该唯一的 extern 实体的直接参考操作。

Point3D origin, *pt = &origin;
origin.x = 0.0;
pt->x = 0.0;

   “从origin 存取” 和 “从pt 存取” 有什么重大的差异? 答案是“ 当 Point3d是一个 derived class, 而在其继承结构中有一个 virtual base class, 并且被存取的 member(如本例的x) 是一个从该 virtual base class 继承而来的 member 时,就会有重大的差异”。 这时候我们不能够说 pt 必然指向哪一种 class type(因此我们也就不知道编译时这个 member 真正的offset 位置),所以这个存取操作必须延迟至执行期,经由一个额外的间接导引,才能够解决。 但如果使用origin, 就不会有这些问题, 其类型无疑是 Point3d class, 而即使它继承自 virtual base class, members 的offset 位置也在编译时期就固定了。 一个积极进取的编译器甚至可以静态地经由 origin 就解决掉对 x 的存取。

 

#include <iostream>
using namespace std;

class A {
public:
    A(int v):a(v) {}
    virtual void print_v() {
        cout << "class A , a= " << a << endl;
    }
    int a = 0;
};

class B : public A {
public:
    B(int a, int b) :A(a), b(b) {}
    void print_v() {
        cout << "class B , a= " << A::a << ", b=" << b<< endl;
    }
    int b = 0;
};

class C :public B {
public:
    C(int a, int b, int c):B(a, b), c(c) {}
    void print_v() {
        cout << "class C , a= " << A::a << ", b=" << b << ",c = " << c << endl;
    }
    int c = 0;
};

int main()
{
    C c1(1,1,1), c2(2,2,2);
    A *a1 = &c1, *a2 = &c2;
    B *b1 = &c1, *b2 = &c2;

    c1.print_v();
    *a1 = *a2;
    c1.print_v();

    *b1 = *b2;
    c1.print_v();

    c1 = c2;
    c1.print_v();

    return 0;
}

输出结果:
1,1,1
2,1,1
2,2,1
2,2,2

基类指针A* a,不管指向的是基类对象,还是派生类对象,对其进行 解引用 操作,得到的结果都是 基类 对象。

 

 

——————本文引用于《深度探索C++对象模型》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值