1. 预处理概述

C 语言代码编译流程为 预处理 --> 编译 --> 汇编 --> 链接。预处理是整个程序编译流程的开始,针对于预处理过程,重点关注

  • 多文件

2. 编译流程

  • 预处理:将 C 语言文件进行预处理操作,主要是完成宏替换,条件编译控制,确定整个代码中参与编译的所有内容准备就绪。

    gcc -E test.c -o test.i
  • 编译: 将预处理之后 C 语言代码 .i 文件通过编译操作,转换为汇编助记符。

    gcc -S test.i -o test.s
  • 汇编: 将汇编之后 .s 文件,利用汇编技术,基于当前 CPU 支持的指令方式,转换为对应的二进制可执行指令。

    gcc -c test.s -o test.o
  • 链接: 将汇编之后 .o 二进制可执行指令,打包为目标可执行文件

    gcc test.o -o a.out

3. 宏

3.1 无参数宏

对应宏常量,定义需要使用 #define 固定格式,提供对应宏名称和当前宏对应的数据,在开发中使用到宏常量的位置,会在预处理阶段直接替换内容。

#include <stdio.h>

/*
当前定义了一个宏常量
    宏名称 PI 
    宏对应的数据为 3.1415926

开发中使用到当前 PI 的位置,在预处理阶段全部替换为 3.1415926
预处理过程中不会对宏替换操作进行任何的语法判断,使用位置直接替换
为目标数据,所以要求
    【宏常量后续数据内容不允许有分号】
*/
#define PI 3.1415926

int main(int argc, char const *argv[])
{
    double ret = PI * 3 * 3;
    printf("ret : %f\n", ret);

    ret = 4 / 3 * PI * 3 * 3 * 3;
    printf("ret : %f\n", ret);

    return 0;
}

预处理操作

gcc -E 02-宏常量.c -o 02-宏常量.i

# 15 "02-宏常量.c"
int main(int argc, char const *argv[])
{
    double ret = 3.1415926 * 3 * 3;
    printf("ret : %f\n", ret);
​
    ret = 4 / 3 * 3.1415926 * 3 * 3 * 3;
    printf("ret : %f\n", ret);
​
    return 0;
}

3.2 有参数宏

有参数宏使用需要小心一点,避免影响代码的正常逻辑。

 

#include <stdio.h>

/*
有参数宏,定义格式类似于函数,但是缺少
    1. 缺少明确的返回值类型
    2. 缺少明确的小括号中参数数据类型告知
    3. 后续内容缺少大括号

使用位置依然按照基本的替换规则完成。在之前的开发手段中,
有参数宏用于代码中的 DEBUG 错误信息获取,错误 LOG 获取
*/
#define ADD(a, b) (a + b)

int main(int argc, char const *argv[])
{
    printf("ADD(10, 20) : %d\n", ADD(10, 20));
    
    /*
    有参数宏声明 
        #define ADD(a, b) a + b
        ADD(5, 2) * 3 替换结果为
            5 + 2 * 3 ==> 11
    
    有参数宏声明 
        #define ADD(a, b) (a + b)
        ADD(5, 2) * 3 替换结果为
            (5 + 2) * 3 ==> 21
    */
    printf("ADD(5, 2) * 3 : %d\n", ADD(5, 2) * 3);

    printf("ADD(5.5, 3.3) : %lf\n", ADD(5.5, 3.3));
    
    return 0;
}

 预处理操作

gcc -E 03-有参数宏.c -o 03-有参数宏.i

 

# 2 "03-有参数宏.c" 2
# 13 "03-有参数宏.c"

# 13 "03-有参数宏.c"
int main(int argc, char const *argv[])
{
    printf("ADD(10, 20) : %d\n", 10 + 20);
# 28 "03-有参数宏.c"
    printf("ADD(5, 2) * 3 : %d\n", 5 + 2 * 3);

    printf("ADD(5.5, 3.3) : %lf\n", 5.5 + 3.3);

    return 0;
}

4. 条件编译

用于控制代码中哪些代码内容参与整个代码执行,同时可以根据代码执行过程中条件情况,控制后续代码的执行效果。主要包括

  • #if 包括 #else #elif

  • #ifndef

  • #ifdef

  • #undef

4.1 #if 条件编译

#include <stdio.h>

#define FLAG 2

int main(int argc, char const *argv[])
{
/*
条件编译控制结构必须有 #endif 结尾,控制内容是从
#if #ifndef #ifdef 到 #endif 之间内容。
*/
#if FLAG
    printf("中午吃热干面!\n");
    printf("中午吃盖浇饭!\n");
#endif

#if FLAG
    printf("中午吃螺蛳粉!\n");
#else
    printf("中午吃饭大盘鸡!\n");
#endif

#if FLAG == 0
    printf("中午吃黄焖鸡米饭!\n");
#elif FLAG == 1
    printf("中午吃小炒肉!\n");
#else
    printf("中午吃烩菜!\n");
#endif

    return 0;
}

4.2 #ifdef 和 #ifndef 条件编译

#include <stdio.h>

/*
#ifndef 和 #ifdef 主要控制的数据为对应的宏是否存在

#ifndef + #else 结构
    如果目标宏存在,#else 内容参与编译
    如果目标宏不存在,#ifndef 内容参与编译 

#ifdef + #else 结构
    如果目标宏存在,#ifdef 内容参与编译
    如果目标宏不存在,#else 内容参与编译 
*/

#define FLAG

int main(int argc, char const *argv[])
{
#ifdef FLAG
    printf("中午吃方中山胡辣汤 + 油饼 + 肉盒\n");
#else
    printf("中午吃焖面!\n");
#endif

#ifndef FLAG
    printf("中午吃驴肉火烧!\n");
#else
    printf("灵宝肉夹馍!\n");
#endif
    
    return 0;
}

4.3 #undef

#include <stdio.h>

#define FLAG

int main(int argc, char const *argv[])
{



#ifdef FLAG
    printf("俄乌战争即将结束!\n");
#else
    printf("世界和平!\n");
#endif

/*
在代码中取消对应宏定义!
*/
#undef FLAG

    return 0;
}

5. 多文件编程【重点】

5.1 多文件的要求

C/C++ 中多文件编程是重要的技术手段,可以利用多文件方式对项目中的模块,工具,进行分割!!!实现模块化开发,从而满足【高内聚,低耦合】。需要使用 【头文件/H文件】和【实现文件/C文件】

5.2 头文件

要求

  • 头文件和 C 文件一一对应

  • 头文件名称和 C 文件名称一致,例如 student.h student.c

  • 头文件的标准格式

#ifndef 宏名称 // 对应当前文件的名称,例如 文件名称为 student.h,对应宏名称为 _STUDENT_
#define 宏名称 // 对应当前文件的名称,例如 文件名称为 student.h,对应宏名称为 _STUDENT_
​
#endif

案例

student.h

#ifndef _STUDENT_
#define _STUDENT_
​
// 内容
​
#endif

代码引入

#include "student.h"
#include "student.h"
​
// 代码

预处理之后,头文件中的内容替换调用 #include "student.h"

#ifndef _STUDENT_
#define _STUDENT_
​
// 内容
​
#endif
#ifndef _STUDENT_
#define _STUDENT_
​
// 内容
​
#endif
​
// 代码
5.3 案例

头文件中主要内容

  • 类型声明

  • 宏定义

  • 函数声明

C 文件中的主要内容

  • 对应头文件函数实现

头文件内容

#ifndef _TEST_
#define _TEST_

/*
因为对应 C 文件,代码实现过程中,需要使用其他资源,需要
导入必要的头文件,一般会在对应的 .h 文件中完成

例如: 当前 test 函数需要使用 printf 函数,必须导入
    <stdio.h> 头文件,要求 .h 文件完成
*/
#include <stdio.h>

/*
头文件内容主要有
    1. 类型声明
    2. 宏定义
    3. 函数声明
*/

/*
类型声明,当前定义了一个 struct student 类型
*/
struct student 
{
    char name[32];
    int id;
    short age;
    char gender; 
};

/*
宏定义
*/
#define VALUE 10
#define ADD(a, b) (a + b)

/**
 * 函数声明,测试 test 函数
 */
void test();

#endif

test.c

/*
首先,test.c 需要导入对应 test.h 头文件
格式常用
    #include "test.h"
    "" 首先在当前工作目录下搜索目标头文件,如果未找到,直接去往系统指定
        路径搜索目标头文件
    <> 直接在系统指定路径搜索目标头文件

tips:
    多文件操作头文件涉及到路径问题。
    例如 项目文件夹存在子文件夹 
        h 文件夹 c 文件夹。
    所有的 h 文件都在 h文件夹中,导入目标头文件需要
        #include "../h/xxx.h"
*/
#include "test.h"

void test()
{
    printf("测试!\n");
}

 main_project.c

#include <stdio.h>

/*
导入自定义头文件,当前 main 函数可以使用头文件中声明
的相关内容
    1. 数据类型
    2. 宏
    3. 函数
*/
#include "test.h"

int main(int argc, char const *argv[])
{
    struct student stu1 = {"James", 23, 40, 'M'};

    printf("ID : %d, Name : %s, Age : %d, Gender : %c\n",
           stu1.id, stu1.name, stu1.age, stu1.gender);

    test();

    printf("VALUE : %d\n", VALUE);
    printf("ADD(5, 3) : %d\n", ADD(5, 3));

    return 0;
}

编译操作和运行结果

 qf@qf:~/桌面/Code/Day11/02-多文件编程/02-多文件案例模版$ gcc main_project.c test.c 
    # 要求所有参与代码执行的 .c 文件参与代码编译过程,.h 文件不需要参与
qf@qf:~/桌面/Code/Day11/02-多文件编程/02-多文件案例模版$ ./a.out 
ID : 23, Name : James, Age : 40, Gender : M
测试!
VALUE : 10
ADD(5, 3) : 8

 

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐