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\n", __TIME__);
  // 下面的_STDC_在vs上是未定义的,编译就会报错
  //printf("%d\n", __STDC__); 
  return 0;
}


d4846f92e017425280ec900bb6ac46d4.png


有了这些预定义符号,我们可以随时随地的知道此时的时间和文件所在位置啦。

预处理指令#define


在C或C++语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。


#define 和 #include 一样,也是以“#”开头的。凡是以“#”开头的均为预处理指令,#define也不例外。


#define又称宏定义,标识符为所定义的宏名,简称宏。标识符的命名规则与前面讲的变量的命名规则是一样的。#define 的功能是将标识符定义为其后的常量。一经定义,程序中就可以直接用标识符来表示这个常量。是不是与定义变量类似?但是要区分开!变量名表示的是一个变量,但宏名表示的是一个常量。可以给变量赋值,但绝不能给常量赋值。


宏定义 #define 一般都写在函数外面,与 #include 写在一起。当然,写在函数里面也没有语法错误,但通常不那么写。#define 的作用域为自 #define 那一行起到源程序结束。如果要终止其作用域可以使用 #undef 命令,下面会介绍。


还需详细了解 #define, 可以点此链接观摩大佬解析。


语法:

#define name stuff


举些个栗子:

#define MAX 100
#define FOREVER for(;;)
#define reg register


可以看到宏的命名习惯都是大写,这样更能区别。


标识符的定义与常量是以空格隔开的。


第一个定义了一个标识符 MAX ,它是常量 100,当我们在用这个标识符时,在预处理阶段,MAX 将会被替换成100。


第二个是用更形象的符号来替换一种实现, for( ; ; ) 相当于死循环,这里用 FOREVER 来形象的表示它。


第三个是为 register这个关键字,创建一个简短的名字。


那么我们在define 定义标识符的时候,要不要在最后加上 ;

比如:

#define MAX 100;


建议不要加上,因为当我们写C语言程序时,都会习惯在后面加上分号,如果是一个变量等于这个MAXint max = MAX;),这时预处理阶段,会把这个 MAX 替换,变成 int max = 100;; ,此时有两个分号,这就出现了语法问题。


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


下面是宏的申明方式:

#define name( parament-list ) stuff


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

注意:

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

如:

#define SQUARE(x)  x * x


这个宏接收一个参数 x ,如果在上述声明后,有以下写法:

SQUARE(5);
SQUARE(2 + 3);


第一种预处理阶段被替换后表达式变为:5 * 5;(计算结果为25

第二种预处理阶段被替换后表达式变为:2 + 3 * 2 + 3;(计算结果为11

可以看出,第二种并不是我们想要的结果,所以这种定义宏的方式有问题


那如果这样子使用呢:

SQUARE((2 + 3));

替换后表达式变为:(2 + 3) * (2 + 3)这样子是可以的,但是这样治标不治本,而且写的代码还不好看,所以我们直接在定义宏处加上括号,这样就更好了。

更新之后:

#define SQUARE(x)  (x) * (x)


那么这样子是否还会有问题呢?实际上还是有的。

如果是一下定义的宏:

#define SQUARE(x)  (x) + (x)


有了上面的声明后,进行一下操作:

int a = 5 * SQUARE(5);


替换后表达式为:

int a = 5 * 5 + 5;


计算结果为:30,这也与我们想要的值不符。

所以我们在定义时要给整体也加上一个括号,这样才不会出错

正确规范定义:

#define SQUARE(x)  ((x) + (x))


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


#define替换规则


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


在调用宏时,首先对参数进行检查,看看是否包含任何由 #define定义的符号。如果是,它们首先

被替换。

替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上

述处理过程。


注意:


4. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。

5. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。


预处理指令 #undef


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

在一个程序块中用完宏定义后,为防止后面标识符冲突需要取消其宏定义。

例如:

#define MAX 100
int a = 100;
#undef MAX


这里第三行就取消了MAX的红定义,在下面还可以继续定义以MAX为标识符的宏。


宏和函数的对比


宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个:

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


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

原因有两点:


用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多(也就是函数调用和函数返回的栈帧的创建和销毁可能比实际的代码功能运行时间还要长)。所以宏比函数在程序的规模和速度方面更胜一筹。

更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用>来比较的类型。宏是类型无关的。


当然宏跟函数比较,也有其缺点:


每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

宏是没法调试的(不可以调试可能程序怎么出错的都不知道)。

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

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

根据宏的优点举个栗子(宏的参数可以出现类型,但是函数做不到):

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


宏和函数的对比图

c8976167e14b4f23bb03e5745ce3fde1.png


命名约定


一般来讲函数和宏的语法很相似。所以语法本身没法帮我们区分二者。

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

  • 把宏名全部大写
  • 函数名不要全部大写


命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:


  • 当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些)。

如有下面的代码(在linux中):


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


可以看到,上面的 ARRAY_SIZE是未定义的,但是我们可以通过以下指令对其赋值:

linux 环境演示

指令: gcc -D ARRAY_SIZE=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;
}


#ifdef 是如果定义了就干嘛干嘛,#endif 是截断 #ifdef的作用继续往下延伸。

可以看到,前面定义了_DEBUG_,所以后面的 printf("%d\n", arr[i]); 这条语句将会被编译执行。

  • 常见的条件编译指令:
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


#include 可以说是再熟悉不过了,我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。


这种替换的方式很简单:

  1. 预处理器先删除这条指令,并用包含文件的内容替换。
  2. 这样一个源文件被包含10次,那就实际被编译10次。


而头文件的包含方式有两种:

  • 一种是本地文件包含:
#include "filename"


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


Linux环境的标准头文件的路径:

/usr/include


VS环境的标准头文件的路径:

注意: 不同编译器可能放在不同地方,要按照自己的安装路径去找。

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include


  • 一种是库文件包含
#include <filename.h>


查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。这样是不是可以说,对于库文件也可以使用 “ ” 的形式包含?答案是肯定的,可以。 但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。


嵌套文件包含


如果出现这样的场景:


6d8e63b06de245aba8e7342e80c8aed7.png

comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。

那么如何解决这样的问题呢? 答案是:条件编译

每个头文件的开头写:

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


或者:

#pragma once


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

注:

推荐 《高质量C/C++编程指南》 中附录的考试试卷(很重要)。

笔试题:

1. 头文件中的 ifndef/define/endif是干什么用的?
2. #include <filename.h> 和 #include "filename.h"有什么区别?


其他预处理指令

#error
#pragma
#line
...


这里就不一一做介绍,可以自己去了解。 #pragma pack()结构体一章 介绍了,可以去看噢。


写在最后


C语言阶段的知识学到这里,差不多就结束了呢,一路过来还是学到了非常多的知识,这也让我更加认清了自己的路还长着呢,接下来我会继续更新 基本数据结构 阶段的相关知识。

感谢阅读本小白的博客,错误的地方请严厉指出噢!


相关文章
|
1月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
62 23
|
1月前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
66 15
|
1月前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
60 24
|
1月前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
63 16
|
1月前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
36 3
|
1月前
|
存储 算法 安全
【C语言程序设计——函数】分数数列求和1(头歌实践教学平台习题)【合集】
if 语句是最基础的形式,当条件为真时执行其内部的语句块;switch 语句则适用于针对一个表达式的多个固定值进行判断,根据表达式的值与各个 case 后的常量值匹配情况,执行相应 case 分支下的语句,直到遇到 break 语句跳出 switch 结构,若没有匹配值则执行 default 分支(可选)。例如,在判断一个数是否大于 10 的场景中,条件表达式为 “num> 10”,这里的 “num” 是程序中的变量,通过比较其值与 10 的大小关系来确定条件的真假。常量的值必须是唯一的,且在同一个。
19 2
|
1月前
|
存储 编译器 C语言
【C语言程序设计——函数】回文数判定(头歌实践教学平台习题)【合集】
算术运算于 C 语言仿若精密 “齿轮组”,驱动着数值处理流程。编写函数求区间[100,500]中所有的回文数,要求每行打印10个数。根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码。如果操作数是浮点数,在 C 语言中是不允许直接进行。的结果是 -1,因为 -7 除以 3 商为 -2,余数为 -1;注意:每一个数据输出格式为 printf("%4d", i);的结果是 1,因为 7 除以 -3 商为 -2,余数为 1。取余运算要求两个操作数必须是整数类型,包括。开始你的任务吧,祝你成功!
51 1
|
C语言 编译器
《C语言编程初学者指南》一1.6 使用指令
本节书摘来自华章出版社《C语言编程初学者指南》一书中的第1章,第1.6节,作者【美】Keith Davenport(达文波特) , M1ichael Vine(维恩),更多章节内容可以访问云栖社区“异步社区”公众号查看 1.6 使用指令 下面再看看本章一开始给出的示例程序。
1506 0
|
2月前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
92 10
|
2月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
68 9

热门文章

最新文章