C语言预处理预编译命令及宏定义详解

目录
  • 程序翻译环境和执行环境
  • 翻译环境:详解编译+链接
    • 1. 编译 — 预处理/预编译 test.c ---- test.i
    • 2. 编译 — 编译 test.i ---- test.s
    • 3. 编译 — 汇编 test.s ---- test.obj
    • 4. 链接 test.obj ---- test.exe
  • 运行环境
  • 预处理/预编译详解
    • #define 定义标识符
  • #和##
    • #的作用
    • ##的作用
    • 命名约定
    • 命令行定义
    • 条件编译
      • 常见的条件编译指令
    • 文件包含
    • offsetof(宏类型,成员名字)偏移量模拟实现

.c 源程序 ----- 编译 ----- 链接 ---- exe ----运行 -------->

程序翻译环境和执行环境

翻译环境:源代码被转换为可执行机器指令(二进制代码)。

执行环境:用于实际执行代码。

翻译环境:详解编译+链接

1.组成程序的每个源文件通过编译过程分别转换成目标代码。

2.每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。

3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且他可以搜索程序员个人的程序库,将其需要的函数也链接到程序。

extern声明外部文件中的函数

1. 编译 — 预处理/预编译 test.c ---- test.i

文本操作

#include 头文件的包含

注释删除:使用空格替换注释

#define 替换,所以宏无法进行调试。

……

2. 编译 — 编译 test.i ---- test.s

把c语言代码翻译成汇编代码

语法分析

词法分析

语义分析

符号汇总

3. 编译 — 汇编 test.s ---- test.obj

把汇编代码转换成二进制代码(指令)。

形成符号表。(符号+地址)

4. 链接 test.obj ---- test.exe

合并段表

符号表的合并和重定位

运行环境

1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

2.程序的执行便开始。接着便调用main函数。

3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

4.终止程序。正常终止main函数;也有可能是意外终止

预处理/预编译详解

预定义符号

本来就有的符号

__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

应用

printf("data: %s\n time: %s" ,__DATE__,__TIME__);

输出

data: Jul 13 2021
time: 15:13:54

#define 定义标识符

宏和define区别,宏是有参数的。

下面是宏的声明方式:

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中

参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuf的一部分。

例如

#define SQUARE(X) (X)*(X)
int main()
{
    int ret = SQUARE(5);
	return 0;
}

宏的参数是替换的,不是传参的。

在定义宏的时候不要吝啬括号。

#和##

#的作用

使用#,把一个宏参数变成对应的字符串。

把参数插入到字符串中

#define PRINT(X) printf("the value of "#X" is %d\n", X)
int main()
{
	int a = 10;
	int b = 20;
	PRINT(a);
	PRINT(b);
	return 0;
}

输出

the value of a is 10
the value of b is 20

##的作用

## 可以把位于他两边的符号合成一个符号,允许宏定义从分离的文本片段创建创建标识符。

#define CAT(X,Y) X##Y
int main()
{
	int class84 = 2021;
	printf("%d\n", CAT(class, 84));
}

输出

2021

带副作用的宏参数

#define MAX(a, b)  ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
//输出的结果是什么?

这里我们得知道预处理器处理之后的结果是什么:

z = ( (x++) > (y++) ? (x++) : (y++));

输出结果

x=6 y=10 z=9

宏和函数的对比

对于上述的宏,也可以用函数实现其功能。

使用宏的优点:

1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。

2.函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之,这个宏可以用于整型、长整型、浮点数等等,宏是类型无关的。

使用宏的缺点:

1.每次调用宏,一份宏定义的代码插入程序中,除非宏比较短,否则可能会大幅度增加代码的长度。

2.宏无法调试。在预编译(预处理)阶段,已经把 # define 给替换了,已经不再是宏了。

3.宏由于类型无关,也就不够严谨。

3.宏可能会带来运算符优先级的问题,更容易导致程序出错。

inline 内联函数

命名约定

函数和宏语法相似,语言本身没法帮我们区分二者。把宏名全部大写,函数名不要全部大写。

#undef 移除宏定义

这条指令用于移除宏定义。

如果现存的一个名字需要被重新定义,那么他的旧名字首先要被移除。

#undef NAME

命令行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号,用于在启动编译过程。例如:当我们根据一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。假设某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。

条件编译

#define DEBUG
#ifdef DEBUG
#endif

常见的条件编译指令

#if 常量表达式
//...
#endif

举例子:为真参与编译,为假 (0)不参与编译。

#if 1
	printf("balabala....");
#endif

二、多个分支的条件编译

#if 常量表达式
//...
#elif 常量表达式
//...
#else
//....
#endif

举例子

#if 1==1
#elif 2==1
#else
#endif

三、判断是否被定义

#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

四、嵌套指令

#if defined(OS_UNIX)
	#ifdef OPTION1
			unix_version_option1();
	#endif
	#ifdef OPTION2
			unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
			msdos_version_option2();
	#endif
#endif

文件包含

我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。

头文件包含的方式:

1.本地文件包含:#include "Filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。

2.库文件包含:#include <Filename.h>

查找策略:查找头文件直接去标准路径下去查找,如果找不到就返回错误信息。

这样是不是可以说,对于库文件也可以使用“”的形式包含?

答案是肯定的,可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

wwww想到自己经常重复包含,留下了悔恨的泪水~~

出现嵌套文件包含解决方法 :条件编译

每个头文件开头这样写:

#ifndef __TEST__H__
#define __TEST__H__
 //头文件的内容
#endif         //__TEST__H__

或者

#pragma once

就可以避免头文件的重复引入。

总结一下:预处理阶段的预处理指令:条件编译指令 / #include / #define / #error /#pragma / ……

offsetof(宏类型,成员名字)偏移量模拟实现

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
struct S
{
	char c1;
	int a;
	char c2;
};
#define OFFSETOF(struct_name, member_name) (int)&(((struct_name*)0)->member_name)
int main()
{
	printf("%d\n", OFFSETOF(struct S, c1));
	printf("%d\n", OFFSETOF(struct S, a));
	printf("%d\n", OFFSETOF(struct S, c2));
	return 0;
}

以上就是C语言预处理预编译命令及宏定义详解的详细内容,更多关于C语言预处理预编译命令及宏的资料请关注我们其它相关文章!

时间: 2021-10-12

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

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

常用C/C++预处理指令详解

预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查.预处理命令以符号"#"开头. 常用的预处理指令包括: 宏定义:#define 文件包含:#include 条件编译:#if.#elif.#ifndef.#ifdef.#endif.#undef 错误信息指令:#error #line指令 布局控制:#pragma 宏定义 宏定义又称为宏代换.宏替换,简称"宏".宏替换只作替换,不做计算,不做表达式求解.宏定义分带参数的宏定义和不带参数的宏

C语言从编译到运行过程详解

目录 C语言从编译到运行 一.前言 二.C程序编译过程 三.阶段过程 1.预处理阶段 2.编译阶段 3.汇编阶段 4.链接阶段 C语言从编译到运行 一.前言 最近在看CSAPP(深入理解计算机系统)然后以前也学过C语言,但是从来没有深究写好的C代码是怎么编译再到执行的. 所以现在自己学习,然后记录下来. 以最常用的hello world!程序为例 程序名: main.c #include <stdio.h> int main() { printf("Hello world!\n&qu

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语言的基本语法详解

目录 1.标识符与关键字 2.常量和符号常量 (1)常量和常量符号 (2)变量 3.C语言数据类型 (1)整型常量 整型变量 原码.反码和补码 (2)实型数据 实型常量 实型变量 实型变量的定义以及初始化 (3)字符型数据 ASCII码 字符型变量 转义字符字符 字符串常量 字符串变量 总结 1.标识符与关键字 给变量所取的名字叫变量名,定义变量的名字需要遵循标识符的命名规则. 标识符是用来标识变量.符号常量.数组.函数.文件等名字的有效字符序列. 标识符的命名规则: 1.只能由字母.数字和下划

详解C语言#define预处理宏定义

目录 #define介绍: #define宏定义无参的一般形式为:#define  标识符 常量 #define宏定义有参的一般形式为:#define  标识符(参数表) 表达式 #运算符: ##运算符: 可变宏...和__VA_ARGS__: 开发项目中常用的宏定义: #define介绍: C语言里可以用#define定义一个标识符来表示一个常量.特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了,也不做类型定义.预编译又叫预处理.预编译就是编译前的处理.这个操作是在

C语言#define拼接宏定义实现方式

使用场合:拼接两个宏,一个是传入的宏.但是传入的宏不会被替换,反而原封不动的接了上去,这就尴尬了.经过各种尝试,居然成了,特此记录分享一下,方便大家学习. char A_param=0; char B_pramm=0; //添加宏定义 #define OBJECT A #define DEFINE_(X) X##_param //一次定义 #define DEFINE(X) DEFINE_(X) //再次定义 #define PARAM DEFINE(OBJECT) void fun() { /

详解C 语言项目中.h文件和.c文件的关系

详解C 语言项目中.h文件和.c文件的关系 在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代,那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件.但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件. 于是人们将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include XXXX这样的语句.这样即

详解C语言gets()函数与它的替代者fgets()函数

在c语言中读取字符串有多种方法,比如scanf() 配合%s使用,但是这种方法只能获取一个单词,即遇到空格等空字符就会返回.如果要读取一行字符串,比如: I love BIT 这种情况,scanf()就无能为力了.这时我们最先想到的是用gets()读取. gets()函数从标准输入(键盘)读入一行数据,所谓读取一行,就是遇到换行符就返回.gets()函数并不读取换行符'\n',它会吧换行符替换成空字符'\0',作为c语言字符串结束的标志. gets()函数经常和puts()函数配对使用,puts

详解易语言的程序的输入方法概念

为了便于输入程序,易语言内置四种名称输入法:首拼.全拼.双拼.英文.三种拼音输入法均支持南方音及多音字.首拼输入法及全拼输入法在系统中被合并为"首拼及全拼输入法",系统自动判别所输入的拼音是首拼方式还是全拼方式.双拼输入法的编码规则与 Windows 系统所提供的双拼输入法一致.例如:欲输入"取整 (1.23)"语句,各种输入法的输入文本为: ・ 首拼及全拼输入法: qz(1.23) 或者 quzheng(1.23) ・ 双拼输入法: quvg(1.23) ・ 英文

详解C语言-二级指针三种内存模型

二级指针相对于一级指针,显得更难,难在于指针和数组的混合,定义不同类型的二级指针,在使用的时候有着很大的区别 第一种内存模型char *arr[] 若有如下定义 char *arr[] = {"abc", "def", "ghi"}; 这种模型为二级指针的第一种内存模型,在理解的时候应该这样理解:定义了一个指针数组(char * []),数组的每个元素都是一个地址. 在使用的时候,若要使用中间量操作元素,那么此时中间量应该定义为 char *tm

详解C语言进程同步机制

本文是对进程同步机制的一个大总结(9000+字吐血总结),涵盖面非常的全,包括了进程同步的一些概念.软件同步机制.硬件同步机制.信号量机制和管程机制,对每种机制结合代码做了详细的介绍,并且对琐碎的知识点和概念解释的非常清晰. ​ 在前面的博客中讲述了进程的状态及其状态的转换,每种状态的含义和转换的原因.同样我们也知道,在OS引入了进程后,可以使系统中的多道程序可以并发的执行,进程的并发执行一方面极大的提高了系统的资源利用率和吞吐量,但是另一方面却使系统变得更加复杂,如果不能采取有效的措施,对多个

详解Go语言中关于包导入必学的 8 个知识点

1. 单行导入与多行导入 在 Go 语言中,一个包可包含多个 .go 文件(这些文件必须得在同一级文件夹中),只要这些 .go 文件的头部都使用 package 关键字声明了同一个包. 导入包主要可分为两种方式: 单行导入 import "fmt" import "sync" 多行导入 import( "fmt" "sync" ) 如你所见,Go 语言中 导入的包,必须得用双引号包含,在这里吐槽一下. 2. 使用别名 在一些场

详解C语言函数返回值解析

详解C语言函数返回值解析 程序一: int main() { int *p; int i; int*fun(void); p=fun(); for(i=0;i<3;i++) { printf("%d\n",*p); p++; } return 0; }; int* fun(void) { static int str[]={1,2,3,4,5}; int*q=str; return q; } //不能正确返回 虽然str是在动态变量区,而该动态变量是局部的,函数结束时不保留的.

详解C语言 三大循环 四大跳转 和判断语句

三大循环for while 和 do{ }while; 四大跳转 : 无条件跳转语句 go to; 跳出循环语句 break; 继续跳出循环语句 continue; 返回值语句 return 判断语句 if,if else,if else if else if...else ifelse 组合 if(0 == x) if(0 == y) error(): else{ //program code } else到底与那个if配对 C语言有这样的规定: else 始终与同一括号内最近的未匹配的if语

详解C语言用malloc函数申请二维动态数组的实例

详解C语言用malloc函数申请二维动态数组的实例 C语言在程序运行中动态的申请及释放内存十分方便,一维数组的申请及释放比较简单. Sample one #include <stdio.h> int main() { char * p=(char *)malloc(sizeof(char)*5);//申请包含5个字符型的数组 free(p); return 0; } 是否申请二维动态内存也如此简单呢?答案是否定的.申请二维数组有一下几种方法 Sample two /* 申请一个5行3列的字符型