上面难免显得故弄玄虚
,那都是因为自定义类型的出现
。考虑成员变量的定义
,如:
struct ABC { long a, b; double c; };
上面给出了类型--long ABC::、long ABC::和double ABC::;给出了名字--ABC::a、ABC::b和ABC::c;给出了地址(即偏移)--0、4和8,因为是结构型自定义类型,故由此语句就可以得出各成员变量的偏移
。上面得出三个信息,即可以填写映射元素的所有信 struct ABC { void AB( float ); };
上面给出了类型--void ( ABC:: )( float );给出了名字--ABC::AB。不过由于没有给出地址,因此无法填写映射元素的所有信息,故上面是成员函数ABC::AB的声明。按照前面说法,只要给出地址就可以了,而无需去管它是定义还是声明,因此也就可以这样:
struct ABC { void AB( float ){} };
上面给出类型和名字的同时,给出了地址,因此将可以完全填写映射元素的所有信息,是定义。上面的用法有其特殊性,后面说明。注意,如果这时再在后面写ABC::AB的定义语句,即如下,将错误:
struct ABC { void AB( float ){} };
void ABC::AB( float ) {}
上面将报错,原因很简单,因为后者只是定义,它只提供了ABC::AB对应的地址这一个信息,但映射元素中的地址栏已经填写了,故编译器将说重复定义。再单独看成员函数的定义,它给出了类型void ( ABC:: )( float ),给出了名字ABC::AB,也给出了地址,但为什么说它只给出了地址这一信息?首先,名字ABC::AB是不符合标识符规则的,而类型修饰符ABC::必须通过类型定义符“{}”才能够加上去,这在前面已多次说明。因此上面给出的信息是:给出了一个地址,这个地址是类型为void ( ABC:: )( float ),名字为ABC::AB的映射元素的地址。结果编译器就查找这样的映射元素,如果有,则填写相应的地址栏,否则报错,即只写一个void ABC::AB( float ){}是错误的,在其前面必须先通过类型定义符“{}”声明相应的映射元素。这也就是前面说的定义仅仅填地址栏,并不生成映射元素。
声明的作用
定义的作用很明显了,有意义的映射(名字对地址)就是它来做,但声明有什么用?它只是生成类型对名字,为什么非得要类型对名字?它只是告诉编译器不要发出错误说变量或函数未定义?任何东西都有其存在的意义,先看下面这段代码。
extern"C" long ABC( long a, long b );
void main(){ long c = ABC( 10, 20 ); }
假设上面代码在a.cpp中书写,编译生成文件a.obj,没有问题。但按照之前的说明,连接时将错误,因为找不到符号_ABC。因为名字_ABC对应的地址栏还空着。接着在VC中为a.cpp所在工程添加一个新的源文件b.cpp,如下书写代码。
extern"C" float ABC( float a ){ return a; }
编译并连接,现在没任何问题了,但相信你已经看出问题了--函数ABC的声明和定义的类型不匹配,却连接成功了?
注意上面关于连接的说明,连接时没有类型,只管符号。上面用extern"C"使得a.obj要求_ABC的符号,而b.cpp提供_ABC的符号,剩余的就只是连接器将b.obj中_ABC对应的地址放到a.obj以完善a.obj,最后连接a.obj和b.obj。
那么上面什么结果,由于需要考虑函数的实现细节,这在《C++从零开始(十五)》中再说明,而这里只要注意到一件事:编译器即使没有地址也依旧可以生成代码以实现函数操作符的功能--函数调用。之所以能这样就是因为声明时一定必须同时给出类型和名字,因为类型告诉编译器,当某个操作符涉及到某个映射元素时,如何生成代码来实现这个操作符的功能。也就是说,两个char类型的数字乘法和两个long类型的数字乘法编译生成的代码不同;对long ABC( long );的函数调用代码和void ABC( float )的不同。即,操作符作用的数字类型的不同将导致编译器生成的代码不同。
那么上面为什么要将ABC的定义放到b.cpp中?因为各源文件之间的编译是独立的,如果放在a.cpp,编译器就会发现已经有这么个映射元素,但类型却不匹配,将报错。而放到b.cpp中,使得由连接器来完善a.obj,到时将没有类型的存在,只管符号。下面继续。
struct ABC { long a, b; void AB( long tem1, long tem2 ); void ABCD(); };
void main(){ ABC a; a.AB( 10, 20 ); }
由上面的说法,这里虽然没有给出ABC::AB的定义,但仍能编译成功,没有任何问题。仍假设上面代码在a.cpp中,然后添加b.cpp,在其中书写下面的代码。
struct ABC { float b, a; void AB( long tem1, long tem2 ); long ABCD( float ); };
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
这里定义了函数ABC::AB,注意如之前所说,由于这里的函数定义仅仅只是定义,所以必须在其前面书写类型定义符“{}”以让编译器生成映射元素。但更应该注意这里将成员变量的位置换了,这样b就映射的是0而a映射的是4了,并且还将a、b的类型换成了float,更和a.cpp中的定义大相径庭。但没有任何问题,编译连接成功,a.AB( 10,20 );执行后a.a为0X41A00000,a.b为0X41200000,而*( float* )&a.a为20,*( flaot* )&a.b为10。
为什么?因为编译器只在当前编译的那个源文件中遵循类型匹配,而编译另一个源文件时,编译其他源文件所生成的映射元素全部无效。因此声明将类型和名字绑定起来,而名字就代表了其所关联的类型的地址类型的数字,而后继代码中所有操作这个数字的操作符的编译生成都将受这个数字的类型的影响。即声明是告诉编译器如何生成代码的,其不仅仅只是个语法上说明变量或函数的语句,它是不可或缺的。
还应注意上面两个文件中的ABC::ABCD成员函数的声明不同,而且整个工程中(即a.cpp和b.cpp中)都没有ABC::ABCD的定义,却仍能编译连接成功,因为声明并不是告诉编译器已经有什么东西了,而是如何生成代码。
头文件上面已经说明,如果有个自定义类型ABC,在a.cpp、b.cpp和c.cpp中都要使用它,则必须在a.cpp、b.cpp和c.cpp中,各自使用ABC之前用类型定义符“{}”重新定义一遍这个自定义类型。如果不小心如上面那样在a.cpp和b.cpp中写的定义不一样,则将产生很难查找的错误。为此,C++提供了一个预编译指令来帮忙。
预编译指令就是在编译之前执行的指令,它由预编译器来解释执行。预编译器是另一个程序,一般情况,编译器厂商都将其合并进了C++编译器而只提供一个程序。在此说明预编译指令中的包含指令--#include,其格式为#include <文件名>。应注意预编译指令都必须单独占一行,而<文件名>就是一个用双引号或尖括号括起来的文件名,如:#include "abc.c"、#include "C:abc.dsw"或#include <C:abc.exe>。它的作用很简单,就是将引号或尖括号中书写的文件名对应的文件以ANSI格式或MBCS格式(关于这两个格式可参考《C++从零开始(五)》)解释,并将内容原封不动地替换到#include所在的位置,比如下面是文件abc的内容。
struct ABC { long a, b; void AB( long tem1, long tem2 ); };
则前面的a.cpp可改为:
#include "abc"
void main() { ABC a; a.AB( 10, 20 ); }
而b.cpp可改为:
#include "abc"
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
这时,就不会出现类似上面那样在b.cpp中将自定义类型ABC的定义写错了而导致错误的结果(a.a为0X41A00000,a.b为0X41200000),进而a.AB( 10, 20 );执行后,a.a为10,a.b为20。
注意这里使用的是双引号来括住文件名的,它表示当括住的只是一个文件名或相对路径而没有给出全路径时,如上面的abc,则先搜索此时被编译的源文件所在的目录,然后搜索编译器自定的包含目录(如:C:Program FilesMicrosoft Visual Studio .NET 2003Vc7include等),里面一般都放着编译器自带的SDK的头文件(关于SDK,将在《C++从零开始(十八)》中说明),如果仍没有找到,则报错(注意,一般编译器都提供了一些选项以使得除了上述的目录外,还可以再搜索指定的目录,不同的编译器设定方式不同,在此不表)。
如果是用尖括号括起来,则表示先搜索编译器自定的包含目录,再源文件所在目录。为什么要不同?只是为了防止自己起的文件名正好和编译器的包含目录下的文件重名而发生冲突,因为一旦找到文件,将不再搜索后继目录。
所以,一般的C++代码中,如果要用到某个自定义类型,都将那个自定义类型的定义分别装在两个文件中,对于上面结构ABC,则应该生成两个文件,分别为ABC.h和ABC.pp,其中的ABC.h被称作头文件,而ABC.cpp则称作源文件。头文件里放的是声明,而源
文件中放的是定义,则ABC.h的内容就和前面的abc一样,而ABC.cpp的内容就和b.cpp一样。然后每当工程中某个源文件里要使用结构ABC时,就在那个源文件的开头包含ABC.h,这样就相当于将结构ABC的所有相关声明都带进了那个文件的编译,比如前面的a.cpp就通过在开头包含abc以声明了结构ABC。
为什么还要生成一个ABC.cpp?如果将ABC::AB的定义语句也放到ABC.h中,则a.cpp要使用ABC,c.cpp也要使用ABC,所以a.cpp包含ABC.h,由于里面的ABC::AB的定义,生成一个符号
?AB@ABC@@QAEXJJ@Z(对于VC);同样c.cpp的编译也要生成这个符号,然后连接时,由于出现两个相同的符号,连接器无法确定使用哪一个,报错。因此专门定义一个ABC.cpp,将函数ABC::AB的定义放到ABC.obj中,这样将只有一个符号生成,连接时也就不再报错。
注意上面的struct ABC { void AB( float ){} };。如果将这个放在ABC.h中,由于在类型定义符中就已经将函数ABC::AB的定义给出,则将会同上,出现两个相同的符号,然后连接失败。为了避开这个问题,C++规定如上在类型定义符中直接书写函数定义而义的函数是inline函数,出于篇幅,下篇介绍。
成员的意义上面从语法的角度说明了成员函数的意思,如果很昏,不要紧,实现不能理解并不代表就不能运用,而程序员重要的是对语言的运用能力而不是语言的了解程度(虽然后者也很重要)。下面说明成员的语义。
本文一开头提出了一种语义--某种资源具有的功能,而C++的自定义类型再加上成员操作符“.”和“->”的运用,从代码上很容易的就表现出一种语义--从属关系。如:a.b、c.d分别表示a的b和c的d。某种资源具有的功能要映射到C++中,就应该将这种资源映射成一自定义类型,而它所具有的功能就映射成此自定义类型的成员函数,如最开始提到的怪物和玩家,则如下:
struct Player { float Life; float Attack; float Defend; };
struct Monster { float Life; float Attack; float Defend; void AttackPlayer( Player &pla ); };
Player player; Monster a; a.AttackPlayer( player );
上面的语义就非常明显,代码执行的操作是怪物a攻击玩家player,而player.Life就代表玩家player的生命值。假设如下书写Monster::AttackPlayer的定义:
void Monster::AttackPlayer( Player &pla )
{
pla.Life -= Attack - pla.Defend;
}
上面的语义非常明显:某怪物攻击玩家的方法就是将被攻击的玩家的生命值减去自己攻击力减被攻击的玩家的防御力的值。语义非常清晰,代码的可读性好。而如原来的写法:
void MonsterAttackPlayer( Monster &mon, Player &pla )
{
pla.Life -= mon.Attack - pla.Defend;
}
则代码表现的语义:怪物攻击玩家是个操作,此操作需要操作两个资源,分别为怪物类型和玩家类型。这个语义就没表现出我们本来打算表现的想法,而是怪物的攻击功能的另一种解释(关于这点,将在《C++从零开始(十二)》中详细阐述),其更适合表现收
银工作。比如收银台实现的是收钱的工作,客户在柜台买了东西,由营业员开出单据,然后客户将单据拿到收银台交钱。这里收银台的工作就需要操作两个资源--钱和单据,这时就应该将收钱这个工作映射为如上的函数而不是成员函数,因为在这个算法中,收银台没有被映射成自定义类型的必要性,即我们对收银的工作由谁做不关心,只关心它如何做。
至此介绍完了自定义类型的一半内容,通过这些内容已经可以编写出能体现较复杂语义的代码了,下篇将说明自定义类型的后半内容,它们的提出根本可以认为就是语义的需要,所以下篇将从剩余内容是如何体现语义的来说明,不过依旧要说明各自是如何实现的.