最近,我遇到了以下语句:
所有指针具有相同的大小是很常见的,但从技术上讲,指针类型可以具有不同的大小。
但后来我遇到了这个,它指出:
虽然指针的大小都相同,因为它们只存储一个内存地址,但我们必须知道它们指向什么样的东西。
现在,我不确定上述陈述中的哪一个是正确的。第二个引用的陈述看起来像是来自佛罗里达州立大学计算机科学的C++笔记。
这就是为什么,在我看来,所有指针都应该具有相同的大小:
1)假设我们有:
int i = 0;
void* ptr = &i;
现在,假设C++标准允许指针具有不同的大小。进一步假设在某些任意机器/编译器上(因为标准允许),void*
的大小为 2 字节,而int*
的大小为 4 字节。
现在,我认为这里有一个问题,即右侧有一个大小为 4 字节的int*
,而在左侧我们有一个大小为 2 字节的void*
。因此,当隐式转换从int*
发生到void*
时,会有一些信息丢失。
2)所有指针都包含地址。由于对于给定的机器,所有地址都具有相同的大小,因此所有指针也应具有相同的大小是非常自然的(合乎逻辑的)。
因此,我认为第二句话是正确的。
我的第一个问题是C++标准对此有何规定?
我的第二个问题是,如果C++标准确实允许指针具有不同的大小,那么是否有原因?我的意思是允许指针具有不同的大小对我来说似乎有点不自然(考虑到我上面解释的 2 点)。所以,我很确定标准委员会一定已经考虑到了这一点(指针可以有不同的大小),并且已经有理由允许指针有不同的大小。请注意,只有当标准确实允许指针具有不同的大小时,我才会问这个问题(第二个问题)。
虽然可能很容易得出所有指针大小相同的结论,因为"指针只是地址,而地址只是相同大小的数字",但它不能由标准保证,因此不能依赖。
C++标准明确保证:
void*
的大小与char*
相同([basic.compound]/5)T const*
、T volatile*
和T const volatile*
的大小与T*
相同。这是因为相同类型的 cv 限定版本与布局兼容,并且指向布局兼容类型的指针具有相同的值表示形式 ([basic.compound]/3)。- 同样,具有相同基础类型的任何两个枚举类型都是布局兼容的 ([dcl.enum]/9),因此指向此类枚举类型的指针具有相同的大小。
标准不能保证,但在实践中基本上总是正确的,指向所有类类型的指针具有相同的大小。其原因如下:指向不完整类类型的指针是完整类型,这意味着即使T
是不完整的类类型,您也有权询问编译器sizeof(T*)
,并且如果您在定义T
后稍后在翻译单元中再次询问编译器sizeof(T*)
, 结果必须相同。此外,在声明T
的所有其他翻译单元中,结果也必须相同,即使它从未在另一个翻译单元中完成。因此,编译器必须能够在不知道T
内部内容的情况下确定T*
的大小。从技术上讲,编译器仍然可以玩一些技巧,例如说如果类名以特定前缀开头,那么编译器将假定您希望该类的实例受到垃圾回收,并使指向它的指针比其他指针更长。实际上,编译器似乎没有使用这种自由,您可以假设指向不同类类型的指针具有相同的大小。如果你依赖这个假设,你可以在你的程序中放一个static_assert
,并说它不支持违反假设的病理平台。
此外,在实践中,通常情况是
- 任意两种函数指针类型具有相同的大小,
- 指向数据成员类型的任意两个指针将具有相同的大小,并且
- 指向函数成员类型的任意两个指针将具有相同的大小。
这样做的原因是,您始终可以从一种函数指针类型reinterpret_cast
到另一种函数指针类型,然后再返回到原始类型,而不会丢失信息,对于上面列出的其他两个类别(expr.reinterpret.cast),依此类推。虽然允许编译器通过为它们提供不同数量的填充来使它们具有不同的大小,但没有实际的理由这样做。
(但是,MSVC 具有一种模式,其中指向成员的指针不一定具有相同的大小。这不是由于填充量不同,而只是违反了标准。因此,如果您在代码中依赖此功能,则可能应该放置一个static_assert
。
如果您有一个具有近指针和远指针的分段体系结构,则不应期望它们具有相同的大小。这是上述规则的例外,这些规则涉及某些指针类型对通常具有相同的大小。
成员函数指针可以不同:
void* ptr;
size_t (std::string::*mptr)();
std::cout << sizeof(ptr) << 'n';
std::cout << sizeof(mptr) << std::endl;
这印刷
8
16
在我的系统上。背景是成员函数指针需要保存其他信息,例如关于虚拟性等的信息。
从历史上看,有些系统存在"近"和"远"指针,它们的大小也不同(16 位与 32 位)——据我所知,它们现在不再发挥任何作用了。
一些规则:
-
普通旧数据指针的大小可以不同,例如
double*
可以(并且经常)大于int*
。(考虑具有板外浮点单元的体系结构。 -
void*
必须足够大才能容纳任何对象指针类型。
任何 非纯旧数据指针的大小都与其他任何指针相同。换句话说
sizeof(myclass*) == sizeof(yourclass*)
.sizeof(const T*)
与任何T
的sizeof(T*)
相同;普通旧数据或其他成员函数指针不是指针。指向非成员函数(包括
static
成员函数)的指针是指针。
假设标准C++允许指针具有不同的大小
指针的大小、结构和格式由底层 CPU 的体系结构决定。 语言标准没有能力对这些事情提出很多要求,因为它不是编译器实现者可以控制的东西。 相反,语言规范侧重于在代码中使用指针时的行为方式。 C99理由文件(语言不同,但推理仍然有效)在第6.3.2.3节中提出以下评论:
C 语言现在已经在各种架构上实现。而 其中一些体系结构具有统一的指针,这些指针是 某些整数类型的大小,最大可移植代码无法假设 不同指针类型之间的任何必要对应关系和 整数类型。在某些实现中,指针甚至可以是 比任何整数类型都宽。
。
没有提到指向函数的指针,这可能是 与对象指针和/或整数不相称。
一个简单的例子是一台纯哈佛架构的计算机。可执行指令和数据存储在单独的存储区域中,每个区域都有单独的信号路径。 哈佛架构系统可以使用 32 位指针来获取数据,但只能使用指向更小指令内存池的 16 位指针。
编译器实现者必须确保他们生成的代码既能在目标平台上正常运行,又能根据语言规范中的规则运行。 有时这意味着所有指针的大小相同,但并非总是如此。
所有指针大小相同的第二个原因 是所有指针保持地址。由于对于给定的机器,所有 地址具有相同的大小
这两种说法都不一定正确。 它们在当今使用的大多数常见架构上都是正确的,但它们并非必须如此。
例如,所谓的"分段"内存架构可以有多种方法来格式化装配操作。 当前内存"段"内的引用可以使用短的"偏移量"值,而对当前段外部内存的引用需要两个值:段 ID 加上偏移量。 在x86上的DOS中,它们分别称为"近"和"远"指针,宽度分别为16位和32位。
我还见过一些专门的芯片(如DSP),它们使用两个字节的内存来存储12位指针。 其余四位是控制内存访问方式(缓存与未缓存等)的标志。 指针包含内存地址,但它不仅仅是这样。
语言规范对所有这些所做的是定义一组规则,定义如何在代码中使用指针,以及如何在代码中使用指针,以及每个与指针相关的操作应该观察到的行为。 只要你坚持这些规则,你的程序就应该按照规范的描述运行。 编译器编写者的工作是弄清楚如何弥合两者之间的差距并生成正确的代码,而无需了解 CPU 架构的怪癖。 超出规范并调用未指定的行为将使这些实现细节变得相关,并且您不再保证会发生什么。 我建议为导致数据丢失的转换启用编译器警告,然后将该警告视为硬错误。
在第一种情况下,你的推理是半正确的。void*
必须能够保存任何int*
值。但反之则不然。因此,void*
很有可能大于int*
。
如果包含其他指针类型(例如指向函数的指针和指向方法的指针),则语句 als 会变得更加复杂。
C++标准委员会考虑的原因之一是DSP芯片,其中硬件字大小为16位,但char
是半字实现的。这意味着与short*
和int*
相比,char*
和void*
需要多一点。
除了 C++ 标准的要求外,任何支持 UNIXdlsym()
库调用的实现都必须能够将函数指针转换为void*
。 所有函数指针的大小也必须相同。
在现实世界中,存在不同类型的指针具有不同大小的体系结构。 以前非常主流的一个示例是MS-DOS,其中紧凑和中型内存模型可以使代码指针大于数据指针,反之亦然。 在分段内存中,也可以具有不同大小的对象指针(例如near
和far
指针)。 最后,一些旧的大型机有复杂的指针,对于不同类型的对象来说,这些指针可能是不同的大小,胖指针甚至在ARM64上卷土重来。
作为一个嵌入式程序员,我想知道即使是这些C语言,是否也让我们离机器太远了! :)
父亲"C"用于设计系统(低级)。 不同的指针变量不必具有相同的大小的部分原因是它们可以引用物理上不同的系统内存。 也就是说,不同存储器地址的不同数据实际上可以位于单独的电子集成电路(IC)上! 例如,常量数据可能位于一个非易失性 IC 上,易失性变量位于另一个 IC 上,等等。 存储器IC可能设计为一次访问1个字节,或一次访问4个字节,等等("指针++"的作用)。
如果特定的内存总线/地址空间只有一个字节宽怎么办? (我以前和那些人一起工作过。 那么指针==0xFFFFFFFFFFFFFFFF将是浪费的,也许是不安全的。
我已经看到了处理16位单元的DSP的实际代码。因此,如果您获取指向 int 的指针,将位解释为整数,并将其增加 1,则指针将指向下一个 16 位 int。
在这个系统上,char 也是 16 位。如果 char 是 8 位,那么 char* 将是一个至少带有一个附加位的 int 指针。
实际上,您会发现一个系统中的所有指针对于几乎所有现代系统都是相同的大小; 从 2000.
开始的"现代"来自使用8086、80386等芯片的旧系统,其中有"近"和"远"指针,大小明显不同。这是编译器(有时是开发人员)的工作来整理 - 并记住!- 什么在近指针中,什么在远指针中。
C++需要与这些时代和环境保持兼容。
在现代C++中,标准库中有智能指针、std::unique_ptr
和std::shared_ptr
。 当唯一指针没有存储删除器函数时,它们的大小可以与常规指针相同。 共享指针可能更大,因为它仍然可以存储指针,也可以存储指向维护对象的引用计数和删除程序的控制块的指针。 此控制块可能与分配的对象(使用std::make_shared
)一起存储,因此它可能会使引用计数对象略大。
看到这个有趣的问题:为什么make_shared两个指针的大小?