一、预处理详解(一) ——预定义符号
1.预定义符号是什么?
在C语言中,一些符号是自定义好的,这些预定义符号是语言本身内置的,即已经定义好的,我们可以直接使用。预定义符号主要有以下几个:
这些符号是已经通过#define定义好的,在代码运行后的预处理阶段会被替换成相应的内容。
2.预定义符号的使用
这些代码的使用很简单,和正常的%d %s是一样!下面展示一组例子
#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;
}
二、#define定义宏和标识符 + 宏和函数的对比
1.#define 定义标识符
举个例子:
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
2.#define 定义宏
用define实现求一个数的平方:
#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+3时,打印的结果却并不是25,而是11。因为宏完成的是替换,它不会先把2+3的值算出来再进行替换,而是直接替换,所以传入2+3替换后相当于:
ret = 2+3*2+3;
基于上述错误对代码进行修改!
#define SQUARE(x) ((x)*(x))
因此在#define 定义宏的时候,不要吝啬括号,该加就加上
3.#define 定义宏的注意事项
3.1 不能出现递归调用
#define FAC(x) (x)*FAC(x-1) 这是错误的
3.2 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#include <stdio.h>
#define MAX 100
int main()
{
printf("MAX = %d\n", MAX);//结果为MAX = 100
return 0;
}
4.宏和函数的对比
例如实现返回两个数之间的较大数
#define MAX(x,y) ((x)>(y)?(x):(y)) //使用宏实现的防暑
//使用函数实现的方式
int Max(int x, int y)
{
return x > y ? x : y;
}
宏的优点:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2.更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。但是宏可以适用于整形、长整型、浮点型等可以用于来比较的类型。宏是类型无关的。
宏的缺点:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是没法调试的。
3.宏由于类型无关,也就不够严谨。
4.宏可能会带来运算符优先级的问题,导致程容易出现错。
三、#undef的作用
1.#undef可以移除#define定义的标识符或宏
#include <stdio.h>
#define MAX 100
int main()
{
printf("%d\n", MAX);//正常使用
#undef MAX
printf("%d\n", MAX);//报错,MAX未定义
}
四、冷知识#和##
1.#的作用
这里所说的#并不是#define和#include中的#,这里所说的#的作用是:把一个宏参数变成对应的字符串。
那么,这个#到底有什么实际的作用呢?
在介绍#的作用的之前,我先向大家说明一下:字符串是有自动连接的特点的。
例如,以下案例:
char arr[] = "hello ""world!";
//等价于char arr[] = "hello world!";
printf("helll ""world!\n");
//等价于printf("helll world!\n");
接下来,给大家举一个#的使用案例。例如,有以下代码:
#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>
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;
}
又因为字符串有自动连接的特点,所以可以打印出期望的结果。
2.##的作用
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。例如,下面定义的宏可以将传入的两个符号合成一个符号。
#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
int workhard = 100;
printf("%d\n", CAT(work, hard));//打印100
return 0;
}
五、命令行定义 + 条件编译 + 文件包含
1.命令行定义
许多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 test.c ARRAY_SIZE = 10
经过该编译指令后,便可以打印出0到9的数字。
2.条件编译
条件编译,即满足条件就参与编译,不满足条件就不参与编译。
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
常见的条件编译指令有以下几种:
2.1 单分支的条件编译
#if 表达式
//待定代码
#endif
如果#if后面的表达式为真,则“待定代码”的内容将参与编译,否则“待定代码”的内容不参与编译。
2.2 多分支的条件编译
#if 表达式
//待定代码1
#elif 表达式
//待定代码2
#elif 表达式
//待定代码3
#else 表达式
//待定代码4
#endif
多分支的条件编译类似于if-else语句,“待定代码1,2,3,4”之中只会有一段代码参与编译。
2.3 判断是否被定义
//第一种的正面
#if defined(表达式)
//待定代码
#endif
//第一种的反面
#if !defined(表达式)
//待定代码
#endif
如果“表达式”被#define定义过,则“第一种的正面”的“待定代码”将参与编译,否则不参与编译。“第一种的反面”的执行机制与“第一种的正面”恰好相反。
//第二种的正面
#ifdef 表达式
//待定代码
#endif
//第二种的反面
#ifndef 表达式
//待定代码
#endif
如果“表达式”被#define定义过,则“第二种的正面”的“待定代码”将参与编译,否则不参与编译。“第二种的反面”的执行机制与“第二种的正面”恰好相反。
2.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;
}
这里条件编译指令的嵌套类似于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;
}
因为#if后面的表达式为假,语句 #if 0 和 #endif 之间的代码将不参与编译,所以在预处理阶段过后,编译器编译的代码是:
//#include <stdio.h>
//预处理阶段头文件也被包含了
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", i);
}
return 0;
}
所以,代码运行后只会打印0到9的数字。
3.文件包含
我们知道,#include指令可以使被包含的文件参与编译,在预处理阶段,就会进行文件的包含。
#include <stdio.h>
在预处理阶段,编译器会先删除该指令,并用stdio.h文件中的内容进行替换。
但是,文件的包含有两种:
#include <stdio.h>
#include "stdio.h"
一种是用尖括号将要包含的文件括起来,另一种是用双引号将要包含的文件引起来。这两种方法,在某些情况下似乎都可行,那么这两种方法到底有什么区别呢?
< >:如果使用尖括号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会自动去自己的安装路径下查找库目录,若库目录中含有该头文件,则将其进行包含,若库目录下不存在该头文件,则提示编译错误。
" ":如果使用双引号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会首先去正在编译的源文件目录下进行查找,若没有找到目标头文件,则再去库目录下进行查找,若两处都没有找到目标头文件,则提示编译错误。
这样看来,当我们要包含的头文件是库函数的头文件的时候,我们使用尖括号或者双引号都可以,但是当我们要包含的头文件是自定义的头文件时,我们只能用双引号进行头文件的包含。
但是如果我们明明知道自己要包含的头文件是库函数的头文件,那我们就没有必要使用双引号去包含,因为那样会降低代码的效率。所以说,为了提高代码执行效率:
< >:一般用于包含C语言提供的库函数的头文件。
" ":一般用于包含自定义的头文件。
关于头文件,还有一点值得注意的是,当我们使用#include来包含头文件时,如果我们重复包含同一个头文件,那么在预处理阶段就会重复包含该头文件的内容,会大大加长代码量,导致代码冗余。
避免该问题的发生,有以下两种方法:
方法一:
#ifndef __ADD_H__
#define __ADD_H__
//头文件内容
#endif
当第一次包含该头文件时,会用#define定义符号__ADD_H__,当第二次重复包含该头文件时,因为__ADD_H__已经被定义过,就无法再次包含该头文件的内容了。
方法二:
#pragma once
//头文件内容
只需在头文件开头加上这句代码,那么该头文件就只会被包含一次,嵌入式的伙伴,使用KEIL5也支持这个用法哦!!~~ !