在C语言中,有一些有意思的预定义符号,这些预定义符号都是语言内置的,即以及定义好的,我们可以直接使用。预定义符号主要有以下几个:
__FILE__ // 进行编译的源文件
__LINE__ // 文件当前的行号
__DATE__ // 文件被编译的日期
__TIME__ // 文件被编译的时间
__FUNCTION__ // 进行编译的函数
__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义 这些预定义符号是已经用#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__); 因为VS不遵循ANSI C,该符号未定义,所以进行了注释
return 0;
}
// 运行结果:
D:\ProgramLanguage\liren\test.cpp
31
May 1 2023
15:50:07
mainⅡ. #define定义标识符
#define定义标识符的格式如下:
#define MAX 100
#define reg register // 有人觉得register太长了 这些被#define定义的标识符都将在预处理阶段被编译器替换成对应的内容,之前看到一个比较有意思的东西,这里和大家分享一下:
#define mian main
#define , ,
#define ( (
#define ) )
#define ture true
#define ; ; 确实,只要在文件前面加上这几句话,就再也不用担心自己的代码中的标点符号写成中文的了,也再也不用担心把 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);
return 0;
}
// 运行结果:
25 但是这并不完全正确,因为当你传入的宏参数为2+3时,打印的结果却并不是25,而是11。因为宏完成的是替换,它不会先把2+3的值算出来再进行替换,而是直接替换,所以传入2+3替换后相当于:
int ret = 2+3*2+3; 因为*的优先级高于+,所以这样算出的结果当然是11了。为了避免这种情况的发生,用宏实现求一个数的平方应该这样:
#define SQUARE(x) ((x)*(x)) 但其实这还是会出现问题的,比如我们调用宏函数使用 ++ 操作:
int x = 2;
int result = SQUARE(x++); 在这个例子中,我们希望 result 的值为 4,但实际上宏函数展开后的表达式为 ((x++) * (x++)),导致 x 的值被计算了两次,因此 result 的值为 10,而不是 4。这是因为宏函数展开后的代码中,x 的值被计算了多次,而且计算的顺序并不是我们期望的。
为了避免这种问题,我们可以使用函数代替宏函数。函数是在编译时被解析的,可以避免上下文影响它的计算顺序,从而保证了表达式的正确性。
总之,在使用宏函数时,需要仔细考虑上下文因素以及参数和表达式的优先级问题,并尽量避免使用复杂的表达式作为宏函数的参数,以保证代码的正确性。
Ⅳ. #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;
}-
调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。 例如,#define定义的宏中含有#define定义的符号MAX,则调用该宏时,首先将MAX替换。
#include <stdio.h> #define SQUARE(x) ((x)*(x)*100) // 将MAX替换 int main() { int ret = SQUARE(5); printf("%d\n", ret); return 0; } -
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。 例如,上例中经过该步骤后,代码等价于:
#include <stdio.h> int main() { int ret = ((5)*(5)*100); printf("%d\n", ret); return 0; } -
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。 上例不再包含任何由#define定义的符号。
注意:
-
宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
-
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索和替换。
#include <stdio.h> #define MAX 100 int main() { printf("MAX = %d\n", MAX); // 结果为MAX = 100 return 0; }
Ⅴ. 宏和函数的对比

Ⅵ. #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未定义
}Ⅶ. #和##的区别
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;
}
// 运行结果:
The value of age is 10
The value of pi is 3.140000
The value of p is 0000000388CFF904 我们发现,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;
}
// 运行结果:
The value of age is 10
The value of pi is 3.140000
The value of p is 0000004A7A53F764 又因为字符串有自动连接的特点,所以可以打印出期望的结果。
2、##的作用
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
例如,下面定义的宏可以将传入的两个符号合成一个符号。
#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
int workhard = 100;
printf("%d\n", CAT(work, hard));
return 0;
}
// 运行结果:
100Ⅷ. 命令行定义
命令行是指在操作系统的命令行界面中,用户通过输入命令和参数来执行各种操作的方式。在编程中,我们可以通过在程序中获取命令行参数来实现命令行功能。
命令行参数是指在命令行中传递给程序的字符串参数,它们通常以空格作为分隔符。而我们在使用 vs 编译器的时候其实是感受不出来,因为它一键就帮我们做完了,如果要实验的话,还是推荐 linux 下的 gcc 编译器!
例如,下面是一个简单的C程序,它使用命令行参数来输出字符串:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Program name: %s\n", argv[0]);
printf("Arguments:\n");
for (int i = 1; i < argc; i++)
printf(" %s\n", argv[i]);
return 0;
} 在上面的程序中,我们首先输出程序的名称,然后依次输出所有的命令行参数。
例如,如果我们在命令行中执行以下命令:
./myprogram hello world 那么程序将输出如下内容:
Program name: ./myprogram
Arguments:
hello
world 通过命令行参数,我们可以为程序提供各种不同的参数和配置选项,从而使程序具有更加灵活的功能。
比如我们可以加上 -D 选项,可以对我们程序中的宏进行替换:
#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 programe.c -D ARRAY_SIZE = 10 经过该编译指令后,便可以打印出0到9的数字。
Ⅸ. 条件编译
条件编译,即满足条件就参与编译,不满足条件就不参与编译。
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
常见的条件编译指令有以下几种:
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 上面两种方式是等价的,但是第一种方式更加的灵活!
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;
}
// 运行结果:
hello 这里条件编译指令的嵌套类似于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 1 2 3 4 5 6 7 8 9Ⅹ. 文件包含
#include 指令可以使被包含的文件参与编译,在预处理阶段,就会进行文件的包含。
例如:
#include <stdio.h> 在预处理阶段,编译器会先删除该指令,并用 stdio.h 文件中的内容进行替换。
但是,文件的包含有两种:
#include <stdio.h>
#include "stdio.h" 一种是用尖括号将要包含的文件括起来,另一种是用双引号将要包含的文件引起来。这两种方法,在某些情况下似乎都可行,那么这两种方法到底有什么区别呢❓❓❓
-
< >:如果使用尖括号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会自动去自己的安装路径下查找库目录,若库目录中含有该头文件,则将其进行包含,若库目录下不存在该头文件,则提示编译错误。
-
" ":如果使用双引号的方式对头文件进行包含,那么当代码运行到预处理阶段,将对头文件进行包含时,编译器会首先去正在编译的源文件目录下进行查找,若没有找到目标头文件,则再去库目录下进行查找,若两处都没有找到目标头文件,则提示编译错误。
这样看来,当我们要包含的头文件是库函数的头文件的时候,我们使用尖括号或者双引号都可以,但是当我们要包含的头文件是自定义的头文件时,我们只能用双引号进行头文件的包含。
但是如果我们明明知道自己要包含的头文件是库函数的头文件,那我们就没有必要使用双引号去包含,因为那样会降低代码的效率。所以说,为了提高代码执行效率:
- < >:一般用于包含C语言提供的库函数的头文件。
- " ":一般用于包含自定义的头文件。
关于头文件,还有一点值得注意的是,当我们使用 #include 来包含头文件时,如果我们重复包含同一个头文件,那么在预处理阶段就会重复包含该头文件的内容,会大大加长代码量,导致代码冗余。
避免该问题的发生,有以下两种方法:
-
方法一:
当第一次包含该头文件时,会用#define定义符号__ADD_H__,当第二次重复包含该头文件时,因为__ADD_H__已经被定义过,就无法再次包含该头文件的内容了。
#ifndef __ADD_H__ #define __ADD_H__ //头文件内容 #endif -
方法二:
只需在头文件开头加上 #pragma once,那么该头文件就只会被包含一次。
#pragma once //头文件内容