C++基础篇—函数重载与Extern C

来源:互联网 发布:js hide 编辑:程序博客网 时间:2024/06/05 11:21

问题引出

    之前提到C存在命名冲突问题,新的C++专门为此引入了namespace机制加以改进(后文介绍),此外还有另一种机制,先看下面例子:

    int add(int i, int j) {    return i+i;  }

    float add(float a, float b, floatc)  {    return a+b+c;   }

    void main()

    {

       int a = add(8, 9);

       float b = add(7.7, 8.8, 9.9);

    }

    代码在C环境下不成立,因为C编译器不允许定义两个同名函数,即使其参数个数和类型不同(参见C命名空间污染一节)。C下唯一的笨方法就是给这系列函数取很多不同名字,如add_int2、add_float3等(opengGL的接口就是这样)。

    C++针对以上应用场景提出一种解决思路:除函数名外,把函数参数信息也加入进来进一步区分不同函数。即C++允许定义一组同名不同参的函数,编译器能够根据参数类型正确地区分并链接,如上例square(8)会调square(int)square(8.8)则去调square(float)。这种机制称为函数重载,它借助函数固有的参数信息,可以只使用单个函数名实现多个功能相似的函数,减少了对命名空间的占用。

    注:习惯C函数调用与函数名一一对应后,起初对C++这种同样函数名包含多个功能(后续继承/多态也类似)可能会感觉别扭。另外overload翻译成重载容易让人想到覆盖,继而和继承/多态混淆。我更倾向叫它函数堆叠,就像叠罗汉J,更贴近点吧。

函数重载机制

    理解函数重载,先要知道两个名词:修饰名(Decorated Name)与命名调整(Name Mangling

    C/C++程序中的函数在链接阶段对应的符号标识称为修饰名,是由编译器按某种规则生成的一个字符串,在compiling阶段创建,linking阶段使用。它就象函数的身份证号,必须唯一,后续才能被正确索引。

    C函数的修饰名一般就是函数名(有些特殊形式如_udiv__udiv多是来自C标准库),里面不包含参数信息,链接阶段自然无法区分同名不同参的函数。

    C++针对性的改进思路就是:除函数名外,把函数固有的其它信息也加入修饰名(通常想到参数和返回值)。修饰名变的更长,如func_arg1type_arg2type…retvaluetype,从而为编译器提供更多特征去区分函数。

    但最终C++标准中修饰名不包括返回值类型,即返回值不同而名字和参数相同的函数不是重载函数,为什么浪费这部分信息呢?

    看两个函数void test(void);和int test (void);第一个没有返回值,第二个返回int。对于int x = test();可判断应调用第二个test()。可C++和C允许忽略返回值调用函数,如:test();这时返回值信息用不上,编译器甚至程序员自己都不知道该调哪个。(凡事有果必有因,现在奉为经典背诵学习的各种复杂规范,很多是基于旧标准的妥协和延伸罢了)。

    C++相对C还多了类作用域,即不同类中同名同参的成员函数编译后的符号名也不能相同而导致混淆,所以类名也应加到修饰名里。还有新的C++还有命名空间的概念,位于不同namespace中的同名函数也要相互区分,因此namespace也应加入(所以命名空间机制也是基于往修饰名里插入人为设置的namespace标识而实现)。

    下面给出一个简单例子以加深对这一过程的理解(参考网上而来):

    1)与类无关的独立函数编为函数名加__F再加一串代表参数类型的字母。参数类型可简写(void→v,int→I,float→f等)。如foo(float,int,void)变为foo__Ffiv。如果参数为class则编码成class名的字母长度后紧跟class名,如foo(class Pair)编为foo__F4Pairclass还可包含内部成员class,用Q加数字标明层深,然后是class名,如First::Second::Third编成Q35First6Second5Third。所以函数foo(Pair,First::Second::Third)编成foo__F4PairQ35First6Second5Third

    2)类成员函数编码成函数名加两个下划线加编码后的类名,最后是F和参数,cl::foo(void)变成foo__2clFv

    3)操作符编码成一串字符,如__ml代表*__aor代表=

    4)如有多个相同类型参数名字太长,还可压缩,如用Tn代表和第n个参数相同的类型,而Nnm代表”n个和第m个参数类型相同的参数。函数foo(Pair,Pair)编成foo__F4PairT1,而foo(Pair,Pair,Pair,Pair)编成foo__F4PairN31

    这种编译器按一定规则生成decorated name的过程称为命名调整name mangling(所以decorated name有时也称mangled name)C++函数重载的本质,就是在编译阶段根据类名/函数名/参数等信息对函数进行name mangling,链接阶段利用生成的decorated name,准确匹配链接同名不同参的函数。

    C++name mangling机制没有具体标准,可用工具分析目标文件,了解某编译器name mangling的规则。如对于gcc可用nm a.outnm –demangle a.out对比察看name mangling前后的符号,总结其规则。而gcc工具集里另一个demangle工具--c++filt则可直接把输入的decorated name逆向还原为函数原型(以下取自c++filt文档):

    The C++ and Java languages provide function overloading, which means that you can write many functions with the same name, providing that each function takes parameters of different types. Inorder to be able to distinguish these similarly named functions C++ and Java encode them into a low-level assembler name which uniquely identifies each different version. This process is known as mangling. The c++filt program does the inverse mapping: it decodes (demangles) low-level names into user-level names so that they can be read.

    Every alpha numeric word (consisting of letters, digits, underscores,dollars, or periods) seen in the input is a potential mangled name. If the name decodes into a C++ name, the C++ name replaces the low-level name in the output, otherwise the original word is output. In this way you can pass anentire assembler source file, containing mangled names, through c++filt and see the same source file containing demangled names.

    You can also use c++filt to decipher individual symbols by passing them on the command line: c++filt  symbol

   注:一般编程不需关心函数修饰名是什么,除非手写汇编代码且要与CC++函数交互,那就必须清楚的知道该编译器的修饰名生成规则,从而在汇编里正确书写该符号。

思考

    C++编译器在链接阶段根据decorated name找到唯一匹配符号,即可实现函数重载,这只是理想说法,实际C++函数重载还要考虑很多特殊情况,比如:

    void add (int, int){……};

    void add (char, char){……}

    void main()

   {

      short a =10;

      short b =20;

      add(a, b);

    }

    这种是否允许,最后又调用谁?

    void add (double, double){……};

    void main()

    {

      int a =10;

      int b =20;

      add(a, b);

    }

    这里实参intdouble不同是否就匹配不上了?

  真正实现很复杂,只能说重载函数链接过程绝不是简单一对一的查找匹配,而是从多个候选中根据规则算法择优录取。具体不深究,哪天用到再说。这里知道重载(包括命名空间)机制是以name mangling生成的decorated name作为中枢索引就足够了

替代连接符extern C

    C++用比C更复杂的name mangling机制实现了函数重载,这样对于同样函数定义,同一套C/C++编译器(如gcc/g++)各自生成的decorated name不同。如void foo(int x, int y)放在.c文件里用gcc编译后在库中的symbolfoo,而放在.cpp里被g++编译则会产生_3foo_Fii之类(仅举例,实际测试为_Z3fooii)包含作用域名(命名空间与类名)、函数名、参数等信息的decorated name。这会导致CC++不能相互调用对方库中函数:C编译生成foo,而C++链接时去找_3foo_Fii;或C++编译成_3foo_Fii,而C linker则找foo

    为实现C/C++的混合编程,C++重载(这里也用重载这个词,可见软件术语中overload重载常表示对事物的复用)了extern关键字,提供功能符extern "C"解决符号匹配问题。

    注意:必须是同一套C/C++编译器,VC++编译的C++无论如何也无法调用gcc编译的C库函数。即使不考虑进出栈顺序等函数调用规范问题(见C函数一节),由于name mangling没有统一规范,不同编译器相互不可能知道对方的name mangling实现机制和生成的decorated name,自然无法通过extern C逆向消除。除非制定和统一所有相关标准。

    多数人都知道把extern “C”放在C库的接口头文件中,可以使C++编译器能调用C库的接口函数。反过来在C++源文件中的函数声明前加上extern "C"后,也可以在C中调用该C++函数(当然相比C++调用C的过程有一个限制,就是C++中的重载函数不能一起逆转)。

    这两个过程中extern “C”的用法和机理实际有着微妙的差别:

    1)C调用C++,用extern"C"告诉C++编译器依照C的方式来编译extern “C”所修饰函数,而该函数内部还是按C++方式编译。如:

    // C++ Code

      extern "C" int foo( int x, int y );   // extern "C" declaration

      int foo( int x, int y){   ... }    //function definition     

    这样C++ compiler会将foo函数编译成foo而不是_3foo_Fii。注意改变的是C++ codecompile过程,以匹配Clinker,从而使C能调用C++函数,即:

    // C Code

      int foo( int x, int y );

      void cc( int x ){    foo( x, y ); }    // can call C++ function in C code.

    2)而C++调用C时,extern"C"的作用是:让C++连接器用C的方式链接接口函数,如:

    // C Code

      void foo( int x, int y);  //生成foo

    // C++ Code

      extern "C" void foo( int x, int y ); // this declaration is laied in C header file generally,and is included intoC++ souce code.

      void cc( int x ){    foo( x ); }    //can call c function in C++ code

      这样C++ linker会去寻找foo,而不是默认的_3foo_Fi  注意改变的是C++link过程,以匹配C的compiling结果

重复:

    如果C要调C++函数,在C++源文件内实现foo(int),并在C++源文件extern “C” foo()声明告诉C++compiler要按Cname mangling方式生成符号fooC++库中,C linker就可正确链接。

    如果C++要调C函数,在C源文件内实现foo(int),在C头文件中用extern “C” foo()声明告诉C++ linker要按Cname mangling方式寻找符号名foo(这里C头文件必须被C++包含,否则extern “C”起不了作用),这样C++就可调用C库中的foo

    所以extern “C”完全是C++编译器的功能符号,C++编译器碰到extern “C”int foo() ,会检查foo为内部自定义还是外部引用,再决定如何处理。如发现int foo()C++内定义,就改变C++ compiler默认的name mangling过程,按C方式编译出foo;如内部未发现定义,说明函数来自外部C模块,extern “C”作用就变成提示C++ linker按C方式的foo而不是C++_3foo_Fii去查找目标函数。

    此外一般C++编译器开发商已经对C标准库的头文件作了extern"C"处理,所以#include 相关头文件就可在C++中调用标准C库函数。这才是C++中能调用printf/memcpy等功能函数的原因。或许很多人认为这种调用本就是顺理成章,没什么可想的。.

 

1 0