C/C++拾遗之extern "C"

extern “C”的主要作用是让C++中的函数名字使用C语言的编译处理方式(也就是在编译阶段,确切来说 是汇编阶段不对函数名进行mangle处理),这样就可以让C语言代码能够通过“与C语言兼容的方式声明的头文件”来链接C++编译器生成的二进制格式文件中函数。由于C++支持函数重载,而C语言不支持,C++编译器为了支持函数重载,就需要为同名但参数列表不同的函数名指定一个全局唯一的名字(该名字被称为mangled name),也就是编译阶段的mangle name过程,这些名字通常会包含相应的参数列表类型等,不同的编译器实现不同。
由于有extern的修饰,表示被修饰的变量或函数的对外部文件可见,或其定义来自其他文件中,与static的作用刚好相反。
SOF上的回答:http://stackoverflow.com/questions/1041866/in-c-source-what-is-the-effect-of-extern-c

下面以代码来分析,环境为ubuntu 14.04.4,gcc 4.8.4:
文件较多,一一分析。
文件名:extern_c.h,该头文件显然是可以被C及C++源码包含的,因为里面没有用到C++的高级特性

1
2
3
4
5
6
7
//与C语言兼容的函数声明,函数定义放在C++的源代码中
#ifndef EXTERN_C_H
#define EXTERN_C_H
int add(int, int);
char get(char);
int mult(int, int);
#endif /* !EXTERN_C_H */

文件名:divi.h,该文件中包含有不兼容于C语言的头文件,所以这个文件是不能被C源码包含的

1
2
3
4
5
6
7
#ifndef DIVI_H
#define DIVI_H
#include <iostream>

float divi(float, float);

#endif /* !DIVI_H */

文件名:extern_add.cpp,该文件中的add函数被声明为extern “C”属性

1
2
3
4
5
extern "C" int add(int x, int y)
{
return x + y;

}

查看其汇编代码(命令g++ -S extern_add.cpp,汇编文件名为extern_add.s)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	.file	"extern_add.cpp"
.text
.globl add
.type add, @function
add:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -8(%rbp), %eax
movl -4(%rbp), %edx
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size add, .-add
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits

显示汇编之后的函数add的名字依然是add

文件名:extern_c.cpp,该文件是extern_c.h头文件中声明的函数的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extern "C" {  
//将头文件声明为extern "C"之后,该头文件中声明的所有函数都将在编译时以C语言的方式处理
#include "extern_c.h"
}

extern "C" {
//此时是否加extern "C"都一样,因为整个头文件都被声明为了extern "C"
char get(char ch)
{
return ch;
}
}

int mult(int a, int b) //不需要extern "C",因为头文件已经被声明为extern "C"
{
return a * b;
}

float divi(float a, float b) //未被声明为extern "C" 属性,将无法被C源代码引用
{
return a / b;
}

对extern_c编译之后读取其符号表内容如下(命令readelf -s extern_c.o):

1
2
3
4
5
6
7
8
9
10
11
12
13
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS extern_c.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 15 FUNC GLOBAL DEFAULT 1 get
9: 000000000000000f 19 FUNC GLOBAL DEFAULT 1 mult
10: 0000000000000022 42 FUNC GLOBAL DEFAULT 1 _Z4diviff

从符号表中可以看到被声明为extern “C”的函数get和mult的函数名在汇编之后没有变,这样当C语言代码的链接这个编译之后的文件时才能找到相应的函数get和mult,但找不到div,因为可以看到div在编译之后名字被mangle成为_Z4diviff。(当然通过查看汇编代码也可以清楚地分析到来其作用,因为mangle是在汇编阶段进行的)

文件名:c_test_extern.c,该文件为C语言源代码,用它来链接C++编译生成的可链接的目标文件

1
2
3
4
5
6
7
8
9
10
11
12
#include "extern_c.h"  //该头文件实际是C++的头文件,里面只有兼容C语言的声明,但由于定义都带有extern "C", 所以可以引用
#include <stdio.h>

int main(void)
{
int a = 3, b = 5;
int a_b_add = add(a, b);
int a_b_mult = mult(a, b);
char ch = get(65);
printf("a + b = %d\na * b = %d\nchar(65) = %c\n", a_b_add, a_b_mult, ch);
return 0;
}

编译方法:

1
2
3
g++ -c extern_c.cpp extern_add.cpp  #编译C++源文件
gcc -c c_test_extern.c #编译C语言源文件
gcc -o c_test_extern extern_c.o extern_add.o c_test_extern.o #将链接C与C++源文件编译出的可重定位的目标文件,并生成可执行的目标文件c_test_extern

没有任何问题,因为gcc在链接时,由于C++的目标文件在编译时函数名没有被mangle,所以可以正常找到函数名
执行c_test_extern输出为:

1
2
3
a + b = 8
a * b = 15
char(65) = A

假如在定义add函数的C++文件extern_c.cpp中不声明为extern “C”,则链接会出错,错误内容为:

1
2
3
c_test_extern.o: In function `main':
c_test_extern.c:(.text+0x21): undefined reference to `add'
collect2: error: ld returned 1 exit status

提示很明显:未定义的引用add,因为add的名字被g++ mangle成其他名字了。

文件名:cpp_test_extern.cpp 用于测试C++引用extern “C”中的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extern "C" {
//由于extern_c.h头文件中的声明的函数在定义时都增加了extern "C"属性,当C++源文件需要
//引用这些函数时都必须声明为extern "C",否则在链接时由于默认情况下C++汇编器会mangle函数名
//从而导致链接器会以mangle之后的名字去找需要引用的函数,但定义这些函数的源代码
//却是以C语言的方式汇编,即函数名都是原名,从而导致找不到需要引用的函数。
#include "extern_c.h"
}
#include "divi.h"

int main(void)
{
int a = 3, b = 5;
int a_b_add = add(a, b);
int a_b_mult = mult(a, b);
float a_b_div = divi(a, b);
char ch = get(65);
std::cout << "a + b = " << a_b_add << std::endl;
std::cout << "a * b = " << a_b_mult << std::endl;
std::cout << "a / b = " << a_b_div << std::endl;
std::cout << "char(65) = " << ch << std::endl;
return 0;
}

编译方法:

1
g++ -o cpp_test_extern cpp_test_extern.cpp extern_c.cpp extern_add.cpp

运行结果为:

1
2
3
4
a + b = 8
a * b = 15
a / b = 0.6
char(65) = A

假如在包含头文件时,不使用extern “C”声明头文件extern_c.h,编译会输出错误:

1
2
3
4
5
cpp_test_extern.o: In function `main':
cpp_test_extern.cpp:(.text+0x22): undefined reference to `add(int, int)'
cpp_test_extern.cpp:(.text+0x34): undefined reference to `mult(int, int)'
cpp_test_extern.cpp:(.text+0x5b): undefined reference to `get(char)'
collect2: error: ld returned 1 exit status

错误意思为: 未定义的引用,即无法找到相应的函数

最后注意extern “C”影响的只是编译器对于编译时函数名的处理方式,并不影响语言特性,也就是说不管带不带extern “C”,代码都是以C++的方式处理,而不是说带了extern “C”的代码就以C语言方式处理。例如对于常量字符,我们知道C语言认为是int型,如’a’,C语言认为其占用4个字节,而C++认为是char型,即1个字节。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
void cpp_print(void)
{
char c='a';
printf("%d %d\n",sizeof(c),sizeof('a'));
}
extern "C" void c_print(void)
{
char c='a';
printf("extern_c:%d %d\n",sizeof(c),sizeof('a'));
}

int main()
{
cpp_print();
c_print();
return 0;
}

输出结果为:

1
2
1 1
extern_c:1 1

显示都是C++的方式处理的