指针变量和引用变量之间的区别是什么?


当前回答

如果你不熟悉以抽象的甚至学术的方式学习计算机语言,那么语义上的差异可能会显得深奥难懂。

在最高级别上,引用的概念是它们是透明的“别名”。你的计算机可能会使用一个地址来使它们工作,但你不必担心:你应该将它们视为现有对象的“另一个名称”,语法反映了这一点。它们比指针更严格,因此编译器可以在您将要创建悬挂引用时比在您将创建悬挂指针时更可靠地警告您。

除此之外,指针和引用之间当然还有一些实际差异。使用它们的语法明显不同,您不能“重新定位”引用、引用虚无或引用指针。

其他回答

在C++中,对指针的引用是可能的,但反之则不可能,这意味着指向引用的指针是不可能的。对指针的引用提供了一种更简洁的语法来修改指针。看看这个例子:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

并考虑上述程序的C版本。在C语言中,你必须使用指针对指针(多重间接寻址),这会导致混乱,程序可能看起来很复杂。

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

有关指针引用的详细信息,请访问以下内容:

C++:指针引用指向指针的指针和指向指针的引用

正如我所说,指向引用的指针是不可能的。尝试以下程序:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}

除了这里的所有答案,

可以使用引用实现运算符重载:

my_point operator+(const my_point& a, const my_point& b)
{
  return { a.x + b.x, a.y + b.y };
}

使用参数作为值将创建原始参数的临时副本,而使用指针将不会调用此函数,因为指针算法。

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

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

例如:

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];
    }
}

优化编译器可能会意识到,我们正在访问一个[0]和一个[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
}

要进行这样的优化,需要证明在调用期间没有任何东西可以改变数组[1]。这很容易做到。i永远不小于2,所以array[i]永远不能引用array[1]。maybeModify()被给定a0作为引用(别名数组[0])。因为没有“引用”算法,编译器只需要证明maybeModify永远不会得到x的地址,并且它已经证明没有任何东西会改变数组[1]。

它还必须证明,当我们在a0中有一个[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];
    }
}

行为是相同的;直到现在,要证明maybeModify从未修改过数组[1]要困难得多,因为我们已经给了它一个指针;猫从袋子里出来了。现在它必须做更困难的证明:对maybeModify进行静态分析,以证明它从未写入&x+1。它还必须证明它从未保存过可以引用数组[0]的指针,这同样棘手。

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

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

编辑:在发布这个答案五年后,我发现了一个实际的技术差异,即引用不同于看待相同寻址概念的不同方式。引用可以以指针无法修改的方式修改临时对象的寿命。

F createF(int argument);

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

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

直接答案

C++中的引用是什么?不是对象类型的特定类型实例。

C++中的指针是什么?某个特定的对象类型实例。

根据ISO C++对对象类型的定义:

对象类型是一种(可能是cv限定的)类型,它不是函数类型,不是引用类型,也不是cv void。

可能需要知道的是,对象类型是C++中类型宇宙的顶级类别。引用也是一个顶级类别。但指针不是。

指针和引用在复合类型的上下文中一起提到。这基本上是由于从(和扩展的)C继承的声明器语法的性质,它没有引用。(此外,自从C++11以来,有不止一种类型的引用声明器,而指针仍然是“unityped”:&+&&vs.*。)因此,在这种情况下,用类似C风格的“扩展”来起草一种特定于语言的语言是有一定道理的。(我仍然认为,声明器的语法浪费了大量的语法表达能力,使人类用户和实现都感到沮丧。因此,它们都不适合内置于新的语言设计中。不过,这是PL设计的一个完全不同的主题。)

否则,指针可以被限定为具有引用的特定类型是无关紧要的。除了语法相似性之外,它们共享的公共财产太少了,所以在大多数情况下没有必要将它们放在一起。

注意,上面的语句只提到“指针”和“引用”作为类型。关于它们的实例(如变量),有一些有趣的问题。还有太多的误解。

顶级类别的差异已经揭示了许多与指针无关的具体差异:

对象类型可以具有顶级cv限定符。引用不能。根据抽象机器语义,对象类型的变量确实占用了存储空间。引用不必占用存储空间(有关详细信息,请参阅下面的误解部分)。...

关于引用的其他一些特殊规则:

复合声明符对引用的限制更大。引用可以折叠。基于模板参数推导过程中引用折叠的&&参数特殊规则(作为“转发引用”)允许参数的“完美转发”。引用在初始化时有特殊规则。声明为引用类型的变量的生存期可以通过扩展与普通对象不同。顺便说一句,其他一些上下文(如涉及std::initializer_list的初始化)遵循引用生命周期扩展的一些类似规则。这是另一罐蠕虫。...

误解

语法糖

我知道引用是语法糖,所以代码更容易读写。

从技术上讲,这显然是错误的。引用不是C++中任何其他特性的语法糖,因为它们不能被没有任何语义差异的其他特性完全替换。

(类似地,lambda-expressions不是C++中任何其他功能的语法糖,因为它不能用捕获变量的声明顺序这样的“未指定”财产精确模拟,这可能很重要,因为这些变量的初始化顺序可能很重要。)

在严格意义上,C++只有几种语法糖。一个实例是(继承自C)内置(非重载)运算符[],它的定义与内置运算符unary*和binary+的特定组合形式具有相同的语义财产。

存储

因此,指针和引用都使用相同的内存量。

上面的说法完全错误。为了避免这种误解,请查看ISO C++规则:

来自[intro.object]/1:

……一个物体在其建造期间、在其整个生命周期和在其毁灭期间占据一个存储区域。。。

来自[dcl.ref]/4:

未指定引用是否需要存储。

请注意,这些是语义财产。

语用学

即使在语言设计的意义上,指针不足以与引用放在一起,但仍有一些争论使得在某些其他上下文中(例如,在对参数类型进行选择时)在它们之间进行选择是有争议的。

但这并不是全部。我的意思是,你需要考虑的不仅仅是指针和引用。

如果你不必坚持这种过于具体的选择,在大多数情况下,答案很简单:你没有必要使用指针,所以你不需要。指针通常很糟糕,因为它们暗示了太多你不期望的东西,而且它们依赖于太多的隐含假设,破坏了代码的可维护性和(甚至)可移植性。不必要地依赖指针绝对是一种糟糕的风格,在现代C++的意义上应该避免。重新考虑一下你的目的,你最终会发现在大多数情况下,指针是最后一种功能。

有时语言规则明确要求使用特定类型。如果您想使用这些功能,请遵守规则。复制构造函数需要特定类型的cv-引用类型作为第一个参数类型。(通常它应该是常量限定的。)移动构造函数需要特定类型的cv-&&引用类型作为第一个参数类型。(通常不应有限定符。)运算符的特定重载需要引用或非引用类型。例如:重载运算符=作为特殊成员函数需要类似于复制/移动构造函数的第一个参数的引用类型。后缀++需要伪int。...如果您知道传递值(即使用非引用类型)就足够了,请直接使用它,特别是在使用支持C++17强制复制省略的实现时。(警告:然而,详尽地解释必要性可能非常复杂。)如果您想使用所有权操作一些句柄,请使用unique_ptr和shared_ptr之类的智能指针(如果您需要自制指针不透明,甚至可以使用它们),而不是原始指针。如果您在一个范围内进行一些迭代,请使用迭代器(或标准库尚未提供的一些范围),而不是原始指针,除非您确信原始指针在非常特定的情况下会做得更好(例如,对于较少的头部依赖性)。如果您知道通过值传递就足够了,并且需要一些显式的可空语义,请使用包装器(如std::optional),而不是原始指针。如果您知道由于上述原因,传递值并不理想,并且您不希望使用可为null的语义,请使用{lvalue,rvalue,forward}-引用。即使您确实需要像传统指针那样的语义,也通常有更合适的方法,例如库基础TS中的observer_ptr。

在当前语言中无法解决以下唯一的例外:

当您在上面实现智能指针时,可能必须处理原始指针。特定的语言互操作例程需要指针,如运算符new。(然而,cv void*与普通对象指针相比仍有很大的不同和安全性,因为它排除了意外的指针算法,除非您依赖于void*上的一些非一致扩展,如GNU的。)函数指针可以从lambda表达式转换而不需要捕获,而函数引用则不能。对于这种情况,您必须在非泛型代码中使用函数指针,即使您故意不希望值为空。

因此,在实践中,答案是显而易见的:当有疑问时,避免使用指针。只有在有非常明确的理由认为没有其他更合适的时候,才必须使用指针。除了上面提到的一些例外情况外,这些选择几乎总是不是纯C++特定的(但可能是特定于语言实现的)。此类实例可以是:

您必须为旧式(C)API服务。您必须满足特定C++实现的ABI要求。您必须基于特定实现的假设,在运行时与不同的语言实现(包括各种程序集、语言运行时和某些高级客户端语言的FFI)进行互操作。在某些极端情况下,您必须提高翻译(编译和链接)的效率。在某些极端情况下,您必须避免符号膨胀。

语言中立警告

如果你通过谷歌搜索结果(不是C++特有的)看到这个问题,这很可能是错误的地方。

C++中的引用相当“奇怪”,因为它本质上不是一级的:它们将被视为被引用的对象或函数,因此它们没有机会支持一些一级操作,例如独立于被引用对象的类型而成为成员访问运算符的左操作数。其他语言可能对其引用有类似的限制,也可能没有。

C++中的引用可能不会保留不同语言之间的含义。例如,引用通常并不意味着像C++中那样的值具有非空财产,因此这种假设在某些其他语言中可能不起作用(并且很容易找到反例,例如Java、C#…)。

一般来说,在不同编程语言中的引用之间仍然可以有一些常见的财产,但让我们把它留给SO中的其他一些问题。

(附带说明:这个问题可能比任何“类C”语言都要早,比如ALGOL 68与PL/I。)

关于引用和指针的一些关键相关细节

指针

使用一元后缀声明符运算符声明指针变量*指针对象被分配一个地址值,例如,通过分配给数组对象、使用一元前缀运算符的对象地址或分配给另一个指针对象的值指针可以重新分配任意次数,指向不同的对象指针是保存指定地址的变量。它占用的内存存储量等于目标机器体系结构的地址大小例如,可以通过增量或加法运算符对指针进行数学操作。因此,可以使用指针等进行迭代。要获取或设置指针引用的对象的内容,必须使用一元前缀运算符*来取消引用它

工具书类

引用在声明时必须初始化。引用使用一元后缀声明符运算符&声明。初始化引用时,可以使用它们将直接引用的对象的名称,而不需要一元前缀运算符&一旦初始化,引用就不能通过赋值或算术操作指向其他对象无需取消引用该引用以获取或设置其引用的对象的内容对引用的赋值操作操作它指向的对象的内容(初始化后),而不是引用本身(不改变它指向的位置)对引用的算术运算操作它指向的对象的内容,而不是引用本身(不会改变它指向的位置)在几乎所有的实现中,引用实际上都存储为被引用对象的内存中的地址。因此,它占用的内存大小与目标机器体系结构的地址大小相同,就像指针对象一样

尽管指针和引用的实现方式几乎相同,但编译器对它们的处理方式不同,导致了上述所有差异。

文章

我最近写的一篇文章比我在这里展示的要详细得多,对这个问题非常有帮助,特别是关于记忆中的事情是如何发生的:

数组、指针和引擎罩下的引用深度文章