【C语言】预处理的使用

news2024/11/13 9:55:16

预处理

  • 一、预处理-宏定义
    • 1、程序编译过程
      • (1) 编写源程序
      • (2) 程序编译过程说明
    • 2、预处理
    • 3、宏的概念
    • 4、无参宏
    • 5、带参宏
    • 6、带参宏的副作用
    • 7、宏定义中的符号粘贴
  • 二、预处理.条件编译
    • 1、无值宏定义
    • 2、条件编译
    • 3、条件编译的使用场景
  • 三、预处理.头文件
    • 1、头文件的作用
    • 2. 头文件的格式
    • 3. 头文件的内容
    • 4. 头文件的使用

一、预处理-宏定义

1、程序编译过程

(1) 编写源程序

在这里插入图片描述

(2) 程序编译过程说明

在这里插入图片描述
a、编译过程-预处理(cpp:Cpreprocesser:C的预处理器)

  • 说明:预处理程序对源文件中以字符#开头的命令进行处理,例如:#include命
    令后面的.h文件内容,嵌入到源程序文件中(对#做替换:头文件、宏,去掉注释,条件编译)
  • 注意:预处理程序的输出结果还是一个源程序文件,以.i为扩展名
  • 命令:
Ubuntu(x86-64):gcc hello.c -o hello.i -E  
开发板(ARM):  arm-linux-gcc hello.c -o hello.i -E
  • 现象:
    在这里插入图片描述

b、编译过程-编译(将C/C++源文件编译为汇编语言源文件)

  • 说明:编译程序对预处理后的源程序进行编译,生成一个汇编语言
    源程序文件,以.s为扩展名。
  • 注意:汇编语言与处理器的体系架构有关
  • 命令:
Ubuntu(x86-64): gcc hello.i -o hello.s -S
开发板(ARM):   arm-linux-gcc hello.i -o hello.s -S
  • 现象:

在这里插入图片描述

c、编译过程-汇编(as:assembly)

  • 说明:汇编程序对汇编语言源程序进行汇编,生成一个可重定位目
    标文件,以.o为扩展名,
  • 注意:目标文件是一个二进制文件,其中的代码已经是机器指
    令。代码、数据、或者其他信息使用二进制表示
  • 命令:
 Ubuntu(x86-64): gcc hello.s -o hello.o -C(注意:是大写c)
 开发板(ARM):    arm-linux-gcc hello.s -o hello.o -C(注意:是小写c)
  • 现象:
    在这里插入图片描述

d、编译过程-链接

  • 说明:链接程序将多个可重定位目标文件和函数库中的可重定位
    目标文件合并
  • 注意:成为一个可执行文件。可执行文件有elf、hex、bin等格式的。
  • 命令:
Ubuntu(x86-64): gcc hello.o -o hello
开发板(ARM):   arm-linux-gcc hello.o -o hello
  • 现象:
    在这里插入图片描述

注意:
1、如果想要和其他程序文件(如:main.c、nihao.c)进行编译,也需要和上述
步骤一致生成(main.o、nihao.o),因为这样后面更改程序时,只需要对其中
单个程序进行更改,而无需重新编译所有程序

2、预处理

在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的阶段统一由所谓预处理器(cpp)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括:
1.头文件:#include
2.定义宏:#define
3.取消宏:#undef
4.条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif
5.显示错误:#error
6.修改当前文件名和行号:#line
7.向编译器传送特定指令:#progma

  • 基本语法
    • 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠(\)连接成一个逻辑行
    • 预处理是整个编译全过程的第一步:预处(预处理指令)- 编译(C语言、C++) - 汇编(汇编语言) - 链接(将各个.o的可链接文件,汇总成一个可执行文件)
    • 可以通过如下编译选项来指定来限定编译器只进行预处理操作:
gcc example.c -o example.i -E

3、宏的概念

宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如:

#define PI 3.14

此处,PI 就是宏(宏一般习惯用大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如:

int main()
{
    printf("圆周率: %f\n", PI); 
    // 此语句将被替换为:printf("圆周率: %f\n", 3.14);
}
  • 宏的作用:
    • 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
    • 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。
    • 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。

4、无参宏

无参宏意味着使用宏的时候,无需指定任何参数,比如:

#define PI          3.14
#define SCREEN_SIZE 800*480*4 
int main()
{
    // 在代码中,可以随时使用以上无参宏,来替代其所代表的表达式:
    printf("圆周率: %f\n", PI); 
    mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, ...);
}

注意到,上述代码中,除了有自定义的宏,还有系统预定义的宏:

// 自定义宏:
#define PI          3.14
#define SCREEN_SIZE 800*480*4 

// 系统预定义宏
#define NULL ((void *)0)
#define PROT_READ    0x1    /* Page can be read.  */
#define PROT_WRITE    0x2    /* Page can be written.  */
#define MAP_SHARED    0x01    /* Share changes.  */

宏的最基本特征是进行直接文本替换,以上代码被替换之后的结果是:

int main()
{
    printf("圆周率: %f\n", 3.14); 
    mmap(((void *)0), 800*480*4, 0x1|0x2, 0x01, ...);
}

5、带参宏

带参宏意味着宏定义可以携带“参数”,从形式上看跟函数很像,例如:

#define MAX(a, b)   a>b ? a : b
#define MIN(a, b)   a<b ? a : b

以上的MAX(a,b) 和 MIN(a,b) 都是带参宏,不管是否带参,宏都遵循最初的规则,即宏是一段待替换的文本,例如在以下代码中,宏在预处理阶段都将被替换掉:

int main()
{
    int x = 100, y = 200;
    printf("最大值:%d\n", MAX(x, y));
    printf("最小值:%d\n", MIN(x, y));
    // 以上代码等价于:
    // printf("最大值:%d\n", x>y ? x : y);
    // printf("最小值:%d\n", x<y ? x : y);
}
  • 带参宏的特点:
    a.直接文本替换,不做任何语法判断,更不做任何中间运算。
    b.宏在编译的第一个阶段就被替换掉,运行中不存在宏。
    c.宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面又节约了切换时间。

6、带参宏的副作用

由于宏仅仅做文本替换,中间不涉及任何语法检查、类型匹配、数值运算,因此用起来相对函数要麻烦很多。例如:

#define MAX(a, b) a>b ? a : b

int main()
{
    int x = 100, y = 200;
    printf("最大值:%d\n", MAX(x, y==200?888:999));
}

直观上看,无论 y 的取值是多少,表达式 y==200?888:999 的值一定比 x 要大,但由于宏定义仅仅是文本替换,中间不涉及任何运算,因此等价于:

printf("最大值:%d\n", x>y==200?888:999 ? x : y==200?888:999);

可见,带参宏的参数不能像函数参数那样视为一个整体,整个宏定义也不能视为一个单一的数据,事实上,不管是宏参数还是宏本身,都应被视为一个字串,或者一个表达式,或者一段文本,因此最基本的原则是:

  • 将宏定义中所有能用括号括起来的部分,都括起来,比如:
#define MAX(a, b) ((a)>(b) ? (a) : (b))

7、宏定义中的符号粘贴

有些时候,宏参数中的符号并非用来传递数据,而是用来形成多种不同的字串,例如在某些系统函数中,系统本身规范了函数接口的部分标准,形如:

void __zinitcall_service_1(void)
{
    ...
}

void __zinitcall_service_2(void)
{
    ...
}

void __zinitcall_feature_1(void)
{
    ...
}

void __zinitcall_feature_2(void)
{
    ...
}

此时,若需要向用户提供一个方便整合字串的宏定义,可以这么写:

#define LAYER_INITCALL(layer, num)  __zinitcall_##layer##_##num

用户的调用如下:

LAYER_INITCALL(service, 1);
LAYER_INITCALL(service, 2);
LAYER_INITCALL(feature, 1);
LAYER_INITCALL(feature, 2);

注意:
在书写非字符串的字串时(如上述例子),使用两边双井号来粘贴字串,并且要注意如果字串出现在最末尾,则最后的双井号必须去除,例如上述代码不可写成:

#define LAYER_INITCALL(num, layer)  __zinitcall_##layer##_##num##

但如果粘贴的字串并非出现在最末尾,则前后都必须加上双井号:

#define LAYER_INITCALL(num, layer)  __zinitcall_##layer##_##num##end

注意:
另外,如果字串本身拼接为字符串,那么只需要使用一个井号即可,比如:

#define domainName(a, b) "www." #a "." #b ".com"

int main()
{
    printf("%s\n", domainName(baidu, lab));
}

执行打印如下:

gec@ubuntu:~$ ./a.out
www.baidu.lab.com
gec@ubuntu:~$

二、预处理.条件编译

1、无值宏定义

定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现,例如:

#define BIG_ENDIAN
#define __cplusplus

2、条件编译

  • 概念:有条件的编译,通过控制某些宏的值,来决定编译哪段代码。
  • 形式:
    • 形式1:判断表达式 MACRO 是否为真,据此决定其所包含的代码段是否要编译
    • 注意:#if形式条件编译需要有值宏
#define A 0
#define B 1
#define C 2

#if A
    ... // 如果 MACRO 为真,那么该段代码将被编译,否则被丢弃
#endif
// 二路分支
#if A
    ... 
#elif B
    ...
#endif
// 多路分支
#if A
    ... 
#elif B
    ...
#elif C
    ...
#endif
  • 形式:
    • 形式2:判断宏 MACRO 是否已被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifdef MACRO
    ...
#endif

// 二路分支
#ifdef MACRO
    ...
#else
   ...
#endif
  • 形式:
    • 形式3:判断宏MACRO是否未被定义,据此决定其所包含的代码段是否要编译
// 单独判断
#ifndef MACRO
    ...
#endif

// 二路分支
#ifndef MACRO
    ...
#else
   ...
#endif
  • 总结:
    • #ifdef、#ifndef此种形式,判定的是宏是否已被定义,这不要求宏有值。
    • #if 、#elif 这些形式,判定的是宏的值是否为真,这要求宏必须有值。

3、条件编译的使用场景

a、控制调试语句:在程序中,用条件编译将调试语句包裹起来,通过gcc编译选项随意控制调试代码的启停状态。例如:

gcc example.c -o example -DMACRO

以上语句中,-D意味着 Define,MACRO 是程序中用来控制调试语句的一个宏,如此一来就可以在完全不需要修改源代码的情况下,通过外部编译指令选项非常方便地控制调试信息的启停。

b、选择代码片段:在一些大型项目中(例如 Linux 内核),某个相同功能的模块往往有不同的实现,需要用户根据具体的情况来“配置”,这个所谓的配置的过程,就是对代码中不同的宏的选择的过程。
例如:

#define A 0  // 网卡1
#define B 1  // 网卡2  √
#define C 0  // 网卡3

// 多路分支
#if A
    ... 
#elif B
    ...
#elif C
    ...
#endif       

三、预处理.头文件

1、头文件的作用

通常,一个常规的C语言程序会包含多个源码文件(.c),当某些公共资源需要在各个源码文件中使用时,为了避免多次编写相同的代码,一般的做法是将这些大家都需要用到的公共资源放入头文件(.h)当中,然后在各个源码文件中直接包含即可。
在这里插入图片描述

2. 头文件的格式

由于头文件包含指令 #include 的本质是复制粘贴,并且一个头文件中可以嵌套包含其他头文件,因此很容易出现一种情况是:头文件被重复包含。

  • 使用条件编译,解决头文件重复包含的问题,格式如下:
#ifndef _HEADNAME_H
#define _HEADNAME_H

...
... (头文件正文)
...

#endif

其中,HEADNAME一般取头文件名称的大写

3. 头文件的内容

头文件中所存放的内容,就是各个源码文件的彼此可见的公共资源,包括:
1.全局变量的声明。
2.普通函数的声明。
3.静态函数的定义。
4.宏定义。
5.结构体、联合体、枚举的定义。
6.枚举常量列表的定义。
7.其他头文件。

// head.h
extern int global; // 1,全局变量的声明
extern void f1();  // 2,普通函数的声明
static void f2()   // 3,静态函数的定义
{
    ...
}
#define MAX(a, b) ((a)>(b)?(a):(b)) // 4,宏定义
struct node    // 5,结构体的定义
{
    ...
};
union attr    // 6,联合体的定义
{
    ...
};
#include <unistd.h> // 7,其他头文件
#include <string.h>
#include <stdint.h>
  • 特别说明:
    a.全局变量、普通函数的定义一般出现在某个源文件(*.c *.cpp)中,其他的源文件想要使用都需要进行声明,因此一般放在头文件中更方便。
    b.静态函数、宏定义、结构体、联合体的定义都只能在其所在的文件可见,因此如果多个源文件都需要使用的话,放到头文件中定义是最方便,也是最安全的选择。

4. 头文件的使用

头文件编写好了之后,就可以被各个所需要的源码文件包含了,包含头文件的语句就是如下预处理指令:

// main.c
#include "head.h"  // 包含自定义的头文件
#include <stdio.h> // 包含系统预定义的文件

int main()
{
    ...
}

可以看到,在源码文件中包含指定的头文件有两种不同的形式:

  • 使用双引号:在指定位置 + 系统标准路径搜索 head.h
  • 使用尖括号:在系统标准路径搜索 stdio.h

由于自定义的头文件一般放在源码文件的周围,因此需要在编译的时候通过特定的选项来指定位置,而系统头文件都统一放在标准路径下,一般无需指定位置。
假设在源码文件 main.c 中,包含了两个头文件:head.h 和 stdio.h ,由于他们一个是自定义头文件,一个是系统标准头文件,前者放在项目 project/inc 路径下,后者存放于系统头文件标准路径下(一般位于 /usr/include),因此对于这个程序的编译指令应写作:

gcc main.c -o main -I /home/xxx/pro/inc

其中,/home/xxx/pro/inc 是自定义头文件 head.h 所在的路径

  • 语法要点:
    • 预处理指令 #include 的本质是复制粘贴:将指定头文件的内容复制到源码文件中。
    • 系统标准头文件路径可以通过编译选项 -v 来获知,比如:
gcc main.c -I /home/xxx/pro/inc -v
... ...
#include "..." search starts here:
#include <...> search starts here:
    /usr/lib/xxx/x86_64-linux-gnu/7/include
    /usr/local/include
    /usr/lib/xxx/x86_64-linux-gnu/7/include-fixed
    /usr/include/x86_64-linux-gnu
    /usr/include
... ...

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2065321.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RTSP/Onvif安防视频监控平台EasyNVR在欧拉系统中启动失败的原因排查

视频安防监控平台EasyNVR可支持设备通过RTSP/Onvif协议接入&#xff0c;并能对接入的视频流进行处理与多端分发&#xff0c;包括RTSP、RTMP、HTTP-FLV、WS-FLV、HLS、WebRTC等多种视频流格式。平台支持轻量化部署&#xff0c;可兼容各类操作系统&#xff0c;包括Windows、Linux…

Admin.NET源码学习(4:基于Furion的后台服务启动方式浅析)

Admin.NET为前后端分离架构&#xff0c;后台服务的入口项目为Admin.NET.Web.Entry&#xff0c;其与其它项目的依赖关系如下图所示。   由于项目采用Furion框架&#xff0c;后台服务启动方式、注册方式、配置方式等方面与常规的asp.net core项目差异明显&#xff0c;初步接触…

计算机的错误计算(七十)

摘要 讨论大数的正割函数 sec(x)的错误计算。 例1. 已知 在 Maple 中计算 在 Maple中输入&#xff1a; restart; sec(30^54.8); 则输出&#xff1a; -5.214386310 若输入&#xff1a; Digits : 16;evalf[16](sec(30^54.8)); 则输出&#xff1a; 1.324455078865824…

中年人开发语言学习之路,反其道而行之

大家都更愿意学习新技术、新架构&#xff0c;代表着新方向新趋势&#xff0c;当大家都这么想的时候&#xff0c;注定了竞争就会激烈。有一部分中年程序员&#xff0c;反其道而行之&#xff0c;学习一些老掉牙的开发语言&#xff0c;向哪些近乎被遗忘的老旧系统进军。 市面上依…

一文了解Ansible原理以及常见使用模块

ansible使用手册 1. 简述 Ansible 是一种开源的自动化工具&#xff0c;主要用于配置管理、应用程序部署和任务自动化。 它使用简单的 YAML 语言来定义自动化的任务【playbook】&#xff0c;使得配置和部署变得更加直观和易于管理。 基于SSH协议连接到远程主机来执行指令。 2…

图像数据处理21

五、边缘检测 5.2基于二阶导数的边缘检测 一阶导数&#xff08;如Sobel、Prewitt算子&#xff09;能够捕捉到灰度值的快速变化&#xff0c;但有时会因检测到过多的边缘点而导致边缘线过粗。为了更加精确地定位边缘位置&#xff0c;可以利用二阶导数的零交叉点。零交叉点是是函…

触想工业一体机辅助DR系统提升医学影像诊断效率

一、行业发展背景 早期X线摄影依赖胶片成像&#xff0c;不便于图像存储管理&#xff0c;且显影过程长&#xff0c;无法进行后期处理&#xff0c;诊断质量和效率受到局限。 随着数字化技术的发展&#xff0c;DR系统问世&#xff0c;利用平板探测器将X射线图像转化为数字信号&…

推荐一款功能全面的层次化笔记应用,支持自由拖拽、缩放、旋转,可视化非常牛逼(附源码)

背景 不知道各位大佬日常生活中笔记软件用的多不&#xff0c;小编在工作中常常用笔记来记录每天的收获和安排。笔记软件的好坏直接影响了工作的心情和效率。今天给大家介绍的这款笔记软件&#xff0c;以其强大的笔记功能为基础&#xff0c;创造性地融入了画布式的自由编辑特性…

关于武汉芯景科技有限公司的RS232通信接口芯片XJ3243EEUI开发指南(兼容MAX3243EEUI)

一、芯片引脚介绍 1.芯片引脚 2.引脚描述 二、典型应用电路 三、功能描述 1.Transmitter 通过T1&#xff0c;T2可以将TTL电平转换为RS232电平 2.Receiver 通过R1&#xff0c;R2可以将RS232电平转换为TTL电平 3.工作模式控制 4.INVALID引脚

EDKII之安全启动详细介绍

文章目录 安全启动简介安全启动流程介绍签名过程BIOS实现小结 安全启动简介 安全启动&#xff08;Secure Boot&#xff09;是一种计算机系统的安全功能&#xff0c;旨在确保系统启动过程中只能加载经过数字签名的受信任的操作系统和启动加载程序。通过使用安全启动&#xff0c…

数据结构之串与KMP算法详解

串 一. 定义&#xff08;了解&#xff09; 串&#xff0c;即字符串&#xff0c;是计算机系统和网络传输中最常用的数据类型&#xff0c;任何非数值型的处理都会以字符串的形式存储和使用。 串&#xff08;String&#xff09;是由零个或多个字符组成的有限序列&#xff0c;一…

多选类型项,点击亮或不亮

用于菜单下拉 多选项 。 <div style"display: flex; flex-wrap: wrap;margin: 0 auto;"><div v-for"(item, index) in prpductnames" :key"item.id"><span :class"{ selected: selectArr.includes(item.id) }" click&q…

《计算机操作系统》(第4版)第7章 文件管理 复习笔记

第7章 文件管理 一、文件和文件系统 1. 数据项、记录和文件 数据组成可分为数据项、记录和文件三级&#xff0c;它们之间的层次关系如图7-1所示。 图7-1 文件、记录和数据项之间的层次关系 (1)数据项 在文件系统中&#xff0c;数据项是最低级的数据组织形式&#xff0c;可以分为…

Grove Vision AI V2之GPIO

一、说明 实现一个LED闪烁的Demo&#xff0c;Grove Vision AI V2开发板上有一个USER_LED&#xff0c;由GPIO SEN_D2驱动&#xff0c;SEN_D2为高电平是USER_LED亮&#xff0c;SEN_D2为低电平时USER_LED灭。 USER_LED部分电路如下&#xff1a; 二、创建例程 1、创建文件 在See…

MySQL的源码安装及基本部署(基于RHEL7.9)

这里源码安装mysql的5.7.44版本 一、源码安装 1.下载并解压mysql , 进入目录: wget https://downloads.mysql.com/archives/get/p/23/file/mysql-boost-5.7.44.tar.gz tar xf mysql-boost-5.7.44.tar.gz cd mysql-5.7.44/ 2.准备好mysql编译安装依赖: yum install cmake g…

上线eleme项目

&#xff08;一&#xff09;搭建主从从数据库 主服务器master 首先下载mysql57安装包&#xff0c;然后解压 复制改目录到/usr/local底下并且改个名字 cp -r mysql-5.7.44-linux-glibc2.12-x86_64 /usr/local/mysql 删掉/etc/my.cnf 这个会影响mysql57的启动 rm -rf /etc…

浪潮服务器主板集成RAID常见问题

★主板集成RAID出现Initialize初始化&#xff0c;如下图 判断及解决方案&#xff1a; 1.机器是否有过插拔硬盘等操作。 2.系统初始化-系统启动会非常的慢。一般为非法关机或者断电导致。 3.出现此情况耐心等待磁盘初始化完成即可。系统初始化时间以具体的数据大小来决定&#…

CLion IDE用MSVC和cmake编译darknet(带GPU)

这个配置教程给用过pytorch&#xff0c;懂得深度学习代码的基本流程&#xff0c;但又不熟悉windows c开发环境的宝子们使用。 安装CUDA&#xff0c;CUDNN 一般都有&#xff0c;不说了。注意上nvidia官网看一下显卡架构&#xff0c;后面要用&#xff0c;比如我的丽台M2000架构…

从零开始Dify本地部署|Windows

参考官方文档部署 Dify本地源码启动 windows最好结合WSL使用&#xff0c;懒得配置WSL&#xff0c;就是硬肝&#xff01; 1.Clone Dify 代码 先找到项目GitHub 开源链接clone 下来&#xff0c;使用docker部署运行&#xff08;Windows配置docker环境这里不赘述&#xff09; gi…

Prometheus Alertmanager告警之邮件、钉钉群、企业微信群机器人报警

文章目录 一、部署alertmanager相关组件1.alertmanager-config2.alertmanager-message-tmpl3.alertmanager 二、调试邮件告警三、钉钉群/企业微信群 报警3.1添加钉钉群机器人3.2添加企业微信群机器人3.3部署alertmanager-webhook-adaptermessage-tmplalertmanager-webhook-adap…