What are the differences between a pointer variable and a reference variable in C++?
我知道引用是句法上的糖分,所以代码更容易读写。
但是有什么区别呢?
以下答案和链接的摘要:
要澄清误解:
The C++ standard is very careful to avoid dictating how a compiler may
implement references, but every C++ compiler implements
references as pointers. That is, a declaration such as:
1 int &ri = i;if it's not optimized away entirely, allocates the same amount of storage
as a pointer, and places the address
ofi into that storage.
因此,指针和引用都使用相同的内存量。
一般来说,
- 使用函数参数和返回类型中的引用提供有用的自文档化接口。
- 使用指针实现算法和数据结构。
有趣的阅读:
- 我最喜欢的C++ FAQ Lite。
- 引用与指针。
- 参考文献介绍。
- 参考文献和常量
可以重新分配指针:
1 2 3 4 5 6 7 8 | int x = 5; int y = 6; int *p; p = &x; p = &y; *p = 10; assert(x == 5); assert(y == 10); |
引用不能,并且必须在初始化时分配:
1 2 3 | int x = 5; int y = 6; int &r = x; |
指针在堆栈上有自己的内存地址和大小(x86上为4个字节),而引用共享相同的内存地址(与原始变量相同),但也占用堆栈上的一些空间。由于引用与原始变量本身具有相同的地址,因此可以安全地将引用视为同一变量的另一个名称。注意:指针指向的内容可以在堆栈或堆上。同上,参考文献。我在这条语句中的声明不是指针必须指向堆栈。指针只是一个保存内存地址的变量。这个变量在堆栈上。因为引用在堆栈上有自己的空间,而且地址与它引用的变量相同。更多关于堆栈和堆的信息。这意味着编译器不会告诉您引用的真实地址。
1 2 3 4 5 | int x = 0; int &r = x; int *p = &x; int *p2 = &r; assert(p == p2); |
您可以有指向指针的指针,指向提供额外间接级别的指针。而参考文献只提供一个间接的层次。
1 2 3 4 5 6 7 8 9 | int x = 0; int y = 0; int *p = &x; int *q = &y; int **pp = &p; pp = &q;//*pp = q **pp = 4; assert(y == 4); assert(x == 0); |
指针可以直接分配给
1 2 3 | int *p = nullptr; int &r = nullptr; <--- compiling error int &r = *p; <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0 |
指针可以在数组上迭代,可以使用
指针需要与
指针是保存内存地址的变量。无论引用是如何实现的,引用都具有与其引用的项相同的内存地址。
不能将引用填充到数组中,而指针可以(由用户@litb提到)
常量引用可以绑定到临时引用。指针不能(没有某些间接方向):
1 2 | const int &x = int(12); //legal C++ int *y = &int(12); //illegal to dereference a temporary. |
这使得
什么是C++引用(对于C程序员)
引用可以被认为是常量指针(不要与指向常量值的指针混淆!)使用自动间接寻址,即编译器将为您应用
必须用非空值初始化所有引用,否则编译将失败。既不可能获取引用的地址—地址操作符将返回引用值的地址—也不可能对引用进行算术处理。
C程序员可能不喜欢C++引用,因为当间接发生时,或者如果一个参数通过值或指针而不查看函数签名,它将不再是显而易见的。
C++程序员可能不喜欢使用指针,因为指针被认为是不安全的——尽管引用在大多数琐碎的情况下并不比常量指针更安全——缺少自动间接指向的便利性,并带有不同的语义内涵。
从C++ FAQ中考虑下面的语句:
Even though a reference is often implemented using an address in the
underlying assembly language, please do not think of a reference as a
funny looking pointer to an object. A reference is the object. It is
not a pointer to the object, nor a copy of the object. It is the
object.
但是如果一个引用真的是对象,怎么会有悬空的引用呢?在非托管语言中,引用不可能比指针更"安全"——通常没有一种方法可以跨范围边界可靠地对值进行别名!
为什么我认为C++引用是有用的C++背景下,C++引用可能看起来是一个有点傻的概念,但是在可能的情况下,仍然应该使用它们来代替指针:自动间接是方便的,引用在处理RAII时变得特别有用,但不是因为感知到的安全优势,而是因为它们使书写成语。代码不那么尴尬。
RAII是C++的核心概念之一,但它与复制语义无关。通过引用传递对象可以避免这些问题,因为不涉及复制。如果语言中没有引用,则必须使用指针,而指针的使用更麻烦,因此违反了语言设计原则,即最佳实践解决方案应该比替代方案更容易。
如果你真的想变得学究,有一件事你可以用一个指针做不到:延长一个临时对象的生命周期。在C++中,如果将const引用绑定到临时对象,则该对象的生存期将成为引用的生存期。
1 2 3 4 5 | std::string s1 ="123"; std::string s2 ="456"; std::string s3_copy = s1 + s2; const std::string& s3_reference = s1 + s2; |
在本例中,s3_copy复制连接后的临时对象。而S3_引用在本质上成为临时对象。它实际上是对一个临时对象的引用,这个临时对象现在与引用具有相同的生存期。
如果您在没有
与流行观点相反,引用可能为空。
1 2 3 | int * p = NULL; int & r = *p; r = 1; // crash! (if you're lucky) |
当然,用推荐信做起来要困难得多——但是如果你能做到这一点,你会为了找到推荐信而把头发扯下来的。引用在C++中不是天生安全的!
从技术上讲,这是一个无效的引用,而不是一个空引用。C++不支持空引用作为一种概念,正如您可能在其他语言中发现的那样。还有其他类型的无效引用。任何无效引用都会引发未定义行为的幽灵,就像使用无效指针一样。
实际错误是在为引用赋值之前取消对空指针的引用。但是我不知道任何编译器会在这种情况下生成任何错误——错误会在代码中进一步传播到某个点。这就是为什么这个问题如此阴险。大多数情况下,如果取消对空指针的引用,就会在该点处崩溃,并且不需要很多调试就可以解决问题。
我上面的例子简短而做作。下面是一个更真实的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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); |
我想重申一下,获得空引用的唯一方法是通过格式错误的代码,一旦获得了空引用,就会得到未定义的行为。检查空引用是没有意义的;例如,您可以尝试使用
避免麻烦的正确方法是避免取消对空指针的引用以创建引用。这是一种实现这一点的自动化方法。
1 2 3 4 5 6 7 8 9 10 | 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)); |
要从有较好写作技巧的人那里了解这个问题,请参阅JimHyslop和Herb Sutter的空引用。
有关取消引用空指针的危险的另一个示例,请参阅Raymond Chen在尝试将代码移植到另一个平台时公开未定义的行为。
除了句法上的糖分,一个引用是一个
更新:现在我再考虑一下,有一个重要的区别。
常量指针的目标可以通过获取其地址并使用常量转换来替换。
引用的目标不能以任何方式替换为ub。
这应该允许编译器对引用进行更多的优化。
你忘了最重要的部分:
带指针的成员访问使用
带引用的成员访问使用
同vi明显优于emacs一样,
实际上,引用并不是真正的指针。
编译器保持对变量的"引用",将名称与内存地址相关联;这是编译时将任何变量名转换为内存地址的工作。
创建引用时,只告诉编译器为指针变量指定了另一个名称;这就是引用不能"指向空"的原因,因为变量不能是也不能是。
指针是变量;它们包含其他变量的地址,或者可以为空。重要的是指针有一个值,而引用只有一个它所引用的变量。
下面是对真实代码的一些解释:
1 2 | int a = 0; int& b = a; |
这里,您没有创建指向
1 2 3 4 5 6 7 | void increment(int& n) { n = n + 1; } int a; increment(a); |
调用函数时,编译器通常会为要复制到的参数生成内存空间。函数签名定义了应该创建的空格,并给出了这些空格应该使用的名称。将参数声明为引用只会告诉编译器使用输入变量内存空间,而不是在方法调用期间分配新的内存空间。说函数将直接操作调用范围中声明的变量似乎很奇怪,但请记住,在执行编译代码时,没有更多的范围;只有纯平面内存,而函数代码可以操作任何变量。
现在,在某些情况下,编译器在编译时可能无法知道引用,例如使用外部变量时。因此,在底层代码中,引用可以实现为指针,也可以不实现为指针。但在我给出的示例中,它很可能不会用指针实现。
引用与指针非常相似,但它们是专门为优化编译器而设计的。
- 引用的设计使得编译器更容易跟踪哪些引用别名哪些变量。两个主要特征非常重要:没有"引用算术"和没有重新分配引用。这些允许编译器在编译时找出哪些引用别名哪些变量。
- 允许引用引用没有内存地址的变量,例如编译器选择放入寄存器的变量。如果获取局部变量的地址,编译器很难将其放入寄存器。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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]。它希望将算法优化为:
1 2 3 4 5 6 7 8 9 10 11 12 13 | void hurtTheCompilersOptimizer(short size, int array[]) { // Do the same thing as above, but instead of accessing array[1] // all the time, access it once and store the result in a register, // which is much faster to do arithmetic with. register int a0 = a[0]; register int a1 = a[1]; // access a[1] once for (int i = 2; i < (int)size; i++) { maybeModify(a0); // Give maybeModify a reference to a register array[i] += a1; // Use the saved register value over and over } a[0] = a0; // Store the modified a[0] back into the array } |
要进行这样的优化,需要证明在调用期间没有什么可以更改数组[1]。这很容易做到。我从不小于2,所以数组[i]永远不能引用数组[1]。maybemodify()被指定为a0作为引用(别名数组[0])。因为没有"引用"算法,编译器只需证明maybemodify永远不会得到x的地址,并且它已经证明没有任何东西改变数组[1]。
它还必须证明,当我们在a0中有一个临时寄存器副本时,将来的调用无法读/写一个[0]。这通常很容易证明,因为在许多情况下,很明显引用从未存储在类实例这样的永久结构中。
现在用指针做同样的事情
1 2 3 4 5 6 7 8 9 10 11 | 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]要困难得多,因为我们已经给了它一个指针;cat已经出局了。现在,它必须做更困难的证明:对maybemodify进行静态分析,以证明它从未写入&x+1。它还必须证明它从来没有保存过一个指向数组[0]的指针,这同样很棘手。
现代编译器越来越擅长静态分析,但是帮助它们并使用引用总是很好的。
当然,除非进行如此巧妙的优化,否则编译器确实会在需要时将引用转换为指针。
编辑:发表这个答案五年后,我发现了一个实际的技术差异,即参考文献不同于仅仅以不同的方式看待相同的寻址概念。引用可以以指针无法修改的方式修改临时对象的寿命。
1 2 3 4 5 6 7 | F createF(int argument); void extending() { const F& ref = createF(5); std::cout << ref.getArgument() << std::endl; }; |
通常,临时对象(如调用
引用不能是
虽然引用和指针都用于间接访问另一个值,但引用和指针之间有两个重要区别。第一个是引用总是引用一个对象:定义一个引用而不初始化它是一个错误。赋值的行为是第二个重要区别:赋值给引用会更改引用绑定到的对象;它不会重新绑定对另一个对象的引用。一旦初始化,引用总是引用同一个基础对象。
考虑这两个程序片段。首先,我们将一个指针分配给另一个:
1 2 3 | int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; pi = pi2; // pi now points to ival2 |
在赋值之后,ival,pi所寻址的对象保持不变。分配会更改pi的值,使其指向不同的对象。现在考虑一个分配两个引用的类似程序:
1 2 | int &ri = ival, &ri2 = ival2; ri = ri2; // assigns ival2 to ival |
此分配更改了ival,即ri引用的值,而不是引用本身。分配之后,这两个引用仍然引用它们的原始对象,这些对象的值现在也相同。
如果你不熟悉以抽象甚至学术的方式学习计算机语言,那么语义上的差异可能会显得深奥。
在最高层次上,引用的概念是它们是透明的"别名"。您的计算机可能会使用一个地址使它们工作,但您不必担心:您应该将它们视为现有对象的"只是另一个名称",并且语法反映了这一点。它们比指针更严格,因此编译器可以在即将创建悬空引用时比即将创建悬空指针时更可靠地警告您。
除此之外,指针和引用之间当然还有一些实际的区别。使用它们的语法明显不同,您不能"重新定位"引用、引用虚无或具有指向引用的指针。
引用是另一个变量的别名,而指针则保存变量的内存地址。引用通常用作函数参数,以便传递的对象不是副本而是对象本身。
1 2 3 | 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. |
它占用多少空间并不重要,因为您实际上无法看到它占用的任何空间的任何副作用(不执行代码)。
另一方面,引用和指针之间的一个主要区别是,分配给常量引用的临时变量一直存在,直到常量引用超出范围。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class scope_test { public: ~scope_test() { printf("scope_test done! "); } }; ... { const scope_test &test= scope_test(); printf("in scope "); } |
将打印:
1 2 | in scope scope_test done! |
这是允许scopeguard工作的语言机制。
引用不是给某些内存指定的另一个名称。它是一个不变的指针,在使用时自动取消引用。基本上可以归结为:
1 | int& j = i; |
它内部变成
1 | int* const j = &i; |
这是基于教程的。写的东西更清楚:
1 2 3 4 5 | >>> 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) |
简单地记住,
1 2 3 | >>> 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.) |
此外,正如我们几乎可以参考任何指针教程一样,指针是一个由指针算法支持的对象,它使指针类似于数组。
看看下面的陈述,
1 2 | int Tom(0); int & alias_Tom = Tom; |
我使用参考文献,除非我需要这些:
空指针不能用作哨兵价值,通常是一种廉价的方法避免函数过载或使用布尔。
你可以在指针上做算术。例如,
p += offset; 。
指针的引用在C++中是可能的,但是反向是不可能的,这意味着指向引用的指针是不可能的。对指针的引用提供了一种更清晰的语法来修改指针。看看这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #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语言中,您必须使用指向指针的指针(多个间接方向),这会导致混乱,程序看起来可能很复杂。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #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++指针
- 指向指针的指针和指向指针的引用
正如我所说,指向引用的指针是不可能的。尝试以下程序:
1 2 3 4 5 6 7 8 9 | #include <iostream> using namespace std; int main() { int x = 10; int *ptr = &x; int &*ptr1 = ptr; } |
指针和引用之间有一个根本区别,我没有看到有人提到过:引用在函数参数中启用了逐引用语义。指针,虽然它最初不可见,但它不可见:它们只提供传递值语义。本文已经很好地描述了这一点。
当做,RZJ
在增加混乱的风险下,我想加入一些输入,我确信这主要取决于编译器如何实现引用,但是在gcc的情况下,引用只能指向堆栈上的变量的想法实际上并不正确,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #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; } |
输出结果如下:
1 2 | THIS IS A STRING 0xbb2070 : 0xbb2070 |
如果您注意到甚至内存地址都是完全相同的,这意味着引用成功地指向了堆上的一个变量!现在,如果你真的想变得怪异,这也很有效:
1 2 3 4 5 6 7 8 9 10 11 12 | 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).*/ } |
输出结果如下:
1 | THIS IS A STRING |
因此,引用是引擎盖下的指针,它们都只是存储一个内存地址,地址指向的地方不相关,如果我在调用delete&str_-ref之后调用std::cout< 换句话说,引用只不过是一个指针,它抽象了指针机制,使其更安全、更容易使用(没有意外的指针数学,没有混淆"."和"->"等),假设您不尝试像上面的例子那样的胡说八道;) 现在,不管编译器如何处理引用,它都会在引擎盖下有某种指针,因为引用必须引用特定内存地址处的特定变量,才能按预期工作,因此无法绕过此问题(因此称为"引用")。 对于引用来说,唯一重要的是必须在声明时定义它们(除了头中的引用,在这种情况下,必须在构造函数中定义它,在构造它所包含的对象之后,定义它太晚了)。 记住,我上面的例子就是,证明引用是什么的例子,你永远不会想用这些方式使用引用!为了正确地使用参考资料,这里已经有了很多答案,这些答案都是非常重要的。
另一个区别是,可以有指向void类型的指针(它意味着指向任何内容的指针),但禁止引用void。
1 2 3 | int a; void * p = &a; // ok void & p = a; // forbidden |
我不能说我对这种特殊的差别很满意。我更喜欢它的意思是引用任何有地址的东西,否则引用的行为也是一样的。它允许使用引用定义一些C库函数的等价物,比如memcpy。
引用的另一个有趣用法是提供用户定义类型的默认参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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"方面。
这个程序可能有助于理解问题的答案。这是一个引用"j"的简单程序,指针"ptr"指向变量"x"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #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
此外,作为内联函数的参数的引用的处理方式可能与指针不同。
1 2 3 4 5 6 7 8 9 10 11 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); } |
许多编译器在导入指针版本1时,实际上会强制写入内存(我们显式地获取地址)。但是,它们会将引用保留在一个更为理想的寄存器中。
当然,对于没有内联的函数,指针和引用生成相同的代码,如果函数不修改和返回内部函数,则最好按值传递内部函数,而不是按引用传递内部函数。
我觉得还有一个问题没有在这里讨论。
与指针不同,引用在语法上等同于它们所引用的对象,即任何可以应用于对象的操作都可以用于引用,并且具有完全相同的语法(当然,例外是初始化)。
虽然这可能看起来很肤浅,但我相信这个属性对于许多C++特性是至关重要的,例如:
模板。因为模板参数是duck类型的,所以类型的语法属性才是最重要的,所以通常同一模板可以同时用于
T 和T& 。(或std::reference_wrapper ,它仍然依赖于一个隐含的演员表至T& )覆盖T& 和T&& 的模板更加常见。左值。考虑一下没有引用的声明
str[0] = 'X'; ,它只适用于C字符串(char* str )。通过引用返回字符允许用户定义的类具有相同的符号。复制构造函数。从语法上讲,将对象传递给复制构造函数是有意义的,而不是指向对象的指针。但复制构造函数无法按值获取对象-这将导致对同一复制构造函数的递归调用。这里只剩下引用作为唯一的选项。
运算符重载。通过引用,可以在保留相同的中缀符号的同时,引入对运算符调用的间接寻址,例如
operator+(const T& a, const T& b) 。这也适用于常规重载函数。
这些点赋予C++和标准库相当大的一部分,因此这是引用的一个主要属性。
指针和引用之间有一个非常重要的非技术性区别:通过指针传递给函数的参数比通过非常量引用传递给函数的参数更为可见。例如:
1 2 3 4 5 6 7 8 9 10 11 12 | 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语言中,一个看起来像
有些人认为,在读取代码时,这是一个非常有用的特性,即指针参数应该始终用于可修改的参数,而不是非非
也许一些隐喻会有所帮助;在桌面屏幕空间的上下文中-
- 引用要求您指定实际窗口。
- 指针需要屏幕上一块空间的位置,您可以确保它将包含该窗口类型的零个或多个实例。
直接的答案
C++中的引用是什么?类型的某些特定实例不是对象类型。好的。
C++中的指针是什么?类型的某个特定实例是对象类型。好的。
从ISO C++定义的对象类型:好的。
An object type is a (possibly cv-qualified) type that is not a function type, not a reference type, and not cv void.
Ok.
重要的是要知道,对象类型是C++中的类型宇宙的顶级类别。引用也是顶级类别。但指针不是。好的。
指针和引用在复合类型的上下文中一起提到。这主要是由于继承自(和扩展的)C的声明符语法的性质,C没有引用。(除C++之外,还有一种以上的引用标识符,而指针仍然是"统一的":EDCOX1,0,EDCOX1,1,EDCX1,2)。因此,在这种情况下,用类似C风格的"扩展"来编写一个特定的语言是有一定道理的。(我仍然认为声明者的语法浪费了大量的语法表达能力,使人类用户和实现都感到沮丧。因此,它们都不具备内置于新语言设计中的资格。然而,这是关于PL设计的完全不同的主题。)好的。
否则,指针可以限定为具有引用的特定类型,这是无关紧要的。除了语法相似性之外,它们共享的共同属性太少,所以在大多数情况下不需要将它们放在一起。好的。
注意上面的语句只提到"指针"和"引用"作为类型。关于它们的实例(比如变量),有一些感兴趣的问题。还有太多的误解。好的。
顶级类别的差异已经揭示了许多与指针不直接相关的具体差异:好的。
- 对象类型可以有顶级的
cv 限定符。引用不能。 - 对象类型的变量确实按照抽象机器语义占用存储空间。参考不必占用存储空间(有关详细信息,请参阅下面关于误解的部分)。
- …
关于引用的一些更特殊的规则:好的。
- 复合声明符对引用的限制更大。
- 引用可能会折叠。
- 在模板参数推导过程中,基于引用折叠的
&& 参数(作为"转发引用")的特殊规则允许"完美转发"参数。
- 在模板参数推导过程中,基于引用折叠的
- 引用在初始化时有特殊的规则。声明为引用类型的变量的生存期可以通过扩展与普通对象不同。
- 顺便说一句,其他一些上下文(如涉及
std::initializer_list 的初始化)遵循一些类似的引用寿命延长规则。又是一罐虫子。
- 顺便说一句,其他一些上下文(如涉及
- …
误解语法糖
I know references are syntactic sugar, so code is easier to read and write.
Ok.
从技术上讲,这是完全错误的。引用不是C++中任何其他特征的语法糖,因为它们不能被其他特征完全替换而没有任何语义差异。好的。
(类似地,lambda表达式不是C++中任何其他特征的语法糖,因为它不能用"未指定"属性精确地模拟,如捕获变量的声明顺序,这可能是重要的,因为这些变量的初始化顺序可能是重要的。)好的。
C++在严格意义上只有几种语法糖。一个实例是(继承自c)内置(非重载)运算符
So, a pointer and a reference both use the same amount of memory.
Ok.
上述说法完全是错误的。为了避免这种误解,请看ISO C++规则:好的。
来自[intro.object]/1:好的。
... An object occupies a region of storage in its period of construction, throughout its lifetime, and in its period of destruction. ...
Ok.
来自[DCL.REF]/4:好的。
It is unspecified whether or not a reference requires storage.
Ok.
注意这些是语义属性。好的。语用学
即使在语言设计的意义上指针没有足够的资格与引用放在一起,仍然有一些参数使得在其他一些上下文中在它们之间进行选择成为有争议的,例如,在参数类型上进行选择时。好的。
但这不是整个故事。我的意思是,有很多东西比指针和参考文献都要考虑。好的。
如果你不必坚持这样的具体选择,在大多数情况下,答案是简短的:你没有必要使用指针,所以你没有必要使用指针。指针通常是足够糟糕的,因为它们意味着太多你不期望的事情,它们将依赖太多的隐式假设,破坏了T的可维护性和(甚至)可移植性。他编码。不必要地依赖指针无疑是一种坏的风格,在现代C++的意义上应该避免。重新考虑你的目的,你最终会发现指针在大多数情况下是最后一类的特征。好的。
- 有时语言规则明确要求使用特定类型。如果您想使用这些功能,请遵守规则。
- 复制构造函数需要特定类型的cv-
& 引用类型作为第一个参数类型。(通常情况下,它应该是const 合格的。) - move构造函数需要特定类型的cv-
&& 引用类型作为第一个参数类型。(通常没有限定符。) - 运算符的特定重载需要引用类型或非引用类型。例如:
- 重载的
operator= 作为特殊的成员函数,需要类似于复制/移动构造函数的第一个参数的引用类型。 - 固定后的
++ 需要虚拟的int 。 - …
- 重载的
- 复制构造函数需要特定类型的cv-
- 如果您知道传递值(即使用非引用类型)就足够了,直接使用它,特别是当使用支持C++ 17强制复制删除的实现时。(警告:但是,详尽地解释必要性可能非常复杂。)
- 如果您想使用所有权操作一些句柄,请使用诸如
unique_ptr 和shared_ptr 之类的智能指针(如果您要求它们不透明,甚至可以自己使用自制指针),而不是原始指针。 - 如果您在一个范围内进行一些迭代,请使用迭代器(或者一些标准库尚未提供的范围),而不是原始指针,除非您确信原始指针在非常特定的情况下会做得更好(例如,对于较少的头依赖性)。
- 如果您知道pass-by值已经足够,并且需要一些显式的可以为空的语义,那么可以使用像
std::optional 这样的包装器,而不是原始指针。 - 如果由于上述原因,您知道pass-by值并不理想,并且您不希望使用可以为空的语义,请使用lvalue、rvalue、forwarding-引用。
- 即使您确实想要像传统指针那样的语义,也经常会有一些更合适的东西,比如库基本TS中的
observer_ptr 。
唯一的例外情况在当前语言中无法解决:好的。
- 当您实现上面的智能指针时,您可能需要处理原始指针。
- 特定的语言互操作例程需要指针,如
operator new 。(然而,cv-void* 与普通的对象指针相比仍然有很大的不同和安全性,因为它排除了意外的指针算法,除非您依赖于像gnu一样的void* 的不一致扩展。) - 函数指针可以从没有捕获的lambda表达式转换,而函数引用不能。对于这种情况,您必须在非泛型代码中使用函数指针,即使您故意不希望使用可以为空的值。
因此,在实践中,答案是显而易见的:当有疑问时,避免使用指针。只有当有非常明确的理由认为没有其他更合适的时候,才必须使用指针。除了上面提到的一些例外情况,这样的选择几乎总是不是纯C++特定的(但可能是语言实现特定的)。这些实例可以是:好的。
- 您必须使用老式(C)API。
- 您必须满足特定C++实现的ABI要求。
- 您必须在运行时根据特定实现的假设与不同的语言实现(包括各种程序集、语言运行时和某些高级客户机语言的FFI)进行互操作。
- 在某些极端情况下,您必须提高翻译(编译和链接)的效率。
- 在某些极端情况下,必须避免符号膨胀。
语言中立警告
如果你通过一些谷歌搜索结果来查看这个问题(不是针对C++的),这很可能是错误的地方。好的。
C++中的引用是相当"奇怪"的,因为它本质上不是一流的:它们将被视为对象或被引用的函数,因此它们没有机会支持一些一流的操作,如成员访问操作符的左操作数独立于所引用对象的类型。其他语言对其引用可能有或可能没有类似的限制。好的。
C++中的引用可能不会保存不同语言之间的含义。例如,一般来说,引用并不意味着在C++中的值上有非空属性,所以这样的假设在某些其他语言中可能不起作用(并且您会很容易找到反例,例如Java、C…、…)。好的。
一般来说,不同编程语言中的引用之间仍然有一些共同的属性,但是让我们把它留给so中的其他一些问题。好的。
(旁注:这个问题可能比涉及的任何"C"类语言都要早,比如algol 68和pl/i。)好的。好啊。
指针和引用之间的差异
指针可以初始化为0,而引用不能初始化为0。实际上,引用也必须引用对象,但指针可以是空指针:
1 | int* p = 0; |
但是我们不能有
事实上,要正确地执行此操作,我们必须先声明并定义一个对象,然后才能引用该对象,因此前面代码的正确实现将是:
1 2 3 4 | Int x = 0; Int y = 5; Int& p = x; Int& p1 = y; |
另一个重要的一点是,我们可以在不初始化的情况下声明指针,但是如果引用必须始终引用变量或对象,则不能执行此类操作。然而,使用指针是有风险的,因此通常我们检查指针是否实际指向某个对象。在引用的情况下,不需要这样的检查,因为我们已经知道在声明期间引用对象是必需的。
另一个区别是指针可以指向另一个对象,但是引用始终引用同一个对象,让我们举个例子:
1 2 3 4 5 6 7 | 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模板的模板时,此类类模板将始终返回一个引用,而不是指针,以便使用运算符[]轻松读取或分配新值:
1 2 | 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"=" |
不同之处在于,非常量指针变量(不要与指向常量的指针混淆)可能在程序执行期间的某个时间发生更改,需要使用指针语义(&;,*)运算符,而引用只能在初始化时设置(这就是为什么可以仅在构造函数初始值设定项列表中设置它们,但不能以某种方式在并使用普通值访问语义。基本上,我们引入了一些引用来支持操作符重载,正如我在一些非常老的书中读到的那样。正如这个线程中所述,指针可以设置为0或任何您想要的值。0(空,nullptr)表示指针初始化为空。取消对空指针的引用是错误的。但实际上指针可能包含一个不指向某个正确内存位置的值。反过来,引用试图不允许用户初始化对某些不能被引用的对象的引用,因为您总是为其提供正确类型的右值。尽管有很多方法可以使引用变量初始化到错误的内存位置,但最好不要深入挖掘细节。在机器级别上,指针和引用都通过指针统一工作。让我们说,在必要的参考是句法糖。右值引用与此不同-它们自然是堆栈/堆对象。
我有一个引用和指针的类比,把引用看作对象的另一个名称,指针看作对象的地址。
1 2 3 4 5 6 7 8 9 10 11 | // receives an alias of an int, an address of an int and an int value public void my_function(int& a,int* b,int c){ int d = 1; // declares an integer named d int &e = d; // declares that e is an alias of d // using either d or e will yield the same result as d and e name the same object int *f = e; // invalid, you are trying to place an object in an address // imagine writting your name in an address field int *g = f; // writes an address to an address g = &d; // &d means get me the address of the object named d you could also // use &e as it is an alias of d and write it on g, which is an address so it's ok } |
我总是从C++核心准则中决定这个规则:
Prefer T* over T& when"no argument" is a valid option
如果对传递给函数的参数遵循约定,则可以使用引用和指针之间的差异。常量引用用于传递给函数的数据,指针用于传递给函数的数据。在其他语言中,您可以用诸如
1 2 3 4 5 | void DoSomething(const Foo& thisIsAnInput, Foo* thisIsAnOutput) { if (thisIsAnOuput) *thisIsAnOutput = thisIsAnInput; } |
使用引用作为输入,使用指针作为输出是谷歌风格指南的一部分。
塔琳?说:
You can't take the address of a reference like you can with pointers.
实际上你可以。
我引用另一个问题的答案:
The C++ FAQ says it best:
Unlike a pointer, once a reference is bound to an object, it can not be"reseated" to another object. The reference itself isn't an object (it has no identity; taking the address of a reference gives you the address of the referent; remember: the reference is its referent).
除了所有的答案,
可以使用引用实现运算符重载:
1 2 3 4 | my_point operator+(const my_point& a, const my_point& b) { return { a.x + b.x, a.y + b.y }; } |
将参数用作值将创建原始参数的临时副本,并且由于指针算术,使用指针不会调用此函数。
C中"&;"和"*"运算符的区别是什么?&;是C中的一元运算符,返回传递操作数的内存地址。这也称为操作员地址。<>The*is a unary operator which returns the value of object pointed by a pointer variable.它被称为运算符的值。它还用于声明指针变量。