【问题标题】:Why are pointers not convertible to references?为什么指针不能转换为引用?
【发布时间】:2013-04-29 17:30:11
【问题描述】:

我在多个来源中读到 C++ 引用只不过是一个具有编译时间限制的指针。

如果这是真的,为什么我不得不取消引用指针以便将它传递给需要参数的函数?

void FooRef(const int&);
void FooPointer(const int*);

int main()
{
    int* p = new int(5);
    FooPointer(p);
    FooRef(*p); // why do I have to dereference the pointer?

    ...
}

据我了解,如果我要将 int 传递给 FooRef,编译器将为我从变量的地址创建指针(引用),但如果类型已经是指针,则取消引用它似乎毫无意义。在我看来,我正在取消引用一个指针,只是为了让编译器从取消引用的值创建另一个指针,这对我来说似乎毫无意义。
仅仅复制指针而不是仅仅引用+引用值不是更简单/更高效吗? (也许这真的是正在发生的事情?)

我在这里遗漏了什么吗?在这种情况下调用FooRef 是否比调用FooPointer 慢?
并且在编译过程中引用和指针真的会产生相同的代码吗?

【问题讨论】:

  • 引用更像是 IMO 的别名 - 对象的另一个名称。有时它可能由指针实现(并不总是必要的,在某些情况下编译器可能会用它代替原始名称)。
  • 这里的解引用操作无非是语法糖。你必须一个解引用,但实际上没有执行。
  • 请注意,您可以拨打FooRef(5);,但不能拨打FooPointer(&5);
  • 另一个例子:int a = 42; int& b = a; b = 33; 还不如优化成int a = 42; a = 33;
  • @Acidic:不,语言不保证这一点。就语言规范而言,每当您取消引用指针时,编译器编写者可以决定在您的代码中放置 10 分钟的睡眠,这将是一个有效的决定。你只需要相信编译器作者不是白痴,或者自己检查代码输出。

标签: c++ pointers reference


【解决方案1】:

引用可以在引擎盖下实现指针的事实是无关紧要的。许多编程概念可以根据其他事物来实现。你不妨问为什么我们有while 循环,而while 可以用gotojmp 来实现。不同语言概念的重点是让程序员更轻松,而引用是为方便程序员而设计的语言概念。

您可能误解了引用的目的。引用为您提供了指针的积极方面(传递成本低),但由于它们与常规值具有相同的语义,因此它们消除了使用指针带来的许多危险:(指针算术、悬空指针等)重要的是,引用与 C++ 类型系统中的指针是完全不同的type,如果允许两者互换(这将违背引用的目的),那将是疯狂的。 p>

引用语法的设计有意反映了常规值语义的语法 - 同时为您提供了以低成本传递内存地址而不是复制整个值的能力。

现在,转向你的例子:

FooRef(*p); // why do I have to dereference the pointer?

您必须在此处取消引用指针,因为FooRef引用 指向int,而不是对int* 的引用。请注意,您还可以有一个 对指针的引用

void FooPointerRef(const int*&);

使用指针引用的函数使您能够从函数内部修改指针的内存地址。在您的示例中,您必须显式取消引用指向镜像值语义的指针。否则,查看函数调用 FooRef(p) 的人会认为 FooRef 要么采用按值指针或按引用指针,但不是(非指针)值或引用。

【讨论】:

  • 我的问题不是关于语法,而是关于为什么我似乎取消引用一个值只是为了让编译器在同一行引用它。
  • 但我没有。我想按值传递指针(如果引用确实是指针),而不是必须取消引用它,以便编译器可以从取消引用的值创建指针。
  • @Acidic void FooRef(const int&); 被设计/旨在具有与void FooRef(int); 相同的调用语法int 基本上没有区别(关于性能),但对于类类型的对象有很大的不同.
  • @DyP 你不需要教我基础知识或向我解释引用的目的......
  • 通过取消引用它,你告诉编译器你实际上知道你在做什么。当您实际上想要传递 X 时,您没有传递 X*。无论如何,按值传递指针隐式发生,但仅作为实现细节。
【解决方案2】:

通过引用传递的参数的实际参数始终与参数的类型相同,无论它是否通过解引用指针获得,也不管按引用传递是如何实现的。

【讨论】:

  • 您的第二条评论将是一个好的答案的开始。对于第一条评论,我并不是要暗示多个实现——只是在这种情况下语言的一致性比担心编译器的实现更有价值。
  • 我不知道这些是什么 cmets,因为一些反社会人士已经通过并删除了这些以及我(和其他人)的其他几个 cmets。
  • 令人不安。您已经注意到通过引用传递只能通过传递地址来实现,并更正了 OP 的假设,即当取消引用的指针传递给引用参数时,在运行时需要工作。
【解决方案3】:

我在多个来源中读到 C++ 引用只不过是一个 具有编译时间限制的指针。

不要相信你读到的一切。

【讨论】:

  • 这是一个危险的假设。
  • @Acidic 这不是假设,而是谎言......或者对您所读内容的误解。 在概念上,引用和指针是完全不同的东西。事实上,在大多数实现中,为调用 foo(int*) 和 foo(int&) 生成的代码都传递地址是另一回事。
  • @JimBalter 再次,我的问题从来不是关于概念上的差异。我的问题是关于两者之间可能存在的性能差异。
  • @Acidic 您说“C++ 引用只不过是一个指针”——再一次,这是错误的并且在概念上很混乱。您深深误解了编译器的语言、概念和性质。编译器编写者尝试生成语言规范允许的尽可能高效的代码。这就是您问题的答案。
  • @Acidic - 每当您看到“仅此而已”时,您的废话标志就应该升起。是的,引用通常实现为指针,但这与您编写使用它们的代码的方式无关。要了解指针和引用之间的区别,您必须研究如何定义和使用引用,以及如何定义和使用指针。它们本质上是不同的,这种对等的断言充其量是误导性的。
【解决方案4】:

C++ 中的指针是不同的数据类型。如果 C++强制将 X* 指针指向 X& 引用,那么下面的代码应该如何表现?

int x=5;
void* px = (void*)&x;
void*& prx = px;

此外,事情会很奇怪 - 因为你想要的是静默取消引用,传递 NULL 将导致调用者代码中的分段,但调用者将无法看到它的语法方式。

你会得到什么来回报这种混乱?实现细节和语言抽象之间的更多混淆。仅此而已。

【讨论】:

    【解决方案5】:

    这里有很多答案,你自己看看吧:

    #include <cstdio>
    
    int main(void) 
    {
        int p = 5;
        int *p_ptr = &p;
        int &p_ref = p;
    
    
        printf("Address of p     = %x\n", &p);
        printf("Address of p_ref = %x\n", &p_ref);
        printf("Value   of p_ptr = %x\n", p_ptr);
        printf("Address of p_ptr = %x\n", &p_ptr);
        return 0;
    }
    

    输出:

    Address of p     = 16fd80
    Address of p_ref = 16fd80
    Value   of p_ptr = 16fd80
    Address of p_ptr = 16fd88
    

    因此,就引用而言——引用与被引用的对象具有相同的地址。指针的是被指向(或引用)的对象的地址,同时仍然有自己独立的地址。

    当然,产生了什么:(cl /FA reftest.cpp)

    _TEXT   SEGMENT
    p$ = 32                         ; all our variables
    p_ptr$ = 40
    p_ref$ = 48
    main    PROC
    ; ... snip ...
    mov DWORD PTR p$[rsp], 5        ; p = 5
    
    lea rax, QWORD PTR p$[rsp]      ; p_ptr = &p
    mov QWORD PTR p_ptr$[rsp], rax
    
    lea rax, QWORD PTR p$[rsp]      ; p_ref = p
    mov QWORD PTR p_ref$[rsp], rax
    

    在我看来是一样的。 不过考虑一下:(cl /O2 /FA reftest.cpp)

    _TEXT   SEGMENT
    p$ = 48
    p_ptr$ = 56
    main    PROC     ; notice p_ref is gone
    ; ... snip ...
    

    关于引用最好的部分是它们很容易在代码之外进行优化。引用在其生命周期内只能引用一个对象,指针在其生命周期内可以引用许多不同的对象,编译器必须警惕这一事实。

    (注意:这显然只是微软编译器产生的程序集,虽然结果可能因编译器而异,我怀疑大多数编译器都可以完成)

    【讨论】:

    • “所以也许引用可能是假设的一两条额外指令”——不,绝对不是。对值的引用必须引用 那个 值... OP 担心 无法 完成的额外工作,否则它将是对副本的引用。
    • 我将把它编辑出来,你说得对。我想得越多,我就越意识到这听起来多么荒谬。在内存地址之后,获取值的内存地址...... IIRC(我的程序集生锈了),首先获取对象的地址没有意义,因为访问对象的唯一方法是使用它的内存地址.
    • 请注意,即使在 C 中,int* q; int* p = &amp;*q; 也不会做任何额外的工作……它不能。它在语义上与p = q; 相同
    【解决方案6】:

    这肯定会产生一些有趣的令人困惑的案例:

    int a(int& x)   {    return x + 1;    }
    int b(int& x)   {    return x + 2;    }
    
    // now for the fun part, 'b' has another valid overload
    int b(int* x)   {    return *x + 3;   }
    
    
    int  myInt = 1;
    int* p = &myInt; // get a pointer to myInt
    
    cout << a(p);    // calls int a(int&) by your rule.  Syntax error in real C++
    cout << b(p);    // calls int b(int*), by your rule and real C++.
                     // Isn't that confusing?
    
    cout << a(*p);   // valid, always calls int a(int&)
    cout << b(*p);   // valid.  Always calls b(int&)
                     // This isn't confusing, in real C++ or with your rule
    

    如果你必须经常记住是否要取消引用,那会很混乱。

    另一个原因是它简化了规范。允许编译器假设一个引用总是引用一个有效的对象。它可以根据该假设进行优化。它保持这种保证的方式是使遵循空指针成为非法。如果您不必在上面的示例中取消引用 p,那么它就无法获得这些保证。

    引用和指针具有非常相似的行为,除了以下几点:

    • 指针可以指向 null,而引用不能指向 null
    • 引用可以引用没有地址的值(例如存储在寄存器中的值)。指针必须始终存储在内存中,这可能会比较慢。

    他们是否可以编写 C++ 来自动从指针转换为引用?当然。他们不能拥有没有真正的技术原因。但该语言的作者认为它提供的问题多于解决方案。

    有一句古老的格言:“当你把你能想到的一切都放进去时,一种语言是不完整的。当你把你可能想到的一切都拿出来时,一种语言才是完整的。”许多人会争辩说,C++ 有太多的包袱,无法兑现这句格言,但它仍然会尽可能地尝试。

    【讨论】:

      猜你喜欢
      • 2011-08-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-22
      相关资源
      最近更新 更多