在 C/C++ 编程中,我们通常使用 sizeof(array)/sizeof(array[0]) 来计算静态数组的元素个数。然而,这种方法存在一个隐蔽的陷阱:如果你不小心传入一个指针,计算就会出错。在64位系统上,sizeof(指针) 固定为8字节,导致计算结果完全错误。有没有办法在编译阶段就发现这类问题,避免运行时隐患呢?答案是肯定的。无论是 Windows 还是 Linux 系统,都提供了精巧的解决方案。本文将带你深入剖析这些实现背后的“魔法”。
Windows 的实现:ARRAYSIZE
我们先看一个 Windows 上的例子,使用 ARRAYSIZE 宏来获取静态数组的大小:
#include <Windows.h>
#include <stdio.h>
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int main(int argc, char* argv[])
{
printf("%llu\n", ARRAYSIZE(arr));
return 0;
}
ARRAYSIZE 宏定义在 winnt.h 头文件中,包含 Windows.h 即可使用。这个宏在不同语境下有不同的实现。在 C 语言中,它采用传统的计算方式:sizeof(array)/sizeof(array[0])。

但是,当你将源文件后缀改为 .cpp 并在 Visual Studio 中查看时,会发现其实现变得“面目全非”,充满了精妙的设计:

实现的关键在于 RtlpNumberOf 这个函数模板指针的声明。我们来逐步拆解它:

首先,这是一个函数指针,指向一个返回类型为 char [N] 数组的函数。
char (*RtlpNumberOf(...))[N];
其次,这个函数的参数是一个引用,指向一个含有 N 个 T 类型元素的数组。UNALIGNED 宏定义为 __unaligned,表示数组可能非自然对齐。使用引用作为参数是为了防止数组在传参时退化为指针。
UNALIGNED T (&)[N]
最后,这是一个模板,接受类型 T 和编译时常量 N。
template <typename T, size_t N>
宏 ARRAYSIZE 最终展开为 sizeof(*RtlpNumberOf(A))。这里的妙处在于:sizeof 计算的是 RtlpNumberOf 返回类型的大小。该函数返回一个 char[N] 类型的数组,所以 sizeof 的结果就是 N。
编译器在实例化模板时,会自动从传入的静态数组中提取出元素个数 N,并将其作为返回数组的维度。sizeof 只关心类型信息,因此 RtlpNumberOf 这个函数根本不需要被定义或调用,微软也确实只提供了声明。
这样做最大的好处是编译时安全。如果你错误地将一个指针传给 ARRAYSIZE,编译器无法匹配到接受指针类型的 RtlpNumberOf 模板,于是直接报错,问题在编译阶段就被暴露出来。

我们完全可以模仿这个思路,自己实现一个同样效果的宏:
#include <stdio.h>
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
template <typename T, size_t N>
char (*NumberOfArray(T (&)[N]))[N];
#define ARRAYSIZE(A) (sizeof(*NumberOfArray(A)))
int main(int argc, char* argv[])
{
printf("%llu\n", ARRAYSIZE(arr));
return 0;
}
通过 C++ Insights 工具,我们可以看到编译器成功实例化了模板,并提取出数组大小 10。

Linux 内核的实现:ARRAY_SIZE
在 Linux 世界中,同样强调编译时检查。Linux 内核中的 ARRAY_SIZE 宏在传统实现的基础上,增加了一个编译时检查项 __must_be_array(arr)。

这个 __must_be_array 宏是如何工作的呢?它的定义如下:

它的逻辑是:
- 使用
__same_type(a, &(a)[0]) 判断 a 和 &(a)[0] 的类型是否相同。
&(a)[0] 是取数组首个元素的地址,其类型是指针。
- 如果
a 是数组,它的类型是数组类型,与指针类型不同,__same_type 返回 0,BUILD_BUG_ON_ZERO(0) 在编译时无事发生,并展开为 0。
- 如果
a 本身就是指针,那么 a 和 &(a)[0] 类型相同,__same_type 返回 1,导致 BUILD_BUG_ON_ZERO(1) 触发编译错误。
__same_type 宏利用 GCC 的内建函数 __builtin_types_compatible_p 来判断两个类型是否一致。

整个 ARRAY_SIZE 宏的设计哲学非常“Linux”:尽可能在编译时发现问题,而不是留到运行时。它巧妙地利用类型系统和编译器内置功能,实现了简洁而强大的安全数组操作。
另一种 C++ 模板方案
除了利用 sizeof 和函数指针返回值类型,我们还可以更直接地使用 C++ 模板函数来获取数组大小,核心同样是利用编译时的模板实例化。
#include <Windows.h>
#include <stdio.h>
int arr1[5] = { 1, 2, 3, 4, 5 };
POINT arr2[3];
template<typename T, size_t N>
constexpr size_t ArraySize(T (&arr)[N])
{
return N;
}
int main(int argc, char* argv[])
{
printf("%llu\n", ArraySize(arr1));
printf("%llu\n", ArraySize(arr2));
return 0;
}
函数 ArraySize 接受一个静态数组的引用,并直接返回其编译时已知的维度 N。使用 C++ Insights 可以看到,编译器为不同类型的数组分别实例化了该模板。

总结
从 Windows 精巧的模板函数指针戏法,到 Linux 内核严肃的类型检查,再到简洁明了的模板函数,这些实现都指向同一个目标:在编译阶段确保操作对象是真正的静态数组,从而安全、准确地获取其大小。
它们不仅仅是“语法糖”,更体现了稳健的编程思想。在 C/C++ 这种赋予开发者极大自由同时也伴随着风险的语言中,善用这些编译时检查工具,能有效提升代码的鲁棒性,将许多潜在错误扼杀在摇篮里。希望本文的解析能帮助你理解这些看似“魔法”背后的原理,并在实际开发中加以运用。如果你想了解更多底层编程技巧,欢迎到 云栈社区 与其他开发者交流探讨。