宏定义
各种预处理指令:
#line
#error
#pragma
// ANSI C定义的几个宏
_LINE_表示正在编译的文件的行号
_FILE_表示正在编译的文件的名字
_DATE_ 表示编译时刻的日期字符串,例如: "25Dec2007"
_TIME_ 表示编译时刻的时间字符串,例如: "12:30:55"
_STDC_ 判断该文件是不是定义成标准C程序
字符串宏常量:
如果路径太长,一行写下来比较别扭怎么办?用反斜杠接续符啊:
C), #define ENG_PATH_3 E:\English\listen_to_this\listen\ _to_this_3 还没发现问题?这里用了4个反斜杠,到底哪个是接续符?回去看看接续符反斜杠。 反斜杠作为接续符时,在本行其后面不能再有任何字符,空格都不行。所以,只有最后一 个反斜杠才是接续符。至于A)和B),那要看你怎么用了,既然define宏只是简单的替换, 那给ENG_PATH_1加上双引号不就成了:“ENG_PATH_1”。
但是请注意:有的系统里规定路径的要用双反斜杠“\”,比如: #define ENG_PATH_4 E:\\English\\listen_to_this\\listen_to_this_3
注意:注释先于预处理指令被处理。
宏定义中的空格
另外还有一个问题需要引起注意,看下面例子:
#define SUM (x)(x)+(x)
这还是定义的宏函数SUM(x)吗?显然不是。编译器认为这是定义了一个宏:SUM, 其代表的是(x)(x)+(x)。为什么会这样呢?其关键问题还是在于SUM后面的这个空 格。所以在定义宏的时候一定要注意什么时候该用空格,什么时候不该用空格。这个空格仅仅在定义的时候有效,在使用这个宏函数的时候,空格会被编译器忽略掉。也就是说,定义好的宏函数SUM(x)在使用的时候在SUM和(x)之间留有空格是没问题的。比 如:SUM(3)和SUM (3)的意思是一样的。
#undef 是用来撤销宏定义的
文件包含
#include <filename>
filename 为要包含的文件名称,用尖括号括起来,也称为头文件,表示预处理器要到系统规定的路径中去获得这个文件(即C编译系统所提供的并存放在指定的子目录下的头 文件)。找到文件后,用文件内容替换该语句。
#include “filename”
,filename 为要包含的文件名称。双引号表示预处理器应在当前目录中查找文件名为 filename 的文件,若没有找到,则按系统指定的路径信息,搜索其他目录。找到文件后,用 文件内容替换该语句。
#error
#error
是 C/C++ 预处理器的一个指令,用于在编译时强制停止编译,并输出一条指定的错误信息。
意义
#error
的主要作用是增强程序的可维护性,确保某些条件得不到满足时,编译立即终止,并给出明确的提示信息。例如:
- 检查编译器版本是否符合要求。
- 检查硬件平台或操作系统是否支持。
- 确保某些宏或条件被正确定义。
应用场景
-
检查编译器版本 很多库或程序需要特定版本的编译器才能正常工作。例如:
#if __cplusplus < 201103L #error "C++11 or newer is required to compile this code." #endif
-
硬件平台检测 在嵌入式开发中,代码可能依赖特定的硬件平台。例如:
#ifndef ARM_CORTEX_M4 #error "This code is intended for ARM Cortex-M4 microcontrollers only." #endif
-
配置检查 如果某些配置选项未正确设置,可以使用
#error
提示开发人员。#ifndef DEBUG #error "Debug mode must be enabled to compile this code." #endif
确保代码在调试模式下编译。
#line
#line
是 C/C++ 预处理器的另一个指令,用于修改编译器内部的行号和文件名跟踪。这会影响编译时的错误信息和调试信息。
意义
#line
的主要作用是帮助开发人员更好地追踪代码的来源,尤其是在代码生成或宏展开时。例如:
- 调试自动生成的代码。
- 维护包含大量宏的代码。
应用场景
-
调试自动生成的代码 当代码是通过工具自动生成时,
#line
可以调整行号,使错误信息指向原始代码,而不是生成的代码。例如:#line 1 "generated_code.c" int main() { // 自动生成的代码... }
如果
generated_code.c
中的某一行有错误,编译器会报告原始代码的行号,而不是生成代码的行号。 -
维护宏展开 宏展开可能导致代码行数增加,原始代码和生成代码的行号不同。例如:
#define INC(x) x++ #line 100 int i = 0; INC(i); // 这行代码的实际行号是 101
-
支持代码生成工具 一些代码生成工具会插入
#line
指令,以确保错误信息和调试信息指向正确的源代码。#line 1 "user_input.c" int x = get_user_input(); // 用户输入的代码
#pragma
#pragma
是 C/C++ 中一个预处理器指令,用于向编译器传递特殊的指令或选项。它的作用是编译器相关,不同的编译器可能支持不同的 #pragma
指令。#pragma
并非 C 标准的一部分,但它的语法在 C99 中被标准化,允许编译器根据需要自定义行为。
一般形式
#pragma <名称> <参数>...
其中 <名称>
是一个标识符,表示要传递给编译器的指令或选项。<参数>
是具体参数,根据指令的不同而变化。
1. 代码优化相关
-
GCC/Clang 编译器
#pragma GCC optimize("O3")
该指令告诉 GCC 编译器在当前的代码块中使用
-O3
优化级别。
2. 编译器行为控制
-
GCC/Clang
#pragma GCC system_header
表示当前文件是一个系统头文件,编译器不会对其中的代码进行严格检查。
#pragma message(“消息文本”)
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。 当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有 正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。
一些比较常用的#pragma编制指令
#pragma code_seg
#pragma hdrstop
#pragma comment
#pragma pack
....
内存对齐
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
使用指令#pragmapack(n),编译器将按照n个字节对齐。
使用指令#pragmapack(),编译器将取消自定义字节对齐方式。
#运算符与##运算符
#运算符
#运算符具体用法如下:
#define SQR(x) printf("The square of "#x" is %d.\n",((x)*(x)));
再使用:
SQR(8);
则输出的是:
The square of 8 is64
##运算符
##
是 C 语言预处理器中的一个特殊操作符,称为 “令牌粘接”(Token Pasting) 或 “标记连接”(Token Concatenation)。它主要用于宏定义中,用于将两个标记合并成一个单一的标记或标识符。
在 C 语言的预处理阶段,宏会被展开,##
操作符此时就可以发挥作用,将两个标记连接在一起,形成一个完整的标识符。
##
运算符的核心规则
- 仅用于宏定义:
##
必须出现在宏定义的替换列表中。 - 生成合法标识符:拼接后的结果必须符合C语言标识符的规则(如不能以数字开头)。
- 参数直接拼接:若参数本身是宏,
##
不会先展开参数,而是直接拼接其字面符号。 - 无空格分隔:
##
两侧的符号必须紧邻,不能有空格。
##
运算符具体作用:
- 符号拼接:将宏参数与其他符号或另一个宏参数拼接。
- 动态生成代码:通过宏生成重复模式的代码,减少冗余。
- 提高灵活性:在宏中根据输入参数生成不同的标识符。
简单来说就是提高代码的复用性。
使用规则
##
必须位于宏定义中。- 拼接后的结果必须是合法的C语言标识符。
- 若参数是宏,
##
不会展开参数,而是直接拼接符号(因此需注意展开顺序)。
示例解析
示例1:基本拼接
#define CONCAT(a, b) a##b
int var1 = 10;
int main()
{
printf("%d\n", CONCAT(var, 1)); // 展开为 var1,输出10
return 0;
}
该用法基本不用。
示例2:生成函数
#define DEFINE_PRINT(type) \
void type##_print(type value) { \
printf(#type ": %d\n", value); \
}
DEFINE_PRINT(int) // 生成 void int_print(int value) { ... }
DEFINE_PRINT(float) // 生成 void float_print(float value) { ... }
int main() {
int_print(42); // 输出 int: 42
float_print(3.14f); // 输出 float: 3
return 0;
}
示例3:生成结构体
#define CREATE_STRUCT(name) \
struct name##_data { \
int id; \
char* value; \
};
CREATE_STRUCT(book) // struct book_data { ... }
CREATE_STRUCT(user) // struct user_data { ... }
- 生成结构体类型
book_data
和user_data
。
应用场景
-
生成枚举常量:
#define COLOR(name) COLOR_##name enum Colors { COLOR(RED), COLOR(GREEN) }; // 展开为 COLOR_RED, COLOR_GREEN
-
工厂模式代码生成:
#define DECLARE_VECTOR(type) \ typedef struct { \ type* data; \ int size; \ } vector_##type; DECLARE_VECTOR(int) // vector_int DECLARE_VECTOR(float) // vector_float
总结
##
用于宏中的符号拼接,提升代码复用性。- 注意参数的展开顺序,必要时使用中间宏。
- 确保拼接结果合法(如不包含空格或非法字符)。
代码示例如下:
##
用于拼接标识符,而 _Generic
提供类型分发机制。
#include <stdio.h>
// 定义泛型交换函数宏
#define DEFINE_SWAP(type) \
void swap_##type(type *a, type *b) \
{ \
type temp = *a; \
*a = *b; \
*b = temp; \
}
// 定义泛型打印函数宏
#define DEFINE_PRINT(type) \
void print_##type(type value) \
{ \
_Generic(value, \
int: printf("int: %d\n", value), \
float: printf("float: %f\n", value), \
double: printf("double: %lf\n", value), \
char: printf("char: %c\n", value)); \
}
// 实例化不同类型的交换和打印函数即生成函数
DEFINE_SWAP(int)
DEFINE_SWAP(float)
DEFINE_PRINT(int)
DEFINE_PRINT(float)
DEFINE_PRINT(double)
DEFINE_PRINT(char)
// 泛型交换和打印的调用接口
#define swap(x, y) _Generic((x), \
int *: swap_int, \
float *: swap_float)(x, y)
#define print(value) _Generic((value), \
int: print_int, \
float: print_float, \
double: print_double, \
char: print_char)(value)
int main(void)
{
// 测试交换函数
int a = 5, b = 10;
printf("Before swap: ");
print(a);
print(b);
swap(&a, &b);
printf("After swap: ");
print(a);
print(b);
float c = 3.14f, d = 6.28f;
printf("Before swap: ");
print(c);
print(d);
swap(&c, &d);
printf("After swap: ");
print(c);
print(d);
// 测试打印函数
double e = 2.71828;
char f = 'X';
print(e);
print(f);
return 0;
}
泛型选择表达式
泛型编程(generic programming)指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。
然而,C11新增了一种表达式,叫作泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是int、double 还是其他类型)选择一个值。
Generic(x, int: 0, float: 1, double: 2, default: 3)
_Generic是C11的关键字。_Generic后面的圆括号中包含多个用逗号分隔的项。第1个项是一个表达式,后面的每个项都由一个类型、一个冒号和一个值组成,如float: 1。
**第1个项的类型匹配哪个标签,整个表达式的值是该标签后面的值。**例如,假设上面表达式中x是int类型的变量,x的类型匹配int:标签,那么整个表达式的值就是0。如果没有与类型匹配的标签,表达式
的值就是default:标签后面的值。泛型选择语句与 switch 语句类似,只是前者用表达式的类型匹配标签,而后者用表达式的值匹配标签。
宏必须定义为一条逻辑行,但是可以用\把一条逻辑行分隔成多条物理行。
// generic.c -- 定义泛型宏
#include <stdio.h>
#include <math.h>
#define RAD_TO_DEG (180 / (4 * atanl(1)))
// 泛型平方根函数
#define SQRT(X) _Generic((X), \
long double: sqrtl, \
default: sqrt, \
float: sqrtf)(X)
// 泛型正弦函数,角度的单位为度
#define SIN(X) _Generic((X), \
long double: sinl((X) / RAD_TO_DEG), \
default: sin((X) / RAD_TO_DEG), \
float: sinf((X) / RAD_TO_DEG))
int main(void)
{
float x = 45.0f;
double xx = 45.0;
long double xxx = 45.0L;
long double y = SQRT(x);
long double yy = SQRT(xx);
long double yyy = SQRT(xxx);
printf("%.17Lf\n", y); // 匹配 float
printf("%.17Lf\n", yy); // 匹配 default
printf("%.17Lf\n", yyy); // 匹配 long double
int i = 45;
yy = SQRT(i); // 匹配 default
printf("%.17Lf\n", yy);
yyy = SIN(xxx); // 匹配 long double
printf("%.17Lf\n", yyy);
return 0;
}
变参宏解析:
变参宏
__VA_ARGS__
是在预处理阶段处理的。_VA_ARGS__
的实现本质上是 字符串替换,没有运行时机制。在C语言中,__VA_ARGS__
就像一个“万能填空符”。它的作用是让一个宏可以接受任意数量、任意类型的参数,然后用这些参数替换到代码中。简单来说:你传什么参数,它就原样复制到哪里。
通过把宏参数列表中最后的参数写成省略号(即,3个点…)来实现这一功能。
这样,预定义宏_ _VA_ARGS__
可用在替换部分中,表明省略号代表什么。例如,下面的定义:
#define PR(...) printf(_ _VA_ARGS_ _)
PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);
对于第1次调用,_ VA_ARGS 展开为1个参数:
"Howdy"
对于第2次调用,_ VA_ARGS _展开为3个参数:
"weight = %d,shipping = $%.2f\n",wt,sp
代码示例如下:
#include <stdio.h>
#include <math.h>
#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)
int main(void)
{
double x = 48;
double y;
y = sqrt(x);
PR(1, "x = %g\n", x);
PR(2, "x = %.2f, y = %.4f\n", x, y);
return 0;
}
注意事项
-
必须配合
...
使用:
只能在定义带有可变参数的宏时用__VA_ARGS__
,例如:#define MACRO(...) do_something(__VA_ARGS__)
-
处理空参数问题(C99之后):
如果宏的参数可能为空,可以用##
避免语法错误:#define LOG(...) printf(##__VA_ARGS__)
变参函数
创建一个变参函数,具体步骤如下:
1.提供一个使用省略号的函数原型;
2.在函数定义中创建一个va_list类型的变量;
3.用宏把该变量初始化为一个参数列表;
4.用宏访问参数列表;
5.用宏完成清理工作。
代码示例如下:
#include <stdio.h>
#include <stdarg.h>
double sum(int, ...);
int main(void)
{
double s, t;
s = sum(3, 1.1, 2.5, 13.3);
t = sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1);
printf("return value for "
"sum(3, 1.1, 2.5, 13.3): %g\n",
s);
printf("return value for "
"sum(6, 1.1, 2.1, 13.1, 4.1, 5.1, 6.1): %g\n",
t);
return 0;
}
double sum(int lim, ...)
{
va_list ap; // 声明一个对象储存参数 va_list 等价于 char*
double tot = 0;
int i;
va_start(ap, lim); // 把ap初始化为参数列表
for (i = 0; i < lim; i++)
tot += va_arg(ap, double); // 访问参数列表中的每一项
va_end(ap); // 清理工作
return tot;
}
代码详细解析如下:
va_list
宏定义如下:
typedef char* va_list;
解析va_start
#define va_start __crt_va_start
#define __crt_va_start(ap, x) __crt_va_start_a(ap, x)
#define __crt_va_start_a(ap, v) ((void)\
(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
#define _ADDRESSOF(v) (&(v))
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
va_start(ap, lim);具体解析如下:
va_start
的底层实现高度依赖于编译器和目标平台,但**核心思想是确定第一个可变参数在内存中的位置,通常基于最后一个固定参数的地址进行计算。**编译器内置的宏或函数处理了这些底层细节,确保跨平台的可移植性。实际实现依赖编译器的内置函数(如 __builtin_va_start
)和平台调用约定。
// 底层实现代码如下:
va_list ap;
va_start(ap, lim);
typedef char* va_list;
va_start(ap, lim);
__crt_va_start(ap, lim);
__crt_va_start_a(ap, lim);
((void)(ap = (va_list)_ADDRESSOF(lim)
+ _INTSIZEOF(lim)));
((void)(ap = (char*)(&(lim))
+ ((sizeof(lim) + sizeof(int) - 1) & ~(sizeof(int) - 1)));
// 7 & ~(3) = 4
( (void)(ap = (char*)(&(lim)) + 4));//ap的数据类型为char*
va_arg(ap, double);的具体解析如下:
核心思想是按类型大小和对齐规则获取当前可变参数的值,并将指针移动到下一个参数。
// 底层实现代码如下:
va_list ap;
va_arg(ap, double);
typedef char* va_list;
#define va_arg __crt_va_arg
#define __crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
va_end(ap);
清理 va_list
对象。在大多数简单实现中为空操作,但在复杂场景(如寄存器保存)中可能需要重置指针。
底层实现代码如下:
#define __crt_va_end(ap) ((void)(ap = (va_list)0))
简单来说就是将字符指针置为空指针。
解析VS2019环境下的宏定义
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
sizeof(n)
向上取整到最接近的4的倍数。无论原来的大小是多少,都会对齐到4的倍数。
- ~3的运算结果为
0xFFFFFFFC
。这个掩码的作用是将任何数对齐到4的倍数。 - 加3确保即使x本身不是4的倍数,加上3后会超过下一个4的倍数,然后通过与操作去掉余数,得到正确的对齐后的值。结果会是比原数大的最近的4的倍数,除非原数已经是4的倍数。