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

What are the differences between a pointer variable and a reference variable?

提问人:prakash 提问时间:9/12/2008 最后编辑:Mateen Ulhaqprakash 更新时间:10/5/2023 访问量:1229401

问:

指针变量和引用变量有什么区别?

C 指针参考 C++-FAQ

评论

133赞 Mark Ransom 10/9/2010
我认为第 2 点应该是“指针可以是 NULL,但引用不是。只有格式错误的代码才能创建 NULL 引用,并且其行为未定义。
31赞 Kerrek SB 6/16/2012
指针只是另一种类型的对象,与 C++ 中的任何对象一样,它们可以是变量。另一方面,引用从来都不是对象,而只是变量。
29赞 Calmarius 8/13/2012
这编译时没有警告:在 gcc 上。引用确实可以指向 NULL。int &x = *(int*)0;
28赞 Khaled.K 12/23/2013
reference 是变量别名
26赞 Lightness Races in Orbit 6/1/2014
我喜欢第一句话完全是谬误。引用有自己的语义。

答:

177赞 user3458 9/12/2008 #1

除了句法糖之外,引用是指针(而不是指向 的指针)。在声明引用变量时,必须确定它所引用的内容,并且以后无法更改它。constconst

更新:现在我再想一想,有一个重要的区别。

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

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

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

评论

16赞 Carlo Wood 1/10/2017
我认为这是迄今为止最好的答案。其他人则谈论参考资料和指针,就像它们是不同的野兽一样,然后列出它们在行为上的不同之处。恕我直言,这并没有让事情变得更容易。我一直将引用理解为具有不同语法糖的引用(这恰好从您的代码中消除了很多 * 和 &)。T* const
10赞 dgnuff 6/22/2018
“const 指针的目标可以通过获取其地址并使用 const 强制转换来替换。”这样做是未定义的行为。有关详细信息,请参见 stackoverflow.com/questions/25209838/...
2赞 curiousguy 6/30/2018
试图更改引用的引用或常量指针(或任何常量标量)的值是非法的。您可以做什么:删除通过隐式转换添加的 const 限定:是可以的。int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);
3赞 6/30/2018
这里的区别在于 UB 与字面上不可能。C++ 中没有语法可以让你更改引用点的位置。
1赞 Nicolas Bousquet 11/18/2018
并非不可能,更难的是,您可以访问正在对该引用进行建模的指针的内存区域并更改其内容。这当然是可以做到的。
2198赞 29 revs, 15 users 37%Brian R. Bondy #2
  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 &q; // error
    int &r = x;
    
  2. 指针变量有自己的标识:一个不同的、可见的内存地址,可以用一元运算符获取,也可以用运算符测量一定的空间量。在引用上使用这些运算符将返回与引用绑定到的任何内容相对应的值;引用本身的地址和大小是不可见的。由于引用以这种方式假定原始变量的标识,因此将引用视为同一变量的另一个名称是方便的。&sizeof

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    
    assert(p == p2); // &x == &r
    assert(&p != &p2);
    
  3. 可以创建指向指针的指针,但不能创建指向引用的指针。

     int **pp; // OK, pointer to pointer
     int &*pr; // ill-formed, pointer to reference
    
  4. 可以创建指针数组,但不能创建引用数组。

    int *ap[]; // OK, array of pointers
    int &ar[]; // ill-formed, array of references
    
  5. 您可以任意嵌套指向指针的指针,以提供额外的间接级别。引用仅提供一种间接级别,因为对引用的引用会折叠

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    
    **pp = 2;
    pp = &q; // *pp is now q
    **pp = 4;
    
    assert(y == 4);
    assert(x == 2);
    
  6. 指针可以被赋值,而引用必须绑定到现有对象。如果您足够努力,则可以将引用绑定到 ,但这是未定义的,并且行为不会一致。nullptrnullptr

    /* the code below is undefined; your compiler may optimise it
     * differently, emit warnings, or outright refuse to compile it */
    
    int &r = *static_cast<int *>(nullptr);
    
    // prints "null" under GCC 10
    std::cout
        << (&r != nullptr
            ? "not null" : "null")
        << std::endl;
    
    bool f(int &r) { return &r != nullptr; }
    
    // prints "not null" under GCC 10
    std::cout
        << (f(*static_cast<int *>(nullptr))
            ? "not null" : "null")
        << std::endl;
    

    但是,您可以引用值为 的指针。nullptr

  7. 指针是 ContiguousIterators(数组)。您可以使用它转到指针指向的下一个项目,以及转到第 5 个元素。+++ 4

  8. 需要取消引用指针才能访问它指向的对象,而引用可以直接使用。指向类/结构的指针用于访问其成员,而引用使用 .*->.

  9. 常量引用和右值引用可以绑定到临时引用(请参见临时具体化)。指针不能(并非没有一些间接):

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

    这使得在参数列表等中使用更加方便。const &

48赞 RichS 9/12/2008 #3

引用永远不能是 。NULL

评论

12赞 cmaster - reinstate monica 6/13/2014
参见 Mark Ransom 的反例回答。这是关于参考文献最常断言的神话,但这是一个神话。根据标准,唯一可以保证的是,当您具有 NULL 引用时,您会立即拥有 UB。但这类似于说“这辆车是安全的,它永远不能离开道路。(如果您将其驶离道路,我们不承担任何责任。它可能会爆炸。
20赞 user541686 8/8/2014
@cmaster:在有效程序中,引用不能为 null。但是指针可以。这不是神话,这是事实。
9赞 cmaster - reinstate monica 12/29/2014
@Mehrdad 是的,有效的程序会继续存在。但是,没有流量障碍可以强制您的程序实际执行。大部分道路实际上都缺少标记。所以晚上下车非常容易。对于调试此类错误至关重要的是,您知道这种情况可能会发生:null 引用可以在程序崩溃之前传播,就像 null 指针一样。当它出现时,你就会有这样的代码,段错误。如果您不知道引用可能为 null,则无法将 null 追溯到其来源。void Foo::bar() { virtual_baz(); }
4赞 uss 10/4/2015
int *p = 空;int &r=*p;指向 NULL 的引用;if(r){} -> boOm ;) –
13赞 cdhowie 3/29/2017
@sree是未定义的行为。在这一点上,你没有一个“指向 NULL 的引用”,你有一个根本无法推理的程序。int &r=*p;
138赞 Mark Ransom 9/12/2008 #4

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

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

诚然,使用参考要困难得多 - 但如果你管理它,你会撕掉你的头发试图找到它。引用在 C++ 中本身并不安全!

从技术上讲,这是一个无效的引用,而不是一个空引用。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);

我想重申,获得 null 引用的唯一方法是通过格式错误的代码,一旦你有了它,你就会得到未定义的行为。检查空引用是没有意义的;例如,您可以尝试,但编译器可能会优化不存在的语句!有效的引用永远不能为 NULL,因此从编译器的角度来看,比较总是错误的,并且可以自由地将子句作为死代码消除 - 这是未定义行为的本质。if(&bar==NULL)...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 引用

有关取消引用空指针的危险的另一个示例,请参阅 Raymond Chen 撰写的 Exposing undefined behavior when trying to port code to another platform(尝试将代码移植到另一个平台时公开未定义的行为)。

评论

71赞 KeithB 9/13/2008
有问题的代码包含未定义的行为。从技术上讲,你不能对 null 指针做任何事情,除非设置它,并比较它。一旦你的程序调用了未定义的行为,它就可以做任何事情,包括在你向大老板演示之前看起来工作正常。
12赞 Johannes Schaub - litb 2/28/2009
mark 有一个有效的参数。指针可以是 NULL 并且您必须检查的论点也不是真实的:如果您说函数需要非 NULL,那么调用方必须这样做。因此,如果调用者不这样做,他将调用未定义的行为。就像马克对糟糕的引用所做的那样
18赞 David Schwartz 8/20/2011
描述有误。此代码可能会也可能不会创建 NULL 引用。其行为未定义。它可能会创建一个完全有效的引用。它可能根本无法创建任何引用。
12赞 Mark Ransom 8/22/2011
@David施瓦茨,如果我说的是事情必须按照标准工作的方式,你是对的。但这不是我要说的 - 我说的是使用非常流行的编译器实际观察到的行为,并根据我对典型编译器和 CPU 架构的了解推断可能发生的事情。如果你认为引用优于指针,因为它们更安全,并且不认为引用可能是坏的,那么有一天你会像我一样被一个简单的问题难倒。
8赞 t0rakka 3/22/2017
取消引用 null 指针是错误的。任何这样做的程序,即使是初始化引用也是错误的。如果要从指针初始化引用,则应始终检查指针是否有效。即使此操作成功,底层对象也可能随时被删除,留下引用以引用不存在的对象,对吗?你说的是好东西。我认为这里真正的问题是,当您看到一个引用时,不需要检查引用是否存在“空”,并且指针至少应该被断言。
228赞 Matt Price 9/12/2008 #5

如果你想变得非常迂腐,那么你可以用引用做一件事,而你不能用指针做:延长临时对象的生存期。在 C++ 中,如果将常量引用绑定到临时对象,则该对象的生存期将成为引用的生存期。

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

评论

7赞 Ahmad Mushtaq 10/22/2009
但是这个用例是什么?
26赞 Matt Price 10/23/2009
好吧,s3_copy将创建一个临时结构,然后将其复制到 s3_copy 中,而s3_reference直接使用临时结构。然后,要真正迂腐,您需要查看返回值优化,在第一种情况下,编译器可以省略复制构造。
7赞 David Rodríguez - dribeas 1/15/2010
@digitalSurgeon:那里的魔力非常强大。对象生存期因绑定的事实而延长,并且仅当引用超出范围时,才会调用实际引用类型的析构函数(与引用类型相比,可以是基类型)。由于它是参考,因此两者之间不会发生切片。const &
11赞 Oktalist 11/11/2013
C++ 的更新:最后一句应为“不能将非常量右值引用绑定到临时值”,因为您可以将非常量右值引用绑定到临时值,并且它具有相同的生存期延长行为。
9赞 Arthur Tacca 11/2/2017
@AhmadMushtaq:它的主要用途是派生类。如果不涉及继承,不妨使用值语义,由于 RVO/move 构造,这将是便宜或免费的。但是如果你有,那么将面临经典的切片问题,同时会正常工作。Animal x = fast ? getHare() : getTortoise()xAnimal& x = ...
130赞 Orion Edwards 9/12/2008 #6

你忘记了最重要的部分:

带指针的 member-access 使用
带引用的 member-access 用途
->.

foo.bar显然优于 vi 明显优于 Emacs :-)foo->bar

评论

6赞 9/12/2008
@Orion Edwards >member-access with pointers uses -> >member-access with references uses .这不是 100% 正确的。可以引用指针。在这种情况下,您将使用 -> struct Node { Node *next; };节点 *first;p 是对指针的引用 void foo(Node*&p) { p->next = first; }节点 *bar = 新节点;foo(条形);-- OP:你熟悉右值和左值的概念吗?
4赞 JBRWilkinson 4/9/2014
智能指针同时具有 .(智能指针类上的方法)和 ->(基础类型的方法)。
2赞 Max Truxa 4/16/2015
@user6105 Orion Edwards 的说法实际上是 100% 正确的。“访问取消引用指针的成员”指针没有任何成员。指针引用的对象具有成员,对这些成员的访问正是提供对指针的引用,就像指针本身一样。->
3赞 artm 8/21/2016
为什么会这样,并且与 vi 和 emacs 有关:).->
14赞 Orion Edwards 8/26/2016
@artM - 这是一个笑话,对于非英语母语的人来说可能没有意义。我很抱歉。解释一下,vi 是否比 emacs 好完全是主观的。有些人认为 vi 要优越得多,而另一些人则认为恰恰相反。同样,我认为使用比使用更好,但就像 vi 与 emacs 一样,这完全是主观的,你无法证明任何事情.->
18赞 Aardvark 9/12/2008 #7

除非我需要以下任何一项,否则我使用参考文献:

  • Null 指针可以用作 哨兵价值,通常是一种廉价的方式 避免函数重载或使用 一个布尔值。

  • 您可以在指针上进行算术运算。 例如p += offset;

评论

5赞 M.M 2/23/2017
您可以写下声明为引用的位置&r + offsetr
14赞 Don Wakefield 9/13/2008 #8

引用的另一个有趣的用法是提供用户定义类型的默认参数:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

默认风格使用引用的“bind const reference to a temporary”方面。

23赞 MSN 9/13/2008 #9

它占用多少空间并不重要,因为您实际上看不到它占用的任何空间的任何副作用(不执行代码)。

另一方面,引用和指针之间的一个主要区别是,分配给 const 引用的临时引用一直有效,直到 const 引用超出范围。

例如:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

将打印:

in scope
scope_test done!

这是允许 ScopeGuard 工作的语言机制。

评论

1赞 Lightness Races in Orbit 4/25/2011
您不能获取引用的地址,但这并不意味着它们不会占用物理空间。除非进行优化,否则他们肯定可以。
3赞 Lightness Races in Orbit 4/26/2011
尽管有影响,但“堆栈上的引用根本不占用任何空间”显然是错误的。
1赞 MSN 4/27/2011
@Tomalak,嗯,这也取决于编译器。但是,是的,这么说有点令人困惑。我想删除它会不那么令人困惑。
1赞 Lightness Races in Orbit 4/27/2011
在任何给定的特定情况下,它可能会也可能不会。因此,“它没有”作为绝对断言是错误的。这就是我要说的。:)[我不记得标准在这个问题上是怎么说的;参考规则成员可能会给出“参考可能会占用空间”的一般规则,但我在海滩上没有我的标准副本:D]
83赞 Vincent Robert 9/19/2008 #10

实际上,引用并不像指针。

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

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

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

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

int a = 0;
int& b = a;

在这里,您不是在创建另一个指向 的变量;您只是在内存内容中添加另一个名称,其值为 。此内存现在有两个名称,和 ,并且可以使用任一名称对其进行寻址。aaab

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

int a;
increment(a);

调用函数时,编译器通常会为要复制到的参数生成内存空间。函数签名定义应创建的空间,并给出应用于这些空间的名称。将参数声明为引用只是告诉编译器使用输入变量内存空间,而不是在方法调用期间分配新的内存空间。说你的函数将直接操作调用作用域中声明的变量可能看起来很奇怪,但请记住,在执行编译后的代码时,没有更多的作用域;只有普通的平面内存,您的函数代码可以操作任何变量。

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

评论

2赞 3/3/2009
引用是对 l 值的引用,不一定是对变量的引用。因此,它更接近指针,而不是真正的别名(编译时构造)。可以引用的表达式示例包括 *p 甚至 *p++
6赞 Vincent Robert 3/4/2009
是的,我只是指出了一个事实,即引用可能并不总是像新指针那样将新变量推送到堆栈上。
4赞 Ben Voigt 2/14/2012
@VincentRobert:它的作用与指针相同......如果函数是内联的,则引用和指针都将被优化掉。如果有函数调用,则需要将对象的地址传递给函数。
1赞 uss 10/4/2015
int *p = 空;int &r=*p;指向 NULL 的引用;if(r){} -> boOm ;)
2赞 underscore_d 10/12/2015
这种对编译阶段的关注似乎很好,直到您记住引用可以在运行时传递,此时静态别名就会消失。(然后,引用通常作为指针实现,但标准不需要此方法。
537赞 Christoph 2/28/2009 #11

什么是 C++ 参考(针对 C 程序员))

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

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

C 程序员可能不喜欢 C++ 引用,因为当间接发生时,或者参数在不查看函数签名的情况下通过值或指针传递时,它不再明显。

C++ 程序员可能不喜欢使用指针,因为它们被认为是不安全的——尽管引用并不比常量指针更安全,除非在最微不足道的情况下——缺乏自动间接的便利性,并且带有不同的语义内涵。

请考虑 C++ FAQ 中的以下语句:

即使引用通常是使用 底层汇编语言,请不要将引用视为 指向对象的滑稽指针。引用对象。是的 不是指向对象的指针,也不是对象的副本。它是 对象。

但是,如果引用真的是对象,怎么会有悬空的引用呢?在非托管语言中,引用不可能比指针“更安全”——通常没有办法跨范围边界可靠地为值添加别名!

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

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

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

评论

22赞 Ben Voigt 11/2/2010
@kriss:不,您也可以通过按引用返回自动变量来获得悬空引用。
20赞 Ben Voigt 11/2/2010
@kriss:在一般情况下,编译器几乎不可能检测到。考虑一个成员函数,它返回对类成员变量的引用:这是安全的,编译器不应该禁止。然后,具有该类的自动实例的调用方调用该成员函数,并返回引用。Presto:悬空引用。是的,这会引起麻烦,@kriss:这就是我的观点。许多人声称引用相对于指针的优势在于引用始终有效,但事实并非如此。
7赞 Ben Voigt 11/2/2010
@kriss:不可以,对具有自动存储持续时间的对象的引用与临时对象有很大不同。无论如何,我只是为您的陈述提供了一个反例,即您只能通过取消引用无效指针来获得无效引用。Christoph 是对的——引用并不比指针更安全,一个只使用引用的程序仍然会破坏类型安全。
11赞 catphive 7/20/2011
引用不是一种指针。它们是现有对象的新名称。
29赞 Christoph 7/23/2011
@catphive:如果你按照语言语义来判断,那就不是真的了,如果你真的看实现的话;C++ 是一种比 C 更“神奇”的语言,如果你从引用中删除魔法,你最终会得到一个指针
15赞 Adisak 10/15/2009 #12

此外,作为内联函数的参数的引用的处理方式可能与指针不同。

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

许多编译器在内联指针版本时,实际上会强制写入内存(我们显式获取地址)。但是,他们会将引用保留在更优化的寄存器中。

当然,对于未内联的函数,指针和引用会生成相同的代码,如果函数未修改和返回内部函数,则按值传递内部函数总是比按引用传递内部函数更好。

17赞 kriss 1/29/2010 #13

另一个区别是,您可以有指向 void 类型的指针(这意味着指向任何内容的指针),但禁止引用 void。

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

我不能说我对这种特殊的差异感到非常满意。我更希望它被允许对任何带有地址的东西进行含义引用,否则引用的行为相同。它允许使用引用定义一些 C 库函数的等效项,例如 memcpy。

37赞 Kunal Vyas 5/21/2011 #14

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

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

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 引用的值,而不是引用本身。赋值后,两个引用仍引用其原始对象,并且这些对象的值现在也相同。

评论

0赞 Ben Voigt 7/22/2017
“引用总是引用一个对象”是完全错误的
18赞 Andrzej 2/6/2012 #15

指针和引用之间有一个根本区别,我没有看到任何人提到过:引用在函数参数中启用按引用传递语义。指针虽然一开始不可见,但并不可见:它们只提供按值传递的语义。这在本文中已经很好地描述过。

问候 &rzej

评论

1赞 Ben Voigt 11/8/2013
引用和指针都是句柄。它们都为您提供了通过引用传递对象的语义,但句柄是复制的。没有区别。(还有其他方法可以拥有句柄,例如用于在字典中查找的键)
0赞 Andrzej 11/12/2013
我以前也是这样想的。但请参阅链接的文章,说明为什么不是这样。
2赞 Ben Voigt 11/12/2013
@Andrzj:这只是我评论中一句话的很长版本:句柄被复制了。
0赞 Asim 12/12/2013
我需要对此“句柄被复制”的更多解释。我理解一些基本概念,但我认为从物理上讲,引用和指针都指向变量的内存位置。是不是像别名存储值变量并在变量的值是变化或其他东西时更新它?我是新手,请不要将其标记为愚蠢的问题。
1赞 Miles Rout 4/27/2014
@Andrzej 错误。在这两种情况下,都会发生按值传递。引用按值传递,指针按值传递。否则会让新手感到困惑。
30赞 fatma.ekici 1/2/2013 #16

引用是另一个变量的别名,而指针保存变量的内存地址。引用通常用作函数参数,因此传递的对象不是副本,而是对象本身。

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 
21赞 tanweer alam 2/26/2013 #17

引用不是为某些内存赋予的另一个名称。它是一个不可变的指针,在使用时自动取消引用。基本上可以归结为:

int& j = i;

它在内部变成

int* const j = &i;

评论

15赞 jogojapan 2/26/2013
这不是 C++ 标准所说的,编译器不需要按照您的答案描述的方式实现引用。
0赞 Ben Voigt 8/27/2015
@jogojapan:C++编译器实现引用的任何有效方式也是实现指针的有效方式。这种灵活性并不能证明引用和指针之间存在差异。const
2赞 jogojapan 8/27/2015
@BenVoigt 一个概念的任何有效实现也可能是另一个概念的有效实现,但从这两个概念的定义来看,这并不是一个明显的方法。一个好的答案应该从定义开始,并证明为什么关于两者最终相同的说法是正确的。这个答案似乎是对其他一些答案的某种评论。
0赞 sp2danny 9/6/2017
引用对象的另一个名称。编译器可以有任何类型的实现,只要你不能分辨出区别,这就是所谓的“假设”规则。这里重要的部分是你无法分辨出区别。如果发现指针没有存储,则编译器出错。如果可以发现引用没有存储,则编译器仍然符合要求。
14赞 Arlene Batada 3/15/2013 #18

该程序可能有助于理解问题的答案。这是一个由引用“j”和指向变量“x”的指针“ptr”组成的简单程序。

#include<iostream>

using namespace std;

int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"

cout << "x=" << x << endl;

cout << "&x=" << &x << endl;

cout << "j=" << j << endl;

cout << "&j=" << &j << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr=" << ptr << endl;

cout << "&ptr=" << &ptr << endl;
    getch();
}

运行程序并查看输出,您就会明白。

另外,请抽出 10 分钟观看此视频: https://www.youtube.com/watch?v=rlJrrGV0iOg

88赞 Cort Ammon 9/1/2013 #19

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

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

举个例子:

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]。这很容易做到。 i 永远不会小于 2,因此 array[i] 永远不能引用 array[1]。maybeModify() 被赋予 a0 作为引用(别名数组 [0])。因为没有“引用”算术,编译器只需要证明 maybeModify 永远不会得到 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];
    }
}

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

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

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

编辑:在发布这个答案五年后,我发现了一个实际的技术差异,其中引用不同,而不仅仅是看待同一寻址概念的不同方式。引用可以修改临时对象的生存期,而指针则无法修改。

F createF(int argument);

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

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

评论

0赞 Cort Ammon 9/11/2013
诚然,身体必须是可见的。但是,确定它不采用与此相关的任何内容的地址,比证明一堆指针算术不会发生要容易得多。maybeModifyx
0赞 Ben Voigt 9/11/2013
我相信优化器已经出于一堆其他原因进行了“一堆指针算术不会发生”检查。
0赞 underscore_d 10/12/2015
“引用与指针非常相似”——从语义上讲,在适当的上下文中——但就生成的代码而言,仅在某些实现中,而不是通过任何定义/要求。我知道你已经指出了这一点,而且我并不反对你的任何帖子,但我们已经有太多问题了,人们过多地阅读了速记描述,比如“参考文献就像/通常作为指针实现”。
1赞 Ben Voigt 12/4/2015
我有一种感觉,有人错误地将评论标记为过时的评论,上面的其他评论正在讨论void maybeModify(int& x) { 1[&x]++; }
23赞 Life 1/13/2014 #20

这是基于教程。所写的内容使其更清楚:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

只是为了记住,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

更重要的是,正如我们几乎可以参考任何指针教程一样,指针是一个由指针算术支持的对象,它使指针类似于数组。

请看下面的语句,

int Tom(0);
int & alias_Tom = Tom;

alias_Tom可以理解为一个(与 不同,这是)。也可以忘记这种语句的术语是创建 的引用。alias of a variabletypedefalias of a typeTomTom

评论

1赞 Misgevolution 6/16/2015
如果一个类有一个引用变量,它应该在初始化列表中使用 nullptr 或有效对象进行初始化。
1赞 underscore_d 10/12/2015
这个答案中的措辞太令人困惑,没有多大实际用处。另外,@Misgevolution,您是否认真建议读者使用 ?你有没有真正阅读过这个线程的任何其他部分,或者......?nullptr
1赞 Misgevolution 10/12/2015
我的错,对不起我说的那句愚蠢的话。那时我一定被剥夺了睡眠。“使用 nullptr 初始化”是完全错误的。
17赞 Tory 10/15/2014 #21

冒着增加混淆的风险,我想加入一些输入,我敢肯定这主要取决于编译器如何实现引用,但在 gcc 的情况下,引用只能指向堆栈上的变量的想法实际上是不正确的,举个例子:

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

输出:

THIS IS A STRING
0xbb2070 : 0xbb2070

如果您注意到甚至内存地址完全相同,这意味着引用已成功指向堆上的变量!现在,如果你真的想变得怪异,这也有效:

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

输出:

THIS IS A STRING

因此,引用是引擎盖下的指针,它们都只是存储一个内存地址,地址指向的位置无关紧要,您认为如果我调用 std::cout << str_ref会发生什么;调用删除后&str_ref?好吧,显然它编译得很好,但在运行时会导致分段错误,因为它不再指向一个有效的变量,我们基本上有一个仍然存在的损坏引用(直到它超出范围),但毫无用处。

换句话说,引用只不过是一个指针,它抽象了指针机制,使其更安全、更易于使用(没有意外的指针数学,没有混淆“.”和“->”等),假设你没有尝试任何废话,就像我上面的例子一样;)

现在,无论编译器如何处理引用,它总是在后台有某种指针,因为引用必须引用特定内存地址上的特定变量才能按预期工作,因此无法绕过这一点(因此称为“引用”)。

对于引用,唯一需要记住的主要规则是,它们必须在声明时定义(除了标头中的引用,在这种情况下,它必须在构造函数中定义,在构造它所包含的对象之后,定义它为时已晚)。

请记住,我上面的例子就是这样,举例说明什么是引用,你永远不会想以这些方式使用引用!为了正确使用参考资料,这里已经有很多答案,一针见血

38赞 Lightness Races in Orbit 10/30/2014 #22

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

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

除此之外,指针和引用之间当然存在一些实际差异。使用它们的语法显然不同,你不能“重新放置”引用、引用虚无或指向引用的指针。

11赞 George R 12/27/2014 #23

也许一些比喻会有所帮助; 在您的桌面屏幕空间的上下文中 -

  • 引用要求您指定实际窗口。
  • 指针需要屏幕上一段空间的位置,以确保它将包含该窗口类型的零个或多个实例。
20赞 Destructor 2/9/2015 #24

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

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

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

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}
7赞 Zorgiev 4/24/2016 #25

不同之处在于,非常量指针变量(不要与指向常量的指针混淆)可能会在程序执行期间的某个时间更改,需要使用指针语义(&,*)运算符,而引用只能在初始化时设置(这就是为什么您只能在构造函数初始值设定项列表中设置它们,但不能以某种方式设置它们)并使用普通值访问语义。基本上,引入了引用,以允许支持运算符重载,正如我在一些非常古老的书中读到的那样。正如有人在此线程中所说 - 指针可以设置为 0 或您想要的任何值。0(NULL, nullptr) 表示指针初始化时不包含任何内容。取消引用 null 指针是错误的。但实际上,指针可能包含一个值,该值不指向某个正确的内存位置。反过来,引用会尽量不允许用户初始化对无法引用的内容的引用,因为您始终为其提供正确类型的右值。虽然有很多方法可以使引用变量初始化到错误的内存位置 - 但你最好不要深入挖掘细节。在机器级别上,指针和参考都统一工作 - 通过指针。假设在基本参考文献中是句法糖。右值引用与此不同 - 它们自然是堆栈/堆对象。

8赞 dhokar.w 1/6/2017 #26

指针和引用之间的区别

指针可以初始化为 0,引用不能初始化。事实上,引用也必须引用对象,但指针可以是空指针:

int* p = 0;

但是我们不能拥有而且.int& p = 0;int& p=5 ;

事实上,要正确地做到这一点,我们必须首先声明并定义一个对象,然后我们才能引用该对象,因此前面代码的正确实现将是:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

另一个重要的一点是,我们可以在不初始化的情况下声明指针,但是在引用的情况下不能做这样的事情,因为引用必须始终引用变量或对象。但是,这种指针的使用是有风险的,因此通常我们会检查指针是否真的指向某些内容。在引用的情况下,不需要进行此类检查,因为我们已经知道在声明期间引用对象是强制性的。

另一个区别是指针可以指向另一个对象,但引用始终引用同一个对象,让我们举个例子:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

另一点:当我们有一个像 STL 模板这样的模板时,这种类模板将始终返回引用,而不是指针,以便于使用运算符 [] 读取或分配新值:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

评论

1赞 Revolver_Ocelot 1/6/2017
我们仍然可以拥有.const int& i = 0
1赞 dhokar.w 1/6/2017
在这种情况下,引用将仅在读取中使用,即使使用“const_cast”,我们也无法修改此常量引用,因为“const_cast”只接受指针,不接受引用。
1赞 Revolver_Ocelot 1/7/2017
const_cast与参考文献配合得很好:coliru.stacked-crooked.com/a/eebb454ab2cfd570
1赞 dhokar.w 1/7/2017
您正在对引用进行强制转换,而不是强制转换引用,请尝试此操作;常量 int& i=;const_cast<国际>(一);我试图抛弃引用的恒定性,以便为引用写入和分配新值,但这是不可能的。请集中注意力!!
13赞 Ap31 7/7/2017 #27

我觉得这里还有一点没有涉及。

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

虽然这可能看起来很肤浅,但我相信这个属性对于许多 C++ 功能至关重要,例如:

  • 模板。由于模板参数是鸭型的,因此类型的句法属性才是最重要的,因此通常可以将同一模板与 和 一起使用。
    (或者仍然依赖于隐式转换 到 )
    涵盖两者的模板,甚至更常见。
    TT&std::reference_wrapper<T>T&T&T&&

  • 左值。考虑语句 Without references it will only for c-strings ()。通过引用返回字符允许用户定义的类具有相同的表示法。str[0] = 'X';char* str

  • 复制构造函数。从语法上讲,将对象传递给复制构造函数而不是指向对象的指针是有意义的。但是复制构造函数无法按值获取对象 - 这将导致对同一复制构造函数的递归调用。这样一来,引用就是这里唯一的选择。

  • 运算符重载。使用引用,可以在保留相同中缀表示法的同时,将间接引入运算符调用。这也适用于常规重载函数。operator+(const T& a, const T& b)

这些点赋予了 C++ 和标准库相当一部分功能,因此这是引用的一个主要属性。

评论

0赞 curiousguy 10/23/2017
"隐式强制转换“强制转换是一种语法结构,它存在于语法中;强制转换始终是显式的
4赞 Hitokage 10/19/2017 #28

我总是按照 C++ 核心指南中的这条规则来决定:

当“无参数”是有效选项时,首选 T* 而不是 T&

评论

1赞 Clearer 12/19/2017
使用不接受指针而不是 allow 的重载函数,或者使用终端对象,可以说是更好的解决方案,而不是 allow 作为参数。nullptrnullptr
1赞 Hitokage 12/19/2017
@Clearer 它可能更简洁,但有时您只需要快速传递指针,并且在某些情况下您可能不在乎指针是否为 null。
12赞 Arthur Tacca 11/2/2017 #29

指针和引用之间有一个非常重要的非技术性区别:通过指针传递给函数的参数比通过非常量引用传递给函数的参数更明显。例如:

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

回到 C 语言中,一个看起来像的调用只能按值传递,所以它肯定不能修改;要修改参数,您需要传递指针。因此,如果一个参数前面没有,你知道它不会被修改。(反之,表示修改,是不正确的,因为有时必须通过指针传递大型只读结构。fn(x)xfn(&x)&&const

一些人认为,在阅读代码时,这是一个非常有用的功能,指针参数应该始终用于可修改的参数,而不是非引用,即使函数从不期望 .也就是说,这些人认为不应该允许像上面这样的函数签名。Google的C++风格指南就是一个例子。constnullptrfn3()

4赞 Immac 11/12/2017 #30

我用引用和指针做类比,将引用视为对象的另一个名称,将指针视为对象的地址。

// 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
}
3赞 Michael Zheng 4/27/2018 #31

Taryn♦ 说:

您不能像使用指针那样获取引用的地址。

其实你可以。

我引用了另一个问题的答案

C++ FAQ说得最好:

与指针不同,一旦引用绑定到一个对象,它就不能“重新驻插”到另一个对象。引用本身不是一个对象(它没有标识;获取引用的地址会给你引用者的地址;记住:引用是它的引用)。

评论

7赞 StoryTeller - Unslander Monica 6/17/2018
你给出的引述与你自己的观点相矛盾。它非常清楚地表明引用本身没有地址。
0赞 curiousguy 1/30/2019
在处理命名引用的表达式时,它是您正在处理的引用对象。
4赞 Mark Lakata 8/3/2018 #32

如果遵循传递给函数的参数的约定,则可以使用引用和指针之间的差异。常量引用用于传递到函数中的数据,指针用于传递到函数中的数据。在其他语言中,您可以使用关键字(如 和 )显式表示这一点。在 C++ 中,您可以(按照约定)声明等效项。例如inout

void DoSomething(const Foo& thisIsAnInput, Foo* thisIsAnOutput)
{
   if (thisIsAnOuput)
      *thisIsAnOutput = thisIsAnInput;
}

使用引用作为输入,使用指针作为输出是 Google 风格指南的一部分。

评论

3赞 Sneftel 8/9/2018
指针与引用之间没有固有的输入或输出。重要的区别在于是否存在 .const
1赞 Mark Lakata 8/11/2018
我同意,没有什么本质上是输入或输出的。我提到的只是一个约定(不是我发明的)。该约定是 Google 风格指南的一部分。指针作为输出有一个优点,如果您不需要输出,它们可以为 null。
2赞 Arthur Tacca 10/13/2018
这与我 8 个月前的回答有何不同?它甚至引用了相同的风格指南!
4赞 ebasconp 1/17/2019 #33

除了这里的所有答案,

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

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

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

评论

3赞 curiousguy 1/30/2019
这并不是说它不会被调用:你甚至不能用两个指针参数声明这样的运算符;根据标准草案,“运算符函数应是非静态成员函数,或者是具有至少一个参数的非成员函数,其类型为类、对类的引用、枚举或对枚举的引用([over.oper]/6)
26赞 FrankHB 2/17/2019 #34

直接答案

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

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

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

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

重要的是要知道,对象类型是 C++ 中 universe 类型的顶级类别。引用也是一个顶级类别。但指针不是。

指针和引用在复合类型的上下文中一起提及。这基本上是由于从C继承(并扩展)的声明器语法的性质,C没有引用。(此外,自 C++ 11 以来,有不止一种引用的声明器,而指针仍然是“统一的”:+ vs. 。因此,在这种情况下,用类似风格的 C 来起草一种特定于“扩展”的语言在某种程度上是合理的。(我仍然会争辩说,声明符的语法浪费了很多语法表达能力,使人类用户和实现都令人沮丧。因此,它们都不符合在新语言设计中内置的条件。不过,这是一个关于PL设计的完全不同的话题。&&&*

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

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

顶级类别的差异已经可以揭示许多与指针没有直接关联的具体差异:

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

关于参考文献的更多特殊规则:

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

误解

句法糖

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

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

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

C++ 在这个严格意义上只有几种句法糖。一个实例是(继承自 C)内置(非重载)运算符,它被定义为与内置运算符一元 * 和二进制 + 完全相同,具有特定组合形式的相同语义属性[]

存储

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

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

[intro.object]/1

...一个物体在其建造期间、整个生命周期和破坏期间都占据了一个存储区域。...

[dcl.ref]/4

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

请注意,这些是语义属性。

语用学

即使指针不够限定,无法与语言设计意义上的引用放在一起,但仍然存在一些论点,使得在其他一些上下文中在它们之间做出选择是值得商榷的,例如,在对参数类型进行选择时。

但这还不是全部。我的意思是,除了指针与引用之外,还有更多的事情需要考虑。

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

  • 有时,语言规则明确要求使用特定类型。如果您想使用这些功能,请遵守规则。
    • 复制构造函数需要特定类型的 cv- 引用类型作为第一个参数类型。(通常它应该是合格的。&const
    • 移动构造函数需要特定类型的 cv- 引用类型作为第一个参数类型。(通常不应该有限定词。&&
    • 运算符的特定重载需要引用或非引用类型。例如:
      • 重载为特殊成员函数需要类似于复制/移动构造函数的第一个参数的引用类型。operator=
      • Postfix 需要虚拟 .++int
      • ...
  • 如果您知道按值传递(即使用非引用类型)就足够了,请直接使用它,尤其是在使用支持 C++17 强制复制省略的实现时。(警告:但是,要详尽地推理必要性可能非常复杂
  • 如果你想操作一些具有所有权的句柄,请使用智能指针,如 and(如果你要求它们不透明,甚至可以自己使用自制指针),而不是原始指针。unique_ptrshared_ptr
  • 如果你在一个范围内进行一些迭代,请使用迭代器(或标准库尚未提供的一些范围),而不是原始指针,除非你确信原始指针在非常特定的情况下会做得更好(例如,对于更少的标头依赖)。
  • 如果您知道按值传递就足够了,并且想要一些显式的可为 null 的语义,请使用包装器,例如 ,而不是原始指针。std::optional
  • 如果您知道由于上述原因,按值传递并不理想,并且您不希望使用可为 null 的语义,请使用 {lvalue, rvalue, forwarding}-references。
  • 即使你确实需要像传统指针这样的语义,通常也有更合适的东西,比如在 Library Fundamental TS 中。observer_ptr

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

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

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

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

语言中立性注意事项

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

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

C++ 中的引用可能不会保留不同语言的含义。例如,引用通常不会像 C++ 中那样对值表示非空属性,因此此类假设在其他一些语言中可能不起作用(并且您很容易找到反例,例如 Java、C# 等)。

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

(旁注:这个问题可能比任何“类C”语言都重要,比如ALGOL 68 vs.PL/I

评论

0赞 Caleth 9/30/2022
吹毛求疵:“使用迭代器而不是指针”指针对迭代器概念进行建模。例如,符合要求的实现可以选择作为别名std::vector<T>::iteratorT*
0赞 Jan Schultke 9/28/2023
下标运算符不是 for 的语法糖。在临时物化、价值类别等方面存在细微差别。老实说,我不确定 C++ 是否有任何真正的语法糖,没有附加星号。*(a + b)
8赞 Xitalogy 8/1/2019 #35

有关引用和指针的一些关键相关详细信息

指针

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

引用

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

尽管指针和引用的实现方式与“后台”大致相同,但编译器对它们的处理方式不同,从而导致上述所有差异。

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

Arrays, Pointers and References Under the Hood 深入文章

评论

1赞 HolyBlackCat 8/1/2019
我建议将文章中的要点添加到答案本身中。通常不鼓励仅链接答案,请参阅 stackoverflow.com/help/deleted-answers
0赞 Xitalogy 8/1/2019
@HolyBlackCat我想知道这一点。本文篇幅长而深入,从第一性原理发展到包含大量代码示例和内存转储的深入处理,最后通过练习进一步发展深入的代码示例和解释。它还有很多图表。我将尝试弄清楚如何直接将一些关键点放在这里,但现在不确定如何以最佳方式做到这一点。非常感谢您的意见。在我的答案被删除之前,我会尽力而为。
1赞 Gerard ONeill 8/6/2019 #36

“我知道引用是语法糖,所以代码更容易阅读和编写”

这。引用不是实现指针的另一种方式,尽管它涵盖了一个巨大的指针用例。指针是一种数据类型,通常指向实际值的地址。但是,它可以设置为零,或者使用地址算术等设置为地址后面的几个位置。引用是具有自身值的变量的“语法糖”。

C 只有传递值语义。获取变量所引用的数据的地址并将其发送到函数是一种通过“引用”传递的方法。引用通过“引用”原始数据位置本身在语义上对此进行了快捷方式。所以:

int x = 1;
int *y = &x;
int &z = x;

Y 是一个 int 指针,指向 x 的存储位置。 X 和 Z 表示相同的存储位置(堆栈或堆)。

很多人都谈到了两者(指针和引用)之间的区别,就好像它们是同一回事,但用法不同。它们根本不一样。

1)“指针可以重新赋值任意次数,而引用在绑定后不能重新赋值”——指针是指向数据的地址数据类型。引用是数据的另一个名称。因此,您可以“重新分配”引用。您只是无法重新分配它所引用的数据位置。就像你不能改变“x”所指的数据位置一样,你不能对“z”这样做。

x = 2;
*y = 2;
z = 2;

一样。这是一次重新分配。

2)“指针可以无处指向(NULL),而引用总是引用一个对象”——再次令人困惑。引用只是对象的另一个名称。空指针意味着(语义上)它没有引用任何内容,而引用是通过说它是“x”的另一个名称来创建的。因为

3)“你不能像使用指针那样获取引用的地址”——是的,你可以。再次令人困惑。如果试图查找用作引用的指针的地址,那就是一个问题 -- 因为引用不是指向对象的指针。他们是对象。所以你可以得到对象的地址,你可以得到指针的地址。因为它们都获取数据的地址(一个是对象在内存中的位置,另一个是指向内存中对象位置的指针)。

int *yz = &z; -- legal
int **yy = &y; -- legal

int *yx = &x; -- legal; notice how this looks like the z example.  x and z are equivalent.

4)“没有”参考算术“” - 再次令人困惑 - 因为上面的例子有z是对x的引用,因此两者都是整数,“参考”算术意味着例如将1添加到x引用的值。

x++;
z++;

*y++;  // what people assume is happening behind the scenes, but isn't. it would produce the same results in this example.
*(y++);  // this one adds to the pointer, and then dereferences it.  It makes sense that a pointer datatype (an address) can be incremented.  Just like an int can be incremented. 
1赞 Hitesh Jangid 8/30/2019 #37

指针(*)的基本含义是“地址值”,这意味着无论您提供什么地址,它都会在该地址上提供值。更改地址后,它将给出新值,而引用变量用于引用任何特定变量,并且将来不能更改以引用任何其他变量。

8赞 S.S. Anne #38

以下答案和链接摘要:

  1. 指针可以重新赋值任意次数,而引用在绑定后不能重新赋值。
  2. 指针可以不指向任何地方(),而引用始终引用对象。NULL
  3. 您不能像使用指针那样获取引用的地址。
  4. 没有“引用算术”(但您可以获取引用指向的对象的地址,并对其执行指针算术,如 )。&obj + 5

澄清误解:

C++ 标准非常小心地避免规定编译器如何 实现引用,但每个 C++ 编译器都实现 引用作为指针。也就是说,声明如下:

int &ri = i;

如果它没有完全优化则分配相同数量的存储 作为指针,并放置地址 的 i 进入那个存储。

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

作为一般规则,

  • 在函数参数和返回类型中使用引用来提供有用的自记录接口。
  • 使用指针实现算法和数据结构。

有趣的阅读:

8赞 Sadhana Singh 1/15/2020 #39

简单来说,我们可以说引用是变量的替代名称,而 指针是一个变量,用于保存另一个变量的地址。 例如

int a = 20;
int &r = a;
r = 40;  /* now the value of a is changed to 40 */

int b =20;
int *ptr;
ptr = &b;  /*assigns address of b to ptr not the value */
12赞 Lewis Kelsey 6/4/2020 #40

引用是常量指针。 与 相同。这就是为什么他们的不是 const 引用,因为它已经是 const,而对 const 的引用是 .当您使用 -O0 进行编译时,编译器会在这两种情况下将 b 的地址放在堆栈上,并且作为类的成员,它也将出现在堆栈/堆上的对象中,与您声明 const 指针时相同。使用 -Ofast,可以自由地优化这一点。常量指针和引用都进行了优化。int * const a = &bint& a = bconst int * const a

与 const 指针不同,没有办法获取引用本身的地址,因为它将被解释为它所引用的变量的地址。因此,在 -Ofast 上,表示引用的 const 指针(被引用变量的地址)将始终在堆栈外进行优化,但是如果程序绝对需要实际 const 指针的地址(指针本身的地址,而不是它指向的地址),即打印 const 指针的地址, 然后 const 指针将放置在堆栈上,以便它有一个地址。

否则,它是相同的,即当您打印该地址时,它指向:

#include <iostream>

int main() {
  int a =1;
  int* b = &a;
  std::cout << b ;
}

int main() {
  int a =1;
  int& b = a;
  std::cout << &b ;
}
they both have the same assembly output
-Ofast:
main:
        sub     rsp, 24
        mov     edi, OFFSET FLAT:_ZSt4cout
        lea     rsi, [rsp+12]
        mov     DWORD PTR [rsp+12], 1
        call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<void const*>(void const*)
        xor     eax, eax
        add     rsp, 24
        ret
--------------------------------------------------------------------
-O0:
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-12], 1
        lea     rax, [rbp-12]
        mov     QWORD PTR [rbp-8], rax
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)
        mov     eax, 0
        leave
        ret

指针已在堆栈外进行了优化,在这两种情况下,指针甚至没有在 -Ofast 上取消引用,而是使用编译时值。

作为对象的成员,它们在 -O0 到 -Ofast 上是相同的。

#include <iostream>
int b=1;
struct A {int* i=&b; int& j=b;};
A a;
int main() {
  std::cout << &a.j << &a.i;
}

The address of b is stored twice in the object. 

a:
        .quad   b
        .quad   b
        mov     rax, QWORD PTR a[rip+8] //&a.j
        mov     esi, OFFSET FLAT:a //&a.i

当您通过引用传递时,在 -O0 上,您传递被引用变量的地址,因此它与通过指针传递相同,即常量指针包含的地址。在 -Ofast 上,如果函数可以内联,则编译器会在内联调用中对其进行优化,因为动态范围是已知的,但在函数定义中,参数始终作为指针被取消引用(期望引用引用的变量的地址),其中它可能被另一个翻译单元使用,并且编译器不知道动态范围, 当然,除非该函数被声明为静态函数,否则它不能在转换单元之外使用,然后它按值传递,只要它没有通过引用在函数中修改,那么它将传递你正在传递的引用所引用的变量的地址,在 -Ofast 上,这将在寄存器中传递,如果有足够的,则将其保留在堆栈之外调用约定中的易失性寄存器。

3赞 Sarath Govind 6/30/2021 #41

指针是一个变量,它保存另一个变量的内存地址,其中作为引用的是现有变量的别名。(已存在变量的另一个名称)

1. 指针可以初始化为:

int b = 15;
int *q = &b;

int *q;
q = &b;

其中作为参考,

int b=15;
int &c=b;

(一步声明和初始化)

  1. 可以将指针分配给 null,但不能将引用指定为 null
  2. 可以对指针执行各种算术运算,而没有所谓的参考算术。
  3. 指针可以重新赋值,但引用不能
  4. 指针在堆栈上有自己的内存地址和大小,而引用共享相同的内存地址

评论

0赞 einpoklum 11/11/2021
引用并不总是现有变量的别名。引用可以延长临时对象的生存期。
-1赞 user8616480 9/22/2021 #42

不能像指针那样取消引用,当取消引用时,指针会在该位置提供值,

不过,引用和指针都可以通过地址工作......

所以

你可以这样做

int* val = 0xDEADBEEF; *val 是 0xDEADBEEF 的东西。

你不能这样做 int& val = 1;

*不允许使用val。

4赞 Ezh 9/27/2022 #43

把指针想象成一张名片:

  • 它让您有机会联系某人
  • 它可以是空的
  • 它可能包含错误或过时的信息
  • 你不确定上面提到的某个人是否还活着
  • 你不能直接对着卡片说话,你只能用它来给别人打电话
  • 也许有很多这样的卡片存在

将参考视为与某人的主动通话:

  • 你很确定你联系过的人还活着
  • 您可以直接通话,无需额外通话
  • 你很确定你不会对一个空旷的地方或一块垃圾说话
  • 你不能确定你是唯一一个正在与这个物体交谈的人

评论

0赞 Maf 3/30/2023
是的,引用必须在原始变量(i..e 引用位于堆栈中原始变量的顶部)。
-1赞 oreubens 4/13/2023 #44

上面有很多有价值的信息,但我对新手 C++ 程序员的建议。

避免使用指针,仅在别无选择的情况下使用指针(因为您正在与仅接受指针的库进行交互)。 我知道很多书籍和教程都是这样做的,但是让新手编写代码来操作原始指针并不是向新程序员介绍该语言的好方法。

如果不需要能够更改为所指向的对象,请使用引用。 如果确实需要更改指向的内容,请使用智能指针。

虽然从“技术上讲”你可以构造一个指向虚假数据的引用,但这要求你已经做了一些糟糕的指针操作,或者你通过引用传递了一些没有立即使用的东西,而是在对象被销毁后存储和使用。 假设引用指向有效构造的对象是完全可以接受的(也是很好的形式)。您不应该编写一堆代码来测试引用是否指向有效对象。

指针具有“指向无”(NULL 或 nullptr)的额外复杂性 您可能需要检查。 指针增加了“谁(以及如何)负责清理指针指向的数据”的复杂性。