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

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

1188316 次浏览

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

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

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

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

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

  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; // errorint &r = x;
  2. 指针变量有自己的标识:一个可以用一元&运算符获取的明显可见的内存地址和一个可以用sizeof运算符测量的一定数量的空间。在引用上使用这些运算符会返回一个对应于引用绑定到的任何值的值;引用自己的地址和大小是不可见的。由于引用以这种方式假定原始变量的标识,因此将引用视为同一变量的另一个名称是很方便的。

    int x = 0;int &r = x;int *p = &x;int *p2 = &r;
    assert(p == p2); // &x == &rassert(&p != &p2);
  3. 您可以拥有任意嵌套的指针,指向提供额外间接级别的指针。引用只提供一个间接级别。

    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);
  4. 指针可以被赋值为nullptr,而引用必须绑定到现有对象。如果你足够努力,你可以将引用绑定到nullptr,但这是未定义,并且不会保持一致。

    /* 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 10std::cout<< (&r != nullptr? "not null" : "null")<< std::endl;
    bool f(int &r) { return &r != nullptr; }
    // prints "not null" under GCC 10std::cout<< (f(*static_cast<int *>(nullptr))? "not null" : "null")<< std::endl;

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

  5. 指针可以遍历数组;您可以使用++转到指针指向的下一个项目,使用+ 4转到第5个元素。这与指针指向的对象的大小无关。

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

  7. 引用不能放入数组中,而指针可以(由user@litb提到)

  8. const引用可以绑定到临时对象。指针不能(不是没有一些间接):

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

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

引用永远不能是NULL

与流行的观点相反,可以有一个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的空引用

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

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

你忘了最重要的部分:

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

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

我使用引用,除非我需要其中任何一个:

  • 空指针可以用作定点价值,通常是一种廉价的方式避免函数重载或使用一个bool。

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

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

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引用到临时”方面。

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

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

例如:

class scope_test{public:~scope_test() { printf("scope_test done!\n"); }};
...
{const scope_test &test= scope_test();printf("in scope\n");}

将打印:

in scopescope_test done!

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

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

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

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

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

现在一些真实代码的解释:

int a = 0;int& b = a;

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

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

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

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

什么是C++引用(对于C程序员

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

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

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

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

考虑C++常见问题中的以下语句:

即使引用通常使用底层汇编语言,请没有将引用视为指向对象的看起来很有趣的指针。对象的引用。它是不是指向对象的指针,也不是对象的副本。它对象。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

问候,rzej

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

    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.

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

int& j = i;

它内部成为

int* const j = &i;

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

#include<iostream>
using namespace std;
int main(){int *ptr=0, x=9; // pointer and variable declarationptr=&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

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

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

举个例子:

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] oncefor (int i = 2; i < (int)size; i++) {maybeModify(a0); // Give maybeModify a reference to a registerarray[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,所以数组[i]永远不会引用数组[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超出范围。

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

>>> The address that locates a variable within memory iswhat we call a reference to that variable. (5th paragraph at page 63)
>>> The variable that stores the reference to anothervariable 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 forseveral 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的引用也是可以的。

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

#include <iostream>int main(int argc, char** argv) {// Create a string on the heapstd::string *str_ptr = new std::string("THIS IS A STRING");// Dereference the string on the heap, and assign it to the referencestd::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 addressesstd::cout << str_ptr << " : " << &str_ref << std::endl;// Exactly the same, now remember to free the memory on the heapdelete str_ptr;}

其输出如下:

THIS IS A STRING0xbb2070 : 0xbb2070

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

int main(int argc, char** argv) {// In the actual new declaration let immediately de-reference and assign it to the referencestd::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 isstoring, and deleting it, which is all a pointer is doing, just we have to specifythe address with '&' whereas a pointer does that implicitly, this is sort of likecalling delete &(*str_ptr); (which also compiles and runs fine).*/}

其输出如下:

THIS IS A STRING

因此,引用是引擎盖下的指针,它们都只是存储一个内存地址,地址指向的地方是无关紧要的,如果我调用std::cout<

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

现在不管编译器如何处理引用,它将总是在引擎盖下有某种指针,因为引用必须引用特定内存地址的特定变量,以便它按预期工作,没有绕过这个(因此术语“引用”)。

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

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

如果您不熟悉以抽象甚至学术的方式学习计算机语言,则可能会出现语义差异。

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

除此之外,指针和引用之间当然还有一些实际的区别。使用它们的语法显然是不同的,你不能“重新定位”引用,引用为空,或者有指向引用的指针。

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

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

对指针的引用在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;}

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

指针和引用的区别

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

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

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

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

  • 模板。由于模板参数是鸭子类型的,类型的语法属性才是最重要的,所以通常相同的模板可以同时用于TT&
    (或者std::reference_wrapper<T>仍然依赖于隐式转换T&
    涵盖T&T&&的模板更为常见。

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

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

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

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

我总是决定这个规则从C++核心准则:

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

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

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 xfn2(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的C++风格指南就是一个例子。

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

// receives an alias of an int, an address of an int and an int valuepublic void my_function(int& a,int* b,int c){int d = 1; // declares an integer named dint &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 objectint *f = e; // invalid, you are trying to place an object in an address// imagine writting your name in an address fieldint *g = f; // writes an address to an addressg = &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}

Taryn♦说:

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

其实你可以。

我引用对另一个问题的回答

C++FAQ表示最好:

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

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

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

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

除了这里所有的答案,

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

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

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

直接的答案

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

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

对象类型的ISOC++定义

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

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

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

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

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

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

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

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

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

的误解

句法糖

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

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

(类似地,lambda表达式是C++中任何其他特征的没有语法糖,因为它不能用捕获变量的声明顺序这样的“未指定”属性精确模拟,这可能很重要,因为这些变量的初始化顺序可能很重要。

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

存储

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

上面的陈述完全是错误的。为了避免这种误解,请查看ISOC++规则:

[intro.object]/1

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

[dcl.ref]/4

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

注意这些是语义属性。

语用学

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

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

如果你不必坚持这种过于具体的选择,在大多数情况下,答案是简短的:你没有必要使用指针,所以你不。指针通常已经够糟糕了,因为它们暗示了太多你不期望的事情,它们会依赖太多隐含的假设,破坏代码的可运维性和(甚至)可移植性。

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

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

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

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

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

语言中立警告

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

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

C++中的引用可能无法在不同语言中保留其含义。例如,一般来说,引用并不意味着像C++中那样的值具有非空属性,因此这些假设可能在其他一些语言中不起作用(你会很容易找到反例,例如Java、C#、…)。

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

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

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

指针

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

参考文献

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

即使指针和引用以几乎相同的方式“底层”实现,编译器也会以不同的方式对待它们,从而导致上述所有差异。

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

数组,指针和引用下的胡德深入文章

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

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

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; -- legalint **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. 指针可以重新分配任意次数,而引用在绑定后不能重新分配。
  2. 指针可以指向任何地方(NULL),而引用总是引用一个对象。
  3. 您不能像使用指针那样获取引用的地址。
  4. 没有“引用算术”(但您可以获取引用指向的对象的地址,并对其进行指针算术,如&obj + 5所示)。

澄清一个误解:

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

int &ri = i;

如果不完全优化分配相同的存储量作为指针,并放置地址i到那个存储中。

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

作为一般规则,

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

有趣的阅读:

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

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 */

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

与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, 24mov     edi, OFFSET FLAT:_ZSt4coutlea     rsi, [rsp+12]mov     DWORD PTR [rsp+12], 1call    std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<void const*>(void const*)xor     eax, eaxadd     rsp, 24ret---------------------------------------------------------------------O0:main:push    rbpmov     rbp, rspsub     rsp, 16mov     DWORD PTR [rbp-12], 1lea     rax, [rbp-12]mov     QWORD PTR [rbp-8], raxmov     rax, QWORD PTR [rbp-8]mov     rsi, raxmov     edi, OFFSET FLAT:_ZSt4coutcall    std::basic_ostream<char, std::char_traits<char> >::operator<<(void const*)mov     eax, 0leaveret

指针已经在堆栈外进行了优化,在这两种情况下,指针甚至都没有在-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.jmov     esi, OFFSET FLAT:a //&a.i

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

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

1。指针可以初始化为:

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

int *q;q = &b;

作为参考,

int b=15;int &c=b;

(在单个步骤中声明和初始化)

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

您不能像指针一样取消引用引用,指针在取消引用时给出该位置的值,

引用和指针都由地址工作,尽管…

所以

你能做到的

int*val=0xDEADBEEF;返回参数

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

*val是不允许的。

简而言之,

指针:指针是保存另一个变量内存地址的变量。指针需要使用*运算符取消引用才能访问它指向的内存位置。-从Geeks for Geeks中提取

引用:引用变量是别名,即已经存在的变量的另一个名称。引用,就像指针一样,也通过存储对象的地址来实现。-从Geeks中提取

另一张图片了解更多细节:

来自网络

将指针视为名片:

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

将引用视为与某人的主动呼叫:

  • 你很确定你联系的人还活着
  • 你可以直接说,不需要额外的电话
  • 你很确定你不是在对一个空地方或者垃圾说话
  • 你不能确定你是唯一一个正在和这个物体说话的人