C语言宏定义的知识大全

宏定义

各种预处理指令:

#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 的主要作用是增强程序的可维护性,确保某些条件得不到满足时,编译立即终止,并给出明确的提示信息。例如:

  • 检查编译器版本是否符合要求。
  • 检查硬件平台或操作系统是否支持。
  • 确保某些宏或条件被正确定义。

应用场景

  1. 检查编译器版本 很多库或程序需要特定版本的编译器才能正常工作。例如:

    #if __cplusplus < 201103L
        #error "C++11 or newer is required to compile this code."
    #endif
    
  2. 硬件平台检测 在嵌入式开发中,代码可能依赖特定的硬件平台。例如:

    #ifndef ARM_CORTEX_M4
        #error "This code is intended for ARM Cortex-M4 microcontrollers only."
    #endif
    
  3. 配置检查 如果某些配置选项未正确设置,可以使用 #error 提示开发人员。

    #ifndef DEBUG
        #error "Debug mode must be enabled to compile this code."
    #endif
    

    确保代码在调试模式下编译。

#line

#line 是 C/C++ 预处理器的另一个指令,用于修改编译器内部的行号和文件名跟踪。这会影响编译时的错误信息和调试信息。

意义

#line 的主要作用是帮助开发人员更好地追踪代码的来源,尤其是在代码生成或宏展开时。例如:

  • 调试自动生成的代码。
  • 维护包含大量宏的代码。

应用场景

  1. 调试自动生成的代码 当代码是通过工具自动生成时,#line 可以调整行号,使错误信息指向原始代码,而不是生成的代码。例如:

    #line 1 "generated_code.c"
    int main() {
        // 自动生成的代码...
    }
    

    如果 generated_code.c 中的某一行有错误,编译器会报告原始代码的行号,而不是生成代码的行号。

  2. 维护宏展开 宏展开可能导致代码行数增加,原始代码和生成代码的行号不同。例如:

    #define INC(x) x++
    #line 100
    int i = 0;
    INC(i); // 这行代码的实际行号是 101
    
  3. 支持代码生成工具 一些代码生成工具会插入 #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 语言的预处理阶段,宏会被展开,## 操作符此时就可以发挥作用,将两个标记连接在一起,形成一个完整的标识符。

##运算符的核心规则

  1. 仅用于宏定义##必须出现在宏定义的替换列表中。
  2. 生成合法标识符:拼接后的结果必须符合C语言标识符的规则(如不能以数字开头)。
  3. 参数直接拼接:若参数本身是宏,##不会先展开参数,而是直接拼接其字面符号。
  4. 无空格分隔##两侧的符号必须紧邻,不能有空格。

##运算符具体作用:

  • 符号拼接:将宏参数与其他符号或另一个宏参数拼接。
  • 动态生成代码:通过宏生成重复模式的代码,减少冗余。
  • 提高灵活性:在宏中根据输入参数生成不同的标识符。

简单来说就是提高代码的复用性。

使用规则

  1. ##必须位于宏定义中。
  2. 拼接后的结果必须是合法的C语言标识符。
  3. 若参数是宏,##不会展开参数,而是直接拼接符号(因此需注意展开顺序)。

示例解析

示例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_datauser_data

应用场景

  1. 生成枚举常量

    #define COLOR(name) COLOR_##name
    enum Colors { COLOR(RED), COLOR(GREEN) }; // 展开为 COLOR_RED, COLOR_GREEN
    
  2. 工厂模式代码生成

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

注意事项

  1. 必须配合 ... 使用
    只能在定义带有可变参数的宏时用 __VA_ARGS__,例如:

    #define MACRO(...) do_something(__VA_ARGS__)
    
  2. 处理空参数问题(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的倍数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值