找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1773

积分

0

好友

237

主题
发表于 13 小时前 | 查看: 0| 回复: 0

在 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])

Windows头文件中ARRAYSIZE等宏定义的代码截图

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

C++环境下ARRAYSIZE宏的复杂定义代码截图

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

RtlpNumberOf模板函数指针声明的代码截图

首先,这是一个函数指针,指向一个返回类型为 char [N] 数组的函数。

char (*RtlpNumberOf(...))[N];

其次,这个函数的参数是一个引用,指向一个含有 NT 类型元素的数组。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 模板,于是直接报错,问题在编译阶段就被暴露出来。

VS编译器提示指针无法匹配ARRAYSIZE宏的错误截图

我们完全可以模仿这个思路,自己实现一个同样效果的宏:

#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

C++ Insights展示模板实例化后提取数组大小为10的截图

Linux 内核的实现:ARRAY_SIZE

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

Linux内核头文件中ARRAY_SIZE宏定义的代码截图

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

Linux内核中__must_be_array宏定义的代码截图

它的逻辑是:

  1. 使用 __same_type(a, &(a)[0]) 判断 a&(a)[0] 的类型是否相同。
  2. &(a)[0] 是取数组首个元素的地址,其类型是指针。
  3. 如果 a 是数组,它的类型是数组类型,与指针类型不同,__same_type 返回 0BUILD_BUG_ON_ZERO(0) 在编译时无事发生,并展开为 0
  4. 如果 a 本身就是指针,那么 a&(a)[0] 类型相同,__same_type 返回 1,导致 BUILD_BUG_ON_ZERO(1) 触发编译错误。

__same_type 宏利用 GCC 的内建函数 __builtin_types_compatible_p 来判断两个类型是否一致。

Linux内核中__same_type宏定义的代码截图

整个 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 可以看到,编译器为不同类型的数组分别实例化了该模板。

C++ Insights展示ArraySize模板函数被多次实例化的截图

总结

从 Windows 精巧的模板函数指针戏法,到 Linux 内核严肃的类型检查,再到简洁明了的模板函数,这些实现都指向同一个目标:在编译阶段确保操作对象是真正的静态数组,从而安全、准确地获取其大小

它们不仅仅是“语法糖”,更体现了稳健的编程思想。在 C/C++ 这种赋予开发者极大自由同时也伴随着风险的语言中,善用这些编译时检查工具,能有效提升代码的鲁棒性,将许多潜在错误扼杀在摇篮里。希望本文的解析能帮助你理解这些看似“魔法”背后的原理,并在实际开发中加以运用。如果你想了解更多底层编程技巧,欢迎到 云栈社区 与其他开发者交流探讨。




上一篇:Windows显示缩放500%为何致系统崩溃?技术解析与修复指南
下一篇:Windows 11/12系统优化:提升效率的5个原生功能设置指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-3 23:16 , Processed in 1.547501 second(s), 46 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表