C语言编程之预处理过程与define及条件编译

目录
  • 名示常量#define
  • 重定义常量
  • 在#define中使用参数
  • 预处理器粘合剂:##运算符
  • 变参宏:… 和_ _ VAG_ARGS_ _
  • 宏与函数
  • 预处理指令
  • #undef指令
  • 从C预处理器的角度看已定义
  • 条件编译
  • offsetof函数

这张图描述了从源文件到可执行文件的整体步骤

这张图展示了大体上步骤。

从代码到运行环境,编译器提供了翻译环境。在一个程序中,会存在多个文件 ,而每个源文件都会单独经过编译器处理。

预编译:
1,会将#include等头文件所包含的内容,库函数全部拷贝过来
2,代码中注释的删除
3,由#define所定义的符号全部替换进代码中
对预处理指令的操作

编译:把C代码翻译成汇编代码
1,语法分析(判断是否存在语言的语法错误而造成无法编译)
2,词法分析
3,语义分析(分析每句代码的意思)
4,符号汇总(会将整个程序中的全局符号进行汇总)

汇编
1,形成符号表

一个程序,两个文件test.c与add.c,在test.c中,有

extern int add(int x,int y);//声明对该函数引用,在其他文件中找该函数。

在汇编时,各个文件都会形参函数符号表,但extern并不会形成地址标记,只是一个0x00。

链接:合并段表
符号表的合并与重定位检查各个函数及其定义声明

名示常量#define

define的预处理指令

#define MACRO substitution
预处理指令 宏 替换体

宏只是起到替换作用,在替换过程中不产生任何运算
举个例子

#define SUM 2+2
int num = SUM * SUM;

不了解的人可能会认为是
num=44;
但,实际是替换作用
num=2+22+2;
这就是宏只起到替换作用的意思。

再来介绍一个原理性概念

记号
从技术角度来看,可以把宏的替换体看作是记号型字符串。而不是字符型字符串。在C预处理器记号是宏定义的替换体的中单独的“词”,用空白把词分开。例如:

#define FOUR 2*2
#define FOURS 2 * 2

这两个对于预处理器要看预处理器把这个替换体看成什么。如果是字符型字符串,这空格也会是字符串的一部分。但如果是记号型字符串,空格就会被认为是分隔符,就和2*2是一个意思。总之要看编译器的规则。
总结一下

如果编译器理解替换体是字符型字符串,那么空格就会被认为是字符串的一部分
2 * 2就和2*2不是一个意思。
如果编译器理解为记号型字符串,那么空格就会被认为只是分隔符,并不影响。空格不算替换体的一部分
2 * 2和2乘2则是一个意思。

重定义常量

假设把MAX设为30,在文件中又把它重新定义为10.这个过程叫重定义常量但不同的标准有不同的规则。有一些允许重定义,但是会报警。ANSI标准则采用,只有新旧定义完全相同才允许重定义。
完全相同意味着替换体中必须记号完全相同,顺序也必须相同 。

#define MAX 2 * 3
#define MAX 2 * 3

这才允许

#define MAX 2 * 3
#define MAX 2*3

这不符合那个标准。(虽然我不知道这个标准的重定义有什么用,我比较菜)
注:根据一个大佬的建议,这类代码非常致命,非常不好,最好不使用。

在#define中使用参数

在#define中也可以创建外形和作用与函数类似的类函数宏
带有函数的宏可以达到部分函数的作用。

#define	SQUARE(X) X*X
mul=SQUARE(2);

与函数调用有些相似。
同时最好使用足够多的括号去确保运算和结合性的正确。

mul=SQUARE(x++)

则会造成运算不符合要求。

用宏参数创建字符串:#运算符

#define PSQRA(X) printf("X is %d\n",((X)*(X));
#define PSQRB(X) printf("#X" is %d\n",((X)*(X));

这两个是可以打印出不同的效果
#作为一个预处理运算符,可以把记号转换成字符串,如果X是一个宏形参,那么#X就是“X”的字符串的形参名。

这叫字符串化

int y=50;
PSQRA(y)
X is 2500
PSQRB(Y)
y is 2500
PSQRA(2+4)
X is 36
PSQRB(2+4)
2+4 is 36

这就是区别。

预处理器粘合剂:##运算符

#运算符可以作用于宏的替换体
而##运算符也可以作用。

#define NUMBER(n) X##n
NUMBER(4)可展开为x4

例如

#include <stdio.h>
#define XNAME(N) x##N
#define PRINT(N) pritnf("x"#N"=%d\n",x##N);
int main(void)
{
	int XNAME(1) = 10;//x1=10
	int XNAME(2) = 20;//x2=10
	int x3 = 0;
	PRINT(1);//printf("x1=%d",x1);
	PRINT(2);//printf("x2=%d",x2);
	PRINT(3);//printf("x3=%d",x3);
}

变参宏:… 和_ _ VAG_ARGS_ _

一些函数可以接受数量可变的参数(就是没有固定传递的参数的数量,如printf()和scanf())。而宏也可以拥有这样的能力。

#define PR(...) printf(_ _VAG_ARGS_ _)
PR("HELLO WORLD");//printf("HELLO WORLD");
PR("x1=%d,x2=%d",10,20);//printf(""x1=%d,x2=%d",10,20);

相当于这样的效果。
省略号只能代替最后的宏参数。不能在省略号加其他参数。

#define PR(x,...,y) #x #_ _VAG_ARGS_ _ #y

是不被允许的。

宏与函数

有相当一部分的宏可以起到和函数一样的效果,但到底该怎么选呢?
宏和函数可以达到同样效果。宏比函数要简单一些,同时,在编译器的消耗时间也要远小于函数。但是稍有不慎就会产生一些副作用,导致结果不可预测。
宏与函数的比较实际上就是关于时间与空间的比较。

宏在预编译的时候会生成内联代码,也就是会在程序中替换生成语句。如果调用20次,则会在程序中插入20行代码。

但如果调用函数20次,函数也只有一份副本,节省了相当一部分空间。但执行函数时,要调用,再执行,再返回,远比宏插入内联语句消耗的时间要多。

宏较函数也存在缺陷

  • 当宏较大时会增加代码长度。
  • 宏是无法调试(在预编译的时候就已完成替换),可能会出现问题。
  • 宏由于不要求类型,会造成不严谨(这也是对于函数的一个好处,函数传参会要求参数类型,而宏只会将参数当作字符串处理,只要是int或float类型都可以)
  • 宏可能会带来运算符优先级的问题,使运算不可预测。

自己按照情况去使用。如果使用宏容易出现副作用,那还是调用函数吧。

但要记住以下几

1,记住宏名中不允许有空格,但在替换字符串中可以有空格。ANSI C允许在参数列表中使用空格。

2,用括号把宏的参数和替换体括起来,正确展开,防止出现副作用。

3,一般用大写字母表示宏常量,一般不全大写表示宏函数

4,如果用宏来加快函数的运行速度,要先确定宏和函数之间是否有差距。且,如果只使用一次那对速度加快影响不大。最好在多重嵌套中使用。

预处理指令

当编译器碰上#include 指令时,会查看后面文件名并把内容添加到当前文件中。

#include <stdio.h>//查找系统目录文件
#include "mtfile.h"//查找当前工作目录
#include "/usr/biff/file.h"//查找/usr/biff/目录

不同的系统有不同的规则,但<>与“”的规则是不变的。

通过头文件的引用,我们才能使用各种函数。头文件中有各种函数的声明。再通过库文件去调用函数的原型。

#undef指令

可以通过该指令去向之前#define定义的宏

#include <stdio.h>
#define MAX 10
#undef MAX
int main(void)
{
	printf("%d", MAX);
}

直接报错
#undef可以取消,某个宏的定义,可以用用来防止某个宏被重复定义,造成错误。

从C预处理器的角度看已定义

预处理器在处理标识符时遵循相同规则。当预处理器发现一个标识符时,会将其当作已定义或未定义。而这里的已定义是由预处理器决定。如果该标识符是由define定义的且没有undef取消,那就是已定义。如果是定义的某个全局变量,那就是未定义(对预处理器而言)。

#define定义的宏的作用域从文件开头开始,延申至文件结尾或者遇到#undef取消定义,如果跨文件使用,那使用的位置要在#include引用的文件后。

条件编译

就跟条件判断语句有着类似的意思。

#ifdef , #else , #endif

举个例子

#include <stdio.h>
#define MAX 10
#undef MAX
#ifdef MAX
	#include <string.h>
	#define MIN 10
#endif //
int main(void)
{
	printf("%d", MIN);
}

结果就是这个。
但屏蔽#undef

#include <stdio.h>
#define MAX 10
//#undef MAX
#ifdef MAX
	#include <string.h>
	#define MIN 10
#endif //
int main(void)
{
	printf("%d", MIN);
}

可以运行。
条件编译指令与条件判断语句类似。
只不过,条件判断语句是判读是否执行,
二条件编译指令是判断是否进行预编译。
#endif用来结束该指令的范围
再引入一个指令

#ifndef DEBUG

如果DEBUG未定义就执行编译,如果已定义就不执行。

还有这几个指令,非常接近条件判断语句

#if ,#elif ,#else

#if和#elif与if和else if类似。但它们的后面接整形常量表达式。
0为假,非0为真
都是判断是否进行预编译

可以通过条件编译指令去防止某些文件被多次调用导致问题出现

#ifndef _FILE_H
#define _FILE_H
文件内容
。
。
#endif

或者直接使用

#pragma once
//可以保证文件只是使用一次

offsetof函数

size_t offsetof( structName, memberName );

用于测算结构体成员相对于起始位置的偏移量

实现

#define OFFSETOF(structName,memberName) (int)&(((struct structName*)0)->memberName)

从0地址处,开始向成员访问再取地址在强转成整形。

以上就是C语言编程之预处理过程与define及条件编译的详细内容,更多关于C语言预处理的资料请关注我们其它相关文章!

(0)

相关推荐

  • 深入理解C预处理器

    C 预处理器不是编译器的组成部分,是编译过程中一个单独的步骤.C预处理器只是一个文本替换工具,它会指示编译器在实际编译之前完成所需的预处理. 所有的预处理器命令都是以井号(#)开头.它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始. 下表包含所有重要的预处理器指令: 指令 描述 #define 定义宏 #include 包含一个源代码文件 #undef 取消已定义的宏 #ifdef 如果宏已经定义,则返回真 #ifndef 如果宏没有定义,则返回真 #if 如果给定条件为真,则

  • C语言之预处理命令的深入讲解

    c提供的预处理功能有: 宏定义 文件包含 条件编译 为了与其她c语句区分,命令经常以符号"#"开头. 宏定义 #define 标识符 字符串 可以避免反复输入字符串,后面不加:宏定义在默认时的有效范围是全部.也可以用#undef终止宏定义区域. 不含参数 宏展开带入程序 含参数 #include<stdio.h> #define PI 3.1415 #define S(r) PI*r*r int main() { int a; float area; scanf("

  • 详解C语言中的#define宏定义命令用法

    #define 命令#define定义了一个标识符及一个串.在源程序中每次遇到该标识符时,均以定义的串代换它.ANSI标准将标识符定义为宏名,将替换过程称为宏替换.命令的一般形式为: #define identifier string 注意: 1.该语句没有分号.在标识符和串之间可以有任意个空格,串一旦开始,仅由一新行结束. 2.宏名定义后,即可成为其它宏名定义中的一部分. 3.宏替换仅仅是以文本串代替宏标识符,前提是宏标识符必须独立的识别出来,否则不进行替换.例如: #define XYZ t

  • C语言中条件编译详解

    通常情况,我们想让程序选择性地执行,多会使用分支语句,比如if-else 或者switch-case 等.但有些时候,可能在程序的运行过程中,某个分支根本不会执行. 比如我们要写一个跨平台项目,要求项目既能在Windows下运行,也能在Linux下运行.这个时候,如果我们使用if-else,如下: Windows 有专有的宏_WIN32,Linux 有专有的宏__linux__ if(_WIN32) printf("Windows下执行的代码\n"); else if(__linux_

  • 详解C语言编程中预处理器的用法

    预处理最大的标志便是大写,虽然这不是标准,但请你在使用的时候大写,为了自己,也为了后人. 预处理器在一般看来,用得最多的还是宏,这里总结一下预处理器的用法. #include <stdio.h> #define MACRO_OF_MINE #ifdef MACRO_OF_MINE #else #endif 上述五个预处理是最常看见的,第一个代表着包含一个头文件,可以理解为没有它很多功能都无法使用,例如C语言并没有把输入输入纳入标准当中,而是使用库函数来提供,所以只有包含了stdio.h这个头文

  • C语言编程之预处理过程与define及条件编译

    目录 名示常量#define 重定义常量 在#define中使用参数 预处理器粘合剂:##运算符 变参宏:- 和_ _ VAG_ARGS_ _ 宏与函数 预处理指令 #undef指令 从C预处理器的角度看已定义 条件编译 offsetof函数 这张图描述了从源文件到可执行文件的整体步骤 这张图展示了大体上步骤. 从代码到运行环境,编译器提供了翻译环境.在一个程序中,会存在多个文件 ,而每个源文件都会单独经过编译器处理. 预编译: 1,会将#include等头文件所包含的内容,库函数全部拷贝过来

  • C语言编程技巧 关于const和#define的区别心得

    #define ASPECT_RATIO 1.653 编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中.如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO.如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去.这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不

  • C语言编程C++编辑器及调试工具操作命令详解

    目录 一.GCC编译器 1.GNU工具 2.GCC简介 3.GCC编译器的版本 4.gcc所支持后缀名解释 5.编译器的主要组件 6.GCC的基本用法和选项 7.GCC的错误类型及对策 8.GCC编译过程 条件编译 二.GDB调试工具 1.Gdb调试流程: 2.进入代码调试模式后 一.GCC编译器 1.GNU工具 编译工具:把一个源程序编译成为一个可执行程序. 调试工具:能对执行程序进行源码及汇编级调试. 软件工程工具:用于协助多人开发或大型软件项目的管理,如make.CVS.Subvision

  • C语言编程简单却重要的数据结构顺序表全面讲解

    目录 前言 一.线性表定义 二.顺序表实现 1概念及结构 2静态顺序表 2.1实现顺序表接口,第一步要对顺序表进行初始化 2.2对顺序表的增删查改的接口函数(以尾插为例) 3动态顺序表 3.1动态顺序表初始化 3.2动态顺序表-尾插 3.3动态顺序表-头插 3.4动态顺序表-尾删 3.5动态顺序表-头删 3.6动态顺序表-任意位置插入数据 3.7动态顺序表-任意位置删除数据 结束 前言 本文主要介绍顺序表的定义和常见静态顺序表的用法. 一.线性表定义 线性表(line list)是n个具有相同特

  • 常用的C语言编程工具汇总

    中国有句古话叫做"工欲善其事,必先利其器",可见我们对工具的利用是从祖辈就传下来的,而且也告诉我们在开始做事之前先要把工具准备好.有了好的工具那么我们做起事来也会事半功倍.学习C语言也是一样的,对于初学者来说往往选择一款好的编程工具是很头大的事情.下面小编就给大家点评几款常用的C语言编程工具,究竟那款适合你,由你自己决定. VC++ 6.0   本站下载地址: 点击下载 这款软件相信大家看到名字就觉得很亲切的,也是大家吐槽最多的.中国大学的计算机专业学习C语言的必备神器,也算是比较古老

  • python语言编程实现凯撒密码、凯撒加解密算法

    凯撒密码的原理:计算并输出偏移量为3的凯撒密码的结果 注意:密文是大写字母,在变换加密之前把明文字母都替换为大写字母 def casar(message): # *************begin************# message1=message.upper() #把明文字母变成大写 message1=list(message1) #将明文字符串转换成列表 list1=[] for i in range(len(message1)): if message1[i]==' ': lis

  • Python函数式编程之面向过程面向对象及函数式简析

    目录 Python 函数式编程 同一案例的不同写法,展示函数式编程 面向过程的写法 面向对象的写法 接下来进入正题,函数式编程的落地实现 Python 函数式编程的特点 纯函数 Python 函数式编程 Python 不是纯粹的函数式语言,但你可以使用 Python 进行函数式编程 典型的听君一席话,如听一席话,说白了就是 Python 具备函数式编程的特性, so,可以借用函数式语言的设计模式和编程技术,把代码写成函数式编程的样子 一般此时我会吹嘘一下,函数式代码比较简洁和优雅~ 好了,已经吹

  • C语言编程数据结构线性表之顺序表和链表原理分析

    目录 线性表的定义和特点 线性结构的特点 线性表 顺序存储 顺序表的元素类型定义 顺序表的增删查改 初始化顺序表 扩容顺序表 尾插法增加元素 头插法 任意位置删除 任意位置添加 线性表的链式存储 数据域与指针域 初始化链表 尾插法增加链表结点 头插法添加链表结点 打印链表 任意位置的删除 双向链表 测试双向链表(主函数) 初始化双向链表 头插法插入元素 尾插法插入元素 尾删法删除结点 头删法删除结点 doubly-Linked list.c文件 doubly-Linkedlist.h 线性表的定

  • C语言编程数据结构的栈和队列

    目录 栈 数组实现 标题全部代码 Stack_array.c Stack_array.h 初始化数组栈 满栈后扩容 是否为空栈 压栈和退栈 链表实现 stack_chain.h stack_chain.c 整个压栈流程 整个弹栈流程 出栈情况 队列 队列的实现 queue_chain.h queue_chain.c 一个结构体类型用于维护这个队列 概念流程图 入队列的实现 出队列的实现 是否空队 栈 栈是一种以后进先出为顺序对对象进行添加或删除的数据结构 对栈进行形象记忆就像是桌子上的一堆书或一

  • C语言编程之扫雷小游戏空白展开算法优化

    目录 写代码前,扫雷需要什么 进行主函数文件的代码 game文件以及函数步骤 在主函数文件中使用game函数 布值棋盘(雷盘和玩家棋盘) 打印棋盘函数 玩家排雷 计算雷数的函数 空白递归算法 写代码前,扫雷需要什么 1,游戏需要初始选择菜单 2,需要布置两个棋盘,一个布置雷,一个展示给玩家看 3,打印棋盘 4,玩家要输入选择的坐标,并且可以多次输入游戏坐标 5,每次输入后打印棋盘,同时判断是否继续还是输赢. 6,玩家每次输入坐标,都进行一次递归展开. 进行主函数文件的代码 void option

随机推荐