size_t与uintptr_t的区别

271

C标准保证size_t是一种可以容纳任何数组索引的类型。这意味着,从逻辑上讲,size_t应该能够容纳任何指针类型。我在谷歌上找到了一些网站,上面写着这是合法的,或者应该总是起作用:

void *v = malloc(10);
size_t s = (size_t) v;

因此,在C99中,标准引入了intptr_tuintptr_t类型,它们是有符号和无符号的类型,保证可以容纳指针:

uintptr_t p = (size_t) v;
那么,使用 size_tuintptr_t 有什么区别?两者都是无符号类型,都应该能够容纳任何指针类型,因此它们在功能上似乎相同。除了清晰度之外,使用 uintptr_t (或者最好是 void *) 而不是 size_t 是否有任何真正的强制性理由?在一个不透明的结构中,如果该字段只会被内部函数处理,那么不这样做是否有任何原因呢?
同样,ptrdiff_t 是一种有符号类型,可容纳指针之间的差异,因此可以容纳大多数指针,那么它与 intptr_t 有何不同?
所有这些类型岂不是基本上提供了同一功能的微不足道的不同版本吗?如果不是,为什么?我能用其中一个做不了什么我不能用另一个做的事情吗?如果是,为什么 C99 在语言中添加了两个基本上是多余的类型?
我愿意忽略函数指针,因为它们与当前问题无关,但请随意提到它们,因为我有一种隐约的感觉它们将是“正确”答案的核心。
8个回答

259

size_t是一种可容纳任何数组索引的类型。这意味着,在逻辑上,size_t应该能够容纳任何指针类型。

但不一定!例如回顾一下16位分段体系结构的时代:一个数组可能被限制在单个段中(因此16位的size_t就可以胜任),但你可能有多个段(因此需要32位的intptr_t类型来选择段以及其中的偏移量)。我知道在这些统一寻址无分段的架构中,这些事情听起来很奇怪,但标准必须考虑比“2009年正常情况”更广泛的情况,你知道的!-)


7
许多人都得出了类似的结论,这就解释了size_tuintptr_t之间的区别。但是ptrdiff_tintptr_t呢?几乎在任何平台上,这两者都能存储相同范围的值,为什么还需要有有符号和无符号的指针大小整数类型,特别是如果ptrdiff_t已经用作有符号指针大小整数类型的话。 - Chris Lutz
9
关键词是“几乎在任何平台上”,@Chris。实现可以将指针限制在0xf000-0xffff范围内 - 这需要一个16位的intptr_t,但仅需要一个12/13位的ptrdiff_t。 - paxdiablo
35
@Chris,只有在同一数组内部的指针差才能被定义良好。因此,在完全相同的分段 16 位架构中(数组必须存在于单个段中,但两个不同的数组可以存在于不同的段中),指针必须为4个字节,但指针之间的差异可能为2个字节! - Alex Martelli
6
除了指针之间的差值可以是正数或负数以外,标准要求size_t至少为16位,但ptrdiff_t至少为17位(实际上意味着它可能至少为32位)。 - Keith Thompson
4
不用担心分段架构,那现代架构x86-64呢?早期实现这个架构时只能寻址48位空间,但指针本身是一个64位数据类型。你可以合理地访问的最大连续内存块大小为48位,所以我想SIZE_MAX不应该是2的64次方。需要注意的是,这里采用的是平面地址寻址方式;即没有必要使用分段来使SIZE_MAX与数据指针范围不匹配。 - Andon M. Coleman
显示剩余8条评论

94
关于您的声明:
“C标准保证size_t是一个可以容纳任何数组索引的类型。这意味着,从逻辑上讲,size_t应该能够容纳任何指针类型。”
很遗憾,这是不正确的。指针和数组索引并不相同。完全可以想象一种符合规范的实现,将数组限制为65536个元素,但允许指针寻址到一个巨大的128位地址空间中的任何值。
C99规定了size_t变量的上限由SIZE_MAX定义,这个值可能低至65535(参见C99 TR3,7.18.3,在C11中未改变)。如果在现代系统中将指针限制在这个范围内,它们将非常有限。
实际上,您可能会发现您的假设成立,但这并不是因为标准保证了它。因为它实际上并没有保证。

与其在Alex Martelli的评论中重新输入我所说的话,我只想说感谢您的澄清,但是重申我的问题的后半部分(即ptrdiff_tintptr_t的区别)。 - Chris Lutz
7
@Ivan,像大多数沟通一样,需要对某些基本项目有共同的理解。如果你认为我的回答是在“开玩笑”,我向你保证这是对我的意图的误解。假设你指的是我的“逻辑谬误”评论(我看不出其他可能性),那是一种客观陈述,而不是针对 OP 的攻击性陈述。如果你想建议一些具体的改进来最小化误解的可能性(而不仅仅是一般的抱怨),我很乐意考虑。 - paxdiablo
2
@Ivan,我对你提出的修改并不满意,已经回滚并尝试删除任何无意冒犯之处。如果你有其他修改建议,我建议我们开始聊天讨论一下。 - paxdiablo
1
@paxdiablo 好吧,我猜“这实际上是一种谬论”听起来不那么居高临下。 - ivan_pozdeev

44

就切片限制、奇特架构等方面的推理,我会让其他答案自行表述。

仅仅因为名称不同,难道不足以充当使用适当类型的理由吗?

如果你要存储一个大小,就使用 size_t。如果你要存储一个指针,就使用 intptr_t。读取你代码的人会立刻知道 "啊哈,这是某个东西的大小,大概是以字节为单位",以及 "哦,这里是将指针值存储为整数的原因"。

否则,你可以把 unsigned long(或者在现代时代,unsigned long long)用于所有东西。大小并非万能,类型名称具有含义,有助于描述程序。


我同意,但是我在考虑一些hack/trick(当然我会清楚地记录)的事情,涉及将指针类型存储在size_t字段中。 - Chris Lutz
1
@MarkAdler 标准并不要求指针完全可表示为整数:任何指针类型都可以转换为整数类型。除非另有规定,否则结果是实现定义的。如果结果无法表示为整数类型,则行为是未定义的。结果不必在任何整数类型的值范围内。 因此,只有 void*intptr_tuintptr_t 保证能够表示指向数据的任何指针。 - Andrew Svietlichnyy
这是过于天真的想法。例如,当您需要对齐通用结构字段时,size_t与指针可能是错误的选择。您需要使用uintptr_t,因为只有它能保证相同的对齐和偏移量。 - rurban

14

最大数组的大小可能比指针还要小。例如,在分段架构中,指针可能是32位,但单个段可能只能寻址64KB(例如旧的实模式8086架构)。

虽然这些架构在桌面计算机上已不常使用,但C标准旨在支持甚至小型的专用架构。例如,仍在开发使用8或16位CPU的嵌入式系统。


但是您可以像数组一样索引指针,那么 size_t 是否也能够处理它呢?或者远处某个段中的动态数组仍然只能在其段内进行索引? - Chris Lutz
索引指针在技术上仅支持它们所指向的数组大小 - 因此,如果一个数组被限制为64KB大小,那么指针算术运算只需要支持这个大小。然而,MS-DOS编译器支持“巨大”内存模型,其中远指针(32位分段指针)被操作以便它们可以将整个内存作为单个数组进行寻址 - 但是在幕后对指针执行的算术运算相当丑陋 - 当偏移量超过16(或其他值)时,偏移量会回到0并增加段部分。 - Michael Burr
8
阅读http://en.wikipedia.org/wiki/C_memory_model#Memory_segmentation,为那些为我们自由而牺牲的MS-DOS程序员感到悲哀。 - Justicle
更糟糕的是,stdlib函数没有注意到HUGE关键词。16位MS-C适用于所有'str'函数,而Borland甚至适用于'mem'函数(memset,memcpy,memmove)。这意味着当偏移量溢出时,您可能会覆盖部分内存,这在我们的嵌入式平台上进行调试非常有趣。 - Patrick Schlüter
@Justicle:8086分段式架构在C语言中支持不太好,但我不知道还有哪种架构比它更高效,尤其是当1MB的地址空间足够使用而64K的地址空间则不够时。一些现代JVM实际上使用了类似x86实模式的寻址方式,通过将32位对象引用左移3位来生成32GB地址空间中的对象基地址。 - supercat

6
我认为(对于所有类型名称),使用更能传达你在代码中的意图。
例如,即使在Windows上unsigned shortwchar_t大小相同(我想是这样的),但使用wchar_t而不是unsigned short表明你将使用它来存储宽字符,而不仅仅是一些任意数字。

但这里有一个区别-在我的系统上,wchar_tunsigned short大得多,所以将一个用于另一个将是错误的,并且会带来严重(而现代)的可移植性问题,而size_tuintptr_t之间的可移植性问题似乎存在于1980年代的遥远土地上(对日期的随意猜测)。 - Chris Lutz
很好!但是,size_tuintptr_t在它们的名称中仍然有暗示的用途。 - dreamlax
他们确实这样做了,我想知道背后是否有除了简洁明了之外的动机。结果证明确实有。 - Chris Lutz

4

回顾过去和展望未来,我们可以发现各种奇特的架构散布在整个计算机领域,我相信他们试图将所有现有系统包含在内,并为所有可能的未来系统提供支持。

当然,事情的结果是,迄今为止我们并不需要太多类型。

但即使在LP64这种常见的范例中,系统调用接口仍需要size_t和ssize_t。可以想象一个更受限制的遗留或未来系统,在其中使用完整的64位类型非常昂贵,他们可能希望放弃大于4GB的I/O操作,但仍然具有64位指针。

我认为你必须思考:可能会开发出什么样的技术,未来会出现什么。 (也许是128位分布式系统互联网广域指针,但系统调用不超过64位,或者甚至是“遗留”32位限制。 :-) 想象一下遗留系统可能会获得新的C编译器……

此外,看看那时存在的东西。除了无数的286实模式内存模型之外,还有CDC 60位字/18位指针大型机? Cray系列呢?别管正常的ILP64、LP64、LLP64。 (我总是觉得微软使用LLP64很炫耀,应该是P64。)我可以想象一个委员会试图涵盖所有方面……


2

size_tuintptr_t

除了其他好的回答之外:

size_t在以下文件中定义:<stddef.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h>, <uchar.h>, <wchar.h>。它至少是16位。

uintptr_t<stdint.h>中定义。它是可选的。合规库可能不定义它,很可能是因为没有足够宽的整数类型来往返一个void*-uintptr_t-void *

两者都是无符号整数类型。

注:可选的伴侣intptr_t是有符号整数类型。


-11
int main(){
  int a[4]={0,1,5,3};
  int a0 = a[0];
  int a1 = *(a+1);
  int a2 = *(2+a);
  int a3 = 3[a];
  return a2;
}

暗示intptr_t必须始终替代size_t,反之亦然。


14
这只是C语言中一种特殊的句法怪癖。数组索引按照 x[y] 等同于 *(x+y) 的方式定义,而且因为 a+3 和 3+a 在类型和值上是相同的,所以你可以使用3[a]或a[3]。 - Fred Nurk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接