为什么在C++中使用寄存器存储类说明符声明的对象可以使用取地址运算符('&')?

23

在C语言中,我们不能对使用register存储类说明符声明的变量使用取地址运算符(&)。

这会导致错误:error: address of register variable ‘var_name’ requested

但如果我们编写一个C++程序并执行相同的任务(即对寄存器存储变量使用&),它不会导致任何错误。

例如:

#include <iostream>
using namespace std;
int main()
{
    register int a;
    int * ptr;
    a = 5;
    ptr = &a;
    cout << ptr << endl;
    return 0;
}

输出:

0x7ffcfed93624

这可能是C++的额外功能,但问题在于C和C++中寄存器类存储的区别。


5
register 是 C++ 中的一个提示,编译器可以忽略它。如果您获取它的地址,编译器也会忽略它。 - Jean-Baptiste Yunès
3
这也是 C 语言中的一个提示。 - Alan Stokes
2
可能现代编译器已经不再支持register关键字了。 - David Haim
你并不是在引用一个寄存器变量,而是在创建一个指向寄存器变量的指针。在许多平台上,寄存器不属于寻址空间,因此没有地址,换句话说,你不能指向一个寄存器。当创建一个指向寄存器变量的指针时,编译器可能会将变量放到堆栈上或将变量分配给临时内存位置,以便创建指针。 - Thomas Matthews
3个回答

21

C++中故意删除了获取地址的限制-这没有任何好处,只会使语言变得更加复杂。(例如,如果将引用绑定到register变量会发生什么?)

register关键字多年来并没有太大作用 - 编译器非常擅长自动将变量放入寄存器。实际上,在C++中该关键字目前已被标记为废弃,最终将被移除。


3
@alanstokes 注册表没有地址。为了获得一个地址,编译器需要将其放置在某个内存中。为了使这有意义,还必须在接收该地址的任何时刻更新内存中的该值,并在之后可以访问并读回它。这听起来很像是忽略提示,不是吗? - sqykly
3
我将担任魔鬼的代言人:一些实现 - 主要是嵌入式设备 - 将 register 作为实际命令而非提示。这是因为你经常需要对寄存器进行控制;程序员知道自己在干什么。我曾经问过这是否被允许,我会不遗余力地提供一个链接 - edmz
1
在IAR嵌入式工作台编译器上,关闭优化后,编译器会真正听取register关键字并使用寄存器来存储变量。如果没有register关键字,编译器将只在更高的优化级别下根据需要使用寄存器。 - Thomas Matthews
2
C语言中的register关键字表示您不能获取变量的地址,这意味着任何指针都不会引用该变量。这可能允许编译器执行它原本无法执行的优化,即使它没有将变量放入寄存器中也是如此。 (是的,现代编译器非常聪明,但提供更多信息可以使它们变得更加聪明。程序员具有他们缺乏的信息。)当然,如果C++允许您获取变量的地址,则在该语言中register关键字的用处较小。 - Ray
1
寄存器没有地址” 你的意思是从来没有存在过寄存器具有地址的CPU吗?(如果是这样,那么你是错误的。)还是你的意思是寄存器不一定有地址? - David Schwartz
显示剩余21条评论

8
“register”存储类最初是提示编译器,限定的变量使用频率很高,将其值保留在内存中会影响性能。绝大多数CPU架构(也许不包括SPARC?甚至没有反例)不能在两个变量之间执行任何操作,而不先将其中一个或两个从内存加载到其寄存器中。从内存加载变量到寄存器中,并在进行操作后写回到内存中,所需的CPU周期比操作本身多得多。因此,如果一个变量经常被使用,通过为其设置一个寄存器并完全不使用内存,可以获得性能提升。
然而,这样做有各种要求。每个CPU架构的要求都不同:
- 所有处理器都有固定数量的寄存器,但每个处理器型号的数量不同。在80年代,您可能只有4个可以合理用于“register”类型变量。 - 大多数处理器不支持在每个指令中使用每个寄存器。在80年代,通常只有一个寄存器可用于加法和减法,您可能无法将该寄存器用作指针。 - 调用约定规定了可以预期被子程序覆盖的不同寄存器集,即函数调用。 - 寄存器的大小因处理器而异,因此存在一些情况下“register”类型变量将不适合寄存器。
由于C旨在独立于平台,因此无法通过标准来强制执行这些限制。换句话说,尽管可能无法编译具有20个“register”变量的过程,但C程序本身不应该是“错误”的,因为没有逻辑原因可以解释机器不能拥有20个寄存器。因此,“register”存储类始终只是一个提示,如果特定的目标平台不支持它,则编译器可以忽略它。
无法引用寄存器是不同的。寄存器明确地没有在内存中保持更新,并且如果对内存进行更改,则不会保持当前状态;这就是存储类的全部意义。由于它们不打算在内存中具有保证的表示形式,因此它们在内存中没有逻辑上有意义的地址,这些地址可能对于可能获得指针的外部代码是有意义的。寄存器对于其自己的CPU没有地址,它们几乎永远没有任何协处理器可以访问的地址。因此,任何试图获取对“register”类型变量的引用的尝试都是错误的。 C标准可以轻松地强制执行此规则。
然而,随着计算机的发展,一些趋势出现,削弱了“register”存储类本身的目的。
处理器现在拥有更多的寄存器,今天您可能至少有16个寄存器,并且它们可以在大多数情况下互换使用。多核处理器和分布式代码执行已变得非常普遍;只有一个核心可以访问任何一个寄存器,而且它们绝不会共享,除非涉及内存。为变量分配寄存器的算法变得非常有效。
事实上,编译器现在非常擅长将变量分配到寄存器中,它们通常比任何人都能更好地进行优化。它们肯定知道哪些变量是最频繁使用的,而无需告诉它们。如果编译器被要求遵守手动输入的“register”提示,则生成这些优化将更加复杂(即不适用于标准或程序员)。编译器忽略这些提示已成为一种普遍现象。当C++存在时,它已经过时。它包含在标准中以保持C ++尽可能接近C的真正超集。遵守提示的要求,以及强制提示可以被遵守的条件的要求相应减弱。今天,这种存储类本身已经被弃用。
因此,即使今天(直到计算机甚至没有寄存器)您仍然不能逻辑上引用CPU寄存器,但是标准要求编译器遵守“register”存储类已经非常过时,因此要求编译器要求您在使用它时合乎逻辑是不合理的。

5
注:我认为你的意思是C++是C的超集(严格来说并非如此,尽管已经付出了很多工作来将其不兼容之处最小化)。 - Alan Stokes
1
x86处理器可以在两个内存操作数上执行字符串操作:请参见CMPSMOVS指令(操作码0xA4到0xA7)。 - Ruslan
@ruslan 现在编译器很少使用 movscmps,但它们确实涉及多个寄存器:dsesecx/rcxesi/rsiedi/rdicmps 还涉及 eflags。通常需要从内存中加载 *cx*di*si,有时在使用前和使用后也需要加载。不过,我想立即到内存地址模式实际上是一个很好的反例。将进行修复。65x 也有像 movs 这样的块移动指令,内存到内存,带有 3 个隐含寄存器。 - sqykly
@ruslan 将 imm 存入 mem;这不是一个操作“在两个变量之间”进行的,正如所述。 - sqykly
@sqykly 我不会说它很少使用。当你执行memcpy()时,gcc在我的32位系统上使用movsd,它不是对libc的调用,而是内联的。 - Ruslan

1
引用寄存器将是寄存器本身。如果调用函数将ESI作为引用参数传递,则被调用的函数将使用ESI作为参数。正如Alan Stokes所指出的那样,问题在于如果另一个函数也使用相同的引用参数调用了同一函数,但这次使用的是EDI。
为了使其工作,需要创建两个重载的函数实例,一个以ESI作为参数,一个以EDI作为参数。我不知道任何实际的C++编译器是否普遍实现了这样的优化,但是可以这样做。
引用寄存器的一个例子是std::swap()的优化方式(两个参数都是引用),这通常会变成内联代码。有时不进行交换:例如,std::swap(a,b),不进行交换,而是在随后的代码中交换a和b的意义(对原来a的引用变成了对b的引用,反之亦然)。
否则,引用参数将强制该变量位于内存中,而不是寄存器中。

如果使用EDI的不同调用函数,那么这该怎么办呢?或者你是在考虑内联调用吗? - Alan Stokes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接