什么是C宏有用?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是C宏有用?相关的知识,希望对你有一定的参考价值。

我已经写了一点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 "
", __FILE__, __LINE__,__VA_ARGS_);

log_message( foo == 7, "x %d", x)

其格式与printf类似。如果防护是真的,它会输出消息以及打印消息的文件和行号。如果它是一个函数调用,它将不知道你从中调用它的文件和行,并且使用vaprintf会更多一些工作。

另一答案

鉴于您的问题中的评论,您可能不完全了解调用函数可能需要相当大的开销。参数和键寄存器可能必须在进入的过程中复制到堆栈中,并且堆栈在出路时展开。旧的英特尔芯片尤其如此。宏让程序员保持函数的抽象(几乎),但避免了函数调用的昂贵开销。 inline关键字是建议性的,但编译器可能并不总是正确。 'C'的荣耀和危险在于你通常可以根据自己的意愿弯曲编译器。

在你的面包和黄油,日常的应用程序编程这种微优化(避免函数调用)通常更糟,然后无用,但如果你正在编写一个由操作系统的内核调用的时间关键函数,那么它可以产生巨大的变化。

另一答案

与常规函数不同,您可以在宏中控制流(if,while,for,...)。这是一个例子:

#include <stdio.h>

#define Loop(i,x) for(i=0; i<x; i++)

int main(int argc, char *argv[])
{
    int i;
    int x = 5;
    Loop(i, x)
    {
        printf("%d", i); // Output: 01234
    } 
    return 0;
} 
另一答案

它有助于内联代码并避免函数调用开销。如果您想稍后更改行为而不编辑大量地点,也可以使用它。它对复杂的东西没有用,但是对于你想要内联的简单代码行,它并不坏。

另一答案

通过利用C预处理器的文本操作,可以构造多态数据结构的C等价物。使用这种技术,我们可以构建一个可以在任何C程序中使用的原始数据结构的可靠工具箱,因为它们利用了C语法而不是任何特定实现的细节。

这里给出了有关如何使用宏来管理数据结构的详细说明 - http://multi-core-dump.blogspot.com/2010/11/interesting-use-of-c-macros-polymorphic.html

另一答案

宏可以让你摆脱复制粘贴的碎片,这是你无法以任何其他方式消除的。

例如(VS 2010编译器的真实代码,语法):

for each (auto entry in entries)
{
        sciter::value item;
        item.set_item("DisplayName",    entry.DisplayName);
        item.set_item("IsFolder",       entry.IsFolder);
        item.set_item("IconPath",       entry.IconPath);
        item.set_item("FilePath",       entry.FilePath);
        item.set_item("LocalName",      entry.LocalName);
        items.append(item);
    }

这是您将同名字段值传递到脚本引擎的位置。这是复制粘贴的吗?是。 DisplayName用作脚本的字符串和编译器的字段名称。那不好吗?是。如果你重构代码并将LocalName重命名为RelativeFolderName(正如我所做的那样)而忘记对字符串执行相同操作(就像我所做的那样),脚本将以您不期望的方式工作(事实上,在我的示例中它取决于您是否忘记在单独的脚本文件中重命名该字段,但如果该脚本用于序列化,那将是100%的错误)。

如果你使用宏,那么bug就没有空间了:

for each (auto entry in entries)
{
#define STR_VALUE(arg) #arg
#define SET_ITEM(field) item.set_item(STR_VALUE(field), entry.field)
        sciter::value item;
        SET_ITEM(DisplayName);
        SET_ITEM(IsFolder);
        SET_ITEM(IconPath);
        SET_ITEM(FilePath);
        SET_ITEM(LocalName);
#undef SET_ITEM
#undef STR_VALUE
        items.append(item);
    }

不幸的是,这为其他类型的错误打开了一扇门。你可以写一个拼写错误的宏,永远不会看到被破坏的代码,因为编译器没有显示它在所有预处理后的外观。其他人可以使用相同的名称(这就是我与#undef尽快“释放”宏的原因)。所以,明智地使用它。如果您看到另一种摆脱复制粘贴代码(如函数)的方法,请使用这种方式。如果您发现删除带有宏的复制粘贴代码不值得,请保留复制粘贴的代码。

另一答案

其中一个显而易见的原因是,通过使用宏,代码将在编译时进行扩展,并且您将获得一个没有调用开销的伪函数调用。

否则,您也可以将它用于符号常量,这样您就不必在几个地方编辑相同的值来更改一个小的东西。

另一答案

宏...当你的&#(* $&编译器只是拒绝内联某些内容时)。

那应该是一张励志海报,不是吗?

严肃地说,谷歌preprocessor abuse(你可能会看到类似的SO问题作为#1结果)。如果我正在编写一个超出assert()功能的宏,我通常会尝试查看我的编译器是否实际内联了一个类似的函数。

其他人会反对使用#if进行条件编译..他们宁愿你:

if (RUNNING_ON_VALGRIND)

而不是

#if RUNNING_ON_VALGRIND

..出于调试目的,因为您可以在调试器中看到if()而不是#if。然后我们深入#ifdef vs #if。

如果它不到10行代码,请尝试内联它。如果无法内联,请尝试优化它。如果它太傻了,不能成为一个函数,那就制作一个宏。

另一答案

虽然我不是宏的忠实粉丝,并且根据我目前的任务,不再倾向于写很多C,这样的事情(显然有一些副作用)很方便:

#define MIN(X, Y)  ((X) < (Y) ? (X) : (Y))

现在我已经多年没有写过这样的东西了,但是像我这样的“函数”都是我职业生涯早期维护的代码。我想扩展可以被认为是方便的。

另一答案

关于像宏一样的功能,我没有看到有人提到这一点,例如:

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))

一般情况下,建议在不需要时避免使用宏,原因很多,可读性是主要问题。所以:

什么时候应该在功能上使用它们?

几乎从来没有,因为有一个更可读的替代品inline,请参阅https://www.greenend.org.uk/rjk/tech/inline.htmlhttp://www.cplusplus.com/articles/2LywvCM9/(第二个链接是一个C ++页面,但据我所知,这一点适用于c编译器)。

现在,略有不同的是,宏由预处理器处理,内联由编译器处理,但现在没有实际的区别。

何时适合使用这些?

适用于小功能(最多两个或三个衬垫)。目标是在程序运行期间获得一些优势,因为像宏(和内联函数)这样的函数是在预处理期间完成的代码替换(或者在内联的情况下编译)并且不是存储在内存中的真实函数,所以没有函数调用开销(链接页面中的更多细节)。

另一答案

通过比较使用C宏的几种方法,以及如何在D中实现它们,这个摘录几乎总结了我对此事的看法。

copied from DigitalMars.com

C被发明时,编译器技术是原始的。在前端安装文本宏预处理器是添加许多强大功能的简单方便的方法。程序规模和复杂程度的增加表明这些功能存在许多固有问题。 D没有预处理器;但D提供了一种更具伸缩性的方法来解决同样的问题。

预处理器宏为C增加了强大的功能和灵活性。但他们有一个缺点:

  • 宏没有范围概念;从定义到结束,它们都是有效的。他们在.h文件,嵌套代码等中删除了一些条带。当#include成功的数万行宏定义时,避免无意的宏扩展会成为问题。
  • 调试器不知道宏。尝试使用符号数据调试程序会被调试器破坏,只知道宏扩展,而不是宏本身。
  • 宏使得无法对源代码进行标记,因为早期的宏更改可以任意重做令牌。
  • 宏的纯文本基础导致任意和不一致的使用,使得使用宏的代码容易出错。 (有些尝试解决这个问题是在C++中使用模板引入的。)
  • 宏仍然用于弥补语言表达能力的不足,例如头文件周围的“包装”。

这是宏的常见用法的枚举,以及D中的相应功能:

  1. 定义文字常量: C预处理器方式 #define VALUE 5 D方式 const int VALUE = 5;
  2. 创建值或标志列表: 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;
  3. 设置函数调用约定: C预处理器方式 #ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 func(); D方式 调用约定可以用块指定,因此不需要为每个函数更改它: extern (Windows) { int onefunc(); int anotherfunc(); }
  4. 简单的通用编程: 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 website上有更多的例子。

另一答案

它们是一种基于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;}
...

即使你想要,因为你不能超载功能。

更不用说条件编译和文件包括(也是宏语言的一部分)......

另一答案

宏允许某人在编译期间修改程序行为。考虑一下: