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


当前回答

我对引用和指针有一个类比,将引用看作对象的另一个名称,将指针看作对象的地址。

// receives an alias of an int, an address of an int and an int value
public void my_function(int& a,int* b,int c){
    int d = 1; // declares an integer named d
    int &e = d; // declares that e is an alias of d
    // using either d or e will yield the same result as d and e name the same object
    int *f = e; // invalid, you are trying to place an object in an address
    // imagine writting your name in an address field 
    int *g = f; // writes an address to an address
    g = &d; // &d means get me the address of the object named d you could also
    // use &e as it is an alias of d and write it on g, which is an address so it's ok
}

其他回答

直接答案

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。)

可以重新分配指针:int x=5;整数y=6;int*p;p=&x;p=&y;*p=10;断言(x==5);断言(y==10);引用不能重新绑定,必须在初始化时绑定:int x=5;整数y=6;整数&q;//错误int&r=x;指针变量有它自己的标识:一个独特的、可见的内存地址,可以用一元&运算符获取,还有一定的空间,可以用sizeof运算符测量。对引用使用这些运算符将返回与引用绑定到的任何对象相对应的值;引用自身的地址和大小是不可见的。由于引用以这种方式假定原始变量的身份,因此可以方便地将引用视为同一变量的另一个名称。int x=0;int&r=x;int*p=&x;int*p2=&r;断言(p==p2);//&x==&r断言(&p!=&p2);可以将任意嵌套的指针指向提供额外间接级别的指针。引用仅提供一个间接级别。int x=0;整数y=0;int*p=&x;int*q=&y;int**pp=&p;**pp=2;pp=&q;//*pp现在是q**pp=4;断言(y==4);断言(x==2);指针可以指定为nullptr,而引用必须绑定到现有对象。如果您足够努力,您可以将引用绑定到nullptr,但这是未定义的,并且行为不一致。/*以下代码未定义;你的编译器可以优化它*不同的是,发出警告,或者干脆拒绝编译*/int&r=*static_cast<int*>(nullptr);//在GCC 10下打印“空”标准::cout<<(&r!=空指针? “not null”:“null”)<<std::endl;bool f(int&r){return&r!=nullptr;}//根据GCC 10打印“非空”标准::cout<<(f(*static_cast<int*>(nullptr))? “not null”:“null”)<<std::endl;但是,可以引用值为nullptr的指针。指针可以遍历数组;您可以使用++转到指针指向的下一个项目,使用+4转到第五个元素。这与指针指向的对象的大小无关。指针需要用*解引用以访问它指向的内存位置,而引用可以直接使用。指向类/结构的指针使用->访问其成员,而引用使用。。引用不能放入数组,而指针可以(由用户@litb提及)Const引用可以绑定到临时项。指针不能(不是没有间接指向):常量int&x=int(12);//法定C++int*y=&int(12);//取临时地址是非法的。这使得const&更便于在参数列表等中使用。

我觉得还有一点还没有在这里讨论。

与指针不同,引用在语法上等同于它们所引用的对象,即可以应用于对象的任何操作都适用于引用,并且具有完全相同的语法(当然,初始化是例外)。

虽然这可能看起来很肤浅,但我认为这一特性对于一些C++特性来说至关重要,例如:

模板。因为模板参数是鸭子类型的,所以类型的语法财产才是最重要的,所以通常同一个模板可以同时用于T和T&。(或std::reference_wrapper<T>,它仍然依赖于隐式转换至T&)覆盖T&和T&&的模板更为常见。L值。考虑语句str[0]=“X”;如果没有引用,它只适用于c字符串(char*str)。通过引用返回字符允许用户定义的类具有相同的符号。复制构造函数。从语法上讲,将对象传递给复制构造函数是有意义的,而不是传递给对象的指针。但复制构造函数无法按值获取对象,这将导致对同一复制构造函数的递归调用。这将引用作为此处的唯一选项。操作员过载。通过引用,可以在保留相同的中缀符号的同时,将间接指向引入运算符调用,例如运算符+(const T&a,const T&b)。这也适用于常规重载函数。

这些点赋予了C++和标准库相当大的一部分权力,因此这是参考文献的一个重要属性。

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

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

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

你忘记了最重要的部分:

带指针的成员访问使用->使用引用的成员访问。

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