首页 > 其他分享 >C语言 预处理详解

C语言 预处理详解

时间:2024-03-27 16:29:05浏览次数:27  
标签:__ 定义 int C语言 编译 详解 参数 预处理 define

目录

前言

一、预定义符号

二、#define定义常量

三、#define定义宏

四、带有副作用的宏参数

五、宏替换的规则

六、宏与函数的对比

七、#和##

7.1#运算符

7.2 ##运算符

八、命名约定

九、#undef

十、命令行定义

十一、条件编译

十二、头文件的包含

12.1 头文件被包含的方式:

12.1.1 本地文件包含

12.1.2 库文件包含

12.2 嵌套文件包含

13、其他预处理指令

13.1#pragma 

总结


前言

C语言中程序需要实现依赖翻译环境和运行环境,而翻译环境可以分为编译和链接两个步骤,其中编译又可以细分为预处理,编译,汇编三个阶段,今天我们就来讲一下预处理阶段。


一、预定义符号

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

比如:

__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
#include<stdio.h>

int main() {
	printf("%s\n", __FILE__); //进⾏编译的源⽂件
	printf("%d\n", __LINE__); //⽂件当前的⾏号
	printf("%s\n", __DATE__); //⽂件被编译的⽇期
	printf("%s", __TIME__); //⽂件被编译的时间
	//printf("%s", __STDC__); //如果编译器遵循ANSI C,其值为1,否则未定义
	return 0;
}

二、#define定义常量

基本语法:

#define name stuff

常见用法:

#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 MAX 1000;
#define MAX 1000
if(condition)
 max = MAX;
else
 max = 0;

如果加上;则MAX就为1000;等替换后,if和else之间就是2条语句,而没有大括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

三、#define定义宏

#define 机制包括了⼀个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。 下面是宏的申明方式:
#define name( parament-list ) stuff

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

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

比如:

#define SQUARE( x ) x * x
这个宏接收⼀个参数 x .如果在上述声明之后,你把 SQUARE( 5 ); 置于程序中,预处理器就会用 下面这个表达式 替换 上面 的表达式: 5 * 5 这个宏存在⼀个问题: 观察下面的代码段:
#include<stdio.h>

#define SQUARE(x) x*x

int main() {
	int a = 5;
	printf("%d", SQUARE(a + 1));
	return 0;
}
输出结果: 我们发现与我们希望的结果不一样,我们想要求a+1及6的平方36,结果输出结果为11。 替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 );
由此可知替换产生的表达式并没有按照预想的次序进行求值。 在宏定义上加上两个括号,这个问题便轻松的解决了:
#define SQUARE(x) (x) * (x)

这样预处理时替换就变成了

printf ("%d\n",(a + 1) * (a + 1) );

让我们再来看一个:

#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));
定义中我们使用了括号,想避免之前的问题,但是这个宏却出现新的错误。这将打印什么值呢?看上去,好像打印100,但事实上打印的是55。 我们发现替换后代码变为:
 printf ("%d\n",10 * (5) + (5));

所以结果还是没有按照预想的次序进行求值。

这个问题,的解决办法是在宏定义表达式两边加上⼀对括号就可以了。
#define DOUBLE( x) ( ( x ) + ( x ) )

总结:

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

四、带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有 副作用 ,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 比如:
x+1;//不带副作⽤
x++;//带有副作⽤
MAX宏可以证明具有副作用的参数所引起的问题。
#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++))

//都为后置++,所以先判断5>8 然后x=6,y=9
//5>8为假,所以为y++,使用先返回y,及z=9,然后y=10

所以当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作用,结果就有可能不符号我们预期。

五、宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。 1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们⾸      先被替换。 2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。 3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复      上述处理过程。 注意: 1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现 递归 。 2. 当预处理器搜索#define定义的符号的时候, 字符串常量 的内容并不被搜索。

六、宏与函数的对比

宏通常被应应用于执行简单的运算。 比如在两个数中找出较⼤的⼀个时,写成下面的宏,更有优势⼀些。
#define MAX(a, b) ((a)>(b)?(a):(b))
为什么不用函数呢? 1. 用 于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏     比函数在程序的规模和速度方面更胜⼀筹。 2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反     之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏的参数是类型无        关的。 和函数相比宏的劣势: 1. 每次使用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加        序的长度。 2. 宏是 没法调试 的。 3. 宏由于类型无关,也就 不够严谨 。 4. 宏可能会带来运算符优先级的问题,导致程 容易出现错 。   宏有时候可以做函数做不到的事情。比如:宏的参数可以出现 类型 ,但是函数做不到。 比如说我们先用malloc函数来开辟一个空间,我们可以用宏来重新定义,此时宏的参数就为类型。
#define MALLOC(num, type)\
 (type )malloc(num sizeof(type))
 ...
//使⽤
 MALLOC(10, int);//类型作为参数

//预处理器替换之后:
 (int *)malloc(10 sizeof(int));
宏和函数的对比
属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用函数的时候,都会调用那个地方的同一个代码
执行速度更快存在函数的调用和返回的额外开销,所以会相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候才开始求值,并将结果值传递给函数。表达式的结果更容易预测。
带有副作用的参数参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制。
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型   函数的参数是与类型有关的,如果类型不同,就需要不同的函数,即使他们执行的任务是不同的。
调试宏是不方便调试的函数是可以逐语句调试的。
递归宏是不能递归的函数是可以递归的。

七、#和##

7.1#运算符

#运算符将宏的⼀个参数转换为字符串字字面量。它仅允许出现在带参数的宏的替换列表中。 #运算符所执行的操作可以理解为” 字符串化 “。 当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 . 就可以写:
#define PRINT(n) printf("the value of "#n " is %d", n);
当我们按照下⾯的方式调⽤的时候: PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,而#a就是转换为"a",时⼀个字符串 代码就会被预处理为
printf("the value of ""a" " is %d", a);

最后打印出:

7.2 ##运算符

## 可以把 位于它两边的符号合成⼀个符号 ,它允许宏定义从分离的文本判断创建标识符。 ## 被称 为 记号粘合 ,这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。 这里我们想想,写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。 比如:
int int_max(int x, int y)
{
   return x>y?x:y;
}
float float_max(float x, float y)
{
   return x>yx:y;
}

但是我们有不同的数据这样写就过于繁琐,我们可以用宏来解决:

//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
       return (x>y?x:y); \
}

通过宏来定义不同的函数:

GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名

int main()
{
	//调⽤函数
	int m = int_max(2, 3);
	printf("%d\n", m);
	float fm = float_max(3.5f, 4.5f);
	printf("%f\n", fm);
	return 0;
}

输出:

八、命名约定

⼀般来讲函数的宏的使用语法很相似。所以语⾔本⾝没法帮我们区分⼆者。

那我们平时的⼀个习惯是: 1.把 宏名全部⼤写 2. 函数名不要全部⼤写

九、#undef

这条指令用于移除⼀个宏定义。
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

比如:

#include<stdio.h>

#define SQORT(x) ((x)*(x))

int main() {
	int a = 3;
	int b = 4;
	printf("%d\n", SQORT(a));
#undef SQORT
	printf("%d\n", SQORT(b));//运行出错,SQORT未定义
}

十、命令行定义

许多C 的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同⼀个源文件要编译出⼀个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了⼀个某个长度的数组,如果机器内存有限,我们需要⼀个很小的数组,但是另外⼀个机器内存大些,我们需要⼀个数组能够大些。)
#include <stdio.h>
int main()
{
   int array [SZ];
   int i = 0;
   for(i = 0; i< SZ; i ++)
   {
    array[i] = i;
    }
   for(i = 0; i<SZ; i ++)
   {
   printf("%d " ,array[i]);
   }
   printf("\n" );
   return 0;
}

通过命令行命令:

//linux 环境演⽰
gcc -D SZ=10 programe.c

十一、条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句) 编译或者放弃 是很方便的。因为我们有 条件编译指令 。 比如说: 调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。
#include<stdio.h>

#define __DEBUG__

int main(){
  int i = 0;
  int arr[10] = { 0 };
  for (i = 0; i < 10; i++)
  {
	  arr[i] = i;
#ifdef __DEBUG__
	printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
#endif //__DEBUG__
  }
 return 0;
}

通过判断是否定义__DEBUG__进行选择编译

常⻅的条件编译指令:
1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif

进行条件判断。条件满足进行编译if到endif的代码,不满足则不进行编译。

2.多个分⽀的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif

多个if进行分支判断,else if此时为elif。

3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

通过if defined判断是否被定义,如果被定义则执行,没被定义则不执行,可简写为ifdef。if !defined 用法则相反,可简写为 ifndef。

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

我们需要注意一个if对应一个endif。

十二、头文件的包含

12.1 头文件被包含的方式:

12.1.1 本地文件包含
#include "filename"
查找策略 :先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。 如果找不到就提示编译错误。 Linux环境的标准头文件的路径:
 /usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径
注意按照自己的安装路径去找。
12.1.2 库文件包含
#include <filename.h>
查找策略 : 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。 这样是不是可以说,对于库文件也可以使用  “” 的形式包含? 答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

12.2 嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个文件被编译。就像它实际出现于 #include 指令的 地方⼀样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。 ⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。 test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
 
   return 0;
}

test.h

void test();
struct Stu
{
   int id;
   char name[20];
};
如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h文件的内容将会被 拷贝 5份在test.c中。 如果test.h 文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被⼤家都能使用,又不做任何的处理,那么后果真的不堪设想。 如何解决头文件被重复引⼊的问题?答案:条件编译。 每个头⽂件的开头写:
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

或者:

#pragma once
就可以避免头文件的重复引入。 注: 推荐《⾼质量C/C++编程指南》中附录的考试试卷。

13、其他预处理指令

C语言中还要其他的其他预处理指令 ,比如:

#error
#pragma
#line
...
...

13.1#pragma 

由 #pragma 指令控制实现定义行为。

#pragma once 

当某个头文件中包含它时,指示编译器只对其分析一次,即使它在同一源文件中(直接或间接)被包含了多次也是如此。

#pragma pack()

用于修改结构体的对齐字数,可以指定。


总结

上述文章讲了预处理阶段做的事,详细讲了#define的使用,和条件编译,头文件的包含等方面的知识,希望对你有所步帮助。

标签:__,定义,int,C语言,编译,详解,参数,预处理,define
From: https://blog.csdn.net/2301_76613753/article/details/137075584

相关文章

  • Linux dmesg命令使用方法详解
    Linuxdmesg命令使用方法详解一、命令简介dmesg(displaymessage)命令用于显示开机信息。kernel会将开机信息存储在ringbuffer中。您若是开机时来不及查看信息,可利用dmesg来查看。开机信息亦保存在/var/log/dmesg中。二、使用方法dmesg[options]三、命令选项[root@loc......
  • Linux系统压缩命令详解
    在Linux系统中,tar(tapearchive)命令是一个非常强大的工具,主要用于文件和目录的打包与归档,同时也支持与其他压缩程序结合实现数据的压缩和解压缩操作。它最初设计用于磁带备份,但现在广泛应用于硬盘和其他存储设备上创建档案文件。以下是tar命令的一些基本用法:打包:不压缩的......
  • 网上的一个用C语言实现FFT算法
     用C语言实现FFT算法/*****************fftprograme*********************/#include"typedef.h"#include"math.h"structcompxEE(structcompxb1,structcompxb2){structcompxb3;b3.real=b1.real*b2.real-b1.imag*b2.imag;b3.imag=b1.real*b2.imag+b1.imag*b2.real;......
  • 宏定义(C语言)
    1、宏定义一个两数相乘#defineMUL(a,b)a*b代码如下:#include<stdio.h>#defineMUL(a,b)a*bintmain(){intvalue;printf("value=%d\n",MUL(2,4));return0;}2、在虚拟机中运行,利用如下命令进行屏蔽屏蔽头文件,就可以完成宏替换。gcc-Edemo.c-o......
  • AXI_Lite协议详解
    文章目录摘要一、AXI_Lite接口全局信号:1、写地址通道:2、写数据通道:3、写响应通道:4、读地址通道:5、读数据通道:二、仿真波形摘要AXI4-lite是AXI4-full的简化版。用于简单、低吞吐量的内存映射通信。主要用于内核和外设寄存器之间的通信。比如当访问串口的数据寄存器时......
  • Java版直播商城免 费 搭 建:平台规划与常见营销模式,电商源码、小程序、三级分销及详解
    【saas云平台】打造全行业全渠道全场景的saas产品,为经营场景提供一体化解决方案;门店经营区域化、网店经营一体化,本地化、全方位、一站式服务,为多门店提供统一运营解决方案;提供丰富多样的营销玩法覆盖所有经营场景,助力商家成功;系统稳定压倒一切,让商家经营有保障,消费者购物更放心......
  • 0基础 三个月掌握C语言(15)
    动态内存管理为什么要有动态内存分配我们已经掌握的内存开辟⽅式有:intval=20; //在栈空间上开辟四个字节chararr[10]={0};//在栈空间上开辟10个字节的连续空间但上述的开辟空间的⽅式有两个特点:•空间开辟⼤⼩是固定的。•数组在申明的时候,必须指定数组的......
  • 基数排序详解
    基数排序详解一、基数排序的基本概念二、基数排序的特点二、基数排序的工作过程三、基数排序的伪代码四、基数排序的C语言代码示例五、基数排序的稳定性六、基数排序的优化与变体七、基数排序的应用场景八、结论在计算机科学中,排序算法是一种非常基础和重要的算法类型......
  • Transformer 预测过程 详解
    我们看到很多文章讲了transformer架构的高层概述,包括其中一些主要组件。但大部分文章没有讲整个预测过程是如何一步步进行的。让我们通过一个简单的例子来详细了解一下。在这个例子中,你将会看到一个翻译任务或者序列到序列任务,这恰好是transformer架构设计者最初的目标。你将......
  • C语言-实现文件操作
    0.前言:    我们知道下载东西,电脑上就会有各种的文件夹及文件里面的内容,那么文件里面的数据怎么通过编写程序来帮我们获取呢,这些文件又是怎么创建的呢?C语言给我们提供了一些可以操作文件的函数。这里我只列举了一部分操作文件的函数,使用这些函数需要引入头文件<stdlib.......