第三章Data语义学(The Semantics of Data)—— 数据成员的绑定、布局、存储

当X,Y,Z,A中没有任何一个class内有明显的数据,仅仅表示继承的关系。 

class X{};
class Y :public virtual X {};
class Z :public virtual X {};
class A :public Y, public Z {};

X,Y,Z,A的大小是多少呢?在本机上的实验结果。

为什么空class的大小是1呢?

其实它隐藏了1byte大小,被编译器安插进去了一个char,这使得class的两个objects在内存中配置独一无二的地址:

X a, b;
if (&a != &b)
	cout << "unique address!" << endl;

 

至于Y和Z的大小都是4(在我的机器上是4),这个大小和机器有关,也和编译器有关。事实上Y和Z的大小受到三个因素的影响:

1.语言本身所造成的额外负担(overhead)。当语言支持虚基类(virtual base classes),就会导致一些额外的负担。在继承类(derived class)中,这个额外负担反映在某种形式的指针上。它或者指向虚基类subobject,或者指向一个相关表格;表格中存放虚基类subobject的地址,或者是其偏移位置(offset)。在我的机器上是4bytes。

【subobject是实例对象中继承自基类A的部分就是类B的基类子对象了,例子中a就是类B的subobject】

class A{
public:
    int a;
}
class B:virtual public A{
public:
    int b;
}

2.编译器对于特殊情况所提供的优化处理。虚基类X subobject的1bytes大小同样出现在class Y和Z身上。某些编译器对empty virtual base class 提供特殊支持(不包含这1bytes大小),我的编译器提供这项特殊处理。所以本来是1byte + 4byte 存储大小会因为对齐变成8bytes,但是却因为优化,成为4bytes。

3.Allgnment(对齐)的限制。class Y 和 Z 如果在不支持优化处理的编译器下,内存大小4bytes + 1bytes,在大部分机器上大小会收到alignment的限制,使得它们能够更有效的在内存中存取,如果没有优化的编译器,必须填补3bytes,最终结果就是8bytes。

NOTE:alignment就是将数值调整到某数的整数倍。在32位机器上,通常alignment为4bytes,使得bus的“运输量”达到最高效率

Empty虚基类已经成C++ OO设计的特有属于,提供一个虚接口,没有任何数据定义。某些新的编译器对此提供特殊处理,在此策略下,一个empty虚基类被视为继承类对象(derived class object)最开头的部分,也就是说它没有花费任何的额外空间,这样就节省了上述第二点的1bytes。

c++对class X Y Z的对象布局:

                                                         没有特殊处理的对象布局

                                                        有特殊处理的编译器的对象布局

class A的大小是多少呢?答案是8。

一个虚基类subobject只会在继承类中保存一份实例,不管它在class继承体系中出现了多少次!class A的大小由以下几点决定:

  • 被大家共享的唯一一个class X实例,大小为1byte。(和我机器上的实际情况不符---没搞懂)
  • Base class Y大小,减去“虚基类X而配置的1byte”的大小,结果是4bytes,Base class Z也是,一共8bytes
  • class A自己的大小:0 byte
  • class A的alignment,此时正好8byte,不用对齐。

NOTE:如果我们在虚基类X中放置一个(及以上)的数据成员,两种编译器就会产生出完全相同的对象布局。

第三章类的数据成员(data members)和类的层次结构(class hierarchy)是中心议题。

Nonstatic data members放置的是“个别的class object”感兴趣的数据;static data members则放置的是“整个class”感兴趣的数据

c++对象模型把数据直接存放在每一个类对象之中,对于继承(无论是虚继承还是非虚继承)而来的非静态数据成员也是一样。并没有定义其间的排列顺序。至于静态数据成员,则被放置在程序的一个全局数据段(??)(global data segment)中,不会影响个别的类对象大小。在程序中,不管类产生多少个对象,静态数据成员永远只有一份实例(即使该类没有任何对象实例,其静态数据成员也是存在)。但是一个模板类的静态数据成员行为稍有不同。

每一个类对象的大小有时候比你想象的要大,原因是:

  1. 由编译器自动加上额外数据成员,用以支持某些语言特性(主要是各种virtual特性)
  2. alignment(边界调整)的需要

 

3.1data member的绑定(The Binding of a Data Member)

类的内部和外部都定义了一个testVal ,如果返回的话肯定是返回class内部的那个,结果为3。

int testVal = 5;
typedef int length;
class Test {
public:
	void test() { cout<<testVal<<endl; }
};
void main() {
	Test t;
	t.test();
}

在早期c++编译器上,如果对testVal 做出取用操作,会返回那个全局的testVal ,这样的绑定时不在大家预期之中的!因此导出早期C++的防御性程序设计风格:

1.把所有数据成员都放在class声明起头出,以保证正确的绑定:

int testVal=5;
class Test {
public:
    int testVal=3;
    void test() { cout<<testVal<<endl; }
};

2.把所有的内敛函数,不管大小都放在class声明之外(就是把内敛函数都在类外定义,这样不是速度就降低了很多么!):

class Test {
public:
    int testVal =3;
    void test();
};
void Test::test(){
    cout<<testVal<<endl;
}

C++ Standard以“member scope resolution rules”来精炼这个“rewriteing rule”,其效果是,如果一个内敛函数在类声明之后立刻被定义,那么就还是对其评估求值(evaluate)。(真是读不懂这句话,但是知识点就是以下两个)

1.如果是在函数体内的数据绑定就是正常的绑定

2.如果是在函数的参数列表中还是会出现问题

通过一个简单的例子就能说明,因为如果对一个成员函数本体分析(就是执行函数体内部的语句),这个操作是在整个类的声明之后才开始的,所以test函数返回的testVal,因为整个类声明完了,所有的成员类型都知道了,这个时候已经由类中的testVal替代了全局的testVal,所以输出结果:3;然而对于一个成员函数的函数列表这样就会出现问题,因为在查看类声明的时候,会检查每个成员函数的返回类型,函数的参数列表,成员变量的类型(就是除了函数体内部不查其他都知道了),所以test2函数时,length类型是int型,输出结果是3,test3成员函数时,由于已经看到length类型定义成double,所以返回值是3.5。

int testVal = 5;
typedef int length;
class Test {
public:
	void test() { cout<<testVal<<endl; }
	void test2(length x) { cout << x << endl; }
	typedef double length;
	void test3(length x) { cout << x << endl; }
	int testVal = 3;
};
void main() {
	Test t;
	t.test();
	t.test2(3.5);
	t.test3(3.5);
}

改正方法:采用防御式编程,直接把length类型声明提到class起始处。

 

3.2data member的布局( Data Member Layout)

已知一下一组数据成员:

class M {
public:
	int a=1;
	int b = 2;
	int c = 3;
	static const int val = 5;
	int d = 4;
	static const int val2 = 5;
	int e = 5;
}

非静态成员在类对象中排列顺序和其被声明的顺序一样,任何中间接入的静态数据成员都会不会放进对象布局之中。静态数据成员存放在程序的数据段之中,和每个类对象无关。

【数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域】

C++标准要求,在同一个访问块(也就是private,public,protected等区段),成员的排列只需要符合“较晚出现的成员在类对象中有较高的地址”即可。这也就是表明成员的存储地址并不是连续排列,什么东西可以介于成员之间呢?答案是成员的边界调整(alignment)可能需要填补一些bytes。

编译器还可能会合成一些内部使用的数据成员来支持整个对象模型,如vptr。目前所有的编译器都把它安插在每一个“内涵虚函数的类”的对象中。vptr会被放在什么位置呢?传统上它放在所有显示声明的成员最后。如今,也有一些编译器把vptr放在类对象的最前端。C++标准秉承“对布局所持放任态度”,允许编译器把那些内部产生的成员自由放在任何位置上,甚至放在程序员声明出来的成员之间。

C++标准也允许编译器将多个访问块(access sections)之间的数据成员自由排列而不必在乎它们class声明的顺序,编译器可以随意把b,c放在a前面,但是没有编译器会这样做,还是按序的排列,如下:

class M {
public:
	int a=1;
protected:
	int b = 2;
	int c = 3;
private:
	int d = 4;
	int e = 5;
}

目前各家编译器都是把一个以上的访问块(access sections)连锁在一起,依照声明的顺序成为一个连续区块。访问块(access sections)的多寡不会带来额外附带,一个块8个变量和八个块1个变量得到的对象大小是一致的。

3.3data member的存取 

point3d origin;
origin.x = 0.0;

你可能会问x的存取成本是什么?答案视x和point3d如何声明而定。

  • x可能是静态成员;也可能是非静态成员。
  • point3d可能是独立(非派生类);也可能是单一基类的派生类;甚至是多重继承或虚继承的。

如果我们定义一个对象和一个指针,通过成员运算符"."和指针运算符“->”,两种存取方式差异大吗?

point3d origin,*pt = &origin;

origin.x = 0.0;
pt->x =0.0;

答案是在本节最后揭晓。

 

静态数据成员(Static Data Members)

静态数据成员,被编译器提出与class之外(???啥意思),并视为一个global变量(只在class声明范围之内课件)。每个成员的存取许可(private,protected或public),以及与类的关联(???),并不会招致空间或者时间的额外负担——无论是每个类对象还是静态数据成员本身。

每一个静态数据成员只有一个实例,存放在程序的数据段之中。每次程序取用静态成员时,就会被内部转换为对该唯一extern实例的直接参考操作(?不理解),简单的例子:

original.chunkSize = 250;
//equals to 
//Point3d::chunkSize  =250;

pt->chunkSize = 250;
//equals to
//Point3d::chunkSize  =250;

也就是说:虽然静态成员变量不在某一个具体的类对象之中,但是我们仍然可以通过成员运算符"."和指针运算符“->”来存取。存取静态成员并不需要通过类对象。

即使chunkSize 是从一个非常复杂的继承关系中继承而来的成员,或者它是一个虚基类的虚基类(virtual base class 的virtual base class)(或者是其他非常复杂的继承结构)的成员。也无关紧要,因为程序中静态成员只有唯一的实例,而其存取路径仍然是那么直接。

【静态成员怎么由函数调用和某些语法获取?没懂】

若静态成员时经函数调用,或者其他某些语法获取?

foobar().chunkSize = 250;

C++标准明确要求foobar()必须被求值(evaluated),虽然其结果并无用处。下面是一种可能的转化:

//foobar().chunkSize = 250;

//evaluate expression,discarding result
(void) foobar();
Point3d.chunkSize = 250;

若取一个静态数据成员的地址,会得到一个指向其数据类型的指针,而不是一个指向其类成员的指针,因为静态成员并不在某一个类对象之中。

若有两个类,每个类都声明一个静态成员变量freeList,那么当他们都放在程序的数据段时,就会导致名称冲突。编译器的解决方法是暗中对每一个静态数据成员编码(name-mangling),以获得一个独一无二的程序识别代码。

 

class object 翻译成类对象好像不对,subobject 翻译成基类中有的那部分。感觉这翻译错了,下面还是再用英文表述。

非静态数据成员(Nonstatic Data Members)

非静态数据成员直接存放在每一个类对象中,除非由显示(explicit)或者隐式的(implicit)类对象,都这无法直接存取它们。

只要程序员在一个成员函数中直接处理一个非静态数据成员,所谓的“implicit”类对象就会发生。例如如下代码:

Point3d
Point3d::translate(const Point3d &pt){
    x += pt.x;
    y += pt.y;
    z += pt.z;
}

表面是对x、y、z直接存取,事实上是经由一个“implicit”类对象(由this指针表达)完成的,事实上这个函数参数是:

//成员函数的内部转化
Point3d
Point3d::translate(Point3d* const this,const Point3d &pt){
    this->x += pt.x;
    this->y += pt.y;
    this->z += pt.z;
}

对一个非静态数据成员进行存取操作,编译器要把class object的起始地址加上数据成员的偏移位置(offset)。举个例子,如:

origin._y = 0.0;

那么&origin._y将等于:

&origin._y + (&Point3d::_y-1);

注意-1操作。指向数据成员的指针,其offset值总是被加上1,使得编译系统分出

“一个指向数据成员的指针,用以指出类的第一个成员”

“一个指向数据成员的指针,没有指出任何成员”

每一个非静态数据成员的偏移位置在编译时期即可获知,甚至如果成员属于一个基类的部分(base class subobject)(派生自单一或多重继承串链)也是一样。(???不明白)。因此存取一个非静态数据成员,其效率和存取一个C 结构体成员或者一个非继承类的成员时一样的。

 

让我们看一看虚拟继承。虚拟继承将为“经由base class subobject(基类中的部分)”来存取类成员导入一层新的间接性,比如:

point3d *pt3d;
pt3d->_x = 0.0;

其执行效率在_x是一个结构体成员,一个类成员,单一继承、多重继承的情况下都完全相同。但如果_x是一个虚基类的成员,存取速度会稍慢一点。

 

如果我们定义一个对象和一个指针,通过成员运算符"."和指针运算符“->”,两种存取方式差异大吗?

point3d origin,*pt = &origin;

origin.x = 0.0;
pt->x =0.0;

答案是:当 point3d 是一个继承类,其继承结构有一个虚基类,并且存取的成员(如本例中的x)是一个从该虚基类继承而来的成员时,就会有重大的差异。此时我们不能说pt必然指向哪一种类类型(因此,我们也就不知道编译时期这个成员这正的偏移位置),所以这个存取操作必须延迟至执行期,经过一个额外的间接导引才能解决。如果使用origin就不会有这些问题,其类型无疑是point3d类,而即使它继承自虚基类,成员的偏移位置也在编译时期就固定了。编译器甚至可以静态地由origin就解决对x的存取。

思考:感觉这就是由于多态“便利”引起的,当使用指针来调用函数的时候。只有在程序运行时才知道是调用子类的函数还是基类的函数。如果使用引用也会有这种原因吧!

 

[编辑本段]Turbo C2.0    介绍      Turbo C2.0不仅是一个快捷、高效的编译程序,同时还有一个易学、易用的集成开发环境。使用Turbo C2.0无需独立地编辑、编译和连接程序,就能建立并运行C语言程序。因为这些功能都组合在Turbo 2.0的集成开发环境内,并且可以通过一个简单的主屏幕使用这些功能。    基本配置要求   Turbo C 2.0可运行于IBM-PC系列微机,包括XT,AT及IBM 兼容机。此时要求DOS2.0或更高版本支持,并至少需要448K的RAM,可在任何彩、单色80列监视器上运行。支持数学协处理器芯片,也可进行浮点仿真,这将加快程序的执行。 [编辑本段]Turbo C 2.0的主要文件的简单介绍   INSTALL.EXE 安装程序文件   TC.EXE 集成编译   TCINST.EXE 集成开发环境的配置设置程序   TCHELP.TCH 帮助文件   THELP.COM 读取TCHELP.TCH的驻留程序README 关于Turbo C的信息文件   TCCONFIG.EXE 配置文件转换程序MAKE.EXE   项目管理工具TCC.EXE   命令行编译TLINK.EXE   Turbo C系列连接器TLIB.EXE   Turbo C系列库管理工具C0?.OBJ 不   同模式启动代码C?.LIB   不同模式运行库GRAPHICS.LIB   图形库EMU.LIB   8087仿真库FP87.LIB 8087库   *.H Turbo C头文件   *.BGI 不同显示器图形驱动程序   *.C Turbo C例行程序(源文件)   其中:上面的?分别为:T Tiny(微型模式)S Small(小模式)C Compact(紧凑模式)M Medium(中型模式)L Large(大模式)H Huge(巨大模式)    Turbo C++ 3.0   “Turbo C++ 3.0”软件是Borland公司在1992年推出的强大的——C语言程序设计与C++面向对象程序设计 的集成开发工具。它只需要修改一个设置选项,就能够在同一个IDE集成开发环境下设计和编译以标准 C 和 C++ 语法设计的程序文件。 [编辑本段]C 语言   C语言起始于1968年发表的CPL语言,它的许多重要思想都来自于Martin Richards在1969年研制的BCPL语言,以及以BCPL语言为基础的与Ken Thompson在1970年研制的B语言。Ken Thompson用B语言写了第一个UNIX操作系统。M.M.Ritchie1972年在B语言的基础上研制了C语言,并用C语言写成了第一个在PDP-11计算机上研制的UNIX操作系统。1977年出现了独立于极其的C语言编译文本《看移植C语言编译程序》,从而大大简化了把C语言编译程序移植到新环境中所做的工作,这本身也就使UNIX的日益广泛使用,C语言也迅速得到推广。   1983年美国国家标准化协会(ANSI)根据C语言问世以来的各种版本,对C语言的发展和扩充制定了新的标准,成为ANSI C。1987年ANSI又公布了新标准————87ANSI C。   目前在微型计算机上使用的有Microsoft C、Quick C、Turbo C等多种版本。这些不同的C语言版本,基本部分是相同的,但是在有关规定上有略有差异。   C 语言发展如此迅速, 而且成为最受欢迎的语言之一, 主要因为它具有强大的功能。许多著名的系统软件, 如DBASE Ⅲ PLUS、DBASE Ⅳ 都是由C 语言编写的。用C 语言加上一些汇编语言子程序, 就更能显示C 语言的优势了,象PC- DOS ,WORDSTAR等就是用这种方法编写的。归纳起来C 语言具有下列特点:   1. C是中级语言   它把高级语言的基本结构和语句与低级语言的实用性结合起来。C 语言可以象汇编语言一样对位、字节和地址进行操作, 而这三者是计算机最基本的工作单元。   2. C是结构式语言   结构式语言的显著特点是代码及数据的分隔化, 即程序的各个部分除了必要的信息交流外彼此独立。这种结构化方式可使程序层次清晰, 便于使用、维护以及调试。C 语言是以函数形式提供给用户的, 这些函数可方便的调用, 并具有多种循环、条件语句控制程序流向, 从而使程序完全结构化。   3. C语言功能齐全   C 语言具有各种各样的数据类型, 并引入了指针概念, 可使程序效率更高。另外C 语言也具有强大的图形功能, 支持多种显示器和驱动器。而且计算功能、逻辑判断功能也比较强大, 可以实现决策目的。   4. C语言适用范围大   C 语言还有一个突出的优点就是适合于多种操作系统, 如DOS、UNIX,也适用于多种机型。   C语言的优点很多,但是也存在一些缺点,如运算优先级太多,运算能力方面不像其它高级语言那样强,语法定义不严格等。但是这些都不能阻止C语言成为一门广受欢迎的计算机编程语言
Turbo C2.0 介绍   Turbo C2.0不仅是一个快捷、高效的编译程序,同时还有一个易学、易用的集成开发环境。使用Turbo C2.0无需独立地编辑、编译和连接程序,就能建立并运行C语言程序。因为这些功能都组合在Turbo 2.0的集成开发环境内,并且可以通过一个简单的主屏幕使用这些功能。 基本配置要求   Turbo C 2.0可运行于IBM-PC系列微机,包括XT,AT及IBM 兼容机。此时要求DOS2.0或更高版本支持,并至少需要448K的RAM,可在任何彩、单色80列监视器上运行。支持数学协处理器芯片,也可进行浮点仿真,这将加快程序的执行。 主要文件的简单介绍   INSTALL.EXE 安装程序文件   TC.EXE 集成编译   TCINST.EXE 集成开发环境的配置设置程序   TCHELP.TCH 帮助文件   THELP.COM 读取TCHELP.TCH的驻留程序README 关于Turbo C的信息文件   TCCONFIG.EXE 配置文件转换程序MAKE.EXE   项目管理工具TCC.EXE   命令行编译TLINK.EXE   Turbo C系列连接器TLIB.EXE   Turbo C系列库管理工具C0?.OBJ 不   同模式启动代码C?.LIB   不同模式运行库GRAPHICS.LIB   图形库EMU.LIB   8087仿真库FP87.LIB 8087库   *.H Turbo C头文件   *.BGI 不同显示器图形驱动程序   *.C Turbo C例行程序(源文件)   其中:上面的?分别为:T Tiny(微型模式)S Small(小模式)C Compact(紧凑模式)M Medium(中型模式)L Large(大模式)H Huge(巨大模式)
### 回答1: 在 TURBO C 2.0 中,您可以使用 biosdisk() 函数来格式化软盘。 下面是一段代码示例: ``` #include <stdio.h> #include <dos.h> int main() { // 定义扇区的大小 unsigned int sector_size[3] = {512, 512, 1024}; // 定义填充字节 unsigned char fill_char[3] = {0xF6, 0xF6, 0x48}; // 格式化磁道 700 的前三个扇区 for (int i = 0; i < 3; i++) { int result = biosdisk(2, 0, 70, 0, i + 1, 1, sector_size[i], fill_char[i]); if (result != 0) { printf("格式化扇区 %d 失败,返回码: %d\n", i + 1, result); } } return 0; } ``` 请注意,在使用 biosdisk() 函数之前,需要包含 dos.h 头文件。 希望这能帮到您! ### 回答2使用TURBO C 2.0语言,可以通过以下代码格式化软盘的70磁道0面,并分别格式化3个扇区,大小分别为512字节、512字节和1024字节。其中,前两个扇区使用F6填充,第三个扇区使用48填充。 ```c #include<stdlib.h> #include<stdio.h> #include<dos.h> void formatFloppyDisk(){ union REGS regs; regs.h.ah = 0x0;// To format a floppy disk, we set AH=0 regs.h.dl = 0;// Drive number (0=A, 1=B, etc.) regs.x.cx = 0;// Track number to format regs.h.dh = 0;// Head number regs.h.al = 0;// Sector size (0=default, 1=512 bytes, 2=1024 bytes, 3=2048 bytes etc.) int FILL_BYTE = 0;// The byte value to fill the sectors with during formatting int NUM_SECTORS = 3;// Number of sectors to format // To format 70th track 0th head regs.x.ax = 0x1301; // 0x13 = Reset disk system, 01H = Reset only specified drive int86(0x13, &regs, &regs); // BIOS interrupt to reset disk system for (int i=0; i<NUM_SECTORS; i++){ regs.x.ax = 0x3101; // 0x31 = Write Format, 01H = Format only current track regs.x.bx = 0x0001; // 0x00 = Drive A:, 01H = Head 1, 0 = Generate ID Field depending on the disk in the drive 1 = Keep the ID Field all zeros regs.x.cx = 0x0170; // Track number=70(0-79 range) regs.h.dh = 0x00; // Head number=0 or 1 regs.h.al = 0x02; // Control byte=always zero regs.x.dx = i+1; // Sector number starting from 1 regs.x.si = 0x0000; // segment and offset of read/write buffer regs.x.di = 0x0000; // segment and offset of result if(i == 2){ FILL_BYTE = 0x48; // Fill the third sector with 48 regs.x.ax = 0x3102; // 0x31 = Write Format, 02H = Format sequential tracks immediately following the one being formatted }else{ FILL_BYTE = 0xF6; // Fill the first two sectors with F6 } regs.h.ah = FILL_BYTE; // Fill the sector with specified byte int86(0x13, &regs, &regs); // BIOS interrupt to format the specified sector } } int main(){ formatFloppyDisk(); return 0; } ``` 上述代码使用了INT 0x13,即BIOS中断服务例程,来执行软盘格式化操作。通过设置寄存器的不同参数,可以指定要格式化的磁道、面、扇区大小和填充字节。在这个例子中,我们格式化了软盘70磁道0面的3个扇区,前两个扇区使用F6填充,第三个扇区使用48填充。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值