【C语言进阶】程序编译&预处理详解_编译器预处理需要开辟空间吗(2)

最后

🍅 硬核资料:关注即可领取PPT模板、简历模板、行业经典书籍PDF。
🍅 技术互助:技术群大佬指点迷津,你的问题可能不是问题,求资源在群里喊一声。
🍅 面试题库:由技术群里的小伙伴们共同投稿,热乎的大厂面试真题,持续更新中。
🍅 知识体系:含编程语言、算法、大数据生态圈组件(Mysql、Hive、Spark、Flink)、数据仓库、Python、前端等等。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

语法:
#define name stuff
举例:
#define MAX 10
#define reg register
  • #define 定义宏

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

例1:

#include <stdio.h>
#define SQUARE(x) x*x//求x的平方
int main()
{
	int ret = SQUARE(5);
	//相当于int ret = 5*5;
	printf("%d\n", ret);//结果为25
	return 0;
}

执行结果:


例2:

#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
    int ret = SQUARE(4+1);
    printf("%d\n", ret);
    return 0;
}

执行结果:


思考:为什么例1跟例2的结果会不一致呢?

  • 例2中,当传入的宏参数为4+1时,宏完成的是替换,它不会先把4+1的值算出来再进行替换,而是直接替换,所以传入4+1替换后相当于:int ret = 4+1*4+1 = 9
  • *的优先级高于+,所以这样算出的结果就为9了。

为了避免这种情况的发生,用宏实现求一个数的平方应该这样:

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

这里将 (x)*(x) 整体再用括号括起来的原因也是一样的,都是为了避免在使用宏时,因操作符的优先级问题而导致不可预料的后果。

注:在使用#define定义宏时,不要吝啬括号,该加括号的地方就要加上

  • #define的替换规则

在程序中替换 #define 定义的宏和标识符时,会涉及几个步骤

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

例:

#include <stdio.h>
#define SQUARE(x) ((x)*(x)*100)
int main()
{
    int ret = SQUARE(3);
    printf("%d\n", ret);
    return 0;
}

等价于:

#include <stdio.h>
int main()
{
	int ret = ((3)*(3)*100);
	printf("%d\n", ret);
	return 0;
}

注意:

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

如:

#define FAC(x) (x)*FAC(x-1)//error
//不能出现如上所述的代码

#include <stdio.h>
#define MAX 100
int main()
{
	printf("MAX = %d\n", MAX);//结果为MAX = 100
	return 0;
}
//代码字符串中的MAX不会被替换为100,而字符串外的MAX会被替换
  • #undef

#undef可以移除一个#define定义的标识符或宏。

例:

#include <stdio.h>
#define MAX 100
int main()
{
	printf("%d\n", MAX);//正常使用
#undef MAX
	printf("%d\n", MAX);//报错,MAX未定义
}


③宏和函数的对比

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

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

那为什么不用下面这个函数来实现这个功能呢?

int Max(int a, int b)
{
	return a > b ? a : b;
}

原因:

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

而且,宏有时候可以做到函数做不到的事情。

例如,宏的参数可以出现类型,但是函数却不可以。我们使用malloc函数开辟内存空间时,可能会觉得代码太多。

例:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int *p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		printf("p开辟失败\n");
		return 1;
	}
	free(p);
	p = NULL;
	return 0;
}

此时我们就可以通过宏来使我们用malloc开辟空间时,只用传入开辟的类型和该类型的元素个数即可。

例:

#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
	int* p2 = MALLOC(10, int);
	if (p2 == NULL)
	{
		printf("p2开辟失败\n");
		return 1;
	}
	free(p2);
	p2 = NULL;
	return 0;
}

当然和宏相比函数也有劣势的地方:

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

对比图

命定约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:把宏名全部大写 函数名不要全部大写


④#和##的介绍

#的作用

这里所说的#并不是#define和#include中的#,这里所说的#的作用是:把一个宏参数变成对应的字符串

我们首先需要知道:字符串是有自动连接的特点的

例:

char* arr = "hello ""world"; 
printf("hello"," world\n"); 
printf("%s", arr); 

例2:

#include <stdio.h>
int main()
{
	int age = 10;
	printf("The value of age is %d\n", age);
	double pi = 3.14;
	printf("The value of pi is %f\n", pi);
	int* p = &age;
	printf("The value of p is %p\n", p);
	return 0;
}

我们发现,printf要打印的内容大部分是一样的,那么,为了避免代码冗余,我们可不可以将其封装成一个函数或是宏呢?
经过思考与实验,发现函数和普通的宏都不能实现该功能。

#的使用常例:

#include <stdio.h>
#define print(data,format) printf("The value of "#data" is "format"\n",data)
int main()
{
	int age = 10;
	print(age, "%d");
	double pi = 3.14;
	print(pi, "%f");
	int* p = &age;
	print(p, "%p");
	return 0;
}

##的作用

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

例:

#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
	int workhard = 100;
	printf("%d\n", CAT(work, hard));//打印100
	return 0;
}


⑤条件编译

条件编译,即满足条件就参与编译,不满足条件就不参与编译。

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

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

1.单分支的条件编译
#if 表达式
	//待定代码
#endif

如果#if后面的表达式为真,则“待定代码”的内容将参与编译,否则“待定代码”的内容不参与编译。

2.多分支的条件编译
#if 表达式
	//待定代码1
#elif 表达式
	//待定代码2
#elif 表达式
	//待定代码3
#else 表达式
	//待定代码4
#endif

多分支的条件编译类似于if-else语句,“待定代码1,2,3,4”之中只会有一段代码参与编译。

3.判断是否被定义
//第一种的正面
#if defined(表达式)
	//待定代码
#endif

//第一种的反面
#if !defined(表达式)
	//待定代码
#endif

如果“表达式”被#define定义过,则“第一种的正面”的“待定代码”将参与编译,否则不参与编译。“第一种的反面”的执行机制与“第一种的正面”恰好相反。

//第二种的正面
#ifdef 表达式
    //待定代码
#endif

//第二种的反面
#ifndef 表达式
    //待定代码
#endif

如果“表达式”被#define定义过,则“第二种的正面”的“待定代码”将参与编译,否则不参与编译。“第二种的反面”的执行机制与“第二种的正面”恰好相反。

4.嵌套指令
#include <stdio.h>
#define MIN 10
int main()
{
    #if !defined(MAX)
    #ifdef MIN
	printf("hello\n");
    #else
	printf("world\n");
    #endif
    #endif
	return 0;
}

注意:未满足条件编译指令的代码,在预处理阶段将被编译器自动删除,不参与后面的代码编译过程。

例:

#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", i);
#if 0
		printf("hello world!\n");
#endif
	}
	return 0;
}

因为#if后面的表达式为假,语句 #if 0 和 #endif 之间的代码将不参与编译,所以在预处理阶段过后,编译器编译的代码是:

//#include <stdio.h>
//预处理阶段头文件也被包含了
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", i);
	}
	return 0;
}

执行结果:


⑥文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。 这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。

头文件被包含的方式:

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

而文件的包含有两种:

#include <stdio.h>
#include "stdio.h"

一种是用尖括号将要包含的文件括起来

另一种是用双引号将要包含的文件引起来

这两种方法,在某些情况下似乎都可行,那么这两种方法到底有什么区别呢?

**< >:**如果使用尖括号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会自动去自己的安装路径下查找库目录,若库目录中含有该头文件,则将其进行包含,若库目录下不存在该头文件,则提示编译错误。

**" ":**如果使用双引号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会首先去正在编译的源文件目录下进行查找,若没有找到目标头文件,则再去库目录下进行查找,若两处都没有找到目标头文件,则提示编译错误。

< >:一般用于包含C语言提供的库函数的头文件。
" ":一般用于包含自定义的头文件。

关于头文件,还有一点值得注意的是,当我们使用#include来包含头文件时,如果我们重复包含同一个头文件,那么在预处理阶段就会重复包含该头文件的内容,会大大加长代码量,导致代码冗余

解决方案:

(1)Python所有方向的学习路线(新版)

这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

最近我才对这些路线做了一下新的更新,知识体系更全面了。

在这里插入图片描述

(2)Python学习视频

包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。

在这里插入图片描述

(3)100多个练手项目

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。

在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值