我正在读一本名为“制作解释器”的书,我看到了这样的代码片段,该函数用于增加动态数组的大小。

#define GROW_ARRAY(type, pointer, oldCount, newCount) \ 
        (type*)reallocate(pointer, sizeof(type)*(oldCount), \
          sizeof(type) * (newCount))
//size_t : 无符号整型,表示对象的大小和数组的index
void* reallocate(void* pointer, size_t oldSize, size_t newSize);

为什么要使用宏来调用函数而不是直接调用函数?这样做有什么好处和缺点?

9

  • 1
    真正的函数调用会导致函数参数(以及一些附近的变量/寄存器)被推送到堆栈上,而这种推送和弹出操作可能会占用大量处理器。有人认为,尽管宏伪函数可能占用更多代码空间,但由于没有上述堆栈操作,处理器开销更少,因此它们的速度更快。这是我/我的同事们的常识……我期待有人在 S/O 上提供更好的信息来改变我们对这个概念的看法。


    – 


  • 1
    @user22873067,使用 C2X 和typeoftype不需要参数:#define GROW_ARRAY(pointer, oldCount, newCount), (typeof(pointer))reallocate((pointer), sizeof pointer[0] *(oldCount), sizeof pointer[0] * (newCount))


    – 


  • @gregspears 如果宏不只是调用函数的话,那将是真的。问题只是将函数调用包装在宏中。除此之外,根据函数的不同,现代编译器可能会生成同样高效的函数调用代码。


    – 


  • 您可能会想,如果您需要将该代码移植到没有reallocate功能而只有标准的系统中,会发生什么realloc。更改宏比修复所有使用它的地方要容易得多。


    – 

  • 我从来不理解这样的宏。如果我能记得输入,x = macro(type, n)那为什么我记不住输入x = (type)macro(n)?程序员需要有一定的意识,他们不能漫无目的地调用任何宏/函数,而根本不关心他们传递了什么参数,否则任何编程实践都无法拯救他们。


    – 


最佳答案
3

在这种情况下,它引入了类型安全。

下面的代码有错误,但是编译器没有捕获它。

A *p;
A *q = reallocate( p, i * sizeof( B ), j * sizeof( B ) );

下面的代码有不同的错误,但编译器也没有捕获这个错误。

A *p;
B *q = reallocate( p, i * sizeof *p, j * sizeof *p );

使用宏不可能犯第一个错误,并且编译器在使用宏时会捕获第二个错误的实例。

此外,使用宏的调用更加简单。

GROW_ARRAY( A, p, i, j )

Compiler Explorer 上的

#include <stdlib.h>

void* reallocate(void* pointer, size_t oldSize, size_t newSize);

#define GROW_ARRAY(type, pointer, oldCount, newCount) \ 
        (type*)reallocate(pointer, sizeof(type)*(oldCount), \
          sizeof(type) * (newCount));          
          
typedef int A;
typedef double B;

int main( void ) {
   int i = 2;
   int j = 4;
   A *p = NULL;  // Whatever. We're not actually running this.

   {
      // This error isn't caught.
      A *q = reallocate( p, i * sizeof( B ), j * sizeof( B ) );
   }

   {
      // This different error isn't caught.
      B *q = reallocate( p, i * sizeof *p, j * sizeof *p );
   }

   {
      // The same error is caught here.
      B *q = GROW_ARRAY( A, p, i, j );
   }
}
<source>:5:55: warning: backslash and newline separated by space
    5 | #define GROW_ARRAY(type, pointer, oldCount, newCount) \
<source>: In function 'main':
<source>:6:9: error: initialization of 'B *' {aka 'double *'} from incompatible pointer type 'A *' {aka 'int *'} [-Wincompatible-pointer-types]
    6 |         (type*)reallocate(pointer, sizeof(type)*(oldCount), \
      |         ^
<source>:29:14: note: in expansion of macro 'GROW_ARRAY'
   29 |       B *q = GROW_ARRAY( A, p, i, j );
      |              ^~~~~~~~~~

请注意,可以对宏进行改进,并且使用宏可能会引发其他问题。问题询问的是宏的用途,而这个答案仅限于解决该问题。

2

  • 哪里有安全可言?我可以轻松编写A *p; B *q = GROW_ARRAY( B, p, i, j);。调用 GROW_ARRAY 更简单,但绝不会更安全。可以添加类型检查,但宏定义没有任何检查。


    – 


  • @Goswin von Brederlow,添加了


    – 

在 C 中使用宏进行元编程的原因有几个,例如根据操作系统或编译标志调用不同的函数。

大多数其他情况已被更好的编译器功能所淘汰,您应该只使用静态内联函数。编译器会做正确的事情,甚至可以在许多情况下生成更好的代码。但在这种情况下并非如此,它是少数需要宏的情况之一。

这里的用法有两种,但本质上是相同的情况:

  1. 它修改了参数以处理数组大小以类型为单位而不是数组占用的内存这一事实。因此,每个大小都必须乘以 sizeof(type)。并且代码中有一个错误,它不处理溢出。

  2. 它修改了返回类型,因此将其分配给不同类型的指针可能会发出警告。它缺少的是检查输入的类型。如果你这样做,你最好不要要求用户指定类型,因为你可以从给定的数组本身中获取它。

用例是使函数具有通用性,这样就可以用任何类型调用它,尽管它为每种类型生成不同的代码。它为您隐藏了繁琐的类型转换或从数组大小到内存大小的转换。由于参数的原因,它在这方面略有失败type。请参阅 Lundins 的回答,了解如何摆脱该参数并使宏真正具有通用性。

5

  • 关于“您可以从给定的数组本身中获取它”,这仅在尚未完成的 C23 中才有可能。


    – 


  • @ikegamitypeof已经存在很久了。如果你是这个意思typeof_unqual,那么我甚至不确定我是否同意在这里使用它。Reallocate 是一个读/写(或至少是复制)操作,所以我不确定在 const 数组上调用它是否是正确的做法。


    – 

  • 1
    @ikegami 自上周以来已经完成。iso.org


    – 

  • @GoswinvonBrederlow “重新分配是一种读/写(或至少是复制)操作,因此我不确定在 const 数组上调用它是否是正确的做法。”但是,像在原始代码中那样默默地转换为合格类型也无法解决这个问题……


    – 

  • @Lundin,太棒了。我在发布之前确实查看了 Wikipedia,但我猜它还没有更新。现在已经更新了。话虽如此,但观点依然成立:这只有在 C23 中才有可能,而 C23 并不是普遍可访问的。


    – 


他们尝试使用宏来增加额外的类型安全性,因为reallocate返回的void*是类型不安全的。但这是一个旧时代的笨重接口,迫使调用者手动输入类型。

在标准 C 中(截至 2024 年),您可以改为执行以下操作:

#define GROW_ARRAY(pointer, oldCount, newCount)                           \
    (typeof_unqual(pointer))                                              \
         reallocate( pointer,                                             \
                     sizeof(*(pointer)) * (oldCount),                     \
                     sizeof(*(pointer)) * (newCount) )

typeof给出指针的类型,typeof(*(pointer))给出所指向项的类型。

typeof_unqual是因为const 正确性而存在的。调用者很可能会传递一个 longconst type*指针,在这种情况下一切都会中断。因为reallocate很可能需要返回一个读/写指针。因此将转换为正确类型但不带有(或)(typeof_unqual(pointer))的指针类型constvolatile

请注意,如果是这样,您根本realloc不需要该参数。oldCount

9

  • 哪里有安全?这是一个便利包装器,它考虑了您指定的类型,因此您不必总是为所有内容键入 sizeof(X)。强制转换返回类型可能会略微提高类型安全性,但宏也可以对输入进行类型检查。或者如您所说,使用typeof_unqual并消除用户提供错误类型的风险。


    – 

  • @GoswinvonBrederlow void* 可以隐式转换为任何其他指针类型,不一定是赋予宏的相同类型。使用问题中的宏,int* x = GROW_ARRAY(double, dblptr, a, b);即使它是明显错误的代码,也可以干净地编译。


    – 

  • 这就是我的观点。该宏缺乏任何类型检查,可以通过将参数分配给临时变量来实现。因此,它不是为了安全而设的宏,而是为了方便和简单。


    – 

  • @GoswinvonBrederlow 强制转换意味着意图是类型安全,否则它就不会存在。如何将什么分配给临时变量?


    – 

  • 在原始版本中如果你添加(从内存中添加,所以可能有点错误),type temp = pointer;编译器会抱怨分配了不兼容的指针,从而添加了类型检查。


    –