C ++ 中的指针变量和引用变量之间有什么区别?

我知道引用是语法糖,因此代码更易于读写。

但是有什么区别呢?

答案

  1. 可以重新分配一个指针:

    int x = 5;
    int y = 6;
    int *p;
    p = &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);

    引用不能,并且必须在初始化时分配:

    int x = 5;
    int y = 6;
    int &r = x;
  2. 指针在堆栈上有自己的内存地址和大小(x86 上为 4 字节),而引用共享相同的内存地址(带有原始变量),但也占用堆栈上的一些空间。由于引用具有与原始变量本身相同的地址,因此可以将引用视为同一变量的另一个名称。注意:指针指向的内容可以在堆栈或堆上。同上一个参考。我在此声明中的主张不是指针必须指向堆栈。指针只是保存内存地址的变量。此变量在堆栈上。由于引用在堆栈上有其自己的空间,并且地址与引用的变量相同。有关堆栈与堆的更多信息。这意味着编译器不会告诉您引用的真实地址。

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
  3. 您可以使用指向提供额外级别间接功能的指针。而引用仅提供一种间接级别。

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
  4. 指针可以直接分配为nullptr ,而引用不能。如果您尽力而为,并且知道如何做,则可以将引用的地址设置为nullptr 。同样,如果您尽力而为,则可以对一个指针进行引用,然后该引用可以包含nullptr

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
  5. 指针可以遍历数组,可以使用++转到指针所指向的下一个项目,并使用+ 4转到第 5 个元素。无论指针指向的对象大小是多少。

  6. 指针需要用*取消引用,以访问其指向的内存位置,而引用可以直接使用。指向类 / 结构的指针使用->来访问其成员,而引用则使用.

  7. 指针是保存内存地址的变量。无论引用如何实现,引用都与其引用的项具有相同的内存地址。

  8. 引用不能填充到数组中,而指针可以(由用户 @litb 提及)

  9. 常量引用可以绑定到临时对象。指针不能(并非没有间接):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.

    这使得const&更安全地用于参数列表等。

什么是 C ++ 参考( 适用于 C 程序员

可以将引用视为具有自动间接指向常量指针 (不要与指向常量值的指针混淆!),即编译器将为您应用*运算符。

必须使用非 null 值初始化所有引用,否则编译将失败。既不可能获得引用的地址 - 地址运算符将返回引用值的地址 - 也不可能对引用进行算术运算。

C 程序员可能不喜欢 C ++ 引用,因为在发生间接调用或参数通过值或指针传递而无需查看函数签名的情况下,它将不再显而易见。

C ++ 程序员可能不喜欢使用指针,因为它们被认为是不安全的 - 尽管除了在大多数琐碎的情况下引用实际上并没有比常量指针更安全的情况 - 缺少自动间接的便利并带有不同的语义含义。

请考虑以下C ++ 常见问题解答

即使参考使用底层汇编语言的地址经常被实现,请不要以为引用作为好笑的看着指针指向的对象。引用对象。它不是指向对象的指针,也不是对象的副本。它对象。

但是,如果引用确实是对象,那么怎么会有悬挂的引用呢?在非托管语言中,引用不可能比指针更 “安全”- 通常只有一种方法才能可靠地跨作用域边界使用别名!

为什么我认为 C ++ 引用有用

来自 C 的背景,C ++ 引用可能看起来有点愚蠢,但是在可能的情况下,仍然应该使用它们而不是指针:自动间接访问方便,并且在处理RAII时引用特别有用 - 但这不是因为任何公认的安全性优势,而是因为它们使编写惯用代码变得不那么尴尬。

RAII 是 C ++ 的核心概念之一,但它与复制语义非平凡地交互。通过引用传递对象避免了这些问题,因为不涉及复制。如果在语言中没有引用,则必须使用指针,因为指针使用起来比较麻烦,因此违反了语言设计原则,即最佳实践的解决方案应该比替代方法更容易。

如果您想成为真正的书呆子,则可以使用引用做某件事,而不能使用指针做这件事:延长临时对象的寿命。在 C ++ 中,如果将 const 引用绑定到临时对象,则该对象的生存期将成为引用的生存期。

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

在此示例中,s3_copy 复制作为连接结果的临时对象。而 s3_reference 本质上成为临时对象。它实际上是对一个临时对象的引用,该对象现在具有与该引用相同的生存期。

如果您在没有const情况下尝试此操作,则它将无法编译。您不能将非常量引用绑定到临时对象,也不能使用它的地址。

与流行观点相反,可能有一个为 NULL 的引用。

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

当然,使用参考要困难得多 - 但是如果您进行管理,就会发现自己的头发而无法找到它。在 C ++ 中,引用并不是天生的安全!

从技术上讲,这是无效引用 ,而不是 null 引用。 C ++ 不像其他语言那样支持将空引用作为概念。还有其他种类的无效引用。 任何无效的引用都会引发未定义行为 ,就像使用无效指针一样。

实际错误在于分配给引用之前对 NULL 指针的取消引用。但是我不知道在这种情况下会产生任何错误的编译器 - 错误会传播到代码中更远的地方。这就是使这个问题如此隐蔽的原因。在大多数情况下,如果取消引用 NULL 指针,则会在该位置立即崩溃,并且无需花费很多调试就能弄清楚。

我上面的例子简短而人为。这是一个更真实的示例。

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

我要重申的是,获取空引用的唯一方法是通过格式错误的代码,一旦获得该引用,您将获得未定义的行为。检查空引用从来没有任何意义。例如,您可以尝试if(&bar==NULL)...但是编译器可能会优化该语句而不存在!有效引用永远不能为 NULL,因此从编译器的角度来看,比较始终为 false,可以自由地将if子句消除为无效代码 - 这是未定义行为的本质。

避免麻烦的正确方法是避免取消引用 NULL 指针来创建引用。这是实现此目的的自动方法。

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

如果您想从写作能力更高的人那里更早地看到这个问题,请参阅 Jim Hyslop 和 Herb Sutter 的Null References

有关取消引用空指针的危险的另一个示例,请参阅 Raymond Chen 试图将代码移植到另一个平台时公开未定义的行为

除了语法糖,参考是一个const指针( 指向一个const )。您必须在声明引用变量时建立它所引用的内容,并且以后不能更改它。

更新:现在我考虑了更多,有一个重要的区别。

可以通过获取其地址并使用 const 强制替换 const 指针的目标。

引用目标不能用 UB 以外的任何方式替换。

这应该允许编译器对参考进行更多优化。

您忘记了最重要的部分:

具有指针的成员访问使用->
具有引用的 member-access 使用.

foo.bar 明显优于foo->bar ,就像vi 明显优于Emacs 一样 :-)

引用与指针非常相似,但是它们经过专门设计,有助于优化编译器。

  • 设计引用时,使编译器更容易跟踪哪些引用别名哪些变量。两个主要功能非常重要:没有 “引用算术” 和没有重新分配引用。这些允许编译器找出哪些引用在编译时别名哪些变量。
  • 引用可以引用没有内存地址的变量,例如编译器选择放入寄存器的变量。如果使用局部变量的地址,则编译器很难将其放入寄存器中。

举个例子:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

一个优化的编译器可能意识到我们正在大量访问 a [0] 和 a [1]。希望将算法优化为:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

为了进行这样的优化,它需要证明在调用过程中什么都不能改变 array [1]。这很容易做到。我从不小于 2,所以 array [i] 永远不能引用 array [1]。 mayModify()被赋予 a0 作为参考(别名为 array [0])。因为没有 “引用” 算法,所以编译器只需要证明 maynyModify 永远不会获得 x 的地址,并且证明没有任何改变 array [1]。

还必须证明,当我们在 a0 中有一个临时寄存器副本时,将来的调用将无法读取 / 写入 a [0]。通常很难证明这一点,因为在许多情况下,很明显,引用从未存储在像类实例这样的永久性结构中。

现在用指针做同样的事情

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

行为是相同的。直到现在,要证明也许 Modify 不会修改 array [1] 更加困难,因为我们已经给了它一个指针。那只猫从书包里掏出来。现在,它必须做更加困难的证明:对 maymayModify 的静态分析,以证明它永远不会写入&x +1。它还必须证明,它从未保存过可以引用 array [0] 的指针,这仅仅是一样棘手。

现代编译器在静态分析方面越来越好,但是帮助他们并使用引用总是很高兴。

当然,除非进行此类巧妙的优化,否则编译器的确会在需要时将引用转换为指针。

编辑:发布此答案的五年后,我发现了实际的技术差异,其中参考文献不同,而不仅仅是查看同一寻址概念的不同方式。引用可以通过指针无法修改的方式来修改临时对象的寿命。

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

通常,临时对象(例如通过调用createF(5)创建的对象createF(5)会在表达式的末尾销毁。但是,通过将该对象绑定到引用ref ,C ++ 将延长该临时对象的寿命,直到ref超出范围。

实际上,引用不是真的像指针。

编译器保留对变量的 “引用”,将名称与内存地址相关联。这是在编译时将任何变量名转换为内存地址的工作。

创建引用时,仅告诉编译器您已为指针变量分配了另一个名称。这就是为什么引用不能 “指向 null” 的原因,因为变量不能是,也不能是。

指针是变量;它们包含其他一些变量的地址,或者可以为 null。重要的是,指针具有一个值,而引用仅具有它所引用的变量。

现在对真实代码进行一些解释:

int a = 0;
int& b = a;

在这里,你不是在创造另一个变量指向a ; 您只是将另一个名称添加到包含a值的内存内容中。现在,此内存有两个名称, ab ,可以使用任何一个名称进行寻址。

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

调用函数时,编译器通常会为要复制到的参数生成内存空间。函数签名定义应创建的空间,并提供应用于这些空间的名称。将参数声明为引用只是告诉编译器在方法调用期间使用输入变量存储空间,而不是分配新的存储空间。说您的函数将直接操作在调用范围中声明的变量似乎很奇怪,但是请记住,在执行编译的代码时,不再有范围了。只有普通的平面内存,您的功能代码可以操纵任何变量。

现在,在某些情况下,例如在使用 extern 变量时,编译器可能无法知道引用。因此,引用可以或可以不被实现为基础代码中的指针。但是在我给您的示例中,很可能不会使用指针来实现它。

引用永远不能为NULL

虽然引用和指针都用于间接访问另一个值,但是引用和指针之间有两个重要区别。首先是引用始终引用对象:定义引用而不初始化它是错误的。分配的行为是第二个重要区别:分配给引用会更改引用所绑定到的对象;它不会将引用重新绑定到另一个对象。初始化后,引用始终引用相同的基础对象。

考虑这两个程序片段。首先,我们将一个指针分配给另一个指针:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

赋值 ival 后,pi 寻址的对象保持不变。赋值会更改 pi 的值,使其指向其他对象。现在考虑分配两个引用的类似程序:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

此分配更改 ival,即 ri 引用的值,而不是引用本身。分配后,两个引用仍然引用其原始对象,并且这些对象的值现在也相同。