C中的指针实现细节

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

  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个字节的存储器地址。

我最习惯在连续的虚拟内存空间中使用指针。 对于这种用法,我通常可以将它们视为数字线上的地址。 请参阅堆栈溢出问题指针比较

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

遵守标准中规定的规则可确保您的代码适用于所有这些情况。

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

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

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

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

和:

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

有关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 ; 问题:这些类型是可选的 – 标准明确提到不需要一个足够大的整数类型来实际保存指针的值!

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

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

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

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

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

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

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

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

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

你不能乘以或除以类型。 ,P

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

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

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

增加指针相当于将sizeof(指向的数据类型)添加到指针存储的内存地址。 如果p是int32 *则p + 1等于p后4个字节的存储器地址。

据我所知, x + y ,其中xT *y是整数,等于(T *)((intptr_t)x + y * sizeof(T)) 。 对齐可能是一个问题,但填充可以以sizeof提供。 我不太确定。

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

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

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

特别:

  1. 无论指向何种数据类型,给定体系结构的所有指针的内存中表示都是相同的。 真的除了极其古怪的过去设计,试图实现保护,不是强类型语言,而是硬件。
  2. 指针的内存中表示与与体系结构相同的位长的整数相同。 也许,肯定某种整数类型是相同的,参见 LP64 vs LLP64 。
  3. 指针数据类型的乘法和除法仅被编译器禁止。
  4. 所有指针值都可以转换为单个整数。 换句话说,哪些架构仍然使用分段和偏移? 今天没有什么可以使用段和偏移量,但是C int通常不够大,你可能需要很longlong long来保持指针。
  5. 增加指针相当于将sizeof(指向的数据类型)添加到指针存储的内存地址。 如果p是int32 *则p + 1等于p后4个字节的存储器地址。 是。

值得注意的是,每个英特尔架构CPU,即每个PeeCee,都包含一个精巧的,具有传奇色彩,复杂性的细分分割单元。 但是,它被有效禁用。 每当PC OS启动时,它会将段基数设置为0,段长度设置为〜0,使段无效并给出平坦的内存模型。

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

基本问题是字处理机器上的char *类型很难,但是你实现它们。 你常常使用sizeof(int *) != sizeof(char *)

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

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

轻微的挑剔:

p是int32 *然后是p + 1

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

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

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

我想我们大多数人都很高兴我们没有在Transputers上工作!

我想知道违反我在下面列出的假设的架构。

我看到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(a单个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字节generics指针 ,它有额外的魔术位指示该指针是否指向RAM或PROGMEM。 (当你从许多不同的子程序中调用该函数时,这是你需要传递给“print_text_to_the_LCD()”函数的那种指针,有时候缓冲区中的变量字符串的地址可能位于RAM中的任何位置,有时使用一个可以在PROGMEM中的任何位置的许多常量字符串)。 这样的编译器通常有特殊的关键字(“短”或“近”,“长”或“远”)让程序员在同一个程序中专门指出三种不同类型的char指针 – 常量字符串只需要2个字节来指示在PROGMEM中,它们位于非常量字符串中,只需要2个字节来指示它们位于RAM中的位置,以及“print_text_to_the_LCD()”接受的3字节指针的类型。

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

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

不必要。 一些例子:

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

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

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

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

不必要。 一些例子:

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

我听说Nova微型计算机在每个单词中都有一个“间接位” ,它启发了“间接线程代码” 。 这听起来像存储一个整数清除该位,而存储指针设置该位。

指针数据类型的乘法和除法仅被编译器禁止。 注意:是的,我知道这是荒谬的。 我的意思是 – 是否有硬件支持禁止这种不正确的用法?

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

正如其他人已经提到的那样,68000和6809中的“乘法”指令只与(某些)“数据寄存器”一起工作; 它们不能直接应用于“地址寄存器”中的值。 (编译器很容易解决这些限制 – 将这些值从地址寄存器MOV转移到适当的数据寄存器,然后使用MUL)。

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

是。

为了使memcpy()正常工作 ,C标准规定每种指针值都可以转换为void指针(“void *”)。

即使对于仍使用段和偏移的体系结构,编译器也需要使其工作。

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

我不确定。

我怀疑所有指针值都可以转换为“ ”中定义的“size_t”和“ptrdiff_t”整数数据类型。

增加指针相当于将sizeof(指向的数据类型)添加到指针存储的内存地址。 如果p是int32 *则p + 1等于p后4个字节的存储器地址。

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

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

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

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

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

问:所以如果我拿指针,把它投成“int”……

答:一种“全世界的VAX异端”。

问:…然后将“int”转换回指针……

答:另一种类型的“全世界的VAX异端”。

问:所以如果我把指针p作为指向int32_t的指针,并将其转换为足够大的包含指针的整数类型,然后将sizeof( int32_t )添加到该整数类型,然后再进行转换那个积分类型回到指针 – 当我做所有这些时,结果指针等于p + 1?

不必要。

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

这些芯片的一些C编译器在每个字中加入2个字符,但它需要2个这样的字来保存int32_t – 所以他们报告sizeof( int32_t )是4.(我听说有传言说有一个C编译器用于这样做的24位摩托罗拉56000)。

编译器需要安排这样的事情,即使用指向int32_t的指针执行“p ++”会将指针增加到下一个int32_t值。 编译器有几种方法可以做到这一点。

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

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

一些PIC和8086以及其他系统具有非连续的RAM – 地址上的几块RAM“使硬件更简单”。 使用内存映射I / O或根本没有附加到这些块之间的地址空间中的间隙。

它比听起来更尴尬。

在某些情况下 – 例如用于避免由读 – 修改 – 写引起的问题的位带硬件 – 可以使用2个或更多不同的地址读取或写入RAM中的完全相同的位。