在C++中,提到类型定义符前还可以书写class,即类型的自定义类型(简称类),它和结构根本没有区别(仅有一点小小的区别,下篇说明),而之所以还要提供一个class,实际是由于C++是从C扩展而成,其中的class是C++自己提出的一个很重要的概念,只是为了与C语言兼容而保留了struct这个关键字。
暂时可以先认为类较结构的长足进步就是多了成员函数这个概念(虽然结构也可以有成员函数),在了解成员函数之前,先来看一种语义需求。
操作与资源
程序主要是由操作和被操作的资源组成,操作的执行者就是CPU,这很正常,但有时候的确存在一些需要,需要表现是某个资源操作了另一个资源(暂时称作操作者),比如游戏中,经常出现的就是要映射怪物攻击了玩家。之所以需要操作者,一般是因为这个操作也需要修改操作者或利用操作者记录的一些信息来完成操作,比如怪物的攻击力来决定玩家被攻击后的状态。这种语义就表现为操作者具有某些功能。为了实现上面的语义,如原来所说进行映射,先映射怪物和玩家分别为结构,如下:
- struct Monster { float Life; float Attack; float Defend; };
- struct Player { float Life; float Attack; float Defend; };
上面的攻击操作就可以映射为void MonsterAttackPlayer( Monster &mon, Player &pla );。注意这里期望通过函数名来表现操作者,但和前篇说的将过河方案起名为sln一样,属于一种本末倒置,因为这个语义应该由类型来表现,而不是函数名。为此,C++提供了成员函数的概念。
成员函数
与之前一样,在类型定义符中书写函数的声明语句将定义出成员函数,如下:
- struct ABC { long a; void AB( long ); };
上面就定义了一个映射元素——第一个变量ABC::a,类型为long ABC::;以及声明了一个映射元素——第二个函数ABC::AB,类型为void ( ABC:: )( long )。类型修饰符ABC::在此修饰了函数ABC::AB,表示其为函数类型的偏移类型,即是一相对值。但由于是函数,意义和变量不同,即其依旧映射的是内存中的地址(代码的地址),但由于是偏移类型,也就是相对的,即是不完整的,因此不能对它应用函数操作符,如:
- ABC::AB( 10 );
这里将错误,因为ABC::AB是相对的,其相对的东西不是如成员变量那样是个内存地址,而是一个结构指针类型的参数,参数名一定为this,这是强行定义的,后面说明。
注意由于其名字为ABC::AB,而上面仅仅是对其进行了声明,要定义它,仍和之前的函数定义一样,如下:
- void ABC::AB( long d ) { this->a = d; }
应注意上面函数的名字为ABC::AB,但和前篇说的成员变量一样,不能直接书写long ABC::a;,也就不能直接如上书写函数的定义语句(至少函数名为ABC::AB就不符合标识符规则),而必须要通过类型定义符“{}”先定义自定义类型,然后再书写,这会在后面说明声明时详细阐述。
注意上面使用了this这个关键字,其类型为ABC*,由编译器自动生成,即上面的函数定义实际等同于
- void ABC::AB( ABC *this, long d ) { this->a = d; }
而之所以要省略this参数的声明而由编译器来代劳是为了在代码上体现出前面提到的语义(即成员的意义),这也是为什么称ABC::AB是函数类型的偏移类型,它是相对于这个this参数而言的,如何相对,如下:
- ABC a, b, c;
- a.ABC::AB( 10 );
- b.ABC::AB( 12 );
- c.AB( 14 );
上面利用成员操作符调用ABC::AB,注意执行后,a.a、b.a和c.a的值分别为10、12和14,即三次调用ABC::AB,但通过成员操作符而导致三次的this参数的值并不相同,并进而得以修改三个ABC变量的成员变量a。注意上面书写
- a.ABC::AB( 10 );
和成员变量一样,由于左右类型必须对应,因此也可
- a.AB( 10 );
还应注意上面在定义ABC::AB时,在函数体内书写
- this->a = d;
同上,由于类型必须对应的关系,即this必须是相应自定义类型的指针,所以也可省略this->的书写,进而有
- void ABC::AB( long d ) { a = d; }
注意这里成员操作符的作用,其不再如成员变量时返回相应成员变量类型的数字,而是返回一函数类型的数字,但不同的就是这个函数类型是无法用语法表示出来的,即C++并没有提供任何关键字或类型修饰符来表现这个返回的类型(VC内部提供了__thiscall这个类型修饰符进行表示,不过写代码时依旧不能使用,只是编译器内部使用)。
也就是说,当成员操作符右侧接的是函数类型的偏移类型的数字时,返回一个函数类型的数字(表示其可被施以函数操作符),函数的类型为偏移类型中给出的类型,但这个类型无法表现。即a.AB将返回一个数字,这个数字是函数类型,在VC内部其类型为void ( __thiscall ABC:: )( long ),但这个类型在C++中是非法的。
C++并没有提供类似__thiscall这样的关键字以修饰类型,因为这个类型是要求编译器遇到函数操作符和成员操作符时,如
- a.AB( 10 );
要将成员操作符左侧的地址作为函数调用的第一个参数传进去,然后再传函数操作符中给出的其余各参数。即这个类型是针对同时出现函数操作符和成员操作符这一特定情况,给编译器提供一些信息以生成正确的代码,而不用于修饰数字(修饰数字就要求能应付所有情况)。即类型是用于修饰数字的,而这个类型不能修饰数字,因此C++并未提供类似__thiscall的关键字。
和之前一样,由于ABC::AB映射的是一个地址,而不是一个偏移值,因此可以
- ABC::AB;
但不能
- ABC::a;
因为后者是偏移值。根据类型匹配,很容易就知道也可有:
- void ( ABC::*p )( long ) = ABC::AB;
- 或
- void ( ABC::*p )( long ) = &ABC::AB;
进而就有:
- void ( ABC::**pP )( long ) = &p; ( c.**pP )( 10.0f );
之所以加括号是因为函数操作符的优先级较“*”高。再回想前篇说过指针类型的转换只是类型变化,数值不变(下篇说明数值变化的情况),因此可以有如下代码,这段代码毫无意义,在此仅为加深对成员函数的理解。
- struct ABC { long a; void AB( long ); };
- void ABC::AB( long d )
- {
- this->a = d;
- }
- struct AB
- {
- short a, b;
- void ABCD( short tem1, short tem2 );
- void ABC( long tem );
- };
- void AB::ABCD( short tem1, short tem2 )
- {
- a = tem1; b = tem2;
- }
- void AB::ABC( long tem )
- {
- a = short( tem / 10 );
- b = short( tem - tem / 10 );
- }
- void main()
- {
- ABC a, b, c; AB d;
- ( c.*( void ( ABC::* )( long ) )&AB::ABC )( 43 );
- ( b.*( void ( ABC::* )( long ) )&AB::ABCD )( 0XABCDEF12 );
- ( d.*( void ( AB::* )( short, short ) )ABC::AB )( 0XABCD, 0XEF12 );
- }
上面执行后,c.a为0X00270004,b.a为0X0000EF12,d.a为0XABCD,d.b为0XFFFF。对于c的函数调用,由于 AB::ABC映射的地址被直接转换类型进而直接被使用,因此程序将跳到AB::ABC处的
- a = short( tem / 10 );
开始执行,而参数tem映射的是传递参数的内存的首地址,并进而用long类型解释而得到tem为43,然后执行。
注意
- b = short( tem - tem / 10 );
实际是
- this->b = short( tem - tem / 10 );
而this的值为c对应的地址,但在这里被认为是AB*类型(因为在函数AB::ABC的函数体内),所以才能this->b正常(ABC结构中没有b这个成员变量),而b的偏移为2,所以上句执行完后将结果39存放到c的地址加2所对应的内存,并且以short类型解释而得到的16位的二进制数存放。
对于
- a = short( tem / 10 );
也做同样事情,故最后得c.a的值为0X0027004(十进制39转成十六进制为0X27)。
同样,对于b的调用,程序将跳到AB::ABCD,但生成的b的调用代码时,将参数0XABCDEF12按照参数类型为long的格式记录在传递参数的内存中,然后跳到AB::ABCD。但编译AB::ABCD时又按照参数为两个short类型来映射参数tem1和tem2对应的地址,因此容易想到 tem1的值将为0XEF12,tem2的值为0XABCD,但实际并非如此。