首页 > 其他分享 >【C语言】预处理

【C语言】预处理

时间:2023-12-10 16:03:20浏览次数:28  
标签:__ 定义 int C语言 编译 printf 预处理 define

1、预定义符号

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

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

举例:

printf("file:%s line:%d\n", __FILE__, __LINE__);

2、#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;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,⽽没有⼤括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

3、#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

警告:

这个宏存在⼀个问题: 观察下⾯的代码段:

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

乍⼀看,你可能觉得这段代码将打印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));

乘法运算先于宏定义的加法,所以出现了 55 .

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

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


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

4、带副作用的宏参数

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

例如:

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++));

所以输出的结果是:x=6 y=10 z=9

5、宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。

  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
  2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

6、宏函数的对比

宏通常被应⽤于执⾏简单的运算。

⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。

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

那为什么不⽤函数来完成这个任务?

原因有⼆:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏是类型无关的。

和函数相比宏的劣势:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
  2. 宏是没法调试的。
  3. 宏由于类型⽆关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type)\
 (type )malloc(num sizeof(type))
 ...
 //使⽤ 
 MALLOC(10, int);//类型作为参数 
 
 //预处理器替换之后: 
 (int )malloc(10 sizeof(int));

宏和函数的一个对比

【C语言】预处理_#define

7、#和##

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.

代码就会被预处理为:

printf("the value of ""a" " is %d", a);

运⾏代码就能在屏幕上打印:

the value of a is 10

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;
}

输出:

3
4.500000

8、命名约定

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

那我们平时的⼀个习惯是:

把宏名全部⼤写

函数名不要全部⼤写

9、#undef

这条指令用于移除一个宏定义

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

10、命令行定义

许多 C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。

例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某 个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个 机器内存⼤些,我们需要⼀个数组能够⼤些。)

#include <stdio.h>
int main()
{
   int array [ARRAY_SIZE];
   int i = 0;
   for(i = 0; i< ARRAY_SIZE; i ++)
   {
   array[i] = i;
   }
   for(i = 0; i< ARRAY_SIZE; i ++)
   {
   printf("%d " ,array[i]);
   }
   printf("\n" );
   return 0;
}

编译指令:

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

11、条件编译

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

⽐如说:

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

#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;
}

常⻅的条件编译指令:

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

12、头文件的包含

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

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


标签:__,定义,int,C语言,编译,printf,预处理,define
From: https://blog.51cto.com/u_16189938/8761433

相关文章

  • 《初学C语言第19天》
    //////——————————————-作业题//////写一个二分查找函数//////功能:在一个升序数组中查找指定的数值,找到了就返回下标,找不到就返回-1.//////arr是查找的数组//////left数组的左下标//////right数组的右下标//////key要查找的数字//#include<stdio.h>//......
  • 轻松拿下C语言的分支与循环结构
    C语言是由顺序结构、选择结构、循环结构组成的结构化程序设计语言。我们日常所见的事情都可以拆分成这三种结构或者这三种结构的组合。顺序结构:按语句出现的先后顺序,以此执行。选择结构(也叫分支结构):根据所给定的条件选择是否执行。循环结构:根据要求,将语句重复执行多次。接下来......
  • 小学生教学辅助系统C语言实验报告
    一、实验目的(1)  掌握变量、函数的使用(2)  掌握选择结构、循环结构的使用(3)  熟悉模块化程序设计的方法二、实验仪器或设备微型计算机三、总体设计(设计原理、设计方案及流程等)设计一个简单的小学生计算机辅助教学系统,帮助小学生学习四则运算。使用模块化程序设计方法......
  • 《初学C语言第18天——扫雷游戏特别篇》
    //(game.h.cpp)#define_CRT_SECURE_NO_WARNINGS1#defineROW9#defineCOL9#defineROWSROW+2#defineCOLSCOL+2#defineLEI10//雷的个数10#include<stdio.h>#include<stdlib.h>#include<time.h>//函数声明voidInitBoard(charboard[ROWS][COLS],......
  • 【C语言】编译和链接
    1、翻译环境和运行环境在ANSIC的任何⼀种实现中,存在两个不同的环境。第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。第2种是执行环境,它⽤于实际执行代码。2、翻译环境那翻译环境是怎么将源代码转换为可执⾏的机器指令的呢?这里我们就得展开开讲解⼀下翻译环境所做......
  • PTA|C语言|结构体
    --------------------------------------------------------------------------------计算两个复数之积本题要求实现一个计算复数之积的简单函数。函数接口定义:structcomplexmultiply(structcomplexx,structcomplexy);其中structcomplex是复数结构体,其定义如下:structcompl......
  • 通过C语言的API预处理管理MySQL
    一、C的API预处理语句MySQL客户端/服务器协议提供了预处理语句。该功能采用了由mysql_stmt_init()初始化函数返回的MYSQL_STMT语句处理程序数据结构。对于多次执行的语句,预处理执行是一种有效的方式。首先对语句进行解析,为执行作好准备。接下来,在以后使用初始化函数返回的语句句柄......
  • 12.9 蓝桥杯 huffuman树c语言
    今天学习了蓝桥杯的huffuman树,总结如下:问题描述Huffman树在编码中有着广泛的应用。在这里,我们只关心Huffman树的构造过程。给出一列数{pi}={p0,p1,…,pn-1},用这列数构造Huffman树的过程如下:1.找到{pi}中最小的两个数,设为pa和pb,将pa和pb从{pi}中删除掉,然后将它们的和加......
  • 通过C语言连接MySQL数据库
    一、如何连接MySQL首先需要安装MySQLsudoapt-getupdatesudoapt-getinstallmysql-serverC语言的API代码是与MySQL一起提供的,它包含在mysqlclient库中,并允许C程序访问数据库,如果安装MySQL后仍提示缺少库,则需要安装如下:sudoapt-getinstalllibmysqlclient-devsudoa......
  • C语言源码的陷波器设计及调试总结
    一前记音频信号处理中,限波器是一个常用的算法。这个算法难度不是很高,可用起来却坑很多。二源码解析1滤波器的核心函数,这里注意两点,一个是带宽不能太宽了,太宽了杀伤力太大了,容易出问题。另外一个就是滤波器的阶数非常重要,假如想滤波宽度尽量窄一些,那就阶数尽量高一些......