一、预定义符号
在C语言中,有一些预定义的符号(也称为预定义宏),它们提供了关于编译器和编译过程的信息。这些符号在编译时由编译器自动定义,可以在程序中使用。以下是一些常见的预定义符号(这里的下划线都是两个连续的下划线):
-
__FILE__
:当前源文件的名称(字符串字面量)。 -
__LINE__
:当前源代码行的行号(整数常量)。 -
__DATE__
:编译日期,格式为 "Mmm dd yyyy"(字符串字面量)。 -
__TIME__
:编译时间,格式为 "hh:mm:ss"(字符串字面量)。 -
__STDC__
:如果编译器遵循ISO C标准,则定义为1。 -
__STDC_VERSION__
:编译器遵循的ISO C标准的版本号,例如199901L表示C99,201112L表示C11。 -
__STDC_HOSTED__
:如果编译器运行在宿主环境中(即操作系统之上),则定义为1;如果编译器运行在独立环境中(没有操作系统),则未定义。 -
__func__
(C99及以后):当前函数的名称(字符串字面量)。
这些预定义符号可以在程序中用于调试、日志记录或其他需要编译时信息的场合。例如,你可以使用它们来打印出错信息的位置。
例子:
#include <stdio.h>
void some_function() {
printf("Function: %s\n", __func__);
printf("File: %s, Line: %d\n", __FILE__, __LINE__);
}
int main() {
printf("Compiled on: %s %s\n", __DATE__, __TIME__);
some_function();
return 0;
}
运行结果:
在这个例子中,__func__
用于打印当前函数的名称,__FILE__
和 __LINE__
用于打印当前文件名和行号,__DATE__
和 __TIME__
用于打印编译日期和时间。
可以通过__STDC__
测试编译器是否遵循ANSI C标准(ANSI C标准和ISO C标准指的是同一个标准,只是在不同的时间段由不同的组织发布)。
可以通过下面的程序测试是否遵循ANSI C标准:
#include <stdio.h>
int main() {
#ifdef __STDC__
printf("followed");
#else
printf("not followed");
#endif
return 0;
}
在Visual Studio 2022中运行得到:
Visual Studio 2022 默认情况下可能不会完全遵守 ANSI 标准。
我们可以通过在项目属性中设置预处理器定义来定义__STDC__
宏。在Visual Studio中,你可以通过以下步骤来定义这个宏:
-
打开你的项目。
-
在菜单栏中选择“项目” > “属性”。
-
在属性页中,选择“配置属性” > “C/C++” > “预处理器”。
-
在“预处理器定义”中添加
__STDC__
。 -
点击“应用”然后“确定”。
完成这些步骤后,重新编译你的程序,就会看到输出"followed"。
设置后的运行结果:
在linux环境下,使用gcc编译这段程序:
然后运行,可以得到:
说明gcc是遵循ANSI C标准的。
二、#define
在C语言中,#define
是一种预处理指令,用于定义宏。它告诉编译器在实际编译之前进行文本替换。
注意:宏定义后面没有分号
1、对象式宏(Object-like Macros)
对象宏是最简单的宏,通常用来定义常量或替换文本。它们没有参数,并在预处理器运行时简单地替换代码中出现的所有实例。例如:
#define BUFFER_SIZE 1024
在这个例子中,BUFFER_SIZE
是一个对象宏,它在所有后续代码中将被替换为 1024
。
对象式宏也可以用来定义字符串:
#define SITE_NAME "example.com"
或者进行更复杂的文本替换:
#define LONG_LINE "This is a very, very long " \
"line of code that we wrap " \
"across several lines for " \
"readability"
这里要使用续行符 \
,在C语言中,\
符号被称为续行符(Line Continuation Character)。它的作用是告诉编译器,当前这一行代码在逻辑上并未结束,而是延续到下一行。这在编写长表达式或宏定义时非常有用,因为它允许开发者将代码分成多行,以提高代码的可读性。
或者甚至可以进行一段代码的替换:
#define PRINT for(int i = 0;i < 10;i++) {\
printf("*");\
}
对象式宏甚至可以定义为空,以便在编译时有选择地启用或禁用代码部分:
#define DEBUG
// 定义为空宏
2、函数式宏(Function-like Macros)
函数式宏看起来像函数调用,但它们不会生成函数调用的开销。它们在预处理时像模板一样展开,可以带参数。例如:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
这个例子定义了一个函数式宏 MAX
,用来计算两个值的最大值。注意,宏参数周围的括号非常重要,它们确保了宏参数作为整体参与计算,避免了运算符优先级的问题,或者说防止实参是表达式的问题。
函数式宏也可以实现更复杂的代码结构,例如:
#define FOREACH(item, array) \
for(int keep = 1, \
count = 0,\
size = sizeof (array) / sizeof *(array); \
keep && count != size; \
keep = !keep, count++) \
for(item = (array) + count; keep; keep = !keep)
这个宏 FOREACH
可以用来遍历数组,它通过一系列的循环和计数变量实现。
#include <stdio.h>
#define FOREACH(item, array) \
for(int keep = 1, \
count = 0,\
size = sizeof (array) / sizeof *(array); \
keep && count != size; \
keep = !keep, count++) \
for(item = (array) + count; keep; keep = !keep)
int main() {
int arr[5] = { 1,2,3,4,5 };
int* item = arr;
FOREACH(item, arr) {
printf("%d ", *item);
}
return 0;
}
运行结果:
这个FOREACH宏是为C语言提供一种类似于其他高级语言中的 foreach 循环的机制。这个宏使用两层 for
循环来遍历数组中的每个元素。
详细介绍这个函数式宏:
#define FOREACH(item, array) \
这一行开始定义了一个名为 FOREACH
的宏,它需要两个参数:item
和 array
。item
将成为指向当前数组元素的指针,array
是要迭代的数组。
for(int keep = 1, \
count = 0,\
size = sizeof (array) / sizeof *(array); \
这里初始化了三个变量:keep
,count
和 size
。
keep
用来控制循环的继续或终止。开始时被设为 1(true),表示循环应该执行。count
用来在数组中迭代,一开始设为 0。size
用来记录数组中元素的数量,通过sizeof (array) / sizeof *(array)
计算得出。sizeof (array)
给出整个数组所占的字节数,sizeof *(array)
给出数组中单个元素所占的字节数。二者相除就得到了数组中元素的数量。
keep && count != size; \
这里是外层 for
循环的条件部分,它表明只要 keep
是 true
且 count
小于 size
,循环就应该继续执行。
keep = !keep, count++) \
这是外层 for
循环的迭代部分。在每次迭代的最后,keep
的值会被取反(这里的作用主要是在内层循环中使用),并将 count
加 1 以指向下一个数组元素。
for(item = (array) + count; keep; keep = !keep)
这是内层 for
循环,它实际上只执行一次。item
被赋值为指向当前数组元素的指针,其偏移是 count
。内层循环的条件是 keep
仍然为 true
。由于内层循环内部没有语句,因此它实际上只用来赋值 item
。在内层循环的迭代部分,keep
再次被取反,这会导致内层循环结束。
对于内层循环,实际上的作用就是给 item
赋值,这里的 keep
就是保证内层循环在每次外层循环中只进行一次。为什么不只用一个单独的语句例如这样:
item = (array) + count;
而是使用一个只循环一次的循环呢?
实际上就是为了使用形式是这样:
int my_array[] = {1, 2, 3, 4, 5};
int *item;
FOREACH(item, my_array) {
// 这里可以使用 *item 来访问数组的当前元素
printf("%d\n", *item);
}
如果使用上面的一个单独语句的形式,就无法像这样的形式使用了。
3、注意事项
1.多加圆括号
宏在编译阶段就被替换,而且这种替换就只是单纯简单的替换。就是因为这里是简单的替换,所以会导致一些小问题。例如下面: