我已经写了一点C,我可以很好地阅读它以大致了解它在做什么,但每次我遇到一个宏它都完全抛弃了我.我最终必须记住宏是什么,并在我阅读时将其替换为我的脑袋.我遇到的那些直观且易于理解的东西总是像迷你小功能一样,所以我总是想知道为什么它们不仅仅是功能.
我可以理解在预处理器中为调试或跨平台构建定义不同构建类型的需要,但是定义任意替换的能力似乎只对使得已经很难理解的语言更加难以理解.
为什么为C引入了如此复杂的预处理器?并且有没有人有一个使用它的例子,这将使我理解为什么它似乎仍然用于除了简单的#debug风格条件编译之外的目的?
编辑:
读了很多答案之后我还是不明白.最常见的答案是内联代码.如果内联关键字没有这样做,那么它有充分的理由不这样做,或者实现需要修复.我不明白为什么需要一个完全不同的机制,这意味着"真正内联这个代码"(除了形成内联之前编写的代码).我也不明白提到"如果它太愚蠢而不能被赋予功能"的想法.当然,任何需要输入并产生输出的代码都最好放在一个函数中.我想我可能没有得到它,因为我不习惯编写C的微观优化,但预处理器只是对一些简单问题的复杂解决方案.
我最终必须记住宏是什么,并在我阅读时将其替换为我的脑袋.
这似乎反映了宏的命名.我认为如果它是一个log_function_entry()
宏,你就不必模拟预处理器.
我遇到的那些直观且易于理解的东西总是像迷你小功能一样,所以我总是想知道为什么它们不仅仅是功能.
通常它们应该是,除非它们需要对通用参数进行操作.
#define max(a,b) ((a)<(b)?(b):(a))
将适用于任何类型的<
运营商.
更多只是函数,宏允许您使用源文件中的符号执行操作.这意味着您可以创建新的变量名称,或引用宏所在的源文件和行号.
在C99中,宏还允许您调用variadic函数,例如printf
#define log_message(guard,format,...) \ if (guard) printf("%s:%d: " format "\n", __FILE__, __LINE__,__VA_ARGS_); log_message( foo == 7, "x %d", x)
其格式与printf类似.如果防护是真的,它会输出消息以及打印消息的文件和行号.如果它是一个函数调用,它将不知道你从中调用它的文件和行,并且使用a vaprintf
会更多一些工作.
这段摘录通过比较C
宏的几种使用方式以及如何实现它们,总结了我对此事的看法D
.
从DigitalMars.com复制
回到
C
发明时,编译器技术是原始的.在前端安装文本宏预处理器是添加许多强大功能的简单方便的方法.程序规模和复杂程度的增加表明这些功能存在许多固有问题.D
没有预处理器; 但D
提供了一种更具伸缩性的方法来解决同样的问题.
预处理器宏增加了强大的功能和灵活性C
.但他们有一个缺点:
宏没有范围概念; 从定义到结束,它们都是有效的.他们在.h文件,嵌套代码等中删除了一些条带.当#include
成千上万行宏定义时,避免无意的宏扩展会成为问题.
调试器不知道宏.尝试使用符号数据调试程序会被调试器破坏,只知道宏扩展,而不是宏本身.
宏使得无法对源代码进行标记,因为早期的宏更改可以任意重做令牌.
宏的纯文本基础导致任意和不一致的使用,使得使用宏的代码容易出错.(有些尝试解决此问题的方法是在模板中引入C++
.)
宏仍然用于弥补语言表达能力的不足,例如头文件周围的"包装".
这是宏的常见用法的枚举,以及D中的相应功能:
定义文字常量:
该C
预处理方法
#define VALUE 5
该D
路
const int VALUE = 5;
创建值或标志列表:
该C
预处理方法
int flags: #define FLAG_X 0x1 #define FLAG_Y 0x2 #define FLAG_Z 0x4 ... flags |= FLAG_X;
该D
路
enum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 }; FLAGS flags; ... flags |= FLAGS.X;
设置函数调用约定:
该C
预处理方法
#ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 func();
该D
路
调用约定可以用块指定,因此不需要为每个函数更改它:
extern (Windows) { int onefunc(); int anotherfunc(); }
简单的通用编程:
该C
预处理方法
根据文本替换选择要使用的功能:
#ifdef UNICODE int getValueW(wchar_t *p); #define getValue getValueW #else int getValueA(char *p); #define getValue getValueA #endif
该D
路
D
启用符号作为其他符号别名的声明:
version (UNICODE) { int getValueW(wchar[] p); alias getValueW getValue; } else { int getValueA(char[] p); alias getValueA getValue; }
DigitalMars网站上有更多示例.
它们是一种基于C的编程语言(更简单的一种),因此它们对于在编译时进行元编程非常有用...换句话说,你可以编写宏代码,以更少的行和时间生成C代码直接用C写
它们对于编写"多态"或"过载"的"类似函数"表达式也非常有用; 例如,最大宏定义为:
#define max(a,b) ((a)>(b)?(a):(b))
适用于任何数字类型; 在C中你不能写:
int max(int a, int b) {return a>b?a:b;} float max(float a, float b) {return a>b?a:b;} double max(double a, double b) {return a>b?a:b;} ...
即使你想要,因为你不能超载功能.
更不用说条件编译和文件包括(也是宏语言的一部分)......
宏允许某人在编译期间修改程序行为.考虑一下:
C常量允许在开发时修复程序行为
C变量允许在执行时修改程序行为
C宏允许在编译时修改程序行为
在编译时,意味着未使用的代码甚至不会进入二进制文件,并且构建过程可以修改值,只要它与宏预处理器集成即可.示例:make ARCH = arm(假设转发宏定义为cc -DARCH = arm)
简单的例子:(从glibc limits.h,定义long的最大值)
#if __WORDSIZE == 64 #define LONG_MAX 9223372036854775807L #else #define LONG_MAX 2147483647L #endif
如果我们正在编译32位或64位,则在编译时验证(使用#define __WORDSIZE).使用multilib工具链,使用参数-m32和-m64可能会自动更改位大小.
(POSIX版本请求)
#define _POSIX_C_SOURCE 200809L
编译期间请求POSIX 2008支持.标准库可能支持许多(不兼容的)标准,但是通过这个定义,它将提供正确的函数原型(例如:getline(),no gets()等).如果库不支持标准,它可能在编译期间给出#error,而不是在执行期间崩溃,例如.
(硬编码路径)
#ifndef LIBRARY_PATH #define LIBRARY_PATH "/usr/lib" #endif
在编译期间定义硬编码目录.例如,可以使用-DLIBRARY_PATH =/home/user/lib进行更改.如果那是一个const char*,你将如何在编译期间配置它?
(pthread.h,编译时的复杂定义)
# define PTHREAD_MUTEX_INITIALIZER \ { { 0, 0, 0, 0, 0, 0, { 0, 0 } } }
可以声明大部分文本(否则将不会被简化)(始终在编译时).使用函数或常量(在编译时)不可能这样做.
为了避免使事情变得非常复杂并避免建议编码风格不佳,我不会举一个在不同的,不兼容的操作系统中编译的代码示例.使用您的交叉构建系统,但应该清楚的是,预处理器在没有构建系统帮助的情况下允许这样做,而不会因为缺少接口而破坏编译.
最后,考虑条件编译在嵌入式系统中的重要性,其中处理器速度和内存有限,系统非常异构.
现在,如果你问,是否有可能用适当的定义替换所有宏常量定义和函数调用?答案是肯定的,但它不会简单地使编译期间改变程序行为的需要消失.仍然需要预处理器.
请记住,宏(和预处理器)来自C的早期.它们曾经是内联'函数'的唯一方法(因为,当然,内联是一个非常新近的关键字),它们仍然是唯一的方法来强制要内联的东西.
此外,宏是唯一的方法,你可以做这样的技巧,如在编译时将文件和行插入字符串常量.
如今,宏以前的许多事情都是通过新机制更好地处理的唯一方法.但他们仍然不时有自己的位置.
除了内联效率和条件编译之外,宏还可用于提高低级C代码的抽象级别.C并没有真正使您免受内存和资源管理的细节以及数据的精确布局的影响,并且支持非常有限的信息隐藏形式以及管理大型系统的其他机制.使用宏,您不再局限于仅使用C语言中的基本结构:您可以定义自己的数据结构和编码结构(包括类和模板!),同时仍然名义上写C!
预处理器宏实际上提供了在编译时执行的图灵完备语言.其中一个令人印象深刻(有点可怕)的例子是在C++方面:Boost预处理器库使用C99/C++ 98预处理器来构建(相对)安全的编程结构,然后将其扩展为任何底层声明和代码你输入,无论是C还是C++.
在实践中,当你没有在更安全的语言中使用高级结构的自由度时,我建议将预处理程序编程作为最后的手段.但有时候,如果你的背靠墙并且黄鼠狼正在关闭......你知道你能做什么是很好的.
从计算机愚蠢:
我在很多针对UNIX的免费游戏程序中看到了这段代码摘录:
/*
*位值.
*/
#define BIT_1
0
#define BIT_2 4
#define BIT_3 8
#define BIT_4
32
#define BIT_6 64
#define BIT_7 128
#define BIT_8 256
#define BIT_9 512
#define BIT_10 1024
#define BIT_11 2048
#define BIT_12 4096
#define BIT_13
8382
#define BIT_14 32768
#define BIT_16 65536
#define BIT_17 131072
#define BIT_18 262144
#define BIT_19 524288
#define BIT_20 1048576
#define BIT_21 2097152
#define BIT_22 4194304
#define BIT_23 8388608
#define BIT_24 16777216
#define BIT_25 33554432
#define BIT_26 67108864
#define BIT_27 134217728
#define BIT_28 268435456
#define BIT_29 536870912
#define BIT_30 1073741824
#define BIT_31 2147483648实现这一目标的更简单方法是:
#define BIT_0 0x00000001
#define BIT_1 0x00000002
#define BIT_2 0x00000004
#define BIT_3 0x00000008
#define BIT_4 0x00000010
...
#define BIT_28 0x10000000
#define BIT_29 0x20000000
#define BIT_30 0x40000000
#define BIT_31 0x80000000更简单的方法是让编译器进行计算:
#define BIT_0(1)
#define BIT_1(1 << 1)
#define BIT_2(1 << 2)
#define BIT_3(1 << 3)
#define BIT_4(1 << 4)
...
#define BIT_28(1 << 28)
#define BIT_29(1 << 29)
#define BIT_30(1 << 30)
#define BIT_31(1 << 31)但为什么要去定义32个常量呢?C语言也有参数化宏.你真正需要的是:
#define BIT(x)(1 <<(x))
无论如何,我想知道编写原始代码的人是使用计算器还是只是在纸上计算出来的.
这只是宏的一种可能用途.
宏实际发光的一个例子就是用它们进行代码生成.
我曾经在一个旧的C++系统上工作,该系统使用插件系统以他自己的方式将参数传递给插件(使用类似自定义地图的结构).一些简单的宏被用来处理这个怪癖并允许我们在插件中使用具有普通参数的真实C++类和函数而没有太多问题.所有胶水代码都是由宏生成的.
我会补充说已经说过了什么.
因为宏可以处理文本替换,所以它们允许您执行使用函数无法完成的非常有用的操作.
这里有一些宏可以真正有用的情况:
/* Get the number of elements in array 'A'. */ #define ARRAY_LENGTH(A) (sizeof(A) / sizeof(A[0]))
这是一个非常受欢迎且经常使用的宏.例如,当您需要遍历数组时,这非常方便.
int main(void) { int a[] = {1, 2, 3, 4, 5}; int i; for (i = 0; i < ARRAY_LENGTH(a); ++i) { printf("a[%d] = %d\n", i, a[i]); } return 0; }
如果另一个程序员a
在decleration中添加了五个元素并不重要.该for
-loop将始终通过所有元素进行迭代.
C库的比较内存和字符串的功能使用起来非常难看.
你写:
char *str = "Hello, world!"; if (strcmp(str, "Hello, world!") == 0) { /* ... */ }
要么
char *str = "Hello, world!"; if (!strcmp(str, "Hello, world!")) { /* ... */ }
检查是否str
指向"Hello, world"
.我个人认为这两种解决方案看起来都很丑陋而且令人困惑(特别是!strcmp(...)
).
以下是一些人(包括我)在需要使用strcmp
/ 来比较字符串或内存时使用的两个巧妙的宏memcmp
:
/* Compare strings */ #define STRCMP(A, o, B) (strcmp((A), (B)) o 0) /* Compare memory */ #define MEMCMP(A, o, B) (memcmp((A), (B)) o 0)
现在您可以编写如下代码:
char *str = "Hello, world!"; if (STRCMP(str, ==, "Hello, world!")) { /* ... */ }
这里的意图很清楚!
这些都是宏用于事物功能无法完成的情况.宏不应该用于替换功能,但它们有其他好的用途.