C语言-----预处理详解

正文:

当使用 集成开发环境(IDE) 进行c语言的编程时,点击”编辑“,整个C程序从源代码便可执行可运行程序的生成过程的,在程序执行的过程中,IDE会在后台位我们执行好所有的编译过程,虽然IDE在后台执行有着许多底层的细节,要想看到他的程序过程也不是不行,接下来就带领大家深入了解的编程的整个过程。 

一、程序的编译环境和执行环境

编译环境就是在当前环境下把源代码转化可可执行的机器指令。

执行环境:代码在经过编译环境后生成二进制指令代码,由当前环境执行生成。

编译环境

翻译环境是由 编译 链接 两个⼤的过程组成的,⽽编译⼜可以分解成:预处理(有些书也叫预编
译)、编译、汇编三个过程。如下图所示

 注意:

  • 一个C语言的项目可能有多个.c文件一起构成,生成一一对应的目标文件(obj)。
  • 在windows环境下的目标后缀名是.obj,Linux 环境下目标我呢见后缀名是.o 。
  • 多个目标文件和链接库一起经过一起处理生成最终的到一个可执行程序。
  • 链接库就是平时用标准库呐,比如stdio.h 、string.h、stdlib.h等等或者本地库 。

翻译环境: 本意就是将C语言代码转化为二进制的指令。而它还能再从编译中细分为,预编译(也有人叫预处理)、编码、汇编 以及链接,这些都是循序渐进的一步步代入,或许有点绕口,接下会再造个知识梳理图,更深层次的吸收消化。

编译过程概述: 

  1. 预处理(Preprocessing):首先,对源文件进行预处理。预处理器将处理源代码中的预处理指令,比如以#开头的指令,如#include、#define等,并展开宏定义。预处理后的代码会生成一个.i文件,通常是在临时目录中。
  2. 编译(Compiling):接下来,编译器前端会将预处理后的源代码编译成汇编代码(.s文件)。此阶段会检查语法和语义错误,并进行优化,但不会生成可执行代码。
  3. 汇编(Assembling):汇编器(as)将汇编代码转换成机器代码,并生成目标文件(.o文件
  4. 链接(Linking):最后,链接器(ld)将目标文件与所需的库文件链接在一起,生成最终的可执行文件。

         以上便是,便是编译环境每个部分所该完成的任务。

运行环境

  • 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码植入只读内存完成。
  • 程序的执行开始,紧接着就是调用main函数。
  • 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储在静态内存中的变量在程序的整个执行过程一直保留他们的值。
  • 终止程序。正常终止main函数;也由可能时意外终止。

预定义符号

C语言设置了一些预定义的符号,可以直接使用的,预定义符号也是在预处理期间处理的。

   __FILE__    // 进行编译的源文件

   __LINE__   // 文件当前的行号

   __DATE__  // 文件被编译的日期

   __TIME__   // 文件被编译的时间

   __STDC__  //如果编译器循序 ANSI C,其值为1,否则未定义(但vs 编译是不循序的哈)

int main()
{
	printf("file:%s line:%d\n", __FILE__, __LINE__);
	printf("date:%s\n", __DATE__);
	printf("time:%s\n", __TIME__);
	//printf("stdc:%s\n", __STDC__); //这个是实现的不了哈 只能在gcc下实现,返回1
	return 0;
}

 #define 定义常量

基本的语法:

#define name  stuff     // 第一个参数就是名称,而第二个参数便就是内容了 

举例: 

#define MAX 100
#define MIN "ABCD"
int main()
{
	printf("%d\n", MAX);
	printf("%s\n", MIN);
	return 0;
}

还要别的很多各种举例方法,这边大家可以参考参考:

# define MAX 1000
# define reg register // register 这个关键字,创建⼀个简短的名字
# define do_forever for(;;) // ⽤更形象的符号来替换⼀种实现
# define CASE break;case // 在写 case 语句的时候⾃动把 break 写上。
// 如果定义的 stuff 过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠 ( 续⾏符 )
# define DEBUG_PRINT printf( "file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

在define定义标识符的时候,不要自作主张加切记!!!否则会出现不必要的错误

比如:

还需要注意一点在很多各种举例,上面的反斜杠后不能添加空格要直接回车才能触发作用。

#define 定义宏 

第一种定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro),或定义宏(define macro)。

基本宏的申明方式:

 #define name( paramen_list ) stuff

#define MAX(x,y) (x>y?x:y)

int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a, b);  //其实语句可以替换为 // int c = (a>b?a:b); 
	printf("%d\n", c);
	return 0;
}
其中的 parament-list 是⼀个由 逗号隔开的符号表 ,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的 ⼀部分。

 其次:就是要注意一些有关括号的问题,接下继续带领大家来看下面的代码例子。

第二种定义宏

这是另一种宏的定义:

#define DOUBLE(x) (x) + (x)

来看下面的例子: 

看上面的代码会打印什么结果呢? 我们让代码跑起来看一下吧:结果是100好像并没有问题

但是如果我们对这个代码稍微的修改一下,结果还会是100吗?

哦吼?跑起来可以看到怎么算成19呢?接下来我们可以俩者替换一下看看,出现了啥问题?

可以看到在预处理后 a+1 直接替换过去心想是10*10,但实际因字符的优先级不对,导致成了9+1*9+1,所以结果计算为19可想而知在定义宏的时候最好不要吝啬自己括号

修改后就把原先的(x)*(x),添加上括号即可,这样在出现式子为参数后不会因优先级导致的错误

注意:

所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,避免在使⽤宏时由于参数中的
操作符或邻近操作符之间不可预料的相互作⽤。

带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果
x+1;  //不带副作用
x++;  //带有副作用

宏和函数对比 

# 和 ##

#运算符

#运算符将宏的一个的参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执行的操作可以理解为 “字符串化”

#define  PRINT(val,format) printf("the value of "#val" is "format"\n",val)
 
 int main()
 {
     int a = 10;
     PRINT(a, "%d");
     
     int b = 20;
     PRINT(b, "%d");
    
     float f = 3.5f;
     PRINT(f, "%.1f");

     return 0;
 }

PRINT(val,format);   //当我们把这俩个参数替换到宏体内,就需要用到 #val 来进行转换位“val”。

## 运算符 

##可以把位与俩边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##也被称为记号粘合。

这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

举例:写一个求2个数的较大值的时候,不同的数据类型就得写不同函数的。

 

可以看到每次要是因为类型变了,又要重新定义函数,不觉得很麻烦嘛?接下试试定义宏的效果怎么样?

//定义宏
#define GENERIC_MAX(type) \
type type##_max(type x,type y) \
{ \
 return (x>y?x:y); \
}  //最后一个就不需要反斜杆了哈

GENERIC_MAX(int)
GENERIC_MAX(float)


 int main()
{
	//定义宏
	 int m = int_max(2, 3);
	printf("%d\n", m);

	float fm = float_max(3.5f, 4.5f);
	printf("%.2f\n", fm);
	return 0;
}

但这个##在实际开发的过程中是用的比较少,但大家也就看看就行哈。需要尝试可以复制上去看看运行看看哈。

命名约定

一般来说怕函数的宏使用语法很相似。所以会让二者语言不好区分。平时就是可以养成一个习惯:

把宏名全写成大写

函数名不要全部写成大写,可以学骆驼命名

 #undef

这条指令用就是移除一个宏定义。其实跟注释很相似的用法

#undef MAX

//当用上这条指令时,上面一条的宏定义就失效,就得再重新定义一条

条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。

⽐如说:
    调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译
 

基本解析:

#define            定义一个预处理宏
#undef            取消宏的定义

#if                   编译预处理中的条件命令,相当于C语法中的if语句
#ifdef              判断某个宏是否被定义,若已定义,执行随后的语句
#ifndef            与#ifdef相反,判断某个宏是否未被定义
#elif                若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
#else              与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
#endif             #if, #ifdef, #ifndef这些条件命令的结束标志.
defined          与#if, #elif配合使用,判断某个宏是否被定义

 

 还有许多常见的条件编译指令:

1. if 常量表达式
//...
# endif
// 常量表达式由预处理器求值。
如:
# define __DEBUG__ 1
# if __DEBUG__
//..
# endif
2. 多个分⽀的条件编译
# if 常量表达式
//...
# elif 常量表达式
//...
# else
//...
# endif
3. 判断是否被定义
# if defined(symbol)
# ifdef symbol
# if !defined(symbol)
# ifndef symbol
4. 嵌套指令
# 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 "add.h"

#include "test.h"

#include "data.h"

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

  • 库文件包含
    我们在来看下面的例子:

#include <stdio.h>

#include <string.h>

#include <stdilb.h>

 查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的
地⽅⼀样。
这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。
如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。 所以便能引用上:条件编译
每个头⽂件的开头写:
#ifdef  __TEST_H__
#define __TEST_H__
//头文件的内容
#endif // __TEST_H__
或者可以用到
#pragma once  //也是比较常见的 一般都是IDE 编译的内部才能看到这样的用法
// 头文件的内容

总结 

以上就是C语言预处理的详解,从对编译环境到执行环境的了解,以及再深层次的编译环境的深度解剖,也对预编译(预处理)、编译、汇编以及链接的生成文件还要文件编译的用法呐。学到这里相必对预编译这章节有着一定的理解和收获。

每一篇都在很用新的写,如果觉得不错的话,可以用你发财的小手点点赞!!!

  • 41
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言中,预处理是指在编译阶段之前对源代码进行处理的过程,它由编译器的预处理器执行。预处理器会根据以符号#开头的预处理命令,对源代码进行一系列的处理和替换操作。预处理命令可以用来包含头文件、定义宏、条件编译等。 其中,预处理命令#include用于包含其他文件。被包含的文件可以是普通的C语言源程序文件(.c文件),也可以是C语言程序的头文件(.h文件)。在C语言系统中,大量的定义与声明是以头文件形式提供的。通过#include命令,我们可以将头文件中的内容插入到当前源文件中,以便在程序中使用头文件中定义的函数、变量或宏等。 总结起来,预处理C语言中的用法主要包括使用#include命令来包含头文件,以及其他预处理命令用于定义宏、进行条件编译等操作。这些预处理命令能够扩展C语言程序设计的环境,提供了更灵活和高效的代码编写方式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C语言中的预处理详解](https://blog.csdn.net/weixin_32445333/article/details/117124370)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值