ChatGPT解决这个技术问题 Extra ChatGPT

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

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

我认为第 2 点应该是“允许指针为 NULL,但引用不是。只有格式错误的代码才能创建 NULL 引用并且其行为未定义。”
指针只是另一种类型的对象,并且与 C++ 中的任何对象一样,它们可以是变量。另一方面,引用永远不是对象,只是变量。
编译时没有警告:gcc 上的 int &x = *(int*)0;。引用确实可以指向NULL。
参考是一个变量别名
我喜欢第一句话完全是谬论。引用有自己的语义。

2
26 revs, 14 users 42%

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


...但是取消引用 NULL 是未定义的。例如,您无法测试引用是否为 NULL(例如,&ref == NULL)。
数字 2 不正确。引用不仅仅是“同一变量的另一个名称”。引用可以以与指针非常相似的方式传递给函数、存储在类中等。它们独立于它们指向的变量而存在。
布赖恩,堆栈不相关。引用和指针不必占用堆栈空间。它们都可以在堆上分配。
Brian,变量(在本例中为指针或引用)需要空间这一事实并不意味着它需要堆栈上的空间。指针和引用可能不仅指向堆,它们实际上可能分配在堆上。
另一个重要的区别:引用不能填充到数组中
P
Peter Mortensen

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

reference 可以被认为是具有自动间接寻址的 常量指针(不要与指向常量值的指针混淆!),即编译器将应用 {1 } 运算符为您服务。

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

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

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

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

尽管引用通常使用底层汇编语言中的地址来实现,但请不要将引用视为指向对象的有趣指针。引用是对象。它不是指向对象的指针,也不是对象的副本。它是对象。

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

为什么我认为 C++ 参考很有用

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

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


@kriss:不,您还可以通过按引用返回自动变量来获得悬空引用。
@kriss:在一般情况下,编译器几乎不可能检测到。考虑一个返回对类成员变量的引用的成员函数:这是安全的,编译器不应禁止。然后具有该类的自动实例的调用者调用该成员函数并返回引用。 Presto:悬空参考。是的,这会造成麻烦,@kriss:这就是我的观点。许多人声称引用优于指针的一个优点是引用总是有效的,但事实并非如此。
@kriss:不,对自动存储持续时间对象的引用与临时对象非常不同。无论如何,我只是为您的陈述提供一个反例,即您只能通过取消引用无效指针来获得无效引用。 Christoph 是正确的——引用并不比指针更安全,只使用引用的程序仍然会破坏类型安全。
引用不是一种指针。它们是现有对象的新名称。
@catphive:如果您按照语言语义,则为 true,如果您实际查看实现,则不是 true; C++ 是一种比 C 更“神奇”的语言,如果你从引用中移除魔法,你最终会得到一个指针
P
Peter Mortensen

如果你想真正迂腐,你可以用引用做一件你不能用指针做的事情:延长临时对象的生命周期。在 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 的情况下尝试此操作,它应该无法编译。您不能将非常量引用绑定到临时对象,也不能为此获取其地址。


但是这个用例是什么?
好吧,s3_copy 将创建一个临时文件,然后将其复制到 s3_copy 中,而 s3_reference 直接使用临时文件。然后要真正迂腐,您需要查看返回值优化,允许编译器在第一种情况下省略复制构造。
@digitalSurgeon:那里的魔法非常强大。 const & 绑定的事实延长了对象的生命周期,并且仅当引用超出范围时,实际引用类型的析构函数(与引用类型相比,这可能是base) 被调用。由于它是参考,因此不会在两者之间进行切片。
C++11 更新:最后一句应为“您不能将非常量左值引用绑定到临时对象”,因为您可以将非常量右值引用绑定到临时对象,并且它具有相同的生命周期延长行为。
@AhmadMushtaq:它的主要用途是派生类。如果不涉及继承,您不妨使用值语义,由于 RVO/move 构造,这将是便宜或免费的。但是如果您有 Animal x = fast ? getHare() : getTortoise(),那么 x 将面临经典的切片问题,而 Animal& x = ... 将正常工作。
佚名

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

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

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

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

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


我认为这是迄今为止最好的答案。其他人谈论引用和指针就像它们是不同的野兽,然后列出它们在行为上的不同之处。恕我直言,这并没有让事情变得更容易。我一直将引用理解为具有不同语法糖的 T* const(这恰好从您的代码中消除了很多 * 和 & )。
“一个常量指针的目标可以通过获取它的地址并使用常量转换来替换。”这样做是未定义的行为。有关详细信息,请参阅 stackoverflow.com/questions/25209838/…
尝试更改引用的所指对象或 const 指针(或任何 const 标量)的值都是非法的。您可以做什么:删除由隐式转换添加的 const 限定条件:int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci); 可以。
这里的区别是UB与字面上不可能。 C++ 中没有可以让您更改参考点的语法。
并非不可能,更难的是,您可以访问正在建模该引用并更改其内容的指针的内存区域。这当然可以做到。
M
Mark Ransom

与流行的观点相反,可以有一个为 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);

我想重申,获得空引用的唯一方法是通过格式错误的代码,一旦你拥有它,你就会得到未定义的行为。 从不检查空引用是有意义的;例如,您可以尝试 if(&bar==NULL)... 但编译器可能会优化该语句不存在!有效的引用永远不能为 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 References

有关取消引用空指针的危险的另一个示例,请参见 Raymond Chen 的 Exposing undefined behavior when trying to port code to another platform


有问题的代码包含未定义的行为。从技术上讲,你不能对空指针做任何事情,除了设置它并比较它。一旦你的程序调用了未定义的行为,它就可以做任何事情,包括在你给大老板演示之前看起来正常工作。
mark 有一个有效的参数。指针可能为 NULL 并且因此您必须检查的参数也不是真实的:如果您说函数需要非 NULL,那么调用者必须这样做。因此,如果调用者不这样做,他将调用未定义的行为。就像标记对坏参考所做的那样
描述是错误的。此代码可能会或可能不会创建 NULL 引用。它的行为是未定义的。它可能会创建一个完全有效的参考。它可能根本无法创建任何引用。
@David Schwartz,如果我在谈论事情必须按照标准工作的方式,那么你是对的。但这不是我在说的——我说的是使用非常流行的编译器实际观察到的行为,并根据我对典型编译器和 CPU 架构的了解推断可能会发生的事情。如果您认为引用优于指针,因为它们更安全并且不认为引用可能是坏事,那么有一天您会像我一样被一个简单的问题难住。
取消引用空指针是错误的。任何这样做的程序,即使是初始化引用也是错误的。如果您从指针初始化引用,则应始终检查指针是否有效。即使这成功了,底层对象也可能随时被删除,留下引用来引用不存在的对象,对吧?你说的是好东西。我认为这里真正的问题是,当您看到引用时,不需要检查“nullness”,并且至少应该断言指针。
P
Peter Mortensen

你忘记了最重要的部分:

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

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


@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 = 新节点;富(酒吧); -- OP:你熟悉右值和左值的概念吗?
智能指针两者都有。 (智能指针类的方法)和 -> (基础类型的方法)。
@user6105 Orion Edwards 声明实际上是 100% 正确的。 “访问 [the] de-referenced pointer 的成员” 指针没有任何成员。指针所指的对象有成员,而对这些成员的访问正是 -> 为指针引用所提供的,就像指针本身一样。
为什么 .-> 与 vi 和 emacs 有关系 :)
@artM - 这是个玩笑,对非英语母语人士来说可能没有意义。我很抱歉。解释一下,vi 是否优于 emacs 完全是主观的。有些人认为 vi 要好得多,而另一些人则认为恰恰相反。同样,我认为使用 . 比使用 -> 更好,但就像 vi 与 emacs 一样,它完全是主观的,你无法证明任何事情
C
Cort Ammon

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

引用的设计使得编译器更容易跟踪哪些引用别名哪些变量。两个主要特性非常重要:没有“参考算术”和没有重新分配参考。这些允许编译器在编译时找出哪些引用别名哪些变量。

允许引用引用没有内存地址的变量,例如编译器选择放入寄存器的变量。如果您获取局部变量的地址,编译器很难将其放入寄存器中。

举个例子:

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

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


诚然,身体必须是可见的。但是,确定 maybeModify 不获取与 x 相关的任何地址比证明一堆指针算术不发生要容易得多。
我相信优化器已经做了“一堆指针算术不会发生”检查其他一些原因。
“引用与指针非常相似” - 在语义上,在适当的上下文中 - 但就生成的代码而言,仅在某些实现中而不是通过任何定义/要求。我知道你已经指出了这一点,我不同意你的任何帖子在实践中,但我们已经有太多的问题,人们阅读了太多的速记描述,比如“引用就像/通常实现为指针” .
我感觉有人错误地将 void maybeModify(int& x) { 1[&x]++; } 的评论标记为过时,上面的其他评论正在讨论
P
Peter Mortensen

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

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

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

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

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

int a = 0;
int& b = a;

在这里,您没有创建另一个指向 a 的变量;您只是在保存 a 值的内存内容中添加另一个名称。该内存现在有两个名称,ab,可以使用任一名称对其进行寻址。

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

int a;
increment(a);

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

现在可能有一些情况,您的编译器在编译时可能无法知道引用,例如使用 extern 变量时。因此,引用可能会也可能不会被实现为底层代码中的指针。但是在我给你的例子中,它很可能不会用指针来实现。


引用是对左值的引用,不一定是对变量的引用。因此,它更接近指针而不是真正的别名(编译时构造)。可以引用的表达式示例有 *p 甚至 *p++
对,我只是指出这样一个事实,即引用可能并不总是像新指针那样将新变量压入堆栈。
@VincentRobert:它的作用与指针相同……如果函数是内联的,则引用和指针都将被优化掉。如果有函数调用,则需要将对象的地址传递给函数。
int *p = NULL; int &r=*p;指向 NULL 的引用; if(r){} -> 轰隆隆 ;)
这种对编译阶段的关注似乎很好,直到您记住可以在运行时传递引用,此时静态别名就消失了。 (然后,引用通常实现为指针,但标准不需要这种方法。)
C
Cole Tobin

引用永远不能是 NULL


有关反例,请参阅 Mark Ransom 的答案。这是关于引用的最常被断言的神话,但它是一个神话。根据标准,您拥有的唯一保证是,当您有一个 NULL 引用时,您立即拥有 UB。但这类似于说“这辆车很安全,它永远不会离开道路。(如果你把它开到路上可能发生的事情,我们不承担任何责任。它可能会爆炸。)”
@cmaster:在有效程序中,引用不能为空。但是指针可以。这不是神话,这是事实。
@Mehrdad 是的,有效的程序还在路上。但是没有流量障碍可以强制您的程序实际执行。大部分道路实际上缺少标记。所以晚上下车非常容易。并且对于调试您知道可能发生的此类错误至关重要:空引用可以在程序崩溃之前传播,就像空指针一样。当它出现时,您有像 void Foo::bar() { virtual_baz(); } 这样的段错误代码。如果您不知道引用可能为 null,则无法将 null 追溯到其来源。
int *p = NULL; int &r=*p;指向 NULL 的引用; if(r){} -> boOm ;) –
@sree int &r=*p; 是未定义的行为。那时,您没有“指向 NULL 的引用”,您有一个无法再对完全进行推理的程序。
L
Lightness Races in Orbit

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

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

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


А
Андрей Беньковский

虽然引用和指针都用于间接访问另一个值,但引用和指针之间有两个重要区别。第一个是引用总是引用一个对象:定义一个引用而不初始化它是错误的。赋值的行为是第二个重要区别:赋值给引用会改变引用绑定的对象;它不会重新绑定对另一个对象的引用。一旦初始化,一个引用总是指向同一个底层对象。

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

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 引用的值,而不是引用本身。赋值后,这两个引用仍然引用它们原来的对象,这些对象的值现在也一样了。


“引用总是指向一个对象”是完全错误的
f
fatma.ekici

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

    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. 

F
FrankHB

直接回答

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

C++中的指针是什么?一些特定的类型实例,它是对象类型。

the ISO C++ definition of object type

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

重要的是要知道,对象类型是 C++ 中类型宇宙的顶级类别。参考也是顶级类别。但指针不是。

指针和引用一起提到in the context of compound type。这基本上是由于从 C 继承(和扩展)的声明符语法的性质,它没有引用。 (此外,自 C++ 11 以来有不止一种引用声明符,而指针仍然是“单一类型的”:&+&& vs. *。)因此,通过“扩展”来起草一种特定的语言在这种情况下,C 的风格是有些合理的。 (我仍然会争辩说,声明符的语法大量浪费了语法表达能力,使人类用户和实现都感到沮丧。因此,它们都没有资格内置 em> 在一种新的语言设计中。不过,这是关于 PL 设计的完全不同的主题。)

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

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

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

对象类型可以具有顶级 cv 限定符。引用不能。

根据抽象机器语义,对象类型的变量确实占用了存储空间。参考不必占用存储空间(详见下文关于误解的部分)。

...

还有一些关于引用的特殊规则:

复合声明器对引用的限制更大。

引用可以折叠。在模板参数推导过程中,基于引用折叠的 && 参数(作为“转发引用”)的特殊规则允许参数的“完美转发”。

在模板参数推导过程中,基于引用折叠的 && 参数(作为“转发引用”)的特殊规则允许参数的“完美转发”。

引用在初始化中有特殊的规则。声明为引用类型的变量的生命周期可以通过扩展与普通对象不同。顺便说一句,其他一些上下文(例如涉及 std::initializer_list 的初始化)遵循一些类似的引用生命周期扩展规则。这是另一罐蠕虫。

顺便说一句,其他一些上下文(例如涉及 std::initializer_list 的初始化)遵循一些类似的引用生命周期扩展规则。这是另一罐蠕虫。

...

误解

语法糖

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

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

(类似地,lambda-expressions 不是 C++ 中任何其他功能的句法糖,因为它不能用像 the declaration order of the captured variables 这样的“未指定”属性精确模拟,这可能是很重要,因为这些变量的初始化顺序可能很重要。)

从严格意义上讲,C++ 只有几种语法糖。一个实例是(继承自 C)内置(非重载)运算符 [],即 is defined exactly having same semantic properties of specific forms of combination over built-in operator unary * and binary +

贮存

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

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

[intro.object]/1

... 一个对象在其建造期间、其整个生命周期和其销毁期间占据一个存储区域。 ...

[dcl.ref]/4

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

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

语用学

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

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

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

有时,语言规则明确要求使用特定类型。如果您想使用这些功能,请遵守规则。复制构造函数需要特定类型的 cv-& 引用类型作为第一个参数类型。 (通常它应该是 const 限定的。)移动构造函数需要特定类型的 cv-&& 引用类型作为第一个参数类型。 (通常不应该有限定符。)运算符的特定重载需要引用或非引用类型。例如:作为特殊成员函数的重载 operator= 需要类似于复制/移动构造函数的第一个参数的引用类型。 Postfix ++ 需要虚拟 int。 ...

复制构造函数需要特定类型的 cv-& 引用类型作为第一个参数类型。 (通常它应该是 const 限定的。)

移动构造函数需要特定类型的 cv-&& 引用类型作为第一个参数类型。 (通常不应该有限定词。)

运算符的特定重载需要引用或非引用类型。例如:作为特殊成员函数的重载 operator= 需要类似于复制/移动构造函数的第一个参数的引用类型。 Postfix ++ 需要虚拟 int。 ...

重载 operator= 作为特殊成员函数需要类似于复制/移动构造函数的第一个参数的引用类型。

Postfix ++ 需要虚拟 int。

...

如果您知道按值传递(即使用非引用类型)就足够了,请直接使用它,尤其是在使用支持 C++17 强制复制省略的实现时。 (警告:然而,详尽地推理必要性可能非常复杂。)

如果您想使用所有权操作某些句柄,请使用诸如 unique_ptr 和 shared_ptr 之类的智能指针(如果您要求它们不透明,甚至可以自己使用自制指针),而不是原始指针。

如果您正在对某个范围进行一些迭代,请使用迭代器(或标准库尚未提供的某些范围),而不是原始指针,除非您确信原始指针在非常具体的情况下会做得更好(例如,对于更少的标头依赖性)案例。

如果您知道按值传递就足够了,并且您想要一些明确的可为空语义,请使用 std::optional 之类的包装器,而不是原始指针。

如果您知道由于上述原因传递值并不理想,并且您不想要可为空的语义,请使用 {lvalue, rvalue, forwarding}-references。

即使您确实需要像传统指针这样的语义,通常也有更合适的东西,例如 Library Fundamental TS 中的observer_ptr。

唯一的例外不能用当前语言解决:

当您在上面实现智能指针时,您可能必须处理原始指针。

特定的语言互操作例程需要指针,例如 operator new。 (然而,与普通对象指针相比,cv-void* 仍然完全不同且更安全,因为它排除了意外的指针算术,除非您依赖 void* 上的一些不符合标准的扩展,例如 GNU。)

函数指针可以从 lambda 表达式转换而无需捕获,而函数引用则不能。对于这种情况,您必须在非泛型代码中使用函数指针,即使您故意不想要可为空的值。

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

您必须服务于旧式 (C) API。

您必须满足特定 C++ 实现的 ABI 要求。

您必须根据特定实现的假设在运行时与不同的语言实现(包括各种程序集、语言运行时和一些高级客户端语言的 FFI)进行互操作。

在某些极端情况下,您必须提高翻译(编译和链接)的效率。

在某些极端情况下,您必须避免符号膨胀。

语言中立性警告

如果您通过some Google search result (not specific to C++)来查看问题,这很可能是错误的地方。

C++ 中的引用非常“奇怪”,因为它本质上不是一流的:they will be treated as the objects or the functions being referred to,因此它们没有机会支持一些一流的操作,例如独立于被引用对象的类型作为 the member access operator 的左操作数.其他语言可能对它们的引用有类似的限制,也可能没有。

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

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

(附带说明:这个问题可能比涉及任何“类 C”语言(如 ALGOL 68 vs. PL/I)更早。)


T
Tim Cooper

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

另一方面,引用和指针之间的一个主要区别是分配给 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 工作的语言机制。


您不能获取引用的地址,但这并不意味着它们在物理上不占用空间。除非进行优化,否则它们肯定可以。
尽管有影响,“堆栈上的引用根本不占用任何空间”显然是错误的。
@Tomalak,嗯,这也取决于编译器。但是,是的,说这有点令人困惑。我想把它去掉就不会那么混乱了。
在任何给定的特定情况下,它可能会或可能不会。所以“它没有”作为一个明确的断言是错误的。这就是我要说的。 :) [我不记得标准在这个问题上说了什么;参考成员的规则可能会传达“参考可能占用空间”的一般规则,但我在海滩上没有我的标准副本:D]
P
Peter Mortensen

这是基于 tutorial。写的更清楚了:

>>> 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 variable(与typedef不同,即alias of a typeTom。也可以忘记此类语句的术语是创建 Tom 的引用。


如果一个类有一个引用变量,它应该在初始化列表中使用 nullptr 或有效对象进行初始化。
这个答案中的措辞太混乱了,以至于没有太多实际用途。另外,@Misgevolution,您是否认真向读者推荐使用 nullptr 初始化引用?你真的读过这个线程的任何其他部分,或者......?
我的错,对不起我说的愚蠢的事情。那时我一定是失眠了。 '用 nullptr 初始化' 是完全错误的。
R
Ryan

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

int& j = i;

它在内部变成

int* const j = &i;

这不是 C++ 标准所说的,编译器不需要按照您的答案描述的方式实现引用。
@jogojapan:C++ 编译器实现引用的任何有效方式也是实现 const 指针的有效方式。这种灵活性并不能证明引用和指针之间存在差异。
@BenVoigt 一个的任何有效实现可能也是另一个的有效实现,但这并没有以明显的方式从这两个概念的定义中得出。一个好的答案应该从定义开始,并证明为什么关于两者最终相同的说法是正确的。这个答案似乎是对其他一些答案的某种评论。
引用是赋予对象的另一个名称。编译器允许有任何类型的实现,只要你不能区分,这就是所谓的“as-if”规则。这里的重要部分是您无法区分。如果你能发现一个指针没有存储空间,那么编译器就出错了。如果您可以发现引用没有存储,则编译器仍然是一致的。
D
Destructor

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

A
Andrzej

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

问候, &rzej


引用和指针都是句柄。它们都为您提供了通过引用传递对象的语义,但句柄被复制。没有不同。 (还有其他方法也可以使用句柄,例如在字典中查找的键)
我以前也是这样想的。但是请参阅链接的文章,描述为什么不是这样。
@Andrzj:这只是我评论中单句的一个很长的版本:句柄被复制了。
我需要更多关于这个“句柄被复制”的解释。我了解一些基本概念,但我认为在物理上引用和指针都指向变量的内存位置。是否像别名存储值变量并在变量值更改或其他内容时对其进行更新?我是新手,请不要将其标记为愚蠢的问题。
@Andrzej 错误。在这两种情况下,都会发生按值传递。引用按值传递,指针按值传递。说否则会使新手感到困惑。
P
Peter Mortensen

除非我需要以下任何一种,否则我会使用参考:

空指针可以用作标记值,这通常是避免函数重载或使用布尔值的廉价方法。

您可以对指针进行算术运算。例如,p += 偏移量;


您可以在 r 被声明为引用的地方编写 &r + offset
T
Tory

冒着增加混乱的风险,我想输入一些输入,我确信这主要取决于编译器如何实现引用,但在 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; 你认为会发生什么在调用 delete &str_ref 之后?好吧,显然它编译得很好,但是在运行时会导致分段错误,因为它不再指向一个有效的变量,我们基本上有一个仍然存在的损坏的引用(直到它超出范围),但是没有用。

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

现在无论编译器如何处理引用,它总会有某种指针,因为引用必须引用特定内存地址的特定变量才能按预期工作,没有解决这个问题(因此术语“参考”)。

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

请记住,我上面的示例只是演示什么是引用的示例,您永远不会希望以这些方式使用引用!为了正确使用参考,这里已经有很多答案一针见血


k
kriss

另一个区别是您可以拥有指向 void 类型的指针(它意味着指向任何东西的指针),但禁止对 void 的引用。

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

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


A
Adisak

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

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

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

当然,对于没有内联的函数,指针和引用生成相同的代码,如果函数没有修改和返回它们,那么通过值传递内在函数总是比通过引用传递更好。


D
Don Wakefield

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

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

默认风格使用引用的“将 const 引用绑定到临时”方面。


A
Arlene Batada

该程序可能有助于理解问题的答案。这是一个引用“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


A
Ap31

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

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

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

模板。由于模板参数是鸭子类型的,类型的句法属性才是最重要的,所以通常同一个模板可以同时用于 T 和 T&。 (或 std::reference_wrapper 仍然依赖于对 T& 的隐式强制转换)同时涵盖 T& 和 T&& 的模板更为常见。

左值。考虑语句 str[0] = 'X';如果没有引用,它只适用于 c-strings (char* str)。通过引用返回字符允许用户定义的类具有相同的符号。

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

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

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


“隐式转换”转换是一种语法结构,它存在于语法中;演员表总是明确的
D
Donald Duck

指针和引用之间有一个非常重要的非技术差异:通过指针传递给函数的参数比通过非常量引用传递给函数的参数更可见。例如:

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) 的调用只能通过值传递,所以它绝对不能修改 x;要修改参数,您需要传递一个指针 fn(&x)。因此,如果参数前面没有 &,您就知道它不会被修改。 (相反,& 表示已修改,但不正确,因为您有时必须通过 const 指针传递大型只读结构。)

一些人认为在阅读代码时这是一个非常有用的特性,指针参数应该始终用于可修改的参数而不是非 const 引用,即使函数从不期望 nullptr。也就是说,那些人认为不应允许像上面的 fn3() 这样的函数签名。 Google's C++ style guidelines 就是一个例子。


G
George R

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

引用要求您指定一个实际的窗口。

指针需要屏幕上一块空间的位置,以确保它将包含该窗口类型的零个或多个实例。


L
Lewis Kelsey

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

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


C
Community

指针和引用的区别

可以将指针初始化为 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 "="

我们仍然可以拥有 const int& i = 0
在这种情况下,引用将仅在读取时使用,即使使用“const_cast”,我们也无法修改此 const 引用,因为“const_cast”只接受指针而不是引用。
const_cast 与引用配合得很好:coliru.stacked-crooked.com/a/eebb454ab2cfd570
您正在对引用进行强制转换,而不是对引用进行强制转换试试这个;常量 int&i=; const_cast(i);我试图抛弃引用的常量性,以使写入和新值分配给引用成为可能,但这是不可能的。请集中注意力!!
C
Community

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

指针

指针变量使用一元后缀声明符运算符 * 声明

指针对象被分配一个地址值,例如,通过分配给数组对象,使用 & 一元前缀运算符的对象的地址,或分配给另一个指针对象的值

一个指针可以被重新分配任意次数,指向不同的对象

指针是保存分配地址的变量。它在内存中占用的存储空间等于目标机器架构的地址大小

例如,可以通过增量或加法运算符对指针进行数学操作。因此,可以使用指针等进行迭代。

要获取或设置指针引用的对象的内容,必须使用一元前缀运算符 * 来取消引用它

参考

引用必须在声明时进行初始化。

使用一元后缀声明符运算符 & 声明引用。

初始化引用时,使用它们将直接引用的对象的名称,而不需要一元前缀运算符 &

一旦初始化,引用就不能通过赋值或算术操作指向其他东西

无需取消引用即可获取或设置它所引用的对象的内容

对引用的赋值操作操作的是它指向的对象的内容(在初始化之后),而不是引用本身(不会改变它指向的位置)

对引用的算术运算操作的是它指向的对象的内容,而不是引用本身(不会改变它指向的位置)

在几乎所有的实现中,引用实际上存储为被引用对象的内存中的地址。因此,它在内存中占用的存储空间等于目标机器架构的地址大小,就像指针对象一样

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

文章

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

Arrays, Pointers and References Under the Hood In-Depth Article


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