关于C和CPP中同名函数的思考

首先看一段代码:

***************************文件名:fun_c.c***********************
int fun(int a);
int fun(int a ,int b);
void fun(int a);
int fun(int a)
  printf("This is int fun(int a)\n");
int main()
    fun(1);
    return 0;

使用gcc编译:

fun_c.c:5: error: conflicting types for ‘fun’
fun_c.c:4: error: previous declaration of ‘fun’ was here
fun_c.c: In function ‘main’:
fun_c.c:14: error: too few arguments to function ‘fun’

使用g++编译:

fun_c.c:6: error: new declaration ‘void fun(int)’
fun_c.c:4: error: ambiguates old declaration ‘int fun(int)’
fun_c.c: In function ‘int fun(int)’:
fun_c.c:7: error: new declaration ‘int fun(int)’
fun_c.c:6: error: ambiguates old declaration ‘void fun(int)’

首先解释一下gcc和g++编译报错原因:

  • gcc编译器默认将代码当做C语言去编译,认为函数名相同的函数为同一个函数,以上代码中声明了三个函数名相同的函数,所以gcc编译器报fun重复定义。
  • g++编译器默认将代码当做CPP语言去编译,认为 int fun(int a); 和 void fun(int a); 两个函数是同一个函数。
    那为什么CPP只报这两个函数重定义呢?
    原因是:CPP拥有重载的特性,在同一个作用域中,函数名相同,参数表不同的函数,构成重载关系。 重载与函数的返回类型无关,与参数名也无关,而只与参数的个数、类型和顺序有关。CPP会将构成重载关系的函数解析成不同函数。

    现在,我们不经要问:为什么CPP要引入重载?CPP是怎样将构成重载关系或不同作用域的函数解析成不同函数的呢?

    1.为什么CPP要引入重载?

    刚开始,编译器编译源代码生成目标文件时,符号名和函数名是一致的,但是随着后来程序越来越大,编写的目标文件不可避免的会出现符号冲突的问题。比如,当程序很大时,不同模块由不同部门开发,如果他们之间命名不规范,很有可能出现符号冲突的问题。于是呢,CPP等后来设计语言就开始引入了重载和命名空间来解决这个问题。

    2.CPP是怎样将构成重载关系或不同作用域的函数解析成不同函数的呢?

    首先,看一段代码:

    int fun(int);
    int fun(int,int);
    class Cfun_class1{
        int fun(int);
        class Cfun_class2{
            int fun(int);
    namespace N {
        int fun(int);
        class Cfun_class3{
            int fun(int);
    

    以上代码中有6个同名函数fun,但是他们的参数类型和参数个数以及所在的namespace不同。CPP利用函数签名来识别不同的函数。函数签名包括函数名,参数类型,所在的类和namespace。以上6个函数的函数签名分别是:

    int fun(int) int fun(int,int) int::Cfun_class1:: fun(int) int::Cfun_class1::Cfun_class2:: fun(int) int::N:: fun(int) int::N::Cfun_class3:: fun(int)

    编译器在将CPP源代码编译成目标文件时,会利用某种名称修饰方法将函数签名编码成一个符号名。此外,以上的签名和修饰的方法不仅用在了函数上,CPP中全局变量和静态变量也用到了同样的方法。

    通过以上的阐述,我们了解到C和CPP的编译链接规约是不同的,也就是说编译器会将C和CPP中国函数名编码成不同的符号名。这里我们想一个问题,如果一个项目中,即有C文件又有CPP文件,该怎么编译?这就涉及到了extern "C"

    3.extern "C"

    先看一段代码:

    cHeader.h

    #ifndef C_HEADER
    #define C_HEADER
    void print_fun(int i);
    #endif C_HEADER
    

    cHeader.c

    #include <stdio.h>
    #include "cHeader.h"
    void print(int i)
        printf("cHeader %d\n",i);
    

    main.c

    #include "cHeader.h"
    int main(int argc,char** argv)
        print(3);
        return 0;
    

    编译链接:

    gcc -c cHeader.c  -o cHeader.o
    ar cqs libCheader.a cHeader.o
    g++ -o mian main.cpp -L/root/Desktop -lCheader
    
    /tmp/ccUgVIT7.o: In function `main':
    main.cpp:(.text+0x19): undefined reference to `print(int)'
    collect2: ld returned 1 exit status
    

    编译后报错:未定义print函数。这就是因为编译器对CPP和C的编译规约不同,编译器认为print是一个CPP函数,将print编码成一个CPP符号,链接器拿着这个CPP符号在静态库中找不到对应的print函数,所以编译器认为print函数为定义。

    为解决上述问题,CPP引入了extern "C"。将CHeader.h中代码改成如下代码,即可编译通过。

     extern "C"{
         void print(int i);
    

    CPP编译器会把在extern "C"大括号内部的代码当做C代码来处理。这样编译器会将print函数编码成一个C符号,链接器就可以从静态库中找到对应的print函数。为进一步方便操作,CPP提供了宏__cplusplus ,CPP编译器会在编译CPP代码时默认这个宏,我们可以使用条件宏来判断当前的编译单元是不是CPP代码。具体代码如下:

    #ifdef __cplusplus
    extern "C"{
    #endif
    void print(int i);
    #ifdef __cplusplus  
    #endif
    

    如果当前编译单元是CPP代码,那么void print(int i);会在 extern "C"里面被声明;如果是C代码,就直接声明。上面代码技巧几乎在所有的系统文件被用到。

    4.弱引用和强引用

    先看一段代码:

    #include <stdio.h>
    #include <stdlib.h>
    void *malloc(unsigned long size)
         printf("I am void *malloc(unsigned long size).\n");
         return NULL;
     int main()
         char *buf = NULL;
         buf = (char *)malloc(10);
         if(NULL == buf)
                   printf("failed.\n");
                   printf("%p.\n", buf);
                   free(buf);         
         return 0;
    

    编译:gcc -g -Wall -Werror test.c -o test 正确无错误输出
    运行:./test
    运行结果:I am void *malloc(unsigned long size). 正确

    按照我们上面的说法C语言不支持同名函数,上面的函数应该报错才对。

    这就涉及到了强引用和弱应用的概念。
    强引用:若函数未定义,则链接时,链接器找不到函数位置报错;
    而对于弱引用则不会报错,链接器默认函数地址为0。我们可以通过attribute((weak))来声明一个外部函数的应用为弱应用。下面,我们举一个例子来说明。

    强引用实例:

    int fun(int a);
    int main()
        fun(1);
        return 0;
    

    编译后报错: undefined reference to `fun',链接器找不到fun
    弱引用实例:

     __attribute__((weak)) int fun(int a);
    int main()
        fun(1);
        return 0;
    

    编译不报错,运行报错:段错误。当main函数调用fun函数时,fun函数入口地址为0,发生了非法地址访问。改进:

     __attribute__((weak)) int fun(int a);
    int main()
        if(fun) fun(1);
        return 0;
    

    弱引用对于库来说十分重要。从上面的强弱引用的特点可看出:

  • 当一个函数为弱引用时,不管这个函数有没有定义,链接时都不会报错,而且我们可以根据判断函数名是否为0来决定是否执行这个函数,这些函数的库就可以以模块、插件的形式和我们的引用组合一起,方便使用和卸载;
  • 并且由于强引用可以覆盖弱引用可知,我们自己定义函数可以覆盖库中的函数。以下,我们给出一个例子予以说明。
  • fun_c.c

    #include "weakref_test.h"
    int fun(int a);
    int fun(int a)
        printf("This is int fun(int a)\n");
    int main()
        fun(1);
        return 0;
    

    weakref_test.h

    #include <stdio.h>
    #include <stdlib.h>
    __attribute__ ((weakref)) int fun(int a);
    

    weakref_test.c

    #include "weakref_test.h"
    __attribute__ ((weakref)) int fun(int a)