金融基础技术与业务

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

2018-01-06  本文已影响0人  FlyingReganMian

首先看一段代码:

***************************文件名: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++编译报错原因:

  1. gcc编译器默认将代码当做C语言去编译,认为函数名相同的函数为同一个函数,以上代码中声明了三个函数名相同的函数,所以gcc编译器报fun重复定义。
  2. 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");
     else
     {
               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;
}

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

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

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)
{
    printf("This is __attribute__ ((weakref)) int fun(int a)\n");
}

编译后运行:This is int fun(int a)。

这个例子从说明了这一节开头抛出的问题,malloc在stdlib库中的定义为弱应用,“重写”的malloc为强引用,覆盖了stdlib库中的弱引用。

上一篇下一篇

猜你喜欢

热点阅读