9c语言基础预定义

预处理详解(一)—— 预定义符号

预定义符号

在C语言中,有一些有意思的预定义符号,这些预定义符号都是语言内置的,即以及定义好的,我们可以直接使用。预定义符号主要有以下几个:

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

这些预定义符号是已经用#define定义好的,在代码运行后的预处理阶段会被替换为相应的内容。

预定义符号的使用

这些预定义符号的使用方法一致,只是需要注意打印的格式是%s还是%d就行了。
例如,运行以下代码:

#include <stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __FUNCTION__);
	//printf("%d\n", __STDC__);
	//因为VS2013不遵循ANSI C,该符号未定义,所以进行了注释
	return 0;
}
123456789101112

运行结果为:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

预处理详解(二)—— #define定义宏和标识符 + 宏和函数的对比 + #和##的作用 + #undef

#define定义标识符

#define定义标识符的格式如下:

#define MAX 100
#define reg register//懒人觉得register太长了
12

这些被#define定义的标识符都将在预处理阶段被编译器替换成对应的内容,之前看到一个比较有意思的东西,这里和大家分享一下:

#define mian main
#define ,
#define (
#define )
#define ture true
#define ;
123456

确实,只要在文件前面加上这几句话,就再也不用担心自己的代码中的标点符号写成中文的了,也再也不用担心把main写成mian,true写成ture了,因为即使写错了也被替换成正确的了。但是我们还是应该在写代码时认真仔细,避免这些低级错误的发生。

#define定义宏

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

例如,用宏实现求一个数的平方:

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

但是这并不完全正确,因为当你传入的宏参数为2+3时,打印的结果却并不是25,而是11。因为宏完成的是替换,它不会先把2+3的值算出来再进行替换,而是直接替换,所以传入2+3替换后相当于:

	int ret = 2+3*2+3;
1

因为*的优先级高于+,所以这样算出的结果当然是11了。为了避免这种情况的发生,用宏实现求一个数的平方应该这样:

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

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

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

#define的替换规则

在程序中替换#define定义的宏和标识符时,需要涉及几个步骤
我们用以下代码进行举例:

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

1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
例如,#define定义的宏中含有#define定义的符号MAX,则调用该宏时,首先将MAX替换。

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

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
例如,上例中经过该步骤后,代码等价于:

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

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

注意:
1.宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
也就是不能出现类似于以下的代码:

#define FAC(x) (x)*FAC(x-1)//error
1

2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
例如,以下代码字符串中的MAX不会被替换为100,而字符串外的MAX会被替换。

#include <stdio.h>
#define MAX 100
int main()
{
	printf("MAX = %d\n", MAX);//结果为MAX = 100
	return 0;
}
1234567

带副作用的宏参数

在介绍带副作用的宏参数之前,我们先看看带有副作用是什么意思

	int a = 10;
	int b = a + 1;//无副作用
	int c = a++;//有副作用
123

代码中,b和c都想得到a+1的值,但不改变a的值。b得到a+1的值后,a的值并没有发生改变,所以无副作用;但是c得到a+1的值后,a的值也变化了,也就是有副作用。简单来说,代码执行后,除了达到我们想要的结果之外,还导致了其他问题的发生,我们就说该条语句带有副作用。

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
例如,我们要比较a和b的大小,并将其较大值赋值给c,之后再将a和b同时加1。

#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a++, b++);
	printf("%d\n", c);
	return 0;
}
12345678910

这段代码看似没有问题,但是结果却是不正确的,因为该宏经过替换后,等价于以下代码:

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = ((a++)>(b++)?(a++):(b++));
	printf("%d\n", c);
	return 0;
}
123456789

经过替换后,我们一分析便可得出答案,c的最后的结果是21,并且代码执行后,a和b的值并不是同时加1,a的值变为了11,而b的值却变为了22。

所以,当我们使用宏的时候,应该避免传入带有副作用的宏参数

宏和函数的对比

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

#define MAX(x,y) ((x)>(y)?(x):(y))
1

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

int Max(int x, int y)
{
	return x > y ? x : y;
}
1234

1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

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

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

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

这时我们可以实现一个宏,使我们用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;
}
123456789101112131415

但是,宏也有劣势的地方,例如:

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

下面我们画出了框架图,能够更加清晰地区分宏和函数的区别:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#undef的作用

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

例如,下列代码将#define定义的标识符MAX移除后,编译器便不能识别之后的MAX。

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

冷门知识点:#和##

这里所说的#和##的使用非常之少,可能有些博友都没有听说过,但是这个知识点在面试的时候也可能会被考到,也是比较重要的。
1.#的作用
这里所说的#并不是#define和#include中的#,这里所说的#的作用是:把一个宏参数变成对应的字符串

那么,这个#到底有什么实际的作用呢?
在介绍#的作用的之前,我先向大家说明一下:字符串是有自动连接的特点的
例如,以下案例:

	char arr[] = "hello ""world!";
	//等价于char arr[] = "hello world!";
	printf("helll ""world!\n");
	//等价于printf("helll world!\n");
1234

接下来,给大家举一个#的使用案例。例如,有以下代码:

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

我们发现,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;
}
123456789101112

这时我们只需将要打印的变量的变量名和打印格式传入即可。该代码经过预处理后等价于以下代码:

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

又因为字符串有自动连接的特点,所以可以打印出期望的结果。

在 C 语言中,宏定义中的 “#” 符号用于字符串化操作。它会将紧跟在其后的宏参数替换为其对应的字符串字面量。
在这个例子中,“#data” 被替换为 “age”(或者 “pi” 或 “p”,取决于调用 print 宏时传递的变量名)。这是因为宏参数 “data” 的实际值是一个标识符(即变量名),而不是一个字符串字面量。因此,在 “#data” 前加上 “#” 符号可以将其转换为一个字符串字面量。
另一方面,“format” 是一个已经表示格式字符串的宏参数,不需要进行字符串化操作。因此,在 “format” 前没有使用 “#” 符号。

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

例如,下面定义的宏可以将传入的两个符号合成一个符号。

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

预处理详解(三)—— 命令行定义 + 条件编译 + 文件包含

命令行定义

许多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;
}
12345678910111213141516

可以看到,代码中没有明确定义数组的大小。在编译这种代码时,我们需要使用命令行对数组的大小进行定义。

例如,在Linux环境下,编译指令如下:

gcc -D programe.c ARRAY_SIZE = 10
1

经过该编译指令后,便可以打印出0到9的数字。

条件编译

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

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

常见的条件编译指令有以下几种:
1.单分支的条件编译

#if 表达式
	//待定代码
#endif
123

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

2.多分支的条件编译

#if 表达式
	//待定代码1
#elif 表达式
	//待定代码2
#elif 表达式
	//待定代码3
#else 表达式
	//待定代码4
#endif
123456789

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

3.判断是否被定义

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

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

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

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

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

如果“表达式”被#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;
}
12345678910111213

这里条件编译指令的嵌套类似于if-else语句的嵌套,博友们可以类比理解。

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

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

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

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

所以,代码运行后只会打印0到9的数字。

文件包含

我们知道,#include指令可以使被包含的文件参与编译,在预处理阶段,就会进行文件的包含。
例如:

#include <stdio.h>
1

在预处理阶段,编译器会先删除该指令,并用stdio.h文件中的内容进行替换。

但是,文件的包含有两种:

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

一种是用尖括号将要包含的文件括起来,另一种是用双引号将要包含的文件引起来。这两种方法,在某些情况下似乎都可行,那么这两种方法到底有什么区别呢?

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

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

这样看来,当我们要包含的头文件是库函数的头文件的时候,我们使用尖括号或者双引号都可以,但是当我们要包含的头文件是自定义的头文件时,我们只能用双引号进行头文件的包含。

但是如果我们明明知道自己要包含的头文件是库函数的头文件,那我们就没有必要使用双引号去包含,因为那样会降低代码的效率。所以说,为了提高代码执行效率:
< >:一般用于包含C语言提供的库函数的头文件。
" ":一般用于包含自定义的头文件。

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

避免该问题的发生,有以下两种方法:
方法一:

#ifndef __ADD_H__
#define __ADD_H__

//头文件内容

#endif
123456

当第一次包含该头文件时,会用#define定义符号__ADD_H__,当第二次重复包含该头文件时,因为__ADD_H__已经被定义过,就无法再次包含该头文件的内容了。

方法二:

#pragma once

//头文件内容
123

只需在头文件开头加上这句代码,那么该头文件就只会被包含一次。

处都没有找到目标头文件,则提示编译错误。

这样看来,当我们要包含的头文件是库函数的头文件的时候,我们使用尖括号或者双引号都可以,但是当我们要包含的头文件是自定义的头文件时,我们只能用双引号进行头文件的包含。

但是如果我们明明知道自己要包含的头文件是库函数的头文件,那我们就没有必要使用双引号去包含,因为那样会降低代码的效率。所以说,为了提高代码执行效率:
< >:一般用于包含C语言提供的库函数的头文件。
" ":一般用于包含自定义的头文件。

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

避免该问题的发生,有以下两种方法:
方法一:

#ifndef __ADD_H__
#define __ADD_H__

//头文件内容

#endif
123456

当第一次包含该头文件时,会用#define定义符号__ADD_H__,当第二次重复包含该头文件时,因为__ADD_H__已经被定义过,就无法再次包含该头文件的内容了。

方法二:

#pragma once

//头文件内容
123

只需在头文件开头加上这句代码,那么该头文件就只会被包含一次。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值