简述
使用C/C++编程大约有三四个年头了。最开始涉及到单片机、嵌入式Linux等,都使用的是C语言,那时主要写Linux驱动,甚至在ARM板上写Linux应用程序时需要应用面向对象的思想的时候,都是使用C语言的结构体和函数指针来实现。当然,使用的编译器自然就是gcc了。
后来,慢慢的转向了使用C++编写应用程序,使用C++编写的代码理所应当的就应该使用g++编译器。
但是,在实际的工作过程中发现,gcc和g++的使用之间却有着一些莫名的暧昧。
这里,要澄清几个概念:
.c
文件:里面可以书写纯C代码,也可以书写C++代码,二者可混合书写.cpp
文件:里面可以书写纯C代码,也可以书写C++代码,二者可混合书写- C程序:通过gcc编译链接纯C代码的
.c
文件而得到的程序 - C++程序:非C程序的C/C++程序
- 编译C程序:通过gcc编译链接纯C代码的.c文件获得C程序的过程
- 编译C++程序:可以是通过gcc编译
.cpp
文件(如果文件中无C++内容,此时仍是在编译链接一个C++程序,不需要额外指定库,反之需要),或g++编译链接.c
、.cpp
及其的组合获得C++程序的过程
gcc
对于gcc编译器,首先,其默认是编译链接.c
源文件的。但是其也可以编译链接.cpp
源文件。详细如下:
使用gcc处理.c源文件
理所应当就是.c
源文件里面不能有任何C++的内容。因为这种情况下gcc不会把C++相应的头文件引用进来,如果有C++的内容预处理都不会通过。即使使用-I
选项引入头文件,那么对于C++中的关键字和符号也会识别不了。
使用gcc处理.cpp源文件
那么gcc编译器会根据源文件后缀名为cpp,进行C++程序编译链接。在预处理,编译,汇编阶段和使用g++编译.cpp
源文件一致,仅仅在链接阶段有所不同。因为gcc默认是使用c库链接,当源文件有使用C++的内容时,需要手动添加-lstdc++
库。可见下面实验代码:
test.cpp
#include <stdio.h>
void TestPrintf()
{
printf("2019-05-16\n");
}
int main(void)
{
TestPrintf();
return 0;
}
上面是一个cpp源文件,当使用gcc -o test test.cpp
编译时,并不会报错,因为里面没有涉及到C++的内容,是纯C特性的。但如果是下面的代码:
test.cpp
#include <stdio.h>
#include <iostream>
using namespace std;
void TestPrintf()
{
printf("2019-05-16\n");
cout << "2019-05-16" << endl;
}
int main(void)
{
TestPrintf();
return 0;
}
如果.cpp
中有含有C++的内容,使用g++ -o test test.cpp
编译时,会有如下打印:
/tmp/ccYyDQm5.o: In function `TestPrintf()':
test.cpp:(.text+0x14): undefined reference to `std::cout'
test.cpp:(.text+0x19): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)'
test.cpp:(.text+0x1e): undefined reference to `std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)'
test.cpp:(.text+0x26): undefined reference to `std::ostream::operator<<(std::ostream& (*)(std::ostream&))'
/tmp/ccYyDQm5.o: In function `__static_initialization_and_destruction_0(int, int)':
test.cpp:(.text+0x60): undefined reference to `std::ios_base::Init::Init()'
test.cpp:(.text+0x6f): undefined reference to `std::ios_base::Init::~Init()'
collect2: error: ld returned 1 exit status
在进入到链接阶段的时候,便发生了链接错误。如果我们使用gcc -o test test.cpp -lstdc++
便能编译链接成功。
g++
对于g++编译器,当他处理.c
和.cpp
源文件时都是统一把它们当做是C++程序编译链接来处理的
,也就是说,此时把.c
文件视为.cpp
文件。二者的编译链接过程基本一致。g++在链接阶段默认是使用C++的库,这个库是完全兼容C语言的,毕竟C++是C语言的超集,在兼容C语言特性上是没有问题的。
gcc同时编译.c
和.cpp
源文件
在这种情况下,在编译,汇编阶段没有错误,但是到了链接阶段,即使添加了-lstdc++
库,也会出现链接错误。比如下面的代码:
fun.h
int fun(int a, int b);
fun.c
#include "fun.h"
int fun(int a, int b)
{
return a+b;
}
text.cpp
#include <iostream>
#include "fun.h"
using namespace std;
int main(void)
{
cout << fun(3,4) << endl;
return 0;
}
使用gcc fun.c test.cpp -lstdc++
命令编译链接会出现如下的错误:
/tmp/ccAdQIJn.o: In function `main':
test.cpp:(.text+0xf): undefined reference to `fun(int, int)'
collect2: error: ld returned 1 exit status
可见,无法找到fun
函数,这是为什么呢?原因是gcc在处理.c文件时,在编译阶段,把.cpp
文件用编译C程序的方式处理,对.c
源文件中的函数和变量符号不做处理,但是在处理.cpp
文件时,把.cpp
文件用编译C++程序的方式处理,对源文件中出现的函数和变量符号会根据返回值和参数类型对其更改。我们执行gcc -S fun.c test.cpp
会得到编译后的汇编文件fun.s
和test.s
,部分截图如下:
由图可知,在main
函数中,call
指令指定的是_Z3funii
,而在fun.s
中的fun
函数的汇编符号任然为fun
,所以两个汇编文件汇编之后进入链接阶段会出现函数未定义的链接错误。此时,如果把fun.c
的后缀名改为.cpp
,执行gcc fun.cpp test.cpp -lstdc++
便没有问题。
g++同时编译.c
和.cpp
源文件
完全没有问题,因为上文提到,g++编译器处理.c
和.cpp
源文件时都是统一把它们当做是C++程序编译链接来处理的
。
extern "C"
和__cplusplus
宏对gcc/g++的行为影响
在.cpp
源文件中,由于C++支持函数重载
,可能出现有多个同名的函数,这些函数仅仅是返回值和参数不一样,那么编译器(这里的编译器既可以是gcc也可以是g++)编译链接C++程序时会把源文件中函数的参数类型和返回值信息在编译过程输出的汇编文件中的函数符号中体现,如图X中所示,而不仅仅是函数名;但是C语言并不支持函数重载
,因此编译链接C程序时对源代码的函数在编译汇编过程中不会带上函数的参数类型等信息,一般只包括函数名,如图Y所示。
extern "C"
的主要作用就是为了能够正确实现C++代码调用其他C语言代码。比如,在C++程序中调用使用.c源文件编写的同时使用gcc编译出来库的时候,在编译链接这个C++程序过程中,引用的头文件中函数声明的地方加上extern "C"
后,会指示编译器在编译C++程序时这部分代码按编译C程序(而不是C++程序)的方式进行编译。如下所示:
fun.h
extern "C"
{
int fun(int a, int b);
}
如果一个函数使用这样的方式声明的话,在编译链接C++源文件(这里的C++源文件可以是.cpp
文件或有C++内容的.c文件,只是这里的.c
文件需要使用g++编译链接,.cpp
文件可以使用gcc和g++编译链接)中,编译的时候就不会对这个函数相关的代码做C++程序的处理,而是用C程序的方式处理。
在编译C++程序时,会有__cplusplus
宏存在,而编译C程序时没有,与使用gcc和g++无关,毕竟gcc也可以编译一个C++程序。
下面我们使用一些实验来说明:
对于如下下代码:
test.c
#include <stdio.h>
void TestPrintf()
{
printf("2019-05-16\n");
}
int main(void)
{
TestPrintf();
return 0;
}
- gcc编译
.c
时,这就是纯C编译,没有问题。如果对上面的代码执行gcc -S test.c
生成的test.s文件中函数名称TestPrintf
没有改变。如图:
- 如果使用g++编译
.c
源文件,.c源文件会当做C++程序编译。如果对上面的代码执行g++ -S test.c
生成的test.s文件中函数名称TestPrintf
发生改变。如图:
但是如果在test.c
改为如下,这效果和执行gcc -S test.c
一样。
test.c
#include <stdio.h>
#include "fun.h"
extern "C"
{
void TestPrintf();
}
void TestPrintf()
{
printf("%d\n", fun(4,5));
}
int main(void)
{
TestPrintf();
return 0;
}
对于如下代码:
test.cpp
#include <stdio.h>
void TestPrintf()
{
printf("2019-05-16\n");
}
int main(void)
{
TestPrintf();
return 0;
}
- gcc和g++编译
test.cpp
文件时,都是在编译一个C++程序,都会对.cpp源文件中的函数和变量符号做变更处理。这时,如果一个函数在声明时使用extern "C"
,如下面代码所示,那么就会用编译C程序的方式处理与这个函数相关的代码。
test.cpp
#include <stdio.h>
extern "C"
{
void TestPrintf()
}
void TestPrintf()
{
printf("2019-05-16\n");
}
int main(void)
{
TestPrintf();
return 0;
}
总结
- gcc在处理
.c
文件时,把这个过程作为编译链接一个C程序的过程 - gcc在处理
.cpp
文件时,把这个过程作为编译链接一个C++程序的过程,但是gcc默认在链接阶段使用C库,如果.cpp
文件中涉及到使用C++库,则需要手动指定库 - gcc既可以编译一个C程序,也可以编译一个C++程序
- g++在处理
.c
文件时,把其看做是一个.cpp文件处理,且其编译链接后的只能是C++程序,而gcc仅仅在处理.cpp
文件时,编译链接之后产生的才是C++程序 - 在编译链接C++程序中存在
__cplusplus
宏,在编译链接C程序时不存在,与使用gcc还是g++编译器无关,可以使用此宏做预编译条件判断。