【深度探索C++对象模型】第一章 关于对象

第一章 关于对象(Object Lessons)

—— 本书作者:Stanley B.Lippman

一、前言

什么是 C++ 对象模型:简单的说,就是 C++ 中面向对象的底层实现机制。

本书组织:

第 1 章,关于对象(Object Lessons),介绍 C++ 对象的基础概念,给读者一个粗略的了解。

第 2 章,构造函数语意学(The Semantics of Constructors),构造函数什么时候会被编译器合成?它给我们的程序效率带来了怎样的影响?

第 3 章,Data语意学(The Semantics of Data),讨论 data members 的处理。

第 4 章,Function语意学(The Semantics of Function),讨论类的各种成员函数,特别是 Virtual 。

第 5 章,构造、析构、拷贝语意学(Semantics of Construction, Destruction, and Copy),探讨如何支持 class 对象模型,以及 object 的生命周期。

第 6 章,执行期语意学(Runtime Semantics),临时对象的生与死,new 与 delete 的支持。

第 7 章,在对象模型的顶端(On the Cusp of the Object Model),专注于 exception handling, template support, runtime type identification(RTTI)。

读完此书,或者此系列blogs,会让你对 C++ 的 class 有更深的了解。你将知道虚函数的实现方式,以及它所带来的负担。等等等等,这里有你想知道关于 class 的一切。

在 C 语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。我们把这种程序方法称为“程序性的”。例如,我们声明一个 struct Point3d:


typedef struct _Point3d

{

float x;

float y;

float z;

} Point3d;

欲打印一个 Point3D,我们可能需要这样一个函数:


void Point3d_print( const Point3d* pd )

{

printf("(%g, %g, %g)", pd->x, pd->y, pd->z);

}

//%g和%G是实数的输出格式符号。它是自动选择%f和%e两种格式中较短的格式输出,并且不输出数字后面没有意义的零。

在 C++ 中,你可能会这样来设计一个双层或者三层的Point3D:


class Point

{

public:

Point( float x = 0.0 ) : _x(x) {}

float x() { return _x; }

void x( float val ) { _x = val; }

// ...

protected:

float _x;

};

class Point2d : public Point

{

public:

Point2d( float x = 0.0, float y = 0.0 ) : Point( x ), _y( y ) {}

// ...

protected:

float _y;

}

class Point3d : public Point2d

{

public:

Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) {}

// ...

protected:

float _z;

}

从软件工程的眼光来看,面向对象的特征,使得 C++ 比 C 看起来似乎更好,C 相对而言,更精瘦和简易,C++ 看起来似乎更复杂,但并不意味着 C++ 不更有威力。

当一个 Point3d 转换到 C++ 之后,第一个可能会问的问题是:加上了封装之后,布局成本增加了多少呢?答案是: class Point3d 并没有增加成本。三个 data members 直接内涵在每一个 class Object 之中,而 成员函数(member functions)虽然在 class 的声明之内,但却不会出现在 class 的对象实体(Object)中。每一个非 inline member function 只会诞生一个函数实体。而
inline function,会在其每一个使用者身上产生一个函数实体。后面你将看到,C++ 在布局和存取时间上主要的负担 是由 virtual 引起的。包括 虚函数 以及 虚基类。

二、C++ 的对象模型

首先,C++ 中,


    2种成员变量(class data members):静态的(static) 和 非静态的(non-static);

3种成员函数(class member functions):静态的、非静态的 和 虚拟的(virtual)。

我们来看这么一个类:


class Point

{

public:

Point( float valx );

virtual ~Point();

float x() const;

static int PointCount();

protected:

virtual ostream& print( ostream &os ) const;

float _x;

static int _point_count;

};

那这个 class Point 在机器中将会被怎么表示呢?这有没有引起你的求知欲?

【注】原书这里介绍了 简单对象模型 和 表格驱动的对象模型 。这里跳过这两个,直接看 C++ 对象模型。

在 C++ 对象模型中,

    非静态的(non-static)成员变量 被配置于每一个 class object 之内;

    静态的(static)成员变量 则被存放在所有 class object 之外,也就是全局数据区。(问:如果是这样,我们的 class 怎么样去全局数据区找到属于它的 static 成员变量?别急,后面会有答案)。

静态和非静态的成员函数,也被配置于 每一个 class 的实体之外。

虚函数的配置方法是:

        1. 每一个 class 产生出一堆指向 virtual functions 的指针,并把这些指针放在表格之中。这个表,既是所谓的 虚函数表(virtual table), 或 vtbl

2. 每一个 class 的实体(object) 被添加了一个指针,指向相关的(注意不一定是同一个) virtual table。通常这个指针被称为 vptr。vptr 的设定和重置都有每一个 class 的 构造函数、析构函数、拷贝以及复制运算符。每一个 class 所关联的 type_info object( 用以支持 runtime type identification,
RTTI )也经由 virtual table 被指出来,通常是放在表格的第一个 slot 处。

三、C++ 如何支持多态

1. 经由一组隐含的转化操作。例如,把一个 派生类 的指针转化为一个指向其 public base type 的指针:

shape* ps = new circle();

2. 经由 virtual functions 机制:

ps->rotate();

3. 经由 dynamic_cast 和 typeid 运算符:

if ( circle *pc = dynamic_cast< circle* >(ps) )...

多态的主要用途,是经由一个共同的接口,来影响类型的封装,我们通常会把这个接口定义在一个抽象基类里面,然后再在派生类里重写这个接口。

四、需要多少内存来表现一个 class object?

猜想下面的代码的 sizeof 结果会是?

.eg.1.

class Base

{

public:

Base();

~Base();

};

// sizeof(Base) = ?

.eg.2.


class Base

{

public:

Base();

~Base();

protected:

double m_Double;

int m_Int;

char m_BaseName;

};

// sizeof(Base) = ?

究竟需要多少内存,才能表现一个 class 的 object 呢?一般而言有:

1. 其 非静态的成员变量( non-static data members ) 的总和大小。

    2. 加上任何由于 内存对齐 的需求而填补上去的控件。

    3. 加上为了支持 virtual 而由内部产生的任何额外的负担。

此外,需要注意的是,一个指针(或是一个 reference),不管它只想哪一种数据类型,指针本身所需内存大小是固定的。比如,在 win32下,一个指针的大小就是4个字节(byte)。

问题的答案:


第一题: 答案是1。

class Base 里只有构造函数和析构函数,由前面的内容所知,class 的 member functions 并不存放在 class 以及其实例内,因此,sizeof(Base) = 1。是的,结构不是0,而是1,原因是因为,class 的不同实例,在内存中的地址各不相同,一个字节只是作为占位符,用来给不同的实例分配不同的内存地址的。

第二题:答案是16。

double 类型的变量占用8个字节,int 占了4个字节,char 只占一个字节,但这里它会按 int 进行对齐,Base 内部会填补3个字节的内存大小。最后的大小就是 8 + 4 + 1 + 3 = 16。

大家可以调整三个成员变量的位置,看看结果会有什么不同。

五、指针的类型


Base* p_Base;

int*    p_Int;

vector<string> * p_vs;

请问,一个指向 Base class 的指针和一个指向 int 的指针是如何产生不同的呢?

1. 以内存需求的观点来说,没有不同;在32位机器上,它们都需要4个字节的内存空间。

2. “指向不同内存的各指针”间的差异,在于其所寻址出来的 object 的类型不同。

也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其涵盖大小。比如:一个指向 int 的指针,假设其地址是 1000,在32位及其上,将涵盖地址空间 1000~1003.

那么,一个指向地址 1000 的 void* 的指针,将涵盖怎样的地址空间呢?没错,我们并不知道!这就是为什么一个类型为 void* 的指针,只能够含有一个地址,而不能够通过它操作所指的 object 的缘故。

所以,转型(cast)其实是一种编译器指令,它所做的,并不是改变指针所含的真正地址,而是教导编译器该去如何解释指针所涵盖的地址空间。

六、小结

第一章——关于对象。本章初步介绍了C++的对象模型是怎样的,后面的章节将继续讨论这个对象模型的底层实现机制。

在读完本篇文章之后,你应该理解:

  • 如何计算 sizeof(classA) 的大小;
  • 了解 class 的内存布局。

在下一章——构造函数语意学中,我们将了解关于类的构造函数的更多知识。

【深度探索C++对象模型】第一章 关于对象

时间: 2024-11-04 07:28:00

【深度探索C++对象模型】第一章 关于对象的相关文章

深度探索c++对象模型 第一章

1,声明与定义. //声明式如下: extern int x;   //对象式(变量式)声明 std::size_t numDigits(int number);  //函数式声明 class Widget;    //类声明 template<typename T>    //模板类声明 class GraphNode; //定义式如下: int x;    //对象的定义 std::size_t numDigits(int number)    //函数的定义 { ... } class

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

在使用C++时,常常会好奇或者抱怨,编译器为我们做了什么事呢? 为什么构造函数没有为我初始化呢?为什么我还要写默认构造函数呢? 2.1 Default Constructor 的构造操作 如果没有声明默认构造函数,编译器会在需要的时候帮我们产生出来. 为了避免在多个地方被需要导致重复,则编译器将产生的构造函数声明为inline方式. class Foo {public:Foo(), Foo(int) }; class Bar {public: Foo foo;char *str;} Bar ba

《深度探索c++对象模型》chapter1关于对象对象模型

在c++中,有2种class data member:static和nostatic,以及3钟class member function:static,nostatic和virtual.已知下面这个class Point声明: class Point { public: Point(float xval); virtual ~Point(); float x() const; static int PointCount(); protected: virtual ostream& print(o

【C++】深度探索C++对象模型读书笔记--关于对象(Object Lessons)

前言中的内容: 1.什么是C++对象模型? 1.语言中直接支持面向对象程序设计的部分 2. 对于各种支持的底层实现机制 2. C++ class的完整virtual functions在编译时期就固定下来了,程序员没有办法再执行器动态增加或取代其中一个.这使得虚拟调用操作得以快速地派送结果,付出的成本则是执行期的弹性. 3. 全局对象在main()函数之前便完成初始化. 第一章 关于对象 1. 在C++中,有两种class data members:static 和 nonstatic,以及三种

《Android深度探索卷一》第一章Android系统移植与驱动开发概述。

<Android深度探索>第一章讲的是Android系统移植与驱动开发概述.这一章节将对Android以及Linux驱动做一个总体的介绍. Android是一个非常优秀的的嵌入式操作系统.经过几年的发展和演进,Android已经形成了非常完善的系统架构.Android的系统架构分为四层,如下图 Android移植可分为两部分:应用移植和系统移植. Android系统移植是指让Android操作系统在某一个特定硬件平台上运行.使一个操作系统在特定硬件平台上运行的一个首要条件就是该操作系统支持硬件

Android深度探索读书笔记 第一章

第一章首先介绍android系统架构(android是一个非常优秀的嵌入式操作系统),总共分为四层:第一层Linux内核,由于android是基于Linux内核的,所以android跟其他Linux系统没什么差异.这一层这一层主要包括Linux的驱动程序以及内存管理.进程管理.电源管理等.第二层编写的代码库,也包括Dalivk虚拟机的运行时.第三层android SDK 层.第四层应用程序(相当于android的UI).而这一层主要靠第三层中的Android SDK API 完成各种功能.其次介

Android深度探索-卷1第一章心得体会

本章介绍了安卓系统移植与驱动开发的概述,安卓的系统架构有四层:1 Linux内核,2 c/c++代码库, 3 Android SDK API, 4 应用程序 在读的过程中看到了专业名词,查了查,长点见识 API(Application Programming Interface,应用程序编程接口)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节.安卓在移植的时候很大程度上市Linux内核的移植,安卓版本的不同是L

Android深度探索-卷1 第一章

Android是一个非常优秀的嵌入式操作系统,android的系统架构分为4层,第一层为Linux内核,这一层主要包括Linux驱动程序以及内存管理.进程管理.电源管理的等程序.并且不同的Android的版本的驱动可能并不通用.第二层为C/C++代码库,这一层主要包括使用C/C++编写的代码库(Linux下的.so文件),也包括.Dalivak虚拟机的运行时(Runtime).第三层为Android SDK API,这一层也可称java API,这一层是用JAVA编写的各种Library,这些l

【深度探索C++对象模型】第二章 构造函数语意学(上)

第二章 构造函数语意学(The Semantics of Constructors) -- 本书作者:Stanley B.Lippman 一.前言 首先让我们来梳理一个概念: 默认构造函数(Default Constructor) : 是在没有显示提供初始化式时调用的构造函数.它由不带任何参数的构造函数,或是为所有形参提供默认实参的构造函数定义.如果定义的某个类的成员变量没有提供显示的初始化式时,就会调用默认构造函数(Default Contructor). 如果用户的类里面,没有显示的定义任何