四、重载函数 前面的移动函数,如果只想移动X和Y坐标,为了不移动Z坐标,就必须如下再编写一个函数:
void Move2( float x, float y );
它为了不和前面的Move函数的名字冲突而改成Move2,但Move2也表示移动,却非要变一个名字,这严重地影响语义。为了更好的从源代码上表现出语义,即这段代码的意义,C++提出了重载函数的概念。
重载函数表示函数名字一样,但参数类型及个数不同的多个函数。如下:
void Move( float x, float y, float z ) { }和void Move( float x, float y ) { }
上面就定义了两个重载函数,虽然函数名相同,但实际为两个函数,函数名相同表示它们具有同样的语义——移动焊枪的程序,只是移动方式不同,前者在三维空间中移动,后者在一水平面上移动。当Move( 12, 43 );时就调用后者,而Move( 23, 5, 12 );时就调用前者。不过必须是参数的不同,不能是返回值的不同,即如下将会报错:
float Move( float x, float y ) { return 0; }和void Move( float a, float b ) { }
上面虽然返回值不同,但编译器依旧认为上面定义的函数是同一个,则将说函数重复定义。为什么?因为在书写函数操作符时,函数的返回值类型不能保证获得,即float a = Move( 1, 2 );虽然可以推出应该是前者,但也可以Move( 1, 2 );,这样将无法得知应该使用哪个函数,因此不行。还应注意上面的参数名字虽然不同,但都是一样的,参数名字只是表示在那个函数的作用域内其映射的地址,后面将说明。改成如下就没有问题:
float Move( float x, float y ) { return 0; }和void Move( float a, float b, float c ) { }
还应注意下面的问题:
float Move( float x, char y ); float Move( float a, short b ); Move( 10, 270 );
上面编译器将报错,因为这里的270在计算函数操作符时将被认为是int,即整型,它即可以转成char,也可以转成short,结果编译器将无法判断应是哪一个函数。为此,应该Move( 10, ( char )270 );。 五、声明和定义
声明是告诉编译器一些信息,以协助编译器进行语法分析,避免编译器报错。而定义是告诉编译器生成一些代码,并且这些代码将由连接器使用。即:声明是给编译器用的,定义是给连接器用的。这个说明显得很模糊,为什么非要弄个声明和定义在这搅和?那都是因为C++同意将程序拆成几段分别书写在不同文件中以及上面提到的编译器只从上朝下编译且对每个文件仅编译一次。
编译器编译程序时,只会一个一个源文件编译,并分别生成相应的中间文件(对VC就是.obj文件),然后再由连接器统一将所有的中间文件连接形成一个可执行文件。问题就是编译器在编译a.cpp文件时,发现定义语句而定义了变量a和b,而在编译b.cpp时,发现使用a和b的代码,如a += b;,但在b.cpp中没有发现a和b的定义语句,则编译器将报错。为什么?如果不报错,说因为a.cpp中已经定义了,那么先编译b.cpp再编译a.cpp将如何?如果源文件的编译顺序是特定的,将大大降低编译的灵活性,因此C++也就规定:编译a.cpp时定义的所有东西(变量、函数等)在编译b.cpp时将全部不算数,就和没编译过a.cpp一样。那么b.cpp要使用a.cpp中定义的变量怎么办?为此,C++提出了声明这个概念。
因此变量声明long a;就是告诉编译器已经有这么个变量,其名字为a,其类型为long,其对应的地址不知道,但可以先作个记号,即在后续代码中所有用到这个变量的地方做上记号,以告知连接器在连接时,先在所有的中间文件里寻找是否有个叫a的变量,其地址是多少,然后再修改所有作了记号的地方,将a对应的地址放进去。这样就实现了这个文件使用另一个文件中定义的变量。
所以声明long a;就是要告诉编译器已经有这么个变量a,因此后续代码中用到a时,不要报错说a未定义。函数也是如此,但是有个问题就是函数声明和函数定义很容易区别,因为函数定义后一定接一复合语句,但是变量定义和变量声明就一模一样,那么编译器将如何识别变量定义和变量声明?编译器遇到long a;时,统一将其认为是变量定义,为了能标识变量声明,可借助C++提出的修饰符extern。
修饰符就是声明或定义语句中使用的用以修饰此声明或定义来向编译器提供一定的信息,其总是接在声明或定义语句的前面或后面,如:
extern long a, *pA, &ra;
上面就声明(不是定义)了三个变量a、pA和ra。因为extern表示外部的意思,因此上面就被认为是告诉编译器有三个外部的变量,为a、pA和ra,故被认为是声明语句,所以上面将不分配任何内存。同样,对于函数,它也是一样的:
extern void ABC( long ); 或 extern long AB( short b );
上面的extern等同于不写,因为编译器根据最后的“;”就可以判断出来上面是函数声明,而且提供的“外部”这个信息对于函数来说没有意义,编译器将不予理会。extern实际还指定其后修饰的标识符的修饰方式,实际应为extern"C"或extern"C++",分别表示按照C语言风格和C++语言风格来解析声明的标识符。
C++是强类型语言,即其要求很严格的类型匹配原则,进而才能实现前面说的函数重载功能。即之所以能几个同名函数实现重载,是因为它们实际并不同名,而由各自的参数类型及个数进行了修饰而变得不同。如void ABC(), *ABC( long ), ABC( long, short );,在VC中,其各自名字将分别被变成“?ABC@@YAXXZ”、“?ABC@@YAPAXJ@Z”、“?ABC@@YAXJF@Z”。而extern long a, *pA, &ra;声明的三个变量的名字也发生相应的变化,分别为“?a@@3JA”、“?pA@@3PAJA”、“?ra@@3AAJA”。上面称作C++语言风格的标识符修饰(不同的编译器修饰格式可能不同),而C语言风格的标识符修饰就只是简单的在标识符前加上“_”即可(不同的编译器的C风格修饰一定相同)。如:extern"C" long a, *pA, &ra;就变成_a、_pA、_ra。而上面的extern"C" void ABC(), *ABC( long ), ABC( long, short );将报错,因为使用C风格,都只是在函数名前加一下划线,则将产生3个相同的符号(Symbol),错误。
为什么不能有相同的符号?为什么要改变标识符?不仅因为前面的函数重载。符号和标识符不同,符号可以由任意字符组成,它是编译器和连接器之间沟通的手段,而标识符只是在C++语言级上提供的一种标识手段。而之所以要改变一下标识符而不直接将标识符作为符号使用是因为编译器自己内部和连接器之间还有一些信息需要传递,这些信息就需要符号来标识,由于可能用户写的标识符正好和编译器内部自己用的符号相同而产生冲突,所以都要在程序员定义的标识符上面修改后再用作符号。既然符号是什么字符都可以,那为什么编译器不让自己内部定的符号使用标识符不能使用的字符,如前面VC使用的“?”,那不就行了?因为有些C/C++编译器及连接器沟通用的符号并不是什么字符都可以,也必须是一个标识符,所以前面的C语言风格才统一加上“_”的前缀以区分程序员定义的符号和编译器内部的符号。即上面能使用“?”来作为符号是VC才这样,也许其它的编译器并不支持,但其它的编译器一定支持加了“_”前缀的标识符。这样可以联合使用多方代码,以在更大范围上实现代码重用,在《C++从零开始(十八)》中将对此详细说明。
当书写extern void ABC( long );时,是extern"C"还是extern"C++"?在VC中,如果上句代码所在源文件的扩展名为.cpp以表示是C++源代码,则将解释成后者。如果是.c,则将解释成前者。不过在VC中还可以通过修改项目选项来改变上面的默认设置。而extern long a;也和上面是同样的。因此如下:
extern"C++" void ABC(), *ABC( long ), ABC( long, short ); int main(){ ABC(); }
上面第一句就告诉编译器后续代码可能要用到这个三个函数,叫编译器不要报错。假设上面程序放在一个VC项目下的a.cpp中,编译a.cpp将不会出现任何错误。但当连接时,编译器就会说符号“?ABC@@YAXXZ”没找到,因为这个项目只包含了一个文件,连接也就只连接相应的a.obj以及其他的一些必要库文件(后续文章将会说明)。连接器在它所能连接的所有对象文件(a.obj)以及库文件中查找符号“?ABC@@YAXXZ”对应的地址是什么,不过都没找到,故报错。换句话说就是main函数使用了在a.cpp以外定义的函数void ABC();,但没找到这个函数的定义。应注意,如果写成int main() { void ( *pA ) = ABC; }依旧会报错,因为ABC就相当于一个地址,这里又要求计算此地址的值(即使并不使用pA),故同样报错。
为了消除上面的错误,就应该定义函数void ABC();,既可以在a.cpp中,如main函数的后面,也可以重新生成一个.cpp文件,加入到项目中,在那个.cpp文件中定义函数ABC。因此如下即可: extern"C++" void ABC(), *ABC( long ), ABC( long, short ); int main(){ ABC(); } void ABC(){}
如果你认为自己已经了解了声明和定义的区别,并且清楚了声明的意思,那我打赌有50%的可能性你并没有真正理解声明的含义,这里出于篇幅限制,将在《C++从零开始(十)》中说明声明的真正含义,如果你是有些C/C++编程经验的人,到时给出的样例应该有50%的可能性会令你大吃一惊。 六、调用规则
调用规则指函数的参数如何传递,返回值如何传递,以及上述的函数名标识符如何修饰。其并不属于语言级的内容,因为其表示编译器如何实现函数,而关于如何实现,各编译器都有自己的处理方式。在VC中,其定义了三个类型修饰符用以告知编译器如何实现函数,分别为:__cdecl、__stdcall和__fastcall。三种各有不同的参数、函数返回值传递方式及函数名修饰方式,后面说明异常时,在说明了函数的具体实现方式后再一一解释。由于它们是类型修饰符,则可如下修饰函数:
void *__stdcall ABC( long ), __fastcall DE(), *( __stdcall *pAB )( long ) = &ABC; void ( __fastcall *pDE )() = DE; 七、变量的作用域
前面定义函数Move时,就说void Move( float a, float b );和void Move( float x, float y );是一样的,即变量名a和b在这没什么意义。这也就是说变量a、b的作用范围只限制在前面的Move的函数体(即函数定义时的复合语句)内,同样x和y的有效范围也只在后面的Move的函数体内。这被称作变量的作用域。
//a.cpp long e = 10; void main() { short a = 10; e++; { long e = 2; e++; a++; } e++; }
上面的第一个e的有效范围是整个a.cpp文件内,而a的有效范围是main函数内,而main函数中的e的有效范围则是括着它的那对“{}”以内。即上面到最后执行完e++;后,long e = 2;定义的变量e已经不在了,也就是被释放了。而long e = 10;定义的e的值为12,a的值为11。 也就是说“{}”可以一层层嵌套包含,没一层“{}”就产生了一个作用域,在这对“{}”中定义的变量只在这对“{}”中有效,出了这对“{}”就无效了,等同于没定义过。 为什么要这样弄?那是为了更好的体现出语义。一层“{}”就表示一个阶段,在执行这个阶段时可能会需要到和前面的阶段具有相同语义的变量,如排序。还有某些变量只在某一阶段有用,过了这个阶段就没有意义了,下面举个例子:
float a[10]; // 赋值数组a for( unsigned i = 0; i < 10; i++ ) for( unsigned j = 0; j < 10; j++ ) if( a[ i ] < a[ j ] ) { float temp = a[ i ]; a[ i ] = a[ j ]; a[ j ] = temp; }
上面的temp被称作临时变量,其作用域就只在if( a[ i ] < a[ j ] )后的大括号内,因为那表示一个阶段,程序已经进入交换数组元素的阶段,而只有在交换元素时temp在有意义,用于辅助元素的交换。如果一开始就定义了temp,则表示temp在数组元素寻找期间也有效,这从语义上说是不对的,虽然一开始就定义对结果不会产生任何影响,但应不断地询问自己——这句代码能不能不要?这句代码的意义是什么?不过由于作用域的关系而可能产生性能影响,这在《C++从零开始(十)》中说明。
下篇将举例说明如何已知算法而写出C++代码,帮助读者做到程序员的最基本的要求——给得出算法,拿得出代码。  
2/2 首页 上一页 1 2 |