快捷搜索:   服务器  安全  linux 安全  MYSQL  dedecms

C++从零开始(七)——何谓函数(2)

    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++代码,帮助读者做到程序员的最基本的要求——给得出算法,拿得出代码。

顶(0)
踩(0)

您可能还会对下面的文章感兴趣:

最新评论