码迷,mamicode.com
首页 > 编程语言 > 详细

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

时间:2015-02-27 20:20:17      阅读:228      评论:0      收藏:0      [点我收藏+]

标签:c++ extern c name-ma

问题引出

    之前提到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_int2add_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.out nm –demangle a.out对比察看name mangling前后的符号,总结其规则。有些demangle工具可根据输入的decorated name逆向还原出函数原型,如gcc 工具集里的c++filt(以下取自c++filt文档):

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

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

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

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

思考

    C++编译器在链接阶段根据decoratedname找到唯一匹配符号,即可实现函数重载,这只是理想说法,实际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编译后在库中的symbolfoo,而C++编译器则会产生_3foo_Fii之类包含作用域名(命名空间与类名)、函数名、参数等信息的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 foo( int x ){   //... }

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

    // C Code

      int foo( int x );

      void cc( int x ){    foo( x ); }

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

    // C Code

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

    // C++ Code

      extern "C" void foo( int x ); //该声明一般放在C的接口头文件中,被C++源文件include

      void cc( int x ){    foo( x ); }

    注意改变的是C++link过程,告诉它直接去找按C方式compile出的foo,而不是按默认方式去找_3foo_Fi

重复总结:

    如果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默认的namemangling过程,按C方式编译出foo;如内部未发现定义,则说明该函数来自外部C模块,extern “C”的作用就变成提示C++ linkerfoo而不是C++默认的_3foo_F去查找链接函数。

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

 

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

标签:c++ extern c name-ma

原文地址:http://blog.csdn.net/ipmux/article/details/43970635

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!