当前位置: 首页 > news >正文

Linux——gcc编译过程详解与ACM时间和进度条的制作

gcc编译过程详解与ACM时间和进度条的制作

文章目录

  • gcc编译过程详解与ACM时间和进度条的制作
    • 1. 编译详细过程与ACM时间
      • 1.1 预处理
      • 1.2 编译
      • 1.3 汇编
      • 1.4 链接
      • 1.5 C语言和C++在函数符号形成上的区别
      • 1.6 编译器与ACM时间的关系
    • 2. 进度条的制作
      • \r
      • C语言标准输出的缓冲区
      • sleep()和usleep()
      • 倒计时
      • 简单的进度条
      • 带颜色的进度条

1. 编译详细过程与ACM时间

我们知道编译分为四大步:预处理、编译、汇编、链接,但是详细的情况并不像我们想的那样,因为编译器会做很多事情,比如:宏替换、头文件展开、语法检查、语义检查、代码优化、生成汇编代码、生成可执行文件等。

1.1 预处理

gcc -E main.c -o main.i

解析:

  • gcc:编译器
  • -E:将源代码文件进行预编译处理,形成.i文件
  • main.c:源代码文件
  • -o:指定目标文件的名称或者存放路径
  • main.i:预处理后的目标文件

预处理主要完成以下工作:

  1. 头文件展开
  • 处理所有#include预处理指令
  • 将头文件的内容复制到当前文件
  • 可以递归展开(头文件中包含的头文件)
  1. 宏定义替换
  • 展开所有#define宏定义
  • 处理条件编译指令(#if、#ifdef、#ifndef等)
  • 展开所有宏调用
#define Max(a, b) ((a) > (b) ? (a) : (b))
int max = Max(3, 4); //展开为:int max = ((3) > (4) ? (3) : (4))
  1. 条件编译处理

    • #if、#ifdef、#ifndef:条件判断
    • #elif、#else:分支处理
    • #endif:结束条件编译
    #ifdef DEBUG
    printf("debug info\n");
    #endif
    
  2. 删除注释

    • 删除所有//和/* */格式的注释
    • 每个注释都替换为一个空格
  3. 添加行号和文件名标识

    • 使用#line指令标记行号和文件名
    • 用于编译器产生调试信息和编译错误提示
  4. 处理特殊预处理指令

    • #pragma:编译器指令
    • #error:产生编译错误
    • #warning:产生编译警告
  5. 保留所有的换行符

    • 确保错误提示的行号正确
    • 方便调试
  6. 字符串常量化

    • 处理#运算符,将宏参数转换为字符串
    #define STR(s) #s
    STR(hello)  // 展开为: "hello"
    
  7. 宏连接操作

    • 处理##运算符,连接两个记号
    #define CONCAT(a,b) a##b
    CONCAT(x,y)  // 展开为: xy
    

示例:

// 源文件 main.c
#include <stdio.h>
#define MAX 100
#define SQUARE(x) ((x)*(x))int main() {int value = SQUARE(MAX);return 0;
}// 预处理后 main.i(简化版)
// ... stdio.h的内容 ...
int main() {int value = ((100)*(100));return 0;
}

预处理的作用:

  1. 将源文件转换为完整的C/C++代码
  2. 处理编译器不关心的细节
  3. 提供代码复用和条件编译的机制
  4. 简化程序的编写和维护

注意事项:

  1. 预处理是编译的第一步,不检查语法
  2. 宏定义要注意括号的使用
  3. 头文件要防止重复包含
  4. 条件编译要注意匹配
  5. 预处理指令必须独占一行
  6. 宏不会检查类型,因此一定要注意类型匹配

1.2 编译

gcc -S main.i -o main.s

解析:

  • gcc:编译器
  • -S:将预处理后的文件进行编译处理,形成.s文件
  • main.i:预处理后的目标文件
  • -o:指定目标文件的名称或者存放路径
  • main.s:编译后的目标文件

编译主要完成以下工作:

  1. 词法分析

    • 将源代码分解成记号(token)
    • 识别关键字、标识符、常量、运算符等
    int main() { return 0; }
    // 分解为: int(关键字) main(标识符) ((符号) )(符号) {(符号) 
    //         return(关键字) 0(常量) ;(符号) }(符号)
    
  2. 语法分析

    • 根据语法规则分析记号序列
    • 构建抽象语法树(AST)
    • 检查语法错误
    if (x > 0) { y = 1; }
    // 构建为:
    //   if
    //  /  \
    // >    =
    // / \  / \
    // x 0  y 1
    
  3. 语义分析

    • 类型检查
    • 变量声明检查
    • 类型转换
    • 检查语义错误
    int x;
    float y = x + 1.5;  // 需要将x从int转换为float
    
  4. 中间代码生成

    • 生成平台无关的中间表示(IR)
    • 方便后续优化
    // 源代码
    x = a + b * c;// 中间代码(三地址码形式)
    t1 = b * c
    t2 = a + t1
    x = t2
    
  5. 代码优化

    • 常量折叠
    int x = 3 + 4;  // 优化为: int x = 7;
    
    • 死代码消除
    if (0) {        // 这段代码永远不会执行x = 1;      // 可以被消除
    }
    
    • 循环优化
    // 循环展开
    for (i=0; i<2; i++) { a[i] = i; }
    // 优化为:
    a[0] = 0;
    a[1] = 1;
    
    • 公共子表达式消除
    // 原代码
    x = a + b;
    y = (a + b) * c;
    // 优化后
    t = a + b;
    x = t;
    y = t * c;
    

    编译可以带上优化选项来指定想要优化的级别,如:gcc -O2 main.c -o main,-O2表示中等优化,-O3表示最大优化。

  6. 目标代码生成

    • 生成特定平台的汇编代码
    • 考虑目标机器的特性
    • 寄存器分配
    # x86汇编示例
    movl    $1, %eax    # 将1移动到eax寄存器
    addl    %ebx, %eax  # 将ebx的值加到eax
    

注意事项:

  1. 编译错误必须全部修复才能继续
  2. 警告可以忽略但最好处理
  3. 优化可能改变代码行为
  4. 不同编译器可能有不同结果
  5. Debug版本应该使用-O0

1.3 汇编

gcc -c main.s -o main.o

解析:

  • gcc:编译器
  • -c:将汇编代码转换为目标文件
  • main.s:汇编代码文件
  • -o:指定输出文件
  • main.o:目标文件

汇编阶段主要工作:

  1. 指令转换

    • 将汇编指令转换为机器码
    • 生成目标文件(二进制格式)
    # 汇编代码
    movl $1, %eax
    # 转换为机器码(十六进制)
    # B8 01 00 00 00
    
  2. 符号表生成

    • 记录全局符号
    • 记录未解析的外部符号
    • 记录调试信息
  3. 重定位表生成

    • 标记需要重定位的地址
    • 为链接阶段做准备

1.4 链接

gcc main.o -o main

链接阶段主要工作:

  1. 符号解析

    • 解析所有外部符号引用
    • 检查符号重定义
    • 建立全局符号表
  2. 重定位处理

    • 计算符号的最终地址
    • 修正代码中的地址引用
    • 合并各个段(代码段、数据段等)
  3. 生成可执行文件

    • ELF格式(Linux)
    • PE格式(Windows)
    • Mach-O格式(macOS)

注意事项:

  1. 静态链接和动态链接的选择
  2. 注意符号冲突
  3. 库的链接顺序很重要
  4. 链接错误通常与符号解析相关

1.5 C语言和C++在函数符号形成上的区别

C语言是没有函数重载的,因此不存在重名函数,那么一个函数名就可以表示一个唯一的函数,在建立符号表时,一个函数名就对应一个符号,因此链接时不会出现符号冲突。
C++是有函数重载的,因此存在重名函数,在建立符号表时,多个函数名可以对应一个符号,因此链接时会出现符号冲突。
那么该如何解决呢?
函数重载,它要求一个函数名可以有多个不同的定义,但是这些定义的参数类型不能完全相同,因此C++编译器在链接时会对函数名进行改编,在函数名后加上函数参数类型,这样就可以区分重名函数,从而解决符号冲突。
比如:

void f(int, int);
void f(double, double);
// 编译后
// f_i_i
// f_d_d

各种参数类型的符号:

  • v - void
  • b - bool
  • c - char
  • a - signed char
  • h - unsigned char
  • s - short
  • t - unsigned short
  • i - int
  • j - unsigned int
  • l - long
  • m - unsigned long
  • x - long long
  • y - unsigned long long
  • f - float
  • d - double
  • e - long double
  • z - … (varargs)

复合类型的符号:

  • P - 指针 (pointer)
  • R - 引用 (reference)
  • K - const
  • V - volatile
  • A - 数组 (array)

示例:

void func(int);           // _Z4funci
void func(int*);          // _Z4funcPi
void func(const int);     // _Z4funcKi
void func(int&);          // _Z4funcRi
void func(const int*);    // _Z4funcPKi
void func(int* const);    // _Z4funcKPi
//_Z是C++编译器改编符号的标志,4表示函数名长度

1.6 编译器与ACM时间的关系

当我们使用makefile管理项目时,makefile中会有一个all目标,它通常会调用所有的编译命令,根据依赖文件,然后逐步的将源代码文件预编译、编译、汇编形成.o文件,最后链接生成可执行文件。
但是这样就会引申出一个问题,我们在链接阶段,实际上就是将.o文件链接到一起,可以每次我们修改代码,并不一定所有的源代码文件都会修改,那么那些没有修改过的源文件最终预编译编译汇编形成的.o文件和之前完全一样,这样链接时就会造成很多不必要的开销。
那么该如何解决呢?
这时我们就要了解一个概念——ACM(Access Time、Modify Time、Create Time)。
对于任何一个文件来说,它都有三个时间戳:

  • 创建时间(Create Time):文件被创建的时间
  • 修改时间(Modify Time):文件内容被修改的时间
  • 访问时间(Access Time):文件被访问的时间

创建时间,顾名思义,就是文件被创建的时间,这个时间戳在文件创建时被设置,并且在文件的整个生命周期中保持不变。
修改时间,这个时间戳在文件内容被修改时被设置,比如我们使用vim编辑一个文件时,每敲一个键,这个时间戳就会被更新。
访问时间,这个时间戳在文件被访问时被设置,比如我们使用ls查看一个文件时,这个时间戳就会被更新。

由于.o文件是由源代码文件编译形成的,因此.o文件的创建时间总是比源代码文件的修改时间要早,且修改时间一开始就是自己的创建时间。
而当我们修改源代码文件时,源代码文件的修改时间就会被更新,而.o文件的修改时间不会被更新,此时.c的修改时间就要比.o的创建时间要早,这就表示.o文件需要重新编译。
所以,当编译器在进行编译时,如果检查到源代码文件的修改时间比.o文件的修改时间要早,那么就会重新编译源代码文件形成新的.o文件,反之则不会重新编译,使用修改过的.o文件和没修改的.o文件链接生成可执行文件,大大节省了时间。

2. 进度条的制作

在学习制作进度条之前,我们需要认识一些概念:

\r

在C语言中,我们用的最多的转义字符大概就是\n了,\n叫做换行符,作用是将光标移动到下一行,而\r是回车符,作用是将光标移动到当前行的行首。(但是终端在解释的时候会让\n同时拥有回车换行的功能)

C语言标准输出的缓冲区

当我们使用printf输出时,我们认为调用完函数就立即输出到终端,但实际上是先输出到一个语言层面的缓冲区(非系统层面),然后缓冲区再刷新到终端。
一般来说,有两种缓冲区模式:行缓冲和全缓冲
对于行缓冲来说,当遇到\n时,缓冲区就会刷新;
而对于全缓冲来说,当缓冲区满时,缓冲区才会刷新;
而标准输出默认是行缓冲。
因此,如果我们想立即看到输出,可以使用\n刷新缓冲区,或者使用fflush(stdout)刷新缓冲区。
由于我们想在终端的同一行看到输出,所以我们采用fflush(stdout)刷新缓冲区。
而对于\r来说,虽然它不能刷新缓冲区,但是我们可以让光标回到行首,然后输出新的内容,这样就实现了终端上覆盖的效果。

sleep()和usleep()

这两个函数是C语言标准库提供的,用于使程序休眠。

  1. sleep(int n):使系统休眠n秒。
  2. usleep(int n):使系统休眠n微秒

如果我们不想让进度条刷新的太快,就可以使用这两个函数进行控制。

倒计时

#include<stdio.h>
#include<unisted.h>int main()
{int n = 10;while(n){printf("%-2d\r", n);n--;fflush(stdout);sleep(1);}return 0;
}  

简单的进度条

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define bar '#'#define MaxSize 101
int main()
{printf("简单的进度条:\n");char arr[MaxSize];char spindle[] = {'|', '/', '-', '\\'};memset(arr, '\0', MaxSize);int i = 0;while (i < MaxSize){printf("[%-100s][%-3d%%][%c]\r", arr, i, spindle[i % 4]);arr[i++] = bar;fflush(stdout);usleep(100000);}printf("\n");return 0;
}       

带颜色的进度条

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define bar '#'#define MaxSize 101
int main()
{srand((unsigned)time(NULL));printf("简单的进度条:\n");char arr[MaxSize];char spindle[] = {'|', '/', '-', '\\'};memset(arr, '\0', MaxSize);int i = 0;while (i < MaxSize){printf("\033[%d;%dm[%-100s]\033[0m\033[40;%dm[%-3d%%]\033[0m\033[40;%dm[%c]\033[0m\r", rand() % 10 + 40, rand() % 10 + 30, arr, rand() % 10 + 30, i, rand() % 10 + 30, spindle[i % 4]);arr[i++] = bar;fflush(stdout);usleep(100000);}printf("\n");return 0;
}

http://www.mrgr.cn/news/70472.html

相关文章:

  • 面试经典 150 题:205,55
  • win10局域网加密共享设置
  • Flink CDC的安装配置
  • 【深度学习之回归预测篇】 深度极限学习机DELM多特征回归拟合预测(Matlab源代码)
  • 泥石流灾害风险评估与模拟丨AI与R语言、ArcGIS、HECRAS融合,提升泥石流灾害风险预测的精度和准确性
  • 【UE5】使用基元数据对材质传参,从而避免新建材质实例
  • 【SpringMVC】基础入门(1)
  • HTTP TCP三次握手深入解析
  • 排序算法(2)
  • 【Linux】网络编程2
  • mysql中数据不存在却查询到记录?
  • 数学与统计计算:Python math 与 statistics库基础教程
  • ima.copilot-腾讯智能工作台
  • Android 各个版本授予应用信息权限及单次弹窗确认权限
  • 每日算法练习
  • 海南华志亿星电子商务有限公司是真的吗?
  • 如何加密源代码?十条策略教你源代码防泄漏
  • 三种读取配置文件的方式
  • 基于卷积神经网络的桃子叶片病虫害识别与防治系统,vgg16,resnet,swintransformer,模型融合(pytorch框架,python代码)
  • Linux网络的基本设置
  • 为什么白帽SEO比黑帽SEO更值得坚持?
  • 大顶堆的基本操作
  • vivado+modelsim: xxx is not a function name
  • 吃透红利!AI绘画变现方法汇总|变现方式:哪一种最适合你?方法加实践,小白也能上手赚钱!
  • 创新体验触手可及 紫光展锐携手影目科技推出AI眼镜开放平台
  • 软件测试基础二十一(接口测试 数据库相关)