【转载】C/C++完整编译过程详解 (拿来读懂makefile文件)

原文链接:C语言编译过程详解 - CarpenterLee - 博客园

目录

原文链接:C语言编译过程详解 - CarpenterLee - 博客园

前言

示例

1.预处理(Preprocessing)

2.编译(Compilation)

3.汇编(Assemble)

4.链接(Linking)

结语

参考文献

gcc -I -L -l区别


前言

C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。

编写hello world C程序:

// hello.c
#include 
int main(){
    printf("hello world!\n");
}

编译过程只需:

$ gcc hello.c # 编译
$ ./a.out # 执行
hello world!

这个过程如此熟悉,以至于大家觉得编译事件很简单的事。事实真的如此吗?我们来细看一下C语言的编译过程到底是怎样的。

上述gcc命令其实依次执行了四步操作:1.预处理(Preprocessing), 2.编译(Compilation), 3.汇编(Assemble), 4.链接(Linking)。

【转载】C/C++完整编译过程详解 (拿来读懂makefile文件)_第1张图片

示例

为了下面步骤讲解的方便,我们需要一个稍微复杂一点的例子。假设我们自己定义了一个头文件mymath.h,实现一些自己的数学函数,并把具体实现放在mymath.c当中。然后写一个test.c程序使用这些函数。程序目录结构如下:

├── test.c
└── inc
    ├── mymath.h
    └── mymath.c

程序代码如下:

// test.c
#include 
#include "mymath.h"// 自定义头文件
int main(){
    int a = 2;
    int b = 3;
    int sum = add(a, b); 
    printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}

头文件定义:

// mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sum(int a, int b);
#endif

头文件实现:

// mymath.c
int add(int a, int b){
	return a+b;
}
int sub(int a, int b){
	return a-b;
}

1.预处理(Preprocessing)

预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc的预处理是预处理器cpp来完成的,你可以通过如下命令对test.c进行预处理:

gcc -E -I./inc test.c -o test.i

或者直接调用cpp命令

$ cpp test.c -I./inc -o test.i

上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。

经过预处理之后代码体积会大很多:

X 文件名 文件大小 代码行数
预处理前 test.c 146B 9
预处理后 test.i 17691B 857

预处理之后的程序还是文本,可以用文本编辑器打开。

2.编译(Compilation)

这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。编译的指定如下:

$ gcc -S -I./inc test.c -o test.s

上述命令中-S让编译器在编译之后停止,不进行后续过程。编译过程完成后,将生成程序的汇编代码test.s,这也是文本文件,内容如下:

// test.c汇编之后的结果test.s
    .file   "test.c"
    .section    .rodata
.LC0:
    .string "a=%d, b=%d, a+b=%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    movl    $2, 20(%esp)
    movl    $3, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, 4(%esp)
    movl    20(%esp), %eax
    movl    %eax, (%esp)
    call    add 
    movl    %eax, 28(%esp)
    movl    28(%esp), %eax
    movl    %eax, 12(%esp)
    movl    24(%esp), %eax
    movl    %eax, 8(%esp)
    movl    20(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret 
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
    .section    .note.GNU-stack,"",@progbits

请不要问我上述代码是什么意思!-_-

3.汇编(Assemble)

汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。gcc汇编过程通过as命令完成:

$ as test.s -o test.o

等价于:

gcc -c test.s -o test.o

这一步会为每一个源文件产生一个目标文件。因此mymath.c也需要产生一个mymath.o文件

4.链接(Linking)

链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)

命令大致如下:

$ ld -o test.out test.o inc/mymath.o ...libraries...

结语

经过以上分析,我们发现编译过程并不像想象的那么简单,而是要经过预处理、编译、汇编、链接。尽管我们平时使用gcc命令的时候没有关心中间结果,但每次程序的编译都少不了这几个步骤。也不用为上述繁琐过程而烦恼,因为你仍然可以:

$ gcc hello.c # 编译
$ ./a.out # 执行

参考文献

1.GCC and Make - A Tutorial on how to compile, link and build C/C++ applications
2.http://www.trilithium.com/johan/2005/08/linux-gate/
3.Collect2 (GNU Compiler Collection (GCC) Internals)

我的makefile的例子:

###--- 5. Dependency ---###
SOURCE := $(wildcard solid-phase/*.cpp)
NOTDIR := $(notdir $(SOURCE))
OBJECT := $(patsubst %.cpp, %.o, $(NOTDIR))



###--- 6.Compile and link ---###
LINK := $(CC) -Xlinker --add-needed -Xlinker --no-as-needed

$(EXE) :$(OBJECT) MPPICFoam.o
	$(LINK) -o $(EXE) MPPICFoam.o \
		$(SOURCE) $(CLDFLAGS) $(CINC) $(CFLAGS) $(FOAMINC) 

MPPICFoam.o: MPPICFoam.cpp
	$(CC) -o MPPICFoam.o -g -c MPPICFoam.cpp $(CINC) $(CFLAGS) $(FOAMINC)

%.o: solid-phase/%.cpp
	$(CC) $(CINC) $(CFLAGS) -o $@ -g -c $^

 解读:

###--- 5. Dependency ---###
SOURCE := $(wildcard solid-phase/*.cpp)
NOTDIR := $(notdir $(SOURCE))
OBJECT := $(patsubst %.cpp, %.o, $(NOTDIR))

SOURCE是带路径的cpp文件

NOTDIR是不带路径的cpp文件

OBJECT是将以上不带路径文件的后缀.cpp换成.o

###--- 6.Compile and link ---###
LINK := $(CC) -Xlinker --add-needed -Xlinker --no-as-needed

用gcc连接的命令和参数

结构:

     targets : prerequisites

       command

       ...

 targets是文件名,command是命令行,prerequisites也就是目标所依赖的文件(或依赖目标)。如果命令太长,你可以使用反斜框(‘\’)作为换行符。

$(EXE) :$(OBJECT) MPPICFoam.o
	$(LINK) -o $(EXE) MPPICFoam.o \
		$(SOURCE) $(CLDFLAGS) $(CINC) $(CFLAGS) $(FOAMINC) 

用gcc连接-o后面的所有文件生成→依赖于$OBJECT和MPPICFoam.o的$(EXE)执行文件

-g 可执行程序包含调试信息 ,生成供调试用的可执行文件,可以在gdb中运行。

-c 只编译不链接:产生.o文件,就是obj文件,不产生执行文件(EXE文件)(c : compile)。

没有-o -g -c则自动生成,./a.out

$@--目标文件,$^--所有的依赖文件,$<--第一个依赖文件

$(CINC) $(CFLAGS)在这个代码里面它们是各种-参数,好像是各种路径,$(FOAMINC)是各种include文件

CFLAGS: 指定头文件(.h文件)的路径,如:CFLAGS=-I/usr/include -I/path/include。同样地,安装一个包时会在安装路径下建立一个include目录,当安装过程中出现问题时,试着把以前安装的包的include目录加入到该变量中来。 

LDFLAGS:gcc 等编译器会用到的一些优化参数,也可以在里面指定库文件的位置。用法:LDFLAGS=-L/usr/lib -L/path/to/your/lib。每安装一个包都几乎一定的会在安装目录里建立一个lib目录。如果明明安装了某个包,而安装另一个包时,它愣是说找不到,可以输那个包的lib路径加入的LDFALGS中试一下。 

LIBS:告诉链接器要链接哪些库文件,如LIBS = -lpthread -liconv 

简单地说,LDFLAGS是告诉链接器从哪里寻找库文件,而LIBS是告诉链接器要链接哪些库文件。不过使用时链接阶段这两个参数都会加上,所以你即使将这两个的值互换,也没有问题。 

MPPICFoam.o: MPPICFoam.cpp
	$(CC) -o MPPICFoam.o -g -c MPPICFoam.cpp $(CINC) $(CFLAGS) $(FOAMINC)

编译MPPICFoam.cpp成MPPICFoam.o,只编译不链接,生成供调试用的可执行文件。

%.o: solid-phase/%.cpp
	$(CC) $(CINC) $(CFLAGS) -o $@ -g -c $^

编译solid-phase/%.cpp成%.o,只编译不链接,生成供调试用的可执行文件。

这两句都是编译依赖文件成目标文件。

gcc -I -L -l区别

我们用gcc编译程序时,可能会用到“-I”(大写i),“-L”(大写l),“-l”(小写l)等参数,下面做个记录:

例:

gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld

上面这句表示在编译hello.c时:

-I /home/hello/include表示将/home/hello/include目录作为第一个寻找头文件的目录,寻找的顺序是:/home/hello/include-->/usr/include-->/usr/local/include

-L /home/hello/lib表示将/home/hello/lib目录作为第一个寻找库文件的目录,寻找的顺序是:/home/hello/lib-->/lib-->/usr/lib-->/usr/local/lib

 -lworld表示在上面的lib的路径中寻找libworld.so动态库文件(如果gcc编译选项中加入了“-static”表示寻找libworld.a静态库文件)

编译器版本查询:

【转载】C/C++完整编译过程详解 (拿来读懂makefile文件)_第2张图片

makefile中“=”和“:=”的区别到底有什么区别

之前一直纠结makefile中“=”和“:=”的区别到底有什么区别,因为给变量赋值时,两个符号都在使用。网上搜了一下,有人给出了解答,但是本人愚钝,看不懂什么意思。几寻无果之下,也就放下了。今天看一篇博客,无意中发现作者对于这个问题做了很好的解答。解决问题之余不免感叹,有时候给个例子不就清楚了吗?为什么非要说得那么学术呢。^_^

      1、“=”

      make会将整个makefile展开后,再决定变量的值。也就是说,变量的值将会是整个makefile中最后被指定的值。看例子:

            x = foo
            y = $(x) bar
            x = xyz

      在上例中,y的值将会是 xyz bar ,而不是 foo bar 。

      2、“:=”

      “:=”表示变量的值决定于它在makefile中的位置,而不是整个makefile展开后的最终值。

            x := foo
            y := $(x) bar
            x := xyz

      在上例中,y的值将会是 foo bar ,而不是 xyz bar 了。

using namespace std

关于iostream与using namespace std 的解析 - 知乎 (zhihu.com)

关于iostream与using namespace std 的解析

(1)通过关于命名空间的定义及使用的介绍,我们不难发现:不同的命名空间之间是相互独立的个体,虽然附加在其中的变量名可能是相同的(比如上面所提到的命名空间A、B、C中都包含有变量a),但是没关系:“命名空间” 这层 “屏障”将这些相同的变量名分隔开来,让他们虽然拥有相同的名字,但是互不影响。

(2)看起来命名空间的引入十分方便,让我们不必再因为变量重名而烦恼。但是在以前,并没有命名空间这个概念,而是将标准库功能定义在全局空间里,并声明在(早期的C++头文件)中。但是由于标准库非常的庞大,那么程序员在选择的类的名称或函数名时,就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都放在一个名为std的命名空间中。后来C++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h,即出现了现在的头文件。

(3)当使用的时候,该头文件没有定义全局命名空间,必须使用C++所规定的标准的命名空间(即:namespace std),这样才能正确使用cout、endl等功能。

#ifdef和#pragma once

#pragma once用法总结_fanyun的博客-CSDN博客_#pragma once

为了避免同一个文件被include多次

1   #ifndef方式
2   #pragma once方式在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别。

    方式一:

    #ifndef __SOMEFILE_H__
    #define __SOMEFILE_H__
    ... ... // 一些声明语句
    #endif

    方式二:

    #pragma once
    ... ... // 一些声明语句

    #ifndef的方式依赖于宏名字不能冲突,这不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件不会被不小心同时包含。当然,缺点就是如果不同头文件的宏名不小心“撞车”,可能就会导致头文件明明存在,编译器却硬说找不到声明的状况

    #pragma once则由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。带来的好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,重复包含更容易被发现并修正。

   方式一由语言支持所以移植性好,方式二 可以避免名字冲突
————————————————
版权声明:本段为CSDN博主「老樊Lu码」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/fanyun_01/article/details/77413992

你可能感兴趣的:(自学,c++,c语言,linux,openfoam)