17

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

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

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

  3. 指针的内存表示形式与架构相同位长的整数相同。

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

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

  6. 增加一个指针相当于增加了指针sizeof(the pointed data type)存储的内存地址。If pis an int32*thenp+1等于 4 字节后的内存地址p

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

4

11 回答 11

10

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

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

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

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

因此,我不知道在哪些平台上我观察到上述情况不成立,但我可以想象可能出现这种情况的系统。

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

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

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

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

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

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

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

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

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

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

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

同样,这是有保证的。如果从概念上考虑,指针不指向地址,而是指向对象,那么这很有意义。向指针添加一个显然会使其指向下一个对象。如果一个对象有 20 个字节长,那么增加指针会将其移动 20 个字节,以便它移动到下一个对象

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

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

遵守标准中指定的规则可以保证您的代码在所有这些情况下都能正常工作。

于 2009-08-30T17:35:17.753 回答
8

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

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

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

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

和:

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

于 2009-08-29T22:18:17.450 回答
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; 问题:这些类型是可选的——标准明确提到不需要足够大的整数类型来实际保存指针的值!
于 2009-08-29T22:46:37.580 回答
3

sizeof(char*) != sizeof(void(*)(void)? - 不适用于 36 位寻址模式的 x86(自 Pentium 1 以来几乎所有 Intel CPU 都支持)

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

“指针相乘是不可能的”——68000族;地址寄存器(持有指针的寄存器)不支持该 IIRC。

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

“增加一个 T* 相当于将 sizeof(T) 添加到内存地址” - 根据定义是真的。也相当于&pointer[1]

于 2009-09-01T10:25:39.093 回答
2

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

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

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

您不能乘以或除以类型。;P

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

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

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

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

x + y据我所知,其中xaT *y是一个整数是等价的。(T *)((intptr_t)x + y * sizeof(T))对齐可能是个问题,但sizeof. 我不太确定。

于 2009-08-29T22:11:53.173 回答
2

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

于 2009-08-29T22:16:56.797 回答
2

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

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

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

具体来说:

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

有趣的是,每一个英特尔架构 CPU,即每一个 PeeCee,都包含一个具有史诗、传奇、复杂性的精细分割单元。但是,它实际上已被禁用。每当 PC 操作系统启动时,它将段基数设置为 0,段长度设置为 ~0,将段归零并提供平坦的内存模型。

于 2009-08-29T23:07:28.960 回答
2

在 1950 年代、1960 年代和 1970 年代有很多“字地址”架构。但我想不起任何具有 C 编译器的主流示例。我记得 1980 年代的ICL / 三河 PERQ 机器是字寻址并具有可写控制存储(微码)。它的一个实例有一个 C 编译器和一种称为PNX的 Unix 风格,但 C 编译器需要特殊的微码。

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

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

于 2009-09-01T10:00:29.817 回答
1

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

小挑剔:

p 是 int32* 然后 p+1

错了,需要是unsigned int32,否则会在2GB处换行。

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

从那以后,我遇到了很多人,他们告诉我这样定义 NULL 是错误的。我同意,但如果你不这样做,你最终会导致 NULL 指针位于地址范围的中间。

我想我们大多数人都会庆幸我们没有在 Transputers 上工作!

于 2010-03-16T17:29:53.183 回答
1

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

我看到 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 字节通用指针它有一个额外的魔法位,指示该指针是指向 RAM 还是 PROGMEM。(当您从许多不同的子例程调用该函数时,您需要将这种指针传递给“print_text_to_the_LCD()”函数,有时使用缓冲区中可能位于 RAM 中任何位置的变量字符串的地址,有时使用一个可以在 PROGMEM 中的任何位置的许多常量字符串)。此类编译器通常具有特殊的关键字(“short”或“near”、“long”或“far”),以让程序员在同一程序中专门指示三种不同类型的 char 指针——常量字符串只需要 2 个字节来指示在哪里在 PROGMEM 中,它们位于非常量字符串,只需要 2 个字节来指示它们在 RAM 中的位置,

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

无论指向的数据类型如何,给定架构的所有指针的内存表示都是相同的?

不必要。一些例子:

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

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

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

指针的内存表示是否与架构相同位长的整数相同?

不必要。一些例子:

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

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

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

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

正如其他人已经提到的,68000 和 6809 中的“乘法”指令仅适用于(某些)“数据寄存器”;它们不能直接应用于“地址寄存器”中的值。(编译器很容易绕过这些限制——将这些值从地址寄存器移动到适当的数据寄存器,然后使用 MUL)。

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

是的。

为了让memcpy() 正常工作,C 标准要求每种类型的每个指针值都可以转换为 void 指针(“void *”)。

需要编译器来完成这项工作,即使对于仍然使用段和偏移量的架构也是如此。

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

我不知道。

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

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

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

问:如果我有一个某种结构或原始数据类型的数组(例如,“ #include <stdint.h> ... 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 等于内存地址 2 甚至 p 之后的 1 个“C 字节”。

问:因此,如果我将指针转换为“int”...

A:“全世界都是 VAX 异端”的一种。

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

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

问:因此,如果我将指针 p(指向 int32_t 的指针)转换为某个足够大以包含指针的整数类型,然后添加sizeof( int32_t )到该整数类型,然后再将该整数类型转换回变成一个指针——当我做所有这些时,结果指针等于 p+1?

不必要。

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

一些用于此类芯片的 C 编译器将 2 个字符塞入每个单词,但需要 2 个这样的单词来保存 int32_t ——所以他们报告说sizeof( int32_t )是 4。(我听说有一个24 位的 C 编译器这样做的摩托罗拉 56000)。

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

一种符合标准的方法是将每个指向 int32_t 的指针存储为“本机字地址”。因为保存一个 int32_t 值需要 2 个字,所以 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 中完全相同的位。

于 2015-01-21T03:08:31.230 回答