深度探索C++对象模型 侯捷

本文深入探讨了C++对象模型,包括C++对象的模式、构造函数的语意学、数据成员的绑定与布局、虚函数的实现、以及执行期的语意学。文章详细阐述了C++中对象的差异,如多态和指针类型的差异,并介绍了构造函数、拷贝构造器的构造过程。此外,还讨论了数据成员的存取、继承与数据成员的关系、函数调用方式以及构造、析构、拷贝语意学。最后,文章触及了执行期类型识别、模板、异常处理等高级主题,揭示了C++对象模型的内在机制。
摘要由CSDN通过智能技术生成

目录

 

什么是C++对象模型

第一章 关于对象

1.1   C++对象模式

1.2   关键词所带来的差异

1.3   对象的差异

多态

指针的类型

第二章   构造函数语意学

2.1  Default Constructor的构造操作

2.2   拷贝构造器的构造操作

2.3   程序转化语意学

2.4  成员们的初始化队伍

第三章   Data语意学

3.1   Data Member的绑定

3.2   Data Member的布局

3.3   Data Member的存取

Static Data Member

Nonstatic Data Members

3.4   “继承”与Data Member

只要继承不要多态

加上多态

多重继承

虚拟继承

3.5   对象成员的效率

3.6   指向Data Member的指针

“指向Member的指针”的效率问题

第4章   Function语意学

4.1   Member的各种调用方式

非静态成员函数

虚拟成员函数

静态成员函数

4.2   虚拟成员函数

多重继承下的虚函数

虚继承下的虚函数

4.4   指向成员函数的指针

第5章   构造、析构、拷贝语意学

1.纯虚函数

5.1   “无继承”情况下的对象构造

5.2   继承体系下的对象构造

虚拟继承

vptr初始化语意学

5.3   对象复制语意学

5.4    对象的效能

5.5   析构语意学

第6章   执行期语意学

6.1   对象的构造和析构

全局对象

局部静态对象

对象数组

Default Constructor和数组

6.2   new和delete运算符

针对数组的new语意

Placement Operator new的语意

6.3   临时性对象

第7章   站在对象模型的尖端

7.1   Template

7.2   Exception handling

7.3   执行期类型识别


什么是C++对象模型

1.语言中直接支持面向对象程序设计的部分。

2.对于各种支持的底层实现机制。

第一章 关于对象

在C语言中,“数据”和“处理数据的操作”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。我们把这种程序方法称为程序性的,由一组“分布在各个以功能为导向的函数中”的算法所驱动,它们处理的是共同的外部数据。

而在C++中,Point3d有可能采用独立的“抽象数据类型(ADT)”来实现

加上封装后的布局成本:主要是由virtual引起的,包括,virtual functionvirtual base class。然而,一般而言,并没有什么天生的理由说C++程序一定比其C兄弟庞大或迟缓。

1.1   C++对象模式

在C++中有两种类数据成员:static 和 nonstatic。以及三种类成员函数:static、nonstatic和virtual。

简单对象模型

一个对象是一系列的slots,每一个slot指向一个成员。成员按其声明顺序,各被指定一个slot。

表格驱动对象模型

为了统一,另一种对象模型是将与成员相关的信息抽出来,一个放入数据成员表中,一个放入成员函数表中,类对象本身则内含指向这两个表格的指针即可。然后二者分别都是一系列的slots,成员函数表中每一个slot指向一个成员函数,而数据成员表则直接持有数据本身。

C++对象模型

非静态数据成员被配置于每一个类对象之内,而静态数据成员、静态和非静态成员函数都被放在个别的类对象之外。虚函数则以两个步骤支持之:每个类产生出一个虚表;每个类对象被安插一个指针指向虚表。(每一个类所关联的type_info object也经由虚表被指出,通常放在表格的第一个slot。)

该模型的优点在于空间和存取时间的效率,主要缺点在于非静态数据成员修改时必须重新编译。

对象模型如何影响程序?

暂时不太懂,总之就是编译器构造和析构的时候,内部会发生一系列的转化,而采用不同的对象模型,转化的效率应该是不一样的。

 

1.2   关键词所带来的差异

观念上,在C语言中,struct代表的是一个数据集合体,因此它没有private data,member function;而在C++中,二者均是代表类,所不同的仅在默认访问权限和默认继承类型不同罢了。

因为template与C不兼容,因此以下代码不合法:

template <struct Type>
struct mumble{...};

C struct 在C++中的一个合理的用途,是当你要传递“一个复杂的class object的全部或部分”到某个C函数去时,struct声明可以将数据封装起来,并保证拥有与C兼容的空间布局。

1.3   对象的差异

C++程序设计模型直接支持三种程序设计范式:

       1.程序模型:即来自C语言的部分;

       2.抽象数据类型模型:即封装与抽象。此模型所谓的“抽象”是和一组表达式(public接口)一起提供的。

       3.面向对象模型:定义基类并派生出子类。

只有通过指针和引用的间接处理,才支持OO程序设计所需的多态性质。

多态

C++以下列方法支持多态:

1.隐含的转化操作:把派生类指针转化为一个指向其基类的指针:shape *ps=new circle();

2.经由虚函数机制:ps->rotate();动态绑定

3.dynamic_cast和typeid运算符,大致算作强制类型转换吧。

 

那么,需要多少内存才能表现一个类对象呢?

  • 其非静态数据成员的总和大小;
  • 加上由alignment的需求而填补上去的空间;(将数值调整到某数的倍数,如32位计算机上通常为4bytes)。
  • 加上为了支持virtual而产生的负担(例如:指向虚函数表的指针的大小)。

指针的类型

这里最重要的是想说,指针类型所带来的影响在于这个指针涵盖的范围是不一样的!!!

一个指向地址1000而类型为void*的指针,由于我们不知道它将涵盖怎样的地址空间,因此不能通过它操作所指之object。

所以cast其实是一种编译器指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。

一个基类指针和一个派生类指针都指向派生类对象的第一个byte,但它们的差别是,派生类指针涵盖整个派生类对象,而基类指针只包含派生类对象的基类部分。

一个指针或一个引用之所以支持多态,是因为它们并不引发内存中任何“与类型有关的内存委托操作”:会受到改变的,只有它们所指向的内存的“大小和内容解释方式”而已。

(自己的理解:比如基类指针和派生类指针都指向的是派生类对象,只不过涵盖范围不同,并没有涉及到类型的转换。)

第二章   构造函数语意学

2.1  Default Constructor的构造操作

default constructor在需要的时候被编译器产生出来,而不是程序需要,差别在于是程序需要还是编译器需要。

//默认构造函数测试
#include<iostream>
using namespace std;
class Foo
{
public:
    int val;
    Foo *ptr;
};
 
int main()
{
    Foo foo;
    if(foo.val || foo.ptr)
        cout<<"参数没有初始化"<<endl;
    else
        cout<<"参数初始为0"<<endl;
    return 0;
}       

1.带有“Default Constructor”的Member Class Object一个类的成员变量为一个对象类型且这个成员对象含有默认的构造函数

一个类没有构造器,但是它的类对象含有默认构造器,那么这个类的隐式默认构造器就是有用的。不过合成的操作只有真正在需要被调用时才会发生。

当default constructor已经被显式定义出来了,编译器没办法合成第二个,但是编译器还需要完成它的部分,那它会怎么办呢?

即:如果class A内含一个或一个以上的member class objects,那么class A的每个构造器必须调用每一个member class的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得用户代码被执行之前,先调用必要的default constructor。

比如:

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

编译器还要初始化它内含的那个类对象的部分,因此将其拓展为:

//C++伪码
Bar::Bar(){
    foo.Foo.Foo();//附加上的comiler code
    str=0;        //显式的用户代码
}

2.一个类的基类带有默认的构造函数。

3.如果一个class它自己或者是他继承串链中的其中一个父类含有virtual函数

                      一个虚函数表vtbl会被编译器产生出来,内放class的虚函数地址;

                      在每一个类对象中,一个额外的pointer member(vptr)会被编译器合成出来,内含相关之类的vtbl的地址。

4.带有一个虚基类的类

           共同点在于必须使虚基类在其每一个派生类中的位置能于执行期准备妥当。需要编译器为这个类的对象合成一个虚基类指针,执行它的虚基类。

具体参见:http://blog.csdn.net/bluedog/article/details/4711169

总结:上面的4种情况,会使得编译器为没有声明构造函数的类合成一个默认的构造函数。在c++标准中把这些合成物称之为隐式有效的默认构造函数。但是被合成的默认构造函数只满足编译器的需要,并不能满足程序员的需要。至于除了这4中情况之外而又没有声明任何构造函数的class,在本书中说它们拥有的是一个隐式的无效的默认构造函数,实际上编译器不会去合成它们。

2.2   拷贝构造器的构造操作

三种情况会使用到拷贝构造器,以一个object的内容作为另一个类对象的初值:对一个object用另一个对象初始化;作为参数传递给某个函数;函数返回一个类对象时。

  • 默认成员初始化

当类没有提供一个显式的拷贝构造器时,拷贝类时内部会以对其内建或派生的data member逐一拷贝,而不拷贝其中的成员类对象。

  • 位逐次拷贝(Bitwise Copy Semantics)

什么时候一个class不展现出“bitwise copy semantics”呢?就需要编译器合成默认拷贝构造器了,有4种情况:

1.当class内含一个类对象而后者的类声明中有一个拷贝构造器。

2.当class继承自一个基类而后者存在一个拷贝构造器时。

3.当class声明了一个或多个虚函数时。

4.当class派生自一个继承串链,其中有一个或多个虚基类时。

  • 重新设定virtual Table的指针

编译期间的两个扩张操作(只要有一个类声明了一个或多个virtual functions就会如此):

1.增加一个虚函数表(vtbl),内含每一个有作用的虚函数。

2.一个指向虚函数表的指针(vptr),安插在每一个类对象内。

当一基类对象用一个派生类对象初始化时,其vptr操作也必须保证安全:

不可以直接将派生类对象的vptr拷贝给基类对象,这样它将指向派生类的虚函数表,而事实上它在初始化时已经将派生类的部分切割掉了。也就是说,合成出来的基类拷贝构造器会显式设定对象的vptr指向基类的虚函数表,而不是直接拷贝派生类的vptr。

2.3   程序转化语意学

  • 显式的初始化操作

1.重写每一个定义,其中的初始化操作会被剥除。

2.class的拷贝构造器调用操作会被安插进去。

  • 参数的初始化

函数参数是一个对象时,编译器实现技术上一般采用两个策略

1.导入所谓的临时性对象,并调用拷贝构造器将它初始化,然后将此临时性对象交给函数。但这样没有释放这个临时对象,因此将函数形参从原先的一个对象改变为一个类引用。

2,拷贝建构方式把实际参数直接建构在其应该的位置上。

  • 返回值的初始化

首先加入一个额外参数,然后在return指令之前安插一个拷贝构造器调用操作,以便将欲传回之对象的内容当作上述新增参数的初值。

  • 在使用者层面做优化

定义另一个“计算用”的构造器,可以直接计算出xx的值。

  • 在编译器层面做优化

NRV优化,以result参数取代named return value。直接处理_result

  • Copy Constructor:要还是不要?

copy constructor的应用,迫使编译器多多少少对程序代码做部分转化,尤其是当一个函数以传值的方式返回一个类对象,而该类有一个copy constructor时,这将导致深奥的程序转化——不论是在定义上的还是使用上。

  • 8
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值