在多大程度上可以将 C++ 指针视为内存地址?

To what extent is it acceptable to think of C++ pointers as memory addresses?

当您学习 C++ 时,或者至少当我通过 C++ Primer 学习 C++ 时,指针被称为它们指向的元素的 "memory addresses"。我想知道这在多大程度上是正确的。

例如,两个元素 *p1*p2 是否具有 属性 p2 = p1 + 1p1 = p2 + 1 当且仅当 它们在物理内存中相邻?

正如很多答案已经提到的,它们不应该被认为是内存地址。查看这些答案 and here 以了解它们。解决你的最后一个陈述

*p1 and *p2 have the property p2 = p1 + 1 or p1 = p2 + 1 if and only if they are adjacent in physical memory

仅当 p1p2 属于同一类型或指向相同大小的类型时才正确。

和其他变量一样,指针存储的数据可以是存储其他数据的内存地址。

所以,指针是一个有地址的变量,可以保存一个地址。

请注意,it is not necessary that a pointer always holds an address。它可能包含 non-address ID/handle 等。因此,将指针说成地址并不是明智之举。


关于你的第二个问题:

Pointer arithmetic 对连续的内存块有效。如果 p2 = p1 + 1 和两个指针的类型相同,则 p1p2 指向连续的内存块。因此,地址 p1p2 holds 彼此相邻。

指针是内存地址,但你不应该假设它们反映了物理地址。当您看到像 0x00ffb500 这样的地址时,这些是逻辑地址,MMU 会将其转换为相应的物理地址。这是最有可能的情况,因为虚拟内存是最扩展的内存管理系统,但可能存在直接管理物理地址的系统

将指针视为内存地址绝对正确。这就是我使用过的所有编译器中的情况——针对许多不同的处理器架构,由许多不同的编译器生产商制造。

然而,编译器做了一些有趣的魔术,以帮助您了解正常内存地址 [至少在所有现代主流处理器中] 是 byte-addresses,而您的指针指向的对象可能不是恰好是一个字节。因此,如果我们有 T* ptr;ptr++ 将执行 ((char*)ptr) + sizeof(T);ptr + n((char*)ptr) + n*sizeof(T)。这也意味着您的 p1 == p2 + 1 需要 p1p2 是同一类型 T,因为 +1 实际上是 +sizeof(T)*1

上述 "pointers are memory addresses" 有一个例外,那就是成员函数指针。它们是 "special",现在,请忽略它们的实际实现方式,足以说明它们不是 "just memory addresses"。

您应该将指针视为 虚拟 内存的地址:现代消费者操作系统和运行时环境在物理内存和您所看到的内存之间放置了至少一个抽象层指针值。

至于你的最后陈述,你不能做出那个假设,即使在虚拟内存地址 space 中也是如此。指针运算仅在连续内存块(例如数组)内有效。虽然允许(在 C 和 C++ 中)将指针分配给数组(或标量)之后的一个点,但 deferencing 这种指针的行为是未定义的。在 C 和 C++ 的上下文中假设物理内存中的邻接是没有意义的。

操作系统为您的程序提供物理机的抽象(即您的程序 运行 在虚拟机中)。因此,您的程序无法访问计算机的任何物理资源,无论是 CPU 时间、内存等;它只需要向 OS 询问这些资源。

就内存而言,您的程序在操作系统定义的虚拟地址 space 中运行。这个地址space有多个区域,比如栈,堆,代码等等,你的指针的值代表这个虚拟地址space里面的地址。实际上,指向连续地址的 2 个指针将指向此地址中的连续位置 space.

但是,这个地址 space 被操作系统分成页和段,根据需要从内存换入和换出,所以你的指针可能会也可能不会指向连续的物理内存位置和在 运行 时间无法判断那是真是假。这也取决于操作系统用于分页和分段的策略。

底线是指针是内存地址。但是,它们是虚拟内存 space 中的地址,由操作系统决定如何将其映射到物理内存 space。

就您的程序而言,这不是问题。这种抽象的原因之一是让程序相信他们是机器的唯一用户。想象一下,如果您在编写程序时需要考虑其他进程分配的内存,您将不得不经历一场噩梦——您甚至不知道哪些进程将与您的进程同时 运行。此外,这是一种加强安全性的好技术:您的进程不能(好吧,至少不应该能够)恶意访问另一个进程的内存 space 因为它们 运行 在 2 个不同的(虚拟) 内存 spaces.

不知何故,这里的答案没有提到一个特定的指针家族——即pointers-to-members。这些是当然不是内存地址。

完全没有。

C++ 是对您的计算机将执行的代码的抽象。我们在几个地方看到了这种抽象泄漏(例如,class 需要存储的成员引用),但通常情况下,如果您只编写抽象代码而不是其他任何东西,您会过得更好。

指针就是指针。他们指向事物。它们会在现实中实现为内存地址吗?可能是。它们也可以被优化掉,或者(在例如 pointers-to-members 的情况下)它们可能比简单的数字地址更复杂。

当您开始将指针视为映射到内存中地址的整数时,您开始忘记例如 undefined 保存指向未定义的对象的指针存在(你不能随便增加和减少指向你喜欢的任何内存地址的指针)。

我认为 的想法是正确的,但术语很糟糕。 C 指针提供的是与抽象完全相反的东西。

抽象提供了一种相对容易理解和推理的心智模型,即使硬件更复杂、更难理解或更难推理。

C 指针与此相反。他们考虑了硬件的 可能 困难,即使实际硬件通常更简单、更容易推理。他们将您的推理限制在最复杂硬件的最复杂部分的联合所允许的范围内,而不管手头的硬件实际上可能有多简单。

C++ 指针添加了 C 不包含的内容。它允许比较所有相同类型的指针的顺序,即使它们不在同一个数组中。这允许更多的心智模型,即使它与硬件不完全匹配。

除非指针被编译器优化掉,否则它们是存储内存地址的整数。它们的长度取决于编译代码的机器,但它们可以通常被视为整数。

事实上,您可以通过使用 printf() 打印存储在它们上的实际数字来检查。

但是请注意,type * 指针 increment/decrement 操作是由 sizeof(type) 完成的。使用此代码亲自查看(在 Repl.it 上在线测试):

#include <stdio.h>

int main() {
    volatile int i1 = 1337;
    volatile int i2 = 31337;
    volatile double d1 = 1.337;
    volatile double d2 = 31.337;
    volatile int* pi = &i1;
    volatile double* pd = &d1;
    printf("ints: %d, %d\ndoubles: %f, %f\n", i1, i2, d1, d2);
    printf("0x%X = %d\n", pi, *pi);
    printf("0x%X = %d\n", pi-1, *(pi-1));
    printf("Difference: %d\n",(long)(pi)-(long)(pi-1));
    printf("0x%X = %f\n", pd, *pd);
    printf("0x%X = %f\n", pd-1, *(pd-1));
    printf("Difference: %d\n",(long)(pd)-(long)(pd-1));
}

所有变量和指针都声明为易变的,因此编译器不会优化它们。另请注意,我使用了递减,因为变量放在函数堆栈中。

输出为:

ints: 1337, 31337
doubles: 1.337000, 31.337000
0xFAFF465C = 1337
0xFAFF4658 = 31337
Difference: 4
0xFAFF4650 = 1.337000
0xFAFF4648 = 31.337000
Difference: 8

请注意,此代码可能不适用于所有编译器,特别是当它们不以相同顺序存储变量时。但是,重要的是指针值实际上可以读取和打印,并且根据指针引用的变量的大小减一 may/will。

另请注意,&*reference ("get the memory address of this variable") 和 dereference[ 的实际运算符=45=] ("get the contents of this memory address").

这也可以用于很酷的技巧,例如通过将 float* 转换为 int*:

来获取浮点数的 IEEE 754 二进制值
#include <iostream>

int main() {
    float f = -9.5;
    int* p = (int*)&f;

    std::cout << "Binary contents:\n";
    int i = sizeof(f)*8;
    while(i) {
        i--;
        std::cout << ((*p & (1 << i))?1:0);
   } 
}

结果是:

Binary contents:
11000001000110000000000000000000 

示例取自 https://pt.wikipedia.org/wiki/IEEE_754。检查任何转换器。

你给出的具体例子:

For example, do two elements *p1 and *p2 have the property p2 = p1 + 1 or p1 = p2 + 1 if and only if they are adjacent in physical memory?

在没有平面地址 space 的平台上会失败,例如 PIC。要访问 PIC 上的物理内存,您需要地址和组号,但后者可能来自外部信息,例如特定的源文件。因此,对来自不同银行的指针进行运算会产生意想不到的结果。

根据 C++14 标准,[expr.unary.op]/3:

The result of the unary & operator is a pointer to its operand. The operand shall be an lvalue or a qualified-id. If the operand is a qualified-id naming a non-static member m of some class C with type T, the result has type “pointer to member of class C of type T” and is a prvalue designating C::m. Otherwise, if the type of the expression is T, the result has type “pointer to T” and is a prvalue that is the address of the designated object or a pointer to the designated function. [Note: In particular, the address of an object of type “cv T” is “pointer to cv T, with the same cv-qualification. —end note ]

所以这清楚而明确地说指向对象类型的指针(即 T *,其中 T 不是函数类型)保存地址。


"address" 由 [intro.memory]/1:

定义

The memory available to a C++ program consists of one or more sequences of contiguous bytes. Every byte has a unique address.

因此,地址可以是用于唯一标识特定内存字节的任何内容。

注:在C++标准术语中,memory仅指正在使用的space。它不是指物理内存、虚拟内存或类似的东西。内存是一组不相交的分配。


重要的是要记住,尽管唯一标识内存中每个字节的一种可能方法是为物理或虚拟内存的每个字节分配一个唯一的整数,但这不是唯一可能的方法。

为避免编写 non-portable 代码,最好避免假设地址与整数相同。无论如何,指针的算术规则与整数的算术规则不同。同样,我们不会说 5.0f1084227584 相同,即使它们在内存中具有相同的位表示(在 IEEE754 下)。