【问题标题】:Why is it allowed to cast a pointer to a reference?为什么允许将指针转换为引用?
【发布时间】:2011-08-20 22:07:53
【问题描述】:

最初是this question 的主题,后来发现 OP 只是忽略了取消引用。同时,this answer 让我和其他一些人开始思考 - 为什么允许使用 C 样式转换或reinterpret_cast 将指针转换为指向引用?

int main() {
    char  c  = 'A';
    char* pc = &c;

    char& c1 = (char&)pc;
    char& c2 = reinterpret_cast<char&>(pc);
}

上面的代码在 Visual Studio 上编译时没有任何警告或错误(关于演员表),而 GCC 只会给你一个警告,如 here 所示。


我的第一个想法是指针以某种方式自动被取消引用(我通常使用 MSVC,所以我没有收到 GCC 显示的警告),并尝试了以下操作:

#include <iostream>

int main() {
    char  c  = 'A';
    char* pc = &c;

    char& c1 = (char&)pc;
    std::cout << *pc << "\n";

    c1 = 'B';
    std::cout << *pc << "\n";
}

here 显示了非常有趣的输出。因此,您似乎正在访问指向的变量,但与此同时,您却没有。

想法?解释?标准引号?

【问题讨论】:

    标签: c++ pointers casting reference


    【解决方案1】:

    嗯,这就是reinterpret_cast 的目的!顾名思义,该转换的目的是将内存区域重新解释为另一种类型的值。因此,使用reinterpret_cast,您始终可以将一种类型的左值强制转换为另一种类型的引用。

    这在语言规范的 5.2.10/10 中有所描述。它还说reinterpret_cast&lt;T&amp;&gt;(x)*reinterpret_cast&lt;T*&gt;(&amp;x) 相同。

    在这种情况下,您正在投射 指针 的事实完全不重要。不,指针不会自动取消引用(考虑到*reinterpret_cast&lt;T*&gt;(&amp;x) 的解释,人们甚至可能会说相反的情况:自动获取该指针的地址)。在这种情况下,指针只是“占用内存中某个区域的某个变量”。该变量的类型没有任何区别。它可以是double、指针、int 或任何其他左值。该变量被简单地视为您重新解释为另一种类型的内存区域。

    至于 C 风格的转换 - 在这种情况下它只是被解释为 reinterpret_cast,所以上面的内容立即适用于它。

    在第二个示例中,您将引用 c 附加到指针变量 pc 占用的内存。当您执行c = 'B' 时,您将值'B' 强制写入该内存,从而完全破坏了原始指针值(通过覆盖该值的一个字节)。现在被破坏的指针指向一些不可预测的位置。后来你试图取消引用那个被破坏的指针。在这种情况下会发生什么纯属运气问题。程序可能会崩溃,因为指针通常是不可延迟的。或者你可能会很幸运,让你的指针指向一些不可预测但有效的位置。在那种情况下,你的程序会输出一些东西。没有人知道它会输出什么,也没有任何意义。

    可以将您的第二个程序重写为等效程序而无需引用

    int main(){
        char* pc = new char('A');
        char* c = (char *) &pc;
        std::cout << *pc << "\n";
        *c = 'B';
        std::cout << *pc << "\n";
    }
    

    从实际的角度来看,在 little-endian 平台上,您的代码会覆盖指针的最低有效字节。这样的修改不会使指针指向离它原来的位置太远。因此,代码更有可能打印一些东西而不是崩溃。在 big-endian 平台上,您的代码会破坏指针的最高有效字节,从而将其疯狂地抛出以指向完全不同的位置,从而使您的程序更容易崩溃。

    【讨论】:

    • 但是为什么呢?在对标准进行了大约 15 分钟的研究后,我想通了,@litb 试图向我解释。但不管“目的”的任何概念,它在哪里说这个演员阵容是良好的?
    • @jalf:在 5.2.10/10 中是这样说的。任何左值都可以转换为任何其他引用类型。
    • 我不知道 reinterpret_cast&lt;T&amp;&gt;(x) &lt;==&gt; *reinterpret_cast&lt;T*&gt;(&amp;x) 等价。很有趣。
    • 好了,到此结束,感谢您提供如此详尽的回答! :)
    • “顾名思义,该转换的目的是将内存区域重新解释为另一种类型的值。”但是,是吗?据我从标准中可以看出,它只定义了将 pointers 转换为不同类型的行为 - 而不是取消引用和重新解释所引用的值。我错过了什么? Fwiw,cppreference.com 也声称具有相同的“重新解释位模式”功能,这是每个人的直觉所暗示的 - 以及主要编译器如何处理这个问题(尽管我找不到他们的文档) - 但我找不到任何标准的基础。
    【解决方案2】:

    我花了一段时间去摸索它,但我想我终于明白了。

    C++ 标准指定转换 reinterpret_cast&lt;U&amp;&gt;(t) 等效于 *reinterpret_cast&lt;U*&gt;(&amp;t)

    在我们的例子中,Uchartchar*

    展开这些,我们看到发生了以下情况:

    • 我们将参数的地址传递给强制转换,产生一个char** 类型的值。
    • 我们把reinterpret_cast这个值改为char*
    • 我们取消引用结果,产生一个char 左值。

    reinterpret_cast 允许您从任何指针类型转换为任何其他指针类型。因此,从 char**char* 的演员表是格式良好的。

    【讨论】:

    • 等效于*reinterpret_cast&lt;U*&gt;(&amp;t),具有&amp;* 运算符的内置/非重载含义,如5.2.10/10 中所强调的。 boost::addressof 充分利用了这种细微差别。
    【解决方案3】:

    我将尝试使用我对引用和指针的根深蒂固的直觉来解释这一点,而不是依赖标准的语言。

    • C 没有引用类型,它只有值和指针,
      因为,物理上在内存中,我们只有值和指针。
    • 在 C++ 中,我们添加了对语法的引用,但您可以将它们视为一种语法糖 - 没有用于保存引用的特殊数据结构或内存布局方案。

    那么,从这个角度看,什么是“参考”?或者更确切地说,您将如何“实施”参考?当然,用指针。因此,每当您在某些代码中看到引用时,您可以假装它实际上只是一个以特殊方式使用的指针:如果 int x;int&amp; y{x}; 那么我们真的有一个 int* y_ptr = &amp;x;如果我们说y = 123;,我们只是指*(y_ptr) = 123;。这与我们使用 C 数组下标 (a[1] = 2;) 时的方式没有什么不同,实际发生的是 a 被“衰减”为指向其第一个元素的指针,然后执行的是 *(a + 1) = 2

    (旁注:编译器实际上并不总是在每个引用后面保存指针;例如,编译器可能会为引用的变量使用寄存器,然后指针不能指向对它。但这个比喻仍然很安全。)

    已经接受了“引用实际上只是伪装的指针”的比喻,现在我们可以用reinterpret_cast&lt;&gt;() 忽略这种伪装也就不足为奇了。

    PS - std::ref 也只是你深入研究时的指针。

    【讨论】:

      【解决方案4】:

      它是允许的,因为 C++ 在你转换时几乎允许任何东西。

      但至于行为:

      • pc是一个4字节的指针
      • (char)pc 尝试将指针解释为一个字节,尤其是四个字节中的最后一个字节
      • (char&)pc 相同,但返回对该字节的引用
      • 当您第一次打印 pc 时,什么都没有发生,您看到了您存储的字母
      • c = 'B' 修改了 4 字节指针的最后一个字节,所以它现在指向别的东西
      • 当您再次打印时,您现在指向的另一个位置可以解释您的结果。

      由于指针的最后一个字节被修改,新的内存地址就在附近,因此它不太可能在您的程序不允许访问的内存中。这就是为什么你没有遇到段错误。获得的实际值是未定义的,但很可能为零,这解释了当它被解释为字符时的空白输出。

      【讨论】:

      • C++ 在你投射时几乎不允许任何东西。尝试将int 转换为std::string。你不能。
      • @jaff,好的。但这并不限制你只使用有意义的演员表。
      【解决方案5】:

      当您进行转换时,使用 C 样式转换或使用 reinterpret_cast,您基本上是在告诉编译器另辟蹊径(“别介意,我知道我在做什么”)。

      C++ 允许您告诉编译器这样做。这并不意味着这是一个好主意...

      【讨论】:

      • 是的,我们已经从链接的答案中得到了这一点。我正在寻找它工作的原因和方式,尤其是第二个例子。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-04-29
      • 1970-01-01
      • 1970-01-01
      • 2015-03-26
      • 2020-12-10
      • 2012-02-08
      • 1970-01-01
      相关资源
      最近更新 更多