索引数组时我应该总是使用 size_t 吗?

Should I always use size_t when indexing arrays?

是否需要在索引数组时始终使用 size_t,即使数组不够大,无法超过 int 的大小?

这不是什么时候应该使用 size_t 的问题。我只想知道,例如,一个程序是否有 2GB 的可用内存(所有这些字段都可以用 int32 索引)但是这个内存(虚拟内存)分配给 "fields" 14GB - 16GB计算机的内存。

在这种情况下,如果我使用 int32 而不是 size_t(或 unsigned long int),索引内存时是否总是会失败?

也许问题更多的是关于虚拟内存而不是指针。

size_t 是一个无符号整数,它能够容纳您可以分配的最大对象的大小。它对索引很有用,因为这意味着它可以索引到您可以分配的最大数组。

这并不意味着它是必需的,甚至不一定推荐用于索引。您可以使用任何大到足以索引数组的整数类型。 int_fast32_t 可能更快,uint_least16_t 在结构中可能更小,等等。了解你的数据,你才能做出好的选择。

您应该考虑的一个因素是,在某些平台上,使用带符号的索引可能需要额外的符号扩展指令。例如,这里是 x86-64:

// ; zero-extending idx (in edx) is "free" by simply using rdx.
// movzx eax, BYTE PTR [rcx+rdx]
// ret
char get_index(char *ptr, unsigned idx)
{
   return ptr[idx];
}

// ; sign extending idx from 32 bits to 64 bits with movsx here.
// movsx rdx, edx     
// movzx eax, BYTE PTR [rcx+rdx]
// ret
char get_index(char *ptr, int idx)
{
   return ptr[idx];
}

虚拟内存在 C 或 C++ 的范围之外。从他们的角度来看,您只需对内存进行索引,让它工作取决于您的平台。实际上,您的应用仅使用虚拟地址;您的 CPU/OS 正在幕后将虚拟地址转换为物理地址。这不是您需要担心的事情。

为了避免程序失败,程序员应该始终使用至少与 size() 方法返回的类型一样大的索引类型。这确保索引永远不会溢出数组的任何可能大小。数组的实现通常确保其运行时大小永远不会溢出 size() 方法返回的类型。这意味着索引类型应该是:

  • size_tchar[N]uint8_t[N]int[N] 等情况下
  • size_tstd::vectorstd::list
  • 的情况下
  • intQListQVector
  • 的情况下
  • 位数组情况下的任意精度整数 (aint)(如果位数组的 size() 方法 returns 是 aint)
  • 如果数组在内存中压缩(如果数组的 size() 方法 returns 是 aint)
  • aint 在数组跨越多台机器的情况下(如果数组的 size() 方法 returns aint)
  • C++以外的其他语言:
    • intjava.util.Collection 及其子类的情况下

总结:安全的索引类型是size()方法返回的类型。

注意:如果 size() 方法 returns 无符号 size_t,则有符号 intssize_t 不是安全索引类型。对于 gcc 和 clang,编译器标志 -Wsign-compare(由 -Wall 启用)和 -Wconversion 可用于防止大多数此类情况。

Do I need to use size_t always when indexing an array even if the array is not big enough to exceed the size of an int?

否,因为索引不是地址,甚至不是字节偏移量。它是一个用于计算指针偏移量的值。只要整数类型足够大,可以表示最大的索引,那么你就可以访问数组中的任何元素。

也就是说,有一些关于如何在数组索引的上下文中处理不同类型的细节。这个问题用 C 和 C++ 标记,虽然有很多相似之处,但 C++ 可以发挥一些额外的作用。但是让我们从共同的部分开始吧。

为了简化解释,我们假设您有一台具有 4GB 地址 space 和 16 位 ints 的机器。

int a[8000];

显然,这声明了一个包含 8000 个 int 的数组,索引从 0 到 7999。编译器将 8000 文字视为 int,因此没有真正的惊喜.

但是如果字面意思是 0x8000,它会变得更复杂。由于 16 位 int 不能表示 32768(0x8000 的十进制等效值),因此它必须选择不同的类型,例如 long。但是,不管你信不信,如果文字是以十六进制表示的,它会选择 unsigned int 代替。 (文字规则与整数提升规则不太一样。)这里实际上并不重要,因为净效果是相同的。

请注意,如果我们使用 0x8000,我们将创建一个可以使用 16 位 int 完全 索引 的数组,但我们有使用不同的类型来表示其 size.

C 中的索引很古怪,C++ 继承了这种古怪之处。这些语言不关心您是否交换数组的索引和名称。

a[3] = 42;
printf("%d\n", 3[a]);  // legal, and prints "42"!

您甚至可以从指针索引到数组并使用负下标(只要引用仍在同一个数组内或超出其末尾的一个元素)。

a[3] = 42;
int *p = a + 6;
printf("%d\n", p[-3]);  // also legal, and prints "42"!

// off-topic
printf("%d\n", -3[p]);  // legal, but does *not* print "42"!

这告诉我们索引可以是有符号整数类型,但 size_t 始终是无符号整数类型。

以上所有内容也适用于 C++,但如果您考虑 std::vectorstd::array.

等标准库容器类型,C++ 还有一些额外的问题

标准容器定义了自己的尺寸类型,如 std::vector::size_type。这些几乎总是与 std::size_t.

相同的基础类型

标准容器只是 类 在库中定义的,所以古怪的 swap-pointer-and-index 语法不适用于它们。

std::vector<int> v = { 0, 1, 2, 3 };
std::cout << 3[v] << '\n';  // std container types cannot emulate this
                            // kind of array indexing

那么索引必须是什么类型?好吧,我们知道这样写是合法的:

std::cout << v[3] << '\n';

而且我们知道文字 3 被视为 int,它是有符号的并且可能比 std::size_t--呃,std::vector<int>::size_type 具有更小的范围.希望这清楚表明您不需要使用索引的大小类型,只要您使用的任何类型都可以表示所需的索引。

通过在容器的大小类型上重载 operator[] 来实现标准容器类型的索引。所以,3可能是一个int,但是,为了匹配重载的定义,它会先被转换成std::vector<int>::size_type.

换句话说,对于 C++ 标准容器类型,无论您实际使用什么类型,您实际上总是使用 size_t 作为索引。