C中的指针实现细节

时间:2009-08-29 21:59:10

标签: c pointers sizeof virtual-memory

我想知道架构违反了我在下面列出的假设。此外,我想知道所有架构的假设是否都是错误的(也就是说,如果它们中的任何一个完全错误的话)。

  1. sizeof(int *)== sizeof(char *)== sizeof(void *)== sizeof(func_ptr *)

  2. 无论指向何种数据类型,给定体系结构的所有指针的内存中表示都是相同的。

  3. 指针的内存中表示与与体系结构相同位长的整数相同。

  4. 编译器仅禁止指针数据类型的乘法和除法。注意:是的,我知道这是荒谬的。我的意思是 - 是否有硬件支持禁止这种错误使用?

  5. 所有指针值都可以转换为单个整数。换句话说,哪些架构仍然使用段和偏移?

  6. 递增指针相当于将sizeof(the pointed data type)添加到指针存储的内存地址。如果pint32*,那么p+1等于p之后4个字节的内存地址。

  7. 我最习惯在连续的虚拟内存空间中使用指针。对于这种用法,我通常可以将它们视为数字线上的地址。请参阅Stack Overflow问题 Pointer comparison

11 个答案:

答案 0 :(得分:10)

我不能给你所有这些的具体例子,但我会尽我所能。

sizeof(int *) == sizeof(char *) == sizeof(void *) == sizeof(func_ptr *)

我不知道任何系统我知道这是假的,但请考虑:

移动设备通常具有一定量的只读存储器,其中存储有程序代码等。可以想象,只读值(常量变量)可以存储在只读存储器中。并且由于ROM地址空间可能小于普通RAM地址空间,因此指针大小也可能不同。同样,指向函数的指针可能具有不同的大小,因为它们可能指向加载程序的只读存储器,否则无法修改(因此您的数据不能存储在其中)。

所以我不知道我观察到的任何平台上面都没有,但我可以想象系统可能就是这种情况。

  

无论指向何种数据类型,给定体系结构的所有指针的内存中表示都是相同的。

考虑成员指针与常规指针。它们没有相同的表示(或大小)。成员指针由this指针和偏移量组成。

如上所述,可以想象一些CPU会将常量数据加载到一个单独的内存区域,该区域使用单独的指针格式。

  

指针的内存中表示与与体系结构相同位长的整数相同。

取决于如何定义位长度。 :) 许多64位平台上的int仍然是32位。但指针是64位。 如前所述,具有分段内存模型的CPU将具有由一对数字组成的指针。同样,成员指针由一对数字组成。

  

编译器仅禁止指针数据类型的乘法和除法。

最终,指针数据类型仅在编译器中存在。 CPU使用的不是指针,而是整数和内存地址。因此,没有其他地方可以禁止指针类型上的这些操作。您可能还要求CPU禁止连接C ++字符串对象。它不能这样做,因为C ++字符串类型仅存在于C ++语言中,而不存在于生成的机器代码中。

但是,要回答您的意思,请查看Motorola 68000 CPU。我相信它们有整数和内存地址的独立寄存器。这意味着他们可以轻易地禁止这种无意义的操作。

  

所有指针值都可以转换为单个整数。

你在那里安全。无论内存空间布局,CPU架构还是其他任何东西,C和C ++标准都保证始终可以实现这一点。具体来说,它们保证了实现定义的映射。换句话说,您始终可以将指针转换为整数,然后将该整数转换回原始指针。但是C / C ++语言没有说明中间整数值应该是什么。这取决于单个编译器及其目标硬件。

  

增加指针相当于将sizeof(指向的数据类型)添加到指针存储的内存地址。

同样,这是有保证的。如果从概念上考虑,指针不指向地址,它指向对象,那么这是完全合理的。然后向指针添加一个显然会指向 next 对象。如果一个对象长度为20个字节,那么递增指针将移动它20个字节,以便它移动到下一个对象

如果一个指针只是一个线性地址空间中的内存地址,如果它基本上是一个整数,那么递增它会给地址加1 - 也就是说,它会移动到下一个字节

最后,正如我在对您的问题的评论中提到的那样,请记住C ++只是一种语言。它并不关心编译它的架构。在现代CPU中,许多这些限制似乎都很模糊。但是,如果你的目标是过去的CPU,那该怎么办?如果您的目标是下一个十年的CPU,该怎么办?你甚至不知道它们是如何工作的,所以你不能对它们做太多假设。如果您的目标是虚拟机怎么办?编译器已经存在,它为Flash生成字节码,准备从网站运行。如果要将C ++编译为Python源代码,该怎么办?

遵守标准中指定的规则可确保您的代码可以在所有这些情况下使用。

答案 1 :(得分:8)

我没有特定的现实世界的例子,但“权威”是C标准。如果标准不需要某些内容,您可以构建符合要求的实现,故意不遵守任何其他假设。这些假设中的一些在大多数情况下都是正确的,因为将指针实现为表示可以由处理器直接获取的存储器地址的整数是方便的,但这只是“方便”的结果而不能保持为一个普遍的事实。

  1. 标准(see this question)不要求。例如,sizeof(int*)可能不等于size(double*)void*保证能够存储任何指针值。
  2. 标准不要求。根据定义,大小是表示的一部分。如果大小可能不同,表示也可能不同。
  3. 不一定。事实上,“架构的位长”是一个模糊的陈述。什么是64位处理器,真的吗?是地址总线吗?寄存器的大小?数据总线?什么?
  4. “乘以”或“除”指针是没有意义的。它被编译器禁止,但你当然可以乘以或除去底层表示(这对我来说没有意义),这会导致未定义的行为。
  5. 也许我不明白你的观点,但数字电脑里的一切只是某种二进制数字。
  6. 是;的种类。它保证指向一个sizeof(pointer_type)更远的位置。它不一定等同于数字的算术加法(即更远这里是一个逻辑概念。实际表示是特定于体系结构的)

答案 2 :(得分:7)

对于6:指针不一定是存储器地址。例如,请参阅Stack Overflow用户The Great Pointer Conspiracy的“jalf”:

  

是的,我在上面的评论中使用了“地址”一词。重要的是要意识到我的意思。我不是指“数据物理存储的内存地址”,而只是一个抽象的“我们需要的任何东西来定位值。我的地址可能是任何东西,但一旦我们拥有它,我们总能找到并修改我。“

  

指针不是内存地址!我在上面提到了这一点,但让我们再说一遍。指针通常由编译器实现为内存地址,是的,但它们不一定是。“

答案 3 :(得分:6)

有关C99标准指针的更多信息:

  • 6.2.5§27保证void*char*具有相同的表示,即它们可以互换地使用而不进行转换,即相同的地址由相同的位模式表示(不是必须对其他指针类型都是真的)
  • 6.3.2.3§1规定任何指向不完整或对象类型的指针都可以转换为(和来自)void*并再次返回并仍然有效;这不包括函数指针!
  • 6.3.2.3§6规定void*可以转换为(和)整数,7.18.1.4§1提供适当的类型intptr_tuintptr_t;问题:这些类型是可选的 - 标准明确提到不需要一个足够大的整数类型来实际保存指针的值!

答案 4 :(得分:3)

sizeof(char*) != sizeof(void(*)(void)? - 在36位寻址模式下不在x86上(自Pentium 1以来几乎支持每个Intel CPU)

“指针的内存中表示与相同位长的整数相同” - 在任何现代架构中都没有内存表示;标记的内存从未流行过,在C标准化之前已经过时了。事实上,内存甚至不包含整数,只有位和可以说是字(不是字节;大多数物理内存不允许你只读8位。)

“指针的乘法是不可能的” - 68000系列;地址寄存器(持有指针的寄存器)不支持IIRC。

“所有指针都可以转换为整数” - 不在PIC上。

“递增T *相当于将sizeof(T)添加到内存地址” - 按定义为true。也相当于&pointer[1]

答案 5 :(得分:2)

  

指针的内存中表示与与体系结构相同位长的整数相同。

我认为这个假设是错误的,因为在80186上,例如,32位指针保存在两个寄存器(一个偏移寄存器和一个段寄存器)中,并且哪个半字进入访问期间哪个寄存器很重要。

  

编译器仅禁止指针数据类型的乘法和除法。

您无法对类型进行乘法或除法。 ; P

我不确定你为什么想要乘以或除以指针。

  

所有指针值都可以转换为单个整数。换句话说,哪些架构仍然使用分段和偏移?

C99标准允许指针存储在intptr_t中,这是一个整数类型。所以,是的。

  

递增指针等效于将sizeof(指向的数据类型)添加到指针存储的内存地址。如果p是int32 *,那么p + 1等于p。

之后的4个字节的存储器地址
{p> x + y其中xT *,而y是一个整数,就我所知,与(T *)((intptr_t)x + y * sizeof(T))是等价的。对齐可能是一个问题,但可以在sizeof中提供填充。我不太确定。

答案 6 :(得分:2)

我不了解其他人,但对于DOS,#3中的假设是不真实的。 DOS为16位,使用各种技巧来映射超过16位的内存。

答案 7 :(得分:2)

一般来说,所有问题的答案都是“”,这是因为只有那些实现流行语言的机器直接看到了光明,并持续到了本世纪。尽管语言标准保留改变这些“不变量”或断言的权利,但它在实际产品中从未发生过,可能的例外情况是第3项和第4项,这些项目需要进行一些重述才能普遍存在。

建立分段MMU设计当然是可能的,这些设计大致与过去几年在学术上流行的基于功能的架构相对应,但是没有这样的系统通常可以使用这些功能。这样的系统可能与断言冲突,因为它可能有大指针。

除了通常具有大指针的分段/能力MMU之外,更极端的设计试图在指针中编码数据类型。其中很少有人建造过。 (这个问题提出了基本的单词导向的所有替代方案,指针是一种单词架构。)

具体做法是:

  1. 无论指向何种数据类型,给定体系结构的所有指针的内存中表示都是相同的。 是真的,除了极其古怪的过去设计试图实现保护,不是强类型语言,而是硬件。
  2. 指针的内存中表示与与体系结构相同位长的整数相同。 也许,肯定某种整数类型是相同的,请参阅 LP64 vs LLP64
  3. 编译器仅禁止指针数据类型的乘法和除法。的 即可。
  4. 所有指针值都可以转换为单个整数。换句话说,哪些架构仍然使用分段和偏移? 今天没有使用细分和偏移,但C int通常不够大,您可能需要longlong long来保持指针。
  5. 递增指针等效于将sizeof(指向的数据类型)添加到指针存储的内存地址。如果p是int32 *则p + 1等于p后4个字节的存储器地址。的
  6. 值得注意的是,每个英特尔架构CPU,即每一个PeeCee,都包含一个复杂的史诗,传奇,复杂的精细分割单元。但是,它被有效禁用。每当PC OS启动时,它会将段基数设置为0,段长度设置为〜0,使段无效并给出平坦的内存模型。

答案 8 :(得分:2)

在20世纪50年代,60年代和70年代,有许多“字处理”架构。但我不记得任何有C编译器的主流示例。我记得20世纪80年代的ICL / Three Rivers PERQ machines是字,并且有一个可写的控制存储(微代码)。其中一个实例具有C编译器和称为PNX的Unix风格,但C编译器需要特殊的微代码。

基本问题是单词寻址机器上的char *类型很笨拙,但是你实现它们。您经常使用sizeof(int *) != sizeof(char *) ...

有趣的是,在C之前有一种名为BCPL的语言,其中基本指针类型是一个字地址;也就是说,递增指针会为您提供下一个单词的地址,而ptr!1会在ptr + 1处为您提供单词。如果我记得,有一个不同的运算符用于寻址字节:ptr%42

答案 9 :(得分:0)

编辑:当你的血糖低时不要回答问题。你的大脑(当然,我的)不像你期望的那样工作。 :-(

次要挑剔:

p是int32 *然后是p + 1

是错误的,它需要是无符号的int32,否则它将换行为2GB。

有趣的奇怪 - 我从Transputer芯片的C编译器的作者那里得到了这个 - 他告诉我,对于那个编译器,NULL被定义为-2GB。为什么?因为Transputer具有签名的地址范围:-2GB到+ 2GB。你能相信吗?太棒了不是吗?

我遇到过各种各样的人,他们告诉我这样定义NULL就是坏了。我同意,但如果你不这样做,你最终会在地址范围的中间指出NULL指针。

我想我们大多数人都很高兴我们不会在Transputers工作!

答案 10 :(得分:0)

  

我想知道违反我所假设的架构   如下所列。

我看到Stephen C提到了PERQ机器,而MSalters提到了68000和PIC。

我很失望没有其他人真正通过命名任何具有符合标准的C编译器的奇怪和精彩架构来回答这个问题,这些编译器不符合某些不合理的假设。

  

sizeof(int *)== sizeof(char *)== sizeof(void *)== sizeof(func_ptr   *)?

不一定。一些例子:

哈佛架构8位处理器的大多数编译器--PIC和8051和M8C - 使sizeof(int *)== sizeof(char *), 但与sizeof(func_ptr *)不同。

这些系列中的一些非常小的芯片具有256字节的RAM(或更少)但是几千字节的PROGMEM(闪存或ROM),因此编译器通常使sizeof(int *)== sizeof(char *)等于1(单个8位字节),但sizeof(func_ptr *)等于2(两个8位字节)。

对于那些具有几千字节RAM和128左右的PROGMEM的家庭中的许多较大芯片的编译器,使sizeof(int *)== sizeof(char *)等于2(两个8位字节),但sizeof(func_ptr *)等于3(三个8位字节)。

一些哈佛架构芯片可以存储完整的2 ^ 16(" 64KByte")PROGMEM(闪存或ROM),另外2 ^ 16(" 64KByte") RAM +内存映射I / O. 这种芯片的编译器使sizeof(func_ptr *)始终为2(两个字节); 但经常有办法让其他类型的指针sizeof(int *)== sizeof(char *)== sizeof(void *)成为一个" long ptr" 3-byte generic pointer具有额外的魔术位,指示该指针是指向RAM还是PROGMEM。 (这是当你从许多不同的子程序中调用该函数时需要传递给" print_text_to_the_LCD()"函数的那种指针,有时使用缓冲区中的变量字符串的地址在RAM中的任何地方,有时可以在PROGMEM中的任何位置使用许多常量字符串之一)。 这些编译器通常有特殊的关键字("短"或"近","长"或"远")让程序员明确指出三种不同的同一程序中的各种char指针 - 常量字符串只需要2个字节来指示它们位于PROGMEM中的位置,非常量字符串只需要2个字节来指示它们位于RAM中的位置,以及3-的类型字节指针" print_text_to_the_LCD()"接受。

大多数建于20世纪50年代和60年代的计算机使用36-bit word length18-bit word length,使用18位(或更少)地址总线。 我听说这类计算机的C编译器经常使用9-bit bytes, sizeof(int *)== sizeof(func_ptr *)= 2,它给出18位,因为所有整数和函数必须是字对齐的;但是sizeof(char *)== sizeof(void *)== 4以利用special PDP-10 instructions将这些指针存储在一个完整的36位字中。 该完整的36位字包括一个18位字地址,以及其他18位中的一些位,以及(除其他外)指示该字中指向字符的位位置。

  

给定体系结构的所有指针的内存中表示   无论指向哪种数据类型都是一样的?

不一定。一些例子:

在我上面提到的任何一种架构中,指针都有不同的大小。那么他们怎么可能有相同的"代表性?

某些系统上的某些编译器使用"descriptors"来实现字符指针和其他类型的指针。 对于指向第一个" char"的指针,这样的描述符是不同的。在" char big_array[4000]"比指针指向第一个" char"在" char small_array[10]"中,它们可以说是不同的数据类型,即使小数组恰好在以前由大数组占用的内存中的相同位置开始。 描述符允许这些机器捕获并捕获导致其他机器出现此类问题的缓冲区溢出。

SAFElite中使用的"Low-Fat Pointers"和类似的"软处理器"有类似的"额外信息"关于指针指向的缓冲区的大小。低胖指针具有捕获和捕获缓冲区溢出的相同优点。

  

指针的内存中表示与整数形式相同   与架构相同的位长度?

不一定。一些例子:

"tagged architecture"机器中,每个存储器字都有一些位,用于指示该字是整数,还是指针,还是其他东西。 使用这样的机器,查看标记位会告诉您该单词是整数还是指针。

我听说Nova微型计算机的每个单词都有"indirection bit",这启发了"indirect threaded code"。这听起来像存储一个整数清除该位,而存储指针则设置该位。

  

仅禁止指针数据类型的乘法和除法   由编译器。注意:是的,我知道这是荒谬的。我的意思是    - 是否有硬件支持禁止这种错误使用?

是的,某些硬件并不直接支持此类操作。

正如其他人已经提到的那样,"乘以" 68000和6809中的指令仅适用于(某些)"数据寄存器&#34 ;;它们不能直接应用于地址寄存器中的值"。 (编译器很容易解决这些限制 - 将这些值从地址寄存器MOV转移到适当的数据寄存器,然后使用MUL)。

  

所有指针值都可以转换为单个数据类型吗?

为了memcpy() to work right,C标准规定每种指针值都可以转换为void指针(" void *")。

即使对于仍然使用段和偏移的架构,编译器也需要这样做。

  

所有指针值都可以转换为单个整数?换一种说法,   什么架构仍然使用分段和偏移?

我不确定。

我怀疑所有指针值都可以强制转换为&#34; size_t&#34;和&#34; ptrdiff_t&#34;在&#34; <stddef.h>&#34;。

中定义的整数数据类型
  

增加指针相当于添加sizeof(指向的数据)   type)指针所存储的内存地址。如果p是int32 *   然后p + 1等于p。

之后的4个字节的存储器地址

目前还不清楚你在这里问的是什么。

问:如果我有某种结构或原始数据类型的数组(例如,&#34; #include <stdint.h> ... int32_t example_array[1000]; ...&#34;),我会增加指向该数组的指针(例如,&#34; int32_t p =&amp; example_array [99]; ... p ++; ...&#34;),指针现在指向该数组的下一个连续成员,即sizeof(指向的数据类型)字节在内存中进一步?

答:是的,编译器必须使指针在递增一次后,指向数组中的下一个独立连续int32_t,sizeof(指向数据类型)字节进一步沿着内存,以符合标准。

问:那么,如果p是int32 *,那么p + 1等于p后的4个字节的内存地址

答:当sizeof(int32_t)实际上等于4时,是的。否则,例如对于某些可单词寻址的机器,包括一些现代DSP,其中sizeof(int32_t)可能等于2或甚至1,则p + 1等于存储器地址2或甚至1&#34; C字节&#34;在p。

之后

问:所以如果我拿指针,把它投入&#34; int&#34; ...

答:一种类型的&#34;全世界都是VAX异端&#34;。

问:...然后施放&#34; int&#34;回到指针......

答:另一种类型的&#34;全世界都是VAX异端&#34;。

问:所以如果我把指针p作为指向int32_t的指针,并将其转换为足够大的包含指针的整数类型,然后将sizeof( int32_t )添加到该整数类型,然后将该整数类型转换回指针 - 当我完成所有操作时,结果指针等于p + 1?

不一定。

许多DSP和其他一些现代芯片都采用面向字的寻址,而不是8位芯片使用的面向字节的处理。

这些芯片的一些C编译器会在每个单词中加入2个字符,但是需要2个这样的单词来保存int32_t - 所以他们报告sizeof( int32_t )是4。 (我已经听说有传言说24-bit摩托罗拉56000的C编译器可以做到这一点。

编译器需要安排诸如做&#34; p ++&#34;使用指向int32_t的指针将指针递增到下一个int32_t值。 编译器有几种方法可以做到这一点。

一种符合标准的方法是将每个指针存储到int32_t作为&#34;本机字地址&#34;。 因为保存单个int32_t值需要2个字,所以C编译器编译&#34; int32_t * p; ... p++&#34;转换为某种汇编语言,将指针值增加2。 另一方面,如果那个&#34; int32_t * p; ... int x = (int)p; x += sizeof( int32_t ); p = (int32_t *)x;&#34;,56000的C编译器可能会将其编译为汇编语言,使指针值增加4。

  

我最习惯在连续的虚拟内存中使用指针   空间。

多个PIC和8086以及其他系统都有非连续的RAM - 地址上的几块RAM,使硬件更简单&#34;。 使用内存映射I / O或者根本没有附加到这些块之间的地址空间中的间隙。

它比听起来更尴尬。

在某些情况下 - 例如使用bit-banding hardware来避免read-modify-write引起的问题 - 可以使用2个或更多不同的地址读取或写入RAM中的完全相同的位。