从名字开始讲——C与C++的编译细节

后缀名

由于历史原因,在很久以前,C++的源文件的后缀名也是 *.c,但这样会引起不少的问题。于是不同的前辈就想了不同的方法来解决这个问题:就是把C++文件的后缀名改为了 *.cc *.cpp *.cxx等等。

到了如今,基本上除了Unix系统外,其他的平台上,C++文件的后缀名基本上都是 *.cpp,但是Unix系统仍然倾向于使用 *.cc作为C++文件的后缀名,比如查看Unix或者Linux内核时,就会看到很多 *.cc 文件。

Google C++ Sytle中可以看到,Google的C++后缀名也倾向于使用 *.cc 。

对于头文件而言,虽然也有 *.hpp *.hh的写法,但基本上都是用的 *.h,没有改变。

补充说明一下,在写C++代码时,有不少的码神喜欢把模板文件(template source)和内敛函数(inline function)的源文件以另一种特别的后缀名存放,比如: *.tcc 和 *.inl。

不同的环境下的C++文件可能的后缀名

Unix uses: C, cc, cxx, c

GNU C++ uses: C, cc, cxx, cpp, c++

Digital Mars uses: cpp, cxx

Borland C++ uses: cpp

Watcom uses: cpp

Microsoft Visual C++ uses: cpp, cxx, cc

Metrowerks CodeWarrior uses: cpp, cp, cc,cxx, c++

Linux下的gcc,cc,g++,CC的区别

基本信息

1. gccC编译器g++C++编译器;gcc和g++都是GNU(组织)的编译器。

2. Linux下cc一般是一个符号链接指向gcc;而CC则一般是makefile里面的一个名字,即宏定义注意:Linux/Unix都是大小写敏感的系统

3. ccUnix系统的C Compiler,而gcc则是GNU Compiler Collection,GNU编译器套装。gcc原名为Gun C语言编译器,因为它原本只能处理c语言,但gcc很快得到扩展,包含很多编译器(C、C++、Objective-C、Ada、Fortran、Java)。因此,ccgcc是不一样的,一个是古老的C编译器,一个是GNU编译器集合,gcc里面的C编译器比cc强大多了,因此一般没必要用cc。(网上下载不到cc的原因在于:cc来自于昂贵的Unix系统,cc是收费的商业软件。Linux则是类Unix系统,开源,免费。)

Linux下的cc是gcc符号链接,可以通过$ls –l /usr/bin/cc来简单察看,该变量是make程序的内建变量默认指向gcc。cc符号链接和变量存在的意义在于源码的可移植性,可以方便的用gcc来编译老的用cc编译的Unix软件,甚至连makefile都不用改在,而且也便于Linux程序在Unix下编译

常见误区

误区一:gcc只能编译C代码,g++只能编译C++代码。

两者都可以,但请注意:

(1)后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序;后缀为.cpp的,两者都会认为是C++程序,注意,虽然C++是C的超集,但是两者对语法的要求是有区别的。C++的语法规则更加严谨一些。

(2)编译阶段,g++会调用gcc,对于C++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,所以通常用g++来完成链接,为了统一起见,干脆编译/链接都用g++了,这就给人一种错觉,好像cpp程序只能用g++似的。

误区二:gcc不会定义__cplusplus宏,而g++会

实际上,这个宏只是标志着编译器将会把代码按C还是C++语法来解释,如上所述,如果后缀为.c,并且采用gcc编译器,则该宏就是未定义的,否则,就是已定义。

误区三:编译只能用gcc,链接只能用g++

严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接,所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价

C++的编译器肯定可以编译C的代码,注意除了C++对C的语法扩充之外,编译和链接C和C++的标准库通常也不一样呢,用gcc而非g++也编译了C++的程序就证明了这一点。

注:符号链接是一种特殊类型的文件,它的内容只是一个字符串。它可能指向一个存在的文件也可能什么都不指向。当您在命令行或程序里提到符号链接的时候,您实际上进入了它指向的文件,前提是这个文件是存在的。

gcc编译四过程

C源文件到可执行文件共经历了4个主要的过程。在使用GCC编译程序时,编译过程可以被细分为四个阶段,包括预处理编译汇编链接

gcc指令的一般格式为:gcc [选项] 要编译的文件 [选项] [目标文件]
其中,目标文件可缺省,gcc默认生成可执行的文件名为:编译文件.out。例:

gcc -o hello hello.c

该命令将hello.c直接生成最终二进制可执行程序a.out。命令隐含执行了预处理==> 编译==>汇编==>链接,形成最终的二进制可执行程序。这里未指定输出文件,默认输出为a.out
如何要指定最终二进制可执行程序名,那么用-o选项来指定名称。比如需要生成执行程序hello.exe:

gcc hello.c -o hello.exe

预处理(Pre-processing)

在该阶段,编译器主要做加载头文件、宏替换、条件编译的工作。

一般处理带“#”的语句。编译器将C源代码中的包含的头文件如stdio.h编译进来,用户可以使用gcc的选项"-E"进行查看。

gcc -E hello.c -o hello.i

作用:将hello.c预处理输出hello.i文件。

编译阶段(Compiling)

第二步进行的是编译阶段,在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。用户可以使用"-S"选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码

gcc –S hello.i –o hello.s

作用:将预处理输出文件hello.i汇编成hello.s文件。

汇编阶段(Assembling)

汇编阶段是把编译阶段生成的".s"文件转成二进制目标代码"-c"

gcc –c hello.s –o hello.o

作用:将汇编输出文件hello.s编译输出hello.o文件。

链接阶段(Link)

在成功编译之后,就进入了链接阶段。链接就是将目标文件、启动代码、库文件链接成可执行文件的过程,这个文件可被加载或拷贝到存储器执行:"-o"

gcc hello.o –o hello.exe

作用:将编译输出文件hello.o链接成最终可执行文件hello.exe

运行该可执行文件:

./hello
//Hello World!

一个重要的概念:函数库

hello.c中并没有定义"printf"的函数实现,且在预编译中包含进的"stdio.h"中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现"printf"函数的呢?答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,gcc会到系统默认的搜索路径"/usr/lib"下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数"printf"了,而这也就是链接的作用。

你可以用ldd命令查看动态库加载情况:

ldd hello.exe
libc.so.6 => /lib/tls/libc.so.6 (0x42000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

函数库一般分为静态库动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为".a"动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为".so",如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库

gcc -c与gcc -o以及不加参数的区别

Makefile编写教程:https://seisman.github.io/how-to-write-makefile/overview.html(陈皓)

以下摘自gcc --help的解释(gcc version 7.3.0):

-c               Compile and assemble, but do not link. 
-o <file>        Place the output into <file>.
                 'none' means revert to the default behavior of guessing the language based on the file's extension.
  • -c:编译和汇编,但不要链接。
  • -o <file>:将输出放入<文件>。
  • '无参数':表示恢复为基于文件扩展名猜测语言的默认行为。

1、通过gcc 不加参数可以一步直接编译生成可执行文件

gcc main.c

这里生成的是可执行文件a.out,当然可以通过-o选项更改生成文件的名字,比如将生成的可执行文件命名为hello.exe

gcc main.c -o main.exe

2、gcc -c 编译生成main.o

gcc -c main.c //生成main.o
gcc main.o //不加参数,gcc自动链接上一步生成的main.o来生成最终可执行文件a.out

当然也可以通过-o选项更改生成的执行文件的名字

gcc main.o -o main.exe

C++中extern “C”含义深层探索

extern “C” 是一个双向都需要用到的语法表示,就是说在cpp引用c头文件,或者c引用cpp文件时都需要用到。但extern “C” 永远只能在cpp引用时出现,c引用时不允许存在。当cpp引用c中的函数时,需要在cpp使用的头文件中声明extern “C”,当c引用cpp中的函数时,需要在cpp使用的头文件中用extern “C”声明,这样编译器在编译时会对函数名进行特殊处理,以使其能够被c引用。如果不进行声明,那么当c引用这个头文件时,就会找不到函数,因为cpp的函数命名规则中包含变量类型,而c编译后的函数名不包含这些。

引言  

C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和链接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。

从标准头文件说起

某企业曾经给出如下的一道面试题:

为什么标准头文件都有类似以下的结构?

#ifndef __INCvxWorksh
#define __INCvxWorksh 
#ifdef __cplusplus
extern "C" {
#endif 
/*...*/ 
#ifdef __cplusplus
}
#endif 
#endif /* __INCvxWorksh */

显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。那么

#ifdef __cplusplus
extern "C" {
#endif 
#ifdef __cplusplus
}
#endif

的作用又是什么呢?我们将在下文一一道来。 

深层揭密extern "C"

extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。

被extern "C"限定的函数或变量是extern类型的;

extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:

extern int a;

仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现链接错误。

通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在链接阶段中从模块A编译生成的目标代码中找到此函数。

与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。被extern "C"修饰的变量和函数是按照C语言方式编译和链接的;

未加extern “C”声明时的编译方式

首先看看C++中对类似C的函数是怎样编译的。

作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:

void foo( int x, int y );

该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。

同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

未加extern "C"声明时的链接方式

假设在C++中,模块A的头文件如下:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif

在模块B中引用该函数:

// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);

实际上,在链接阶段,链接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

加extern "C"声明后的编译和链接方式

加extern "C"声明后,模块A的头文件变为:

// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif

在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

  (1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

  (2)链接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++与C及其它语言的混合编程。

明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。

extern "C"的惯用法

(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

extern "C"
{
#include "cExample.h"
}

而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:

/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}

c++实现文件,调用add:cppFile.cpp

extern "C" 
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3); 
return 0;
}

如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。

(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。

笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:

//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 ); 
return 0;
}

如果深入理解了第3节中所阐述的extern "C"在编译和链接阶段发挥的作用,就能真正理解本节所阐述的从C++引用C函数和C引用C++函数的惯用法。对第4节给出的示例代码,需要特别留意各个细节。

(整理自网络)

参考资料:

http://www.linuxidc.com/Linux/2014-10/108646.htm

https://www.cnblogs.com/yuwei0911/p/7094008.html

https://blog.csdn.net/BobYuan888/article/details/88709449

https://www.cnblogs.com/orangebook/p/3621849.html

Min是清明的茗
原文地址:https://www.cnblogs.com/MinPage/p/13878231.html