一.编译过程

1.概述

预处理:包括预编译、编译、汇编、链接

  • 1.预编译(不进行语法检查)

    .c中的头文件展开、宏展开生成.i文件

  • 2.编译

    将预处理之后的.i文件生成.s汇编文件

  • 3.汇编

    .s汇编文件生成.o目标文件

  • 4.链接

    .o文件链接成目标文件

2.Linux下GCC编译器编译过程

(1) 单文件程序编译

**假设:**要编译的时hello.c这个C语言程序

1.预编译:

gcc -E hello.c -o hello.i

2.编译:

gcc -S hello.i -o hello.s

3.汇编:

gcc -c hello.s -o hello.o

4.链接

目标名可以随便起,不需要后缀

gcc  hello -o hello_elf

(2)多文件程序编译

注意:多文件链接必须有包含main方法的文件,否则程序会因为找不到主入口二无法运行。

① 概述

1.对所有.c文件进行预处理:

先对所有的.c文件进行预编译、编译、汇编、链接操作,得到所有程序文件的.o文件

2.链接阶段,链接所有.o文件:

在链接阶段,在输出那里填入所有涉及的程序的.o文件,然后进行链接即可

② 概述

例如要链接main.c、hello.c、test.c这3个c文件,对应命令如下(省略了编译的命令)

gcc main.o hello.o test.o -o test

二.相关指令

1.#include

(1)概述

#include指令用于告诉编译器在实际编译之前要包含标准输入输出头文件,

编译器会在预编译的时候将#include指令所处位置替换成头文件的内容

(2)参数

注意:

  • include虽然可以包.c文件,但不要包,容易导致函数重复定义
  • 该指令后面不要加分号;

用**尖括号<>**包裹头文件:编译器找头文件时,只会去系统指定路径找,不会在本地找

用**双引号""**包裹头文件:编译器会先在当前目录找头文件,找不到才回去系统指定路径找

(3)示例

头文件: max.h

int max(int x,int y);

调用文件: main.c

#include "max.h"
int main(){
    int num;
    num = max(10,20);
    retrun 0;
}

预处理结果:

#include指令所处位置替换成头文件的内容

int max(int x,int y);
int main(){
    int num;
    num = max(10,20);
    retrun 0;
}

2.#define

(1)概述

用来进行宏定义的指令,使用宏定义的位置会在预编译时期替换成宏所指代的内容

(2)分类

① 不带参宏

在预编译后,所有使用到宏的地方,都会被替换成其指向的值

Ⅰ.格式

注意:

  • 宏定义后面不加分号;
  • 宏名称用大写
#define 宏名称 宏指向的值
Ⅱ.优点
  • 使程序更见见名知意
  • 方便后期宏指向数据的修改
Ⅲ.例子

预编译前

使用宏定义了PI

#define PI 3.1415926
int main(){
    double f;
    printf("%lf\n"PI);
    f = PI;
    retrun 0;
}

预编译后:

在预编译后,所有使用到PI的地方,都会被替换成其指向的值

int main(){
    double f;
    printf("%lf\n"3.1415926);
    f = 3.1415926;
    retrun 0;
}
② 带参宏

在预处理的时候,参数列表内的值会替换宏体内的变量的到一个"字符串",程序中所有用到宏的地方,都会被这个字符串所替换

Ⅰ.格式

参数不用加数据类型

  • 宏名称 为宏定义的名称
  • 参数列表 是用逗号分隔的参数列表
  • 宏体 是宏的功能代码块
#define 宏名称(参数列表) 宏体
Ⅱ.相关问题

问题:

宏,归根结底就是替换,如果给带参宏传递表达式,就很可能会造成问题

例如:带参宏是#define S(a,b) a*b,使用时写成了S(2+4,3)

期望: 6 * 3 =18

结果: 2+4*3=2+12=14

**解决:**为宏体内每一个参数都加括号,保证运算顺序不因传入值是式子而改变

修改:#define S(a,b) (a)*(b)

Ⅲ.带参宏和带参函数的区别

带参宏:

  • 节省时间,浪费空间

  • 形参无数据类型

带参函数:

  • 浪费时间,节约空间

  • 形参有数据类型

Ⅳ.例子

编译前:

#define S(a,b) a*b

int main(){
    int num;
    num = S(2,4);
    return 0;
}

预编译后:

预编译时代码中用到宏的地方会被宏体替换

int main(){
    int num;
    num = 2*4;
    return 0;
}

(3)宏的终止

定义的宏不做其它操作一般是自上到下全文生效的,如果想要在代码的中间结束宏,可以使用undef指令来终止宏的定义范围

① 格式
undef 宏名称
② 示例
#define PI 3.1415926
int main(){
    double f;
    printf("%lf\n"PI);//3.1415926
    
    undef PI;
    f=PI;//编译到这里会直接报错
    return 0;
}

三.选择性编译

1.根据宏定义编译

(1)已定义就编译 ifdef

① 概述

当设置的条件宏被定义后就指定代码段,反之不执行

② 格式
  • 和 if else 语句是有区别的,if else 语句都会被编译,通过条件选择性执行代码而 选择性编译,只有一块代码被编译

  • 它不能加花括号{},切记!!!

如果条件宏是被定义的就会执行代码段一,如果是没有被定义的,就会执行代码段二

#ifdef 条件宏
	代码段一
#else
    代码段二
#endif
③ 示例

因为AAA宏已经被定义,所以此时只会打印helloWorld

#define AAA

int main() {
#ifdef AAA
    printf("helloWorld\n");
#else
    printf("helloChina\n");//因为上面的条件宏AAA已经被定义了,所以这里不会被编译
#endif
    return 0;
}

(2)未定义就编译 ifndef

① 概述

刚好和上面那个反着来

当设置的条件宏未被定义后就指定代码段,反之不执行

② 格式

如果条件宏是未被定义的就会执行代码段一,如果是已被定义的,就会执行代码段二

#ifndef 条件宏
    程序段一
#else
    程序段二
#endif
    return 0;
}
③ 示例

因为AAA宏已经被定义,所以此时只会打印helloChina

#define AAA

int main() {
#ifndef AAA
    printf("helloWorld\n");
#else
    printf("helloChina\n");//因为上面的条件宏AAA已经被定义了,所以这里会被编译
#endif
    return 0;
}
④ 应用

**应用:**防止头文件重复包含

**原理:**第一次引用头文件时,假设__FUN_H__尚未被定义,那么整个#ifndef区块下面的内容都会被编译进去。同时,#define __FUN_H__ 这一行会定义__FUN_H__,这样下次引用时就因为已定义了该宏,而不再进入#ifndef的区块,进入达到方法头文件重复调用而导致的重复展开问题

示例头文件:

#ifndef __FUN_H__ 
#define __FUN_H__
extern int fun(int x,int y);
#endif

2.据真假定编译

多用于软件裁剪,根据情况决定是某编译实现某部分功能(付费功能😂)

(1)概述

顾名思义,就是根据条件的真假判断要编译那个代码段

(2)格式

如果表达式为真,编译第一段代码,否则编译第二段代码

#if 表达式
	程序段一
#else
    程序段二
#endif

(3)示例

当if后是1(真)时,就执行第一个代码块,当if后是0(假)时执行第二个代码块

int main() {
#if 1
    printf("helloWorld\n");
#else
    printf("helloChina\n");//因为上面的条件宏AAA已经被定义了,所以这里不会被编译
#endif
    return 0;
}

四.静态库

**库的作用:**主要还是用来替换明文的库文件来完成程序的编译,多用于商业目的的库文件提供

1.静/动态编译

(1)静态编译

① 特点

**特点:**编译的时候还会把静态库文件也一并打包到可执行文件中

② 优缺点

**缺点:**可执行文件体积较大(毕竟包含了库文件)

**优点:**运行时不依赖库文件,系统环境适应性高

③ 编译指令
gcc -static hello.c -o hello

(2)动态编译

① 特点

**特点:**编译时时不会把动态库文件打包编译到可执行程序中,其与库只是编译衔接的关系

② 优缺点

**缺点:**运行时需要链接库文件,如果运行环境没有库文件,就会运行失败

**优点:**可执行文件体积小

③ 编译指令
gcc hello.c -o hello

2.创建库文件

(1)静态库

① 静态库制作

① 先直接将库文件.c汇编成.o文件

gcc -c mylib.c -o mylib.o

② 执行指令将汇编的文件制作成静态库

静态库起名的时候必须以 lib 开头以.a 结尾

ar rc libtestlib.a mylib.o
② 静态库的使用

使用静态库.a替换明文代码的库文件.c进行编译

Ⅰ.编译方式一(文件在同目录下)

将头文件和库文件放到一个目录下

gcc -static mytest.c libtestlib.a -o mytest
Ⅱ.编译方式二(文件不在目录下)

**概述:**通过指定头文件及库文件的路径完成编译

**前件:**库函数、头文件假设在/home/edu 目录

-L:是指定库文件的路径的

-l(小L):是指定库名的,其值只要库文件名lib后面.a前面的部分

-I:指定头文件路径

gcc static mytest.c -o mytest -L/home/edu -ltestlib -I/home/edu

(3)动态库

① 动态链接库制作

直接使用下面的命令编译就好了

-shared:是动态库的标记

gcc -shared mylib.c -o libtestlib.so
② 动态库的使用

动态库编译完成使用时可能会报找不到库文件,此时需要手动将库文件设置成环境变量

Ⅰ.编译一(文件在同一目录)

库文件、头文件均在当前目录下

编译命令:

gcc mytest.c libtestlib.so -o mytets

设置环境变量:(在库文件所在文件夹运行)

export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
Ⅱ.编译二(文件不在目录)

整体编译使用和静态库一样

编译命令:

库函数、头文件假设在/opt 目录

gcc mytest.c -o mytest -L/home/edu -ltestlib -I/home/edu
最后修改:2024 年 03 月 11 日
如果觉得我的文章对你有用,请随意赞赏