笔记24-2(C语言进阶 程序环境和预处理)

目录

注:

预定义详解

预处理符号

举例

 使用例

#define

#define 定义标识符

#define定义宏 - 括号很重要

#define 替换规则

# 和 ##

带副作用的宏参数

宏和函数的对比

命名约定

#undef

命名行定义

条件编译

常见的条件编译命令

文件包含

头文件被包含的方式

嵌套文件包含

其他预处理指令


注:

        本笔记参考B站up:鹏哥C语言的视频


预定义详解

预处理符号

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

 这些预处理符号都是语言内置的,通过它们,可以记录一些日志。

举例

printf("%s\n", __FILE__);

打印结果为:

        d:\code\exp\lab.c

即代码源文件所在路径及源文件的名字。


printf("%d\n", __LINE__);

打印结果为:

        6

即该行代码所在的行号。


printf("%s\n", __DATE__);    //打印日期
printf("%s\n", __TIME__);    //打印时间

打印结果为:

        Jun 29 2022
        17:26:06

刚刚好是程序执行时对应的时间。


printf("%s\n", __FUNCTION__);    //对应__FUNCTION_所在的函数名

打印结果为:
        main

因为该函数的打印是在main函数上进行的。


printf("%d\n", __STDC__);

注:VS编译器应该是不支持该符号的,同时也不支持ANSI C标准。所以这里使用的是VS Code。

打印结果为:

        1

 使用例

#include<stdio.h>

int main()
{
	int i = 0;
	FILE* pf = fopen("log.txt", "a+");

	if (pf == NULL)
	{
		perror("fopen\n");
		return 1;
	}

	for ( i = 0; i < 10; i++)
	{
		fprintf(pf, "%s %d %s %s %d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
	}
		
	fclose(pf);
	pf = NULL;

	return 0;
}

执行程序,在源文件所在文件夹下就生成了一个 log.txt ,打开文件:

这就生成了一个日志。因为使用的读写形式的 "a+" ,是追加的方式,所以每运行一次,就会追加一段日志内容。

#define

#define 定义标识符

语法

#define name stuff

#define的作用:

  • 定义符号

举例

#define MAX 1000
#define reg register为 register 这个关键字创建一个更简短的名字。
#define do_forever for(;;)用更形象的符号替换一种实现。
#define CASE break;case在写 case 语句时,自动写上 break 。

#define DEBUG_PRINT printf("file:%s\tline:%d\t \

                                                date:%s\ttime:%s\n" ,\

                                                __FILE__ ,__LINE__ ,

                                                __DATE__ ,__TIME__ )

如果定义的 stuff 过长,可以分成几行写。

最后一行除外,每行的最后都要加上一个反斜杠(续行符)。

提问:在使用 #define 时,后面需不需要加上 ; (分号)?即:

#define M 1000;

#define M 1000

这两者之间的区别。

答:这两种写法在语法上都是行得通的。但是存在一个小小的区别。

会在替换时把1000后面的分号也给替换过来,也就是

在替换时不会存在这种分号

只是从实际上讲,通常不会多加一个分号。

但是还是会有无法多加一个分号的情况,如:

#include<stdio.h>
#define M 1000;

int main()
{
	int a = 10;
	int b = 0;
	if (a > 10)
		b = M;
	else
		b = -M;

	return 0;
}

这种情况是会报错的,如果我们把 M 替换成 1000; 就会发现变成


if (a > 10)
	b = 1000;
;
else
	b = -M;

if语句后面跟了两条语句,这时候 else 匹配 if 就会发生混乱。因为存在两条语句,else 就会不知道到底匹配那条语句。

所以,一般情况下建议不要在使用 #define 定义标识符时在末尾加上一个分号,除非是必要情况。

#define定义宏 - 括号很重要

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

宏的申明方式:

#define name( parament-list ) stuff

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

注意:

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

举个例子

定义一个宏 #define SQUARE(X) X*X

#include<stdio.h>
#define SQUARE(X) X*X

int main()
{
	printf("%d\n", SQUARE(3));
	return 0;
}

预处理器会把 SQUARE(3) 替换为 SQUARE(3*3),最终的打印结果就是 9 。

举一反三:如果写入 SQUARE(3+1),那么打印结果会是多少?

打印结果:

这里的打印结果是 7,而不是 16 。

这里就涉及一个重要的知识点:宏是先完成替换,再完成计算的。上面的SQUARE(3+1)到底是怎么进行工作的呢?

首先,SQUARE(3+1)被替换成 3 + 1*3 + 1,然后再计算,等于 7。

修改:如果我们需要上述的宏可以实现表达式的运算,最好应该怎么做?

定义宏:#define SQUARE(X) ( (X) * (X) )

(X)使得宏会优先计算括号内的内容,而为了计算的严谨,通常会把宏的计算的整体结果也用括号括起来,变成( (X) * (X) )

但是,即使加上括号,如果括号不到位,宏仍然可能存在问题。

如:

#include<stdio.h>
#define DOUBLE(X) (X) + (X)

int main()
{
	printf("%d\n", 10 * DOUBLE(4));
	return 0;
}

按照设想,计算应该是 10 * ( 4 + 4 ) = 80,那么实际结果究竟是什么呢?

为什么是44呢?这就是替换不到位了,实际上 10 * DOUBLE(4) 在预处理器之后,变成的是 10 * 4 + 4,因为最外面少了一个括号,所以应该把宏定义为 DOUBLE(X) ( (X) + (X) )

#define 替换规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果包含,这些符号会首先被替换。
  2. 替换文本随后被插入到程序中原本文本的位置。如果是宏,参数名被它们的值替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果仍有包含,则重复上述处理过程。

比如:

#define M 100
#define MAX(X, Y) ( (X)>(Y)?(X):(Y) )

int main()
{
	int max = MAX(101, M);
	return 0;
}

在上述的#define替换中:

首先被替换的就是 int max = MAX(101, M); 中的 M ,替换为:int max = MAX(101, 100);

然后对于宏,替换参数名,即 MAX(101, 100) 变为 ( (101)>(100)?(101):(100) )

注意

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

# 和 ##

#的使用运行我们把参数输入字符串中。(只能在宏内使用)

  • # 的作用就是把#X替换成 X的内容所对应的字符串。
  • ## 可以进行符号的组合。

提问:下面这串代码执行结果是什么?

#include<stdio.h>

int main()
{
	printf("Hello world\n");
	printf("Hello " "world\n");

	return 0;
}

打印结果:

此处,第二个printf函数内存在 两个字符串,但是两个printf函数最终的处理结果是相同的,也就是说,第二个printf函数内的两个字符串是天然连接到一起的

那么假设:

int a = 10;        ||| 要求打印 the value of a is 10

int b = 20;        ||| 要求打印 the value of b is 20

int c = 30;        ||| 要求打印 the value of c is 30

可以看出,上面的三个要求十分相似,在这种情况下如果使用三条 printf语句 打印,就较为冗余了,那怎么办呢?

我们可以写成一个功能函数:

#include<stdio.h>

void print(int x)
{
	printf("the value of ? is %d\n", x);
}

int main()
{
	int a = 10;
	print(a);

	int b = 20;
	print(b);

	int c = 30;
	print(c);
}

但是我们要怎么打印 的内容呢?说到底,x 和谁配对函数怎么知道呢?无论字符串内写成什么内容,都无法完美完成要求。

虽然函数不行,但是我们还有宏可以使用。

先尝试一种错误的写法:

#include<stdio.h>
#define PRINT(X) printf("the value of " X " is %d\n", X);

int main()
{
	int a = 10;
	PRINT(a);

	int b = 20;
	PRINT(b);

	int c = 30;
	PRINT(c);

	return 0;
}

发现报错了:

报错是发生在链接之后,替换已经完成,说明发生的问题存在于 #define 中。说明上面的 #define 写法是有问题的。

那么什么才是正确的写法呢?

#define PRINT(X) printf("the value of " #X " is %d\n", X);

这里就体现了 # 的作用了,进行打印,发现可行:

# 的作用就是把#X替换成 X的内容所对应的字符串。

基于这种功能,如果我们需要这个宏不仅可以打印整型数据,并且可以打印浮点型的数据,譬如:

float f = 5.5f;
PRINT(f);

此时原本的宏内使用的 %d 就无法满足我们的需求了。要怎么办呢?

可以让宏多出来一个参数,用来处理类型的问题,即:

#define PRINT(X, FORMAT) printf("the value of " #X " is "FORMAT"\n", X);

#include<stdio.h>
#define PRINT(X, FORMAT) printf("the value of " #X " is "FORMAT"\n", X);

int main()
{
	int a = 10;
	PRINT(a, "%d");

	int b = 20;
	PRINT(b, "%d");

	int c = 30;
	PRINT(c, "%d");

	float f = 5.5f;
	PRINT(f, "%f");

	return 0;
}

打印结果:

为什么可以完成这种操作呢?最主要的,是考虑这里的FORMAT到底被替换成了什么

就拿PRINT(f, "%f")举例,这里的函数名被替换成了:

printf("the value of " "f" " is ""%f""\n", X);  这时候就是一段完整的代码了。


再来看看 ## :

#include<stdio.h>
#define CAT(X, Y) X##Y

int main()
{
	int class101 = 100;
	printf("%d\n", CAT(class, 101));
	
	return 0;
}

打印结果为:

为什么这里是100?这就是 ## 的作用了。

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

注:链接产生的标识符必须合法。

而这里,就是把 class 和 101 组合成了 class101 这个变量了。

printf("%d\n", CAT(class, 101));
printf("%d\n", class101);
//这两条语句是等价的。

带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么这个宏在被使用的过程中可能出现危险,导致不可预测的结果。

副作用就是表达式求值时出现的永久性效果。

int main()
{
	int a = 1;

	int b = a + 1;	//程序执行后,b = 2, a = 1
	int b = a++;	//程序执行后,b = 2, a = 2 —— 存在副作用

	return 0;
}

在上面的代码中,a++ 在 b 被赋值的同时,也改变了 a 本身的大小,这就是存在副作用的。

例子

#include<stdio.h>
#define MAX(X,Y) ( (X)>(Y)?(X):(Y))

int main()
{
	int a = 5;
	int b = 8;
	int m = MAX(a++, b++);
	printf("m = %d\n", m);

	return 0;
}

打印结果:

不是 8,而是 9

解析:

先看看MAX到底替换了什么?

int m = MAX(a++, b++) 变为了:int m = ( (a++) > (b++) ? (a++) : (b++) )。此处进行的替换是三目操作符,从左向右依次执行:

  1. 在判断部分(也就是(a++) > (b++)),由于后置++,故取 5 > 8,是假命题,接下来要执行 (b++)
  2. 但是,在执行 ? (a++) : (b++) 中的 (b++) 之前,(a++) > (b++) 中的 a++ b++ 会优先执行,此时a = 5 + 1 = 6,b = 8 + 1 = 9。
  3. 接下来执行? (a++) : (b++) 中的 (b++) ,也就是先返回 b 当前的值,然后进行 b++ 操作,故 m 被赋值为 9,接下来 b++ 使 b 的值变为 10。

验证:通过观察最终 a 和 b 的值判断解析是否出错。

#include<stdio.h>
#define MAX(X,Y) ( (X)>(Y)?(X):(Y))

int main()
{
	int a = 5;
	int b = 8;
	int m = MAX(a++, b++);

	printf("a = %d, b = %d\n", a, b);

	return 0;
}

打印结果:

a = 6, b = 10,符合解析。

宏和函数的对比

宏通常被应用于执行简单的运算,比如求较大值的宏。

#define MAX(X,Y) ((X)>(Y)?(X):(Y))//宏

int Max(int x, int y)//函数
{
	return x > y ? x : y;
}

为什么不使用函数来完成该任务呢?

宏相比于函数的优势

  1. 相比于实际执行这个小型计算工作,用于调用函数和从函数返回的代码 可能花费比实际执行更多的时间。所以,比起函数,宏在程序的规模和速度方面更胜一筹
  2. 更为重要的,函数的参数必须声明为特定的类型,所以函数只能在类型适合的表达式上使用。宏则可以适用于整型、长整型和浮点型等可以用 > 来比较的类型。宏是类型无关的

举例来证明优势1,现在需要比较 a 和 b 的较大值:

#define MAX(X,Y) ( (X)>(Y)?(X):(Y) )//宏

int Max(int x, int y)//函数
{
	return x > y ? x : y;
}

int main()
{
	int a = 5;
	int b = 8;

	int m = MAX(a, b);//使用宏

	m = Max(a, b);//使用函数

	return 0;
}

使用宏 [ MAX(a, b) ]的那一行代码在预编译阶段就已经被替换成:int m = ( (a)>(b)?(a):(b) )了,在这种情况下开始调试,打开反汇编,查看汇编代码:

这是执行一个宏所需要的汇编代码:

这是执行一个函数所需要的汇编代码:

1. 首先进入代码

2. 然后呼叫函数

3. 接着正式进入函数

4. 开始执行函数内部内容

5. 接着要把函数执行的结果带回主函数

最后结果被放到[m]中。

  可以看到,为了调用一个函数,需要做大量的准备工作。同时,函数的返回也需要进行额外的操作。最后就会发现,调用一个函数需要大量的时间开销,而使用对等的时间,程序甚至可以进行好几个简单运算。

 当然,宏相比于函数,也是存在劣势的:

  1. 每次使用宏的时候,一份宏定义的代码将被插入到程序中。所以,除非宏比较短,否则宏的使用可能会大幅度增加程序的长度。
  2. 宏是没法调试的。(宏的替换会在预处理阶段被完成,但是调试是在可执行程序的基础上进行的)
  3. 宏由于类型无关,也就会存在不够严谨的情况。
  4. 宏可能会带来运算符优先级的问题,导致程序容易出现错误。(譬如:如果传入宏内部的是一个表达式,而这个宏本身也是由各种表达式组成的,当操作符交集在一起时,就有可能因为优先级问题而出现错误

额外的例子(宏可以完成,而函数无法完成的任务)

通过更加简便的方式开辟一块空间:

#include<stdio.h>
#include<stdlib.h>

#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
int main()
{
	int *p = MALLOC(10, int);
	return 0;
}

总结

属性#define定义宏函数
代码长度

每次使用时,宏代码都会被插入到程序中。

使用的宏,除了非常小的宏,都会导致程序的长度大幅度增长。

函数代码只会出现在一个地方。

每次使用这个函数时,都是从同一个地方调用的代码。

执行速度更快存在函数的调用和返回的额外开销,所以会相对慢一些。
操作符优先级

宏参数的求值是在周围所有表达式的上下文环境内进行的。

除非加上括号,否则临近操作符的优先级问题可能会导致不可预料的后果,所以建议宏在书写的时候多加一些括号。

函数参数只在函数调用的时候进行一次求值,它的结果值将被传递给某一个函数。

表达式的求值结果更容易被预测。

带有副作用参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候被进行一次求值,更方便控制结果。
参数类型

宏的参数是于类型无关的。

只要对参数的操作是合法的,宏就可以使用任何参数类型。

函数的参数是于类型有关的。

如果参数的类型不同,就需要不同的函数,即使它们执行的任务是不同的。

调试宏是不方便调试的。

函数是可以逐语句调试的。

递归宏是不能递归的。函数是可以递归的。

如果一个运算的功能足够简单,可以考虑使用宏来完成该功能。反之,复杂的实现就交给函数来完成。

可扩展(内敛函数)

关键字:inline   —   结合了宏与函数的优点。

命名约定

一般来讲,函数和宏的使用语法是很相似的,语言本身没办法帮助我们区别二者。所以作为一个约定,我们习惯:

||| 宏的名字全部大写

||| 函数的名字不要全部大写

#undef

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

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

命名行定义

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

例如:当我们根据一个源文件要编译出不同的程序的不同版本的时候,这个特性就会有点用处。

举例

#include<stdio.h>

int main()
{
    int arr[M] = { 0 };
    int i = 0;

    for(i = 0; i < M; i++)
    {
        arr[i] = i;
    }

    for(i = 0; i < M; i++)
    {
        printf("%d ", i);
    }
}

如果只是直接编译,那么在Linux环境下,会出现如下的报错

那么有没有什么办法,能够在不改变代码的前提下,使得程序正常运行呢?

接下来我们输入:

gcc test.c -D M=10

通过这种形式的输入,我们把 M 定义为了 10 这个数字。

可以发现,程序确实被成功编译。同理,我们可以把这个 M 定义为 100、1000、10000……而唯一需要改变的,就是 M 所对应的值。

这就是把 M 的值设置成100时执行程序的结果。

ls -a
ls -l
ls -al

在Linux环境下,形如 -a-l-al 这种形式的参数,被统称为命令行参数

而上述的 M 是在命令行中被定义的,这就是命令行定义

条件编译

在编译一个程序的时候,我们如果要将一条语句(或者一组语句)编译或者放弃是很方便的,只要使用条件编译指令即可。

比如说:调试性的代码,删除的话会很可惜,保留的话又会碍事,此时我们就可以进行选择性的编译。

举个例子

上述的代码中的 printf语句 ,只有当 PRINT 被定义的时候才会被执行。使用 printf语句 才会是灰色的。所以如果是下面这种情况,程序就会正常执行:

执行程序

常见的条件编译命令

1.

#if 常量表达式

        //...

#endif

//常量表达式有预处理器求值

如:

#define __DEBUG__ 1

#if __DEBUG__

        //...

#endif

比如

此时 0 为假,不会执行 printf语句


2. 多个分支的条件编译

#if 常量表达式

        //...

#elif 常量表达式

        //...

#else

        //...

#endif

举例

只有当判断条件结果为真时,才会执行 printf语句 ,所以程序只执行第一条 printf语句 ,打印结果:


3. 判断是否被定义

#if defined(symbol)

#ifdef symbol

#if !defned(symbol)

#ifndef symbol

 举例1(定义的情况)

 或者

打印结果:

 -----

 举例2(不定义的情况)

或者

打印结果


4. 嵌合命令

#if defined(OS_UNIX)  //如果是UNIX操作系统,则执行下列语句

    #ifdef OPTION1

        unix_version_option1();

    #endif

    #ifdef OPTION2

        unix_version_option2();

    #endif

#endif defined(OS_MSDOS)  //如果是WINDOWS操作系统,则执行下列语句

    #ifdef OPTION2

        msdos_version_option2();

    #endif

#endif

文件包含

我们已经知道,#include 指令可以使另外一个文件被编译,就像这个被编译的文件实际出现在了原本 #include 指令的地方一样。

#include 指令所使用的替换的方式很简单:

  • 首先,预处理器会删除这条 #include 指令。
  • 然后,使用包含文件的内容进行替换。

这样一个源文件被包含10次,实际上就会被编译10次。

头文件被包含的方式

  • 本地文件包含
#include "filename"  //一般后缀的 .h 是要带上的。

查找策略:

  1. 先在源文件所在的目录下进行查找。
  2. 如果没有找到指定头文件,编译器就会像查找库函数的头文件那样,在标准位置查找头文件。

    如果没有找到指定文件,就会提示程序错误。

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

/usr/include

VS环境下的标准头文件的路径(仅供参考):

C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt
D:\mingw64\x86_64-w64-mingw32\include  //这个是个人安装的。

查找时需要注意自己的安装路径。

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

(库文件:C语言库中提供函数的头文件。)

查找策略:

  • 查找库函数所在头文件时,编译器会直接到标准路径下去查找,如果找不到就会提示编译错误。

由上述可知,对于库文件的包含也可以使用 " " 的形式。如:

#include "stdio.h"

但是这样查找有两个缺点:

        1. 降低效率。

        2. 不容易区分库文件和本地文件。

< > 和 " " 的区别本质上就是查找策略的区别。

嵌套文件包含

一般而言,我们会在头文件内会写入:

  1. 类型的定义
  2. 函数的声明

而我们在写程序时,可能会出现同一个头文件被重复、多次包含的情况。

相当于 act模块 将公共模块的内容包含了两次,即一个头文件被包含了两次,而如果包含的次数过多,就会使得这个程序过于冗余。那么如何避免这种头文件被重复包含的情况呢

举例(本次使用Ubantu系统):

首先,创建一个头文件 test.h

int Add(int x, int y);

struct S
{
    char c;
    int i;
};

然后,创建一个源文件 test.c,先包含两次头文件 test.h

#include "test.h"
#include "test.h"

int main()
{
    int a = 10;
    return 0;
}

接下来使用命令:gcc test.c -E > test.i ,只预处理 test.c ,并把预处理完的内容重定向到 test.i ,打开 test.i :

通过两次出现的 结构体类型S ,可以断定:头文件test.h 的内容被包含了两份(如果被定义的是变量,就会发生重复定义变量的情况。所以一般不会在头文件内定义变量)。要避免这种情况的发生,最简单的一种方法就是头文件内写上:

#pragma once

此时即使多次包含该头文件,在预处理时该头文件也只会被写入一次。

或者

在 test.h 中:

#ifndef __TEST_H__    //如果没有定义 __TEST_H__ ,那么接下来的内容正常执行,头文件被包含。
#define __TEST_H__    //定义 __TEST_H__ 。

int Add(int x, int y);

struct S
{
    char c;
    int i;
};

#endif   

当第一次包含该头文件时,__TEST_H__ (名字随便取)被定义,当第二次想要包含该头文件时,由于 __TEST_H__ 已经被定义,所以包含不会进行。

其他预处理指令

参考《C语言深度解剖》

预处理名称意    义
#line

改变当前行数和文件名称,它们是在编译程序中预先定义的标识符。

命令的基本形式如下:

        #line number["filename"]

#error编译程序时,只要遇到 #error 就会生成一个编译错误提示信息,并停止编译。
#pragma

为实现时定义的命令,它允许向编译程序传送各种指令,例如:

        编译程序可能有一种选择,它支持对程序执行的跟踪。可用 #pragma语句 指定一个跟踪选择。

------

再比如,可用通过

#pragma pack(number)
#
pragma pack()

这样的一对指令设置和取消默认对齐数。(其中 number 是需要设置的默认对齐数的大小)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值