我知道引用是语法糖,因此代码更容易读写.
但有什么区别?
以下答案和链接摘要:
指针可以重新分配任意次数,而在绑定后无法重新分配引用.
指针可以指向nowhere(NULL
),而引用总是指对象.
您不能使用指针来获取引用的地址.
没有"参考算术"(但您可以获取引用所指向的对象的地址,并对其进行指针运算&obj + 5
).
澄清一个误解:
C++标准非常谨慎,以避免规定编译器如何实现引用,但每个C++编译器都将引用实现为指针.也就是说,声明如下:
int &ri = i;如果它没有完全优化,则分配与指针相同的存储量,并将地址
i
放入该存储中.
因此,指针和引用都使用相同数量的内存.
作为基本规则,
使用函数参数和返回类型中的引用来提供有用的自我文档化接口.
使用指针实现算法和数据结构.
有趣的读物:
我最喜欢的C++ FAQ lite.
参考与指针.
参考文献简介.
参考文献和常量.
Brian R. Bon.. 1613
可以重新分配指针:
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 &r = x;
指针在堆栈上有自己的内存地址和大小(x86上为4个字节),而引用共享相同的内存地址(使用原始变量),但也会占用堆栈上的一些空间.由于引用与原始变量本身具有相同的地址,因此可以将引用视为同一变量的另一个名称.注意:指针指向的内容可以在堆栈或堆上.同上参考.我在这个陈述中的主张并不是指针必须指向堆栈.指针只是一个保存内存地址的变量.此变量位于堆栈上.由于引用在堆栈上有自己的空间,并且因为地址与它引用的变量相同.更多关于堆栈与堆.这意味着编译器不会告诉您有一个引用的实际地址.
int x = 0; int &r = x; int *p = &x; int *p2 = &r; assert(p == p2);
您可以指向指向提供额外间接级别的指针的指针.而引用仅提供一个间接层.
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);
指针可以直接分配nullptr
,而引用则不能.如果你努力尝试,并且知道如何,你可以制作一个参考地址nullptr
.同样,如果你努力尝试,你可以引用一个指针,然后该引用可以包含nullptr
.
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
指针可以遍历数组,您可以使用++
指向指针所指向的下一个项目,并+ 4
转到第5个元素.无论指针指向的对象是什么大小.
需要取消引用指针*
以访问它指向的内存位置,而可以直接使用引用.指向类/结构的指针用于->
访问它的成员,而引用使用a .
.
指针是保存内存地址的变量.无论引用如何实现,引用都具有与其引用的项相同的内存地址.
引用不能填充到数组中,而指针可以是(由用户提供@litb)
Const引用可以绑定到临时值.指针不能(不是没有一些间接):
const int &x = int(12); //legal C++ int *y = &int(12); //illegal to dereference a temporary.
这使得const&
在参数列表等中使用更安全.
2号是*不是真的.引用不仅仅是"同一个变量的另一个名称".引用可以以与指针非常相似的方式传递给函数,存储在类等中.它们独立于它们指向的变量而存在. (62认同)
另一个重要的差异:引用不能填充到数组中 (36认同)
布莱恩,这个堆栈并不重要.引用和指针不必占用堆栈空间.它们都可以在堆上分配. (29认同)
...但是取消引用NULL是未定义的.例如,您无法测试引用是否为NULL(例如,&ref == NULL). (21认同)
Brian,变量(在这种情况下是一个指针或引用)需要空间的事实*不*意味着它需要堆栈上的空间.指针和引用不仅可以*指向堆*,它们实际上可以在堆上*分配*. (20认同)
Derek,标准中没有提到引用需要空间.无论你使用引用做什么,即使你使用它的地址,也不会涉及额外的空间,引用只能是它引用的另一个对象的"真正"别名. (10认同)
如何实现引用的内部结构无关紧要.重要的是引用的地址与变量本身的地址相同.因此它们可以互换使用. (7认同)
@litb:虽然标准可能不需要为引用分配空间,但在许多情况下,编译器将被强制保留空间.考虑一个类中的引用,当然每个类的实例都可以引用不同的外部对象,并且它们引用的对象必须以某种方式保存 - 在大多数实现中作为自动解引用的指针(我实际上无法想到任何其他可能的实现)适合所有用例,但话说再说一次,我不是专家) (7认同)
@JohannGerell:你错了.您的示例不会重新分配引用.赋值的左操作数解析为对象`a`,而不是引用`c`. (6认同)
还有:"指针在堆栈上有自己的内存地址和大小".指针不需要在堆栈上分配(大多数都不是.) (5认同)
Derek指针是一个变量,就像任何其他变量一样.在x86系统上,它是4个字节.这4个字节在堆栈中.我没有声称他们指向堆栈中的内容. (4认同)
我不认为这样做是好的和安全的,因为这是错误的:-p我倾向于从其他好的答案中删除第2点. (3认同)
@Leushenko中的C代码`(int){12}`是复合文字,而不是临时的 (3认同)
引用可以为null.在某些情况下,你将refrence params传递给:function(*ptr); 如果ptr为NULL,那么将是引用. (2认同)
尼克你可以使用与你的链接相同的参数来说指针被实现为指针的指针,但你没有提到它.int x = 0; int y = 0; int*p =&x;*(int**)(&p)=&y; 因为没关系. (2认同)
我通过引用确实占用了堆栈上的一些空间来验证,我修改了#3以包含此信息.虽然运算符的地址仍然指定与它引用的变量相同. (2认同)
同意,但是我只是想在这种情况下弄清楚点:int x = 0; int * p =&x;,p在堆栈中占据4个字节。我知道可以说(new int *)在堆上分配指针本身。 (2认同)
我发现在这个页面上看图像:http://www-numi.fnal.gov/offline_software/srt_public_context/WebDocs/Companion/cxx_crib/pointers.html阅读这篇文章时让它变得非常容易理解差异;) (2认同)
@Brian:引用可以占用内存,就像指针一样。例如,如果您有一个作为引用的类的成员变量,则该类的每个实例将为该引用分配内存。在某些情况下,编译器可以优化分配内存以供参考的需求。而且没有很好的方法在代码中获取引用的地址(&my_reference只是返回引用对象的地址)。在这些方面,我同意引用的作用就像它们不占用内存一样,但是可以。 (2认同)
@DavidRodríguez-dribeas:我相信您在先前的评论中回想着。确实,可以强制编译器保留空间以供参考,但是在很多情况下不需要编译器,并且这种情况在编译时通常很容易检测到。发生这种情况时,编译器将不使用任何空间作为参考。对于编译器来说,优化指针要困难得多:他们首先必须确保没有代码占用指针的地址或不能从上下文中获取指针的地址。 (2认同)
@BenVoigt:你是对的!http://ideone.com/SwRZ6r-感谢您今天教我一些东西,由于这种误解,我很可能在我的生活中造成了一两个错误。 (2认同)
@Ben Voigt:我不确定我是否理解您的评论。对于不使用lambda的代码,lambda的介绍会发生什么变化?同样,在优化离开指针时(在某些情况下确实可以做到),还必须确保它不是空指针。如果查看clang程序集输出,则在某些情况下会引入这种检查,然后再优化后续代码中的指针。 (2认同)
@BenVoigt:我仍然不明白为什么它特别重要,尤其是在引入lambda时,存储引用外部存储是完全可以的,因为这就是引用的含义(引用只是一些现有存储的别名)。优化引用比这简单得多,它只是将其视为原始变量的别名。通过函数接口很难做到这一点,但是在函数体内部或内联中却很容易(因为引用将始终是同一存储的别名,对于指针而言是错误的)。 (2认同)
@BenVoigt:我相信我将不得不对此进行解释,因为“获取引用本身的地址”对我来说仍然是毫无意义的句子(因为引用中是否包含地址应该是实现细节)。我理解C ++ lambda等同于带有闭包上下文的函数调用(您可以使用functor对象完成对lambda的所有操作)。一旦通过引用将参数传递给函数或类,优化器就已经存在麻烦(隐藏的指针),而我期望使用lambda会遇到同样的麻烦。但是我不明白为什么会有更多的麻烦。 (2认同)
Int'restin'ly,第9点是C++与C不同的区域之一 - 在C中你*可以*取一个临时的地址:`int*y =&(int){12};`. (2认同)
Re 4:这是一个编译错误,因为你没有取消引用它.它不是对nullptr的保护,它只是因为没有特殊的空引用,它不能用作没有类型转换的初始化器,而有nullptr可以在没有类型转换的情况下使用.编写nullreference_t很容易,它会做同样的事情. (2认同)
指针在elipsis/varargs函数(`int printf(char const*format,...)`)之前作为最后一个参数.**参考文献不**,如果你尝试的话会有相当惊人的结果...... (2认同)
Christoph.. 362
什么是C++参考(适用于C程序员)
一个参考,可以看作是一个常量指针自动间接(不要用一个指针指向一个恒定值混淆!),即编译器将应用*
运营商为您服务.
必须使用非null值初始化所有引用,否则编译将失败.获取引用的地址既不可能 - 地址运算符将返回引用值的地址 - 也不可能在引用上进行算术运算.
C程序员可能不喜欢C++引用,因为当间接发生时,或者如果参数通过值或指针传递而不查看函数签名,它将不再明显.
C++程序员可能不喜欢使用指针,因为它们被认为是不安全的 - 虽然引用并不比常量指针更安全,除非在最琐碎的情况下 - 缺乏自动间接的便利性并带有不同的语义内涵.
请考虑C++ FAQ中的以下语句:
即使参考使用底层汇编语言的地址经常被实现,请不要不认为引用作为好笑的看着指针指向的对象.参考是对象.它不是指向对象的指针,也不是对象的副本.这是对象.
但如果参考确实是对象,那么怎么会有悬空参考?在非托管语言中,引用不可能比指针更"安全" - 通常只是不能跨范围边界可靠地别名值!
为什么我认为C++引用很有用来自C背景,C++引用可能看起来像一个有点愚蠢的概念,但是在可能的情况下仍然应该使用它们而不是指针:自动间接是方便的,并且在处理RAII时引用变得特别有用- 但不是因为任何感知的安全性优点,而是因为它们使写作惯用代码不那么尴尬.
RAII是C++的核心概念之一,但它与复制语义非常简单地交互.通过引用传递对象避免了这些问题,因为不涉及复制.如果语言中没有引用,则必须使用指针,这些指针使用起来比较麻烦,因此违反了语言设计原则,即最佳实践解决方案应该比替代方案更容易.
可以重新分配指针:
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 &r = x;
指针在堆栈上有自己的内存地址和大小(x86上为4个字节),而引用共享相同的内存地址(使用原始变量),但也会占用堆栈上的一些空间.由于引用与原始变量本身具有相同的地址,因此可以将引用视为同一变量的另一个名称.注意:指针指向的内容可以在堆栈或堆上.同上参考.我在这个陈述中的主张并不是指针必须指向堆栈.指针只是一个保存内存地址的变量.此变量位于堆栈上.由于引用在堆栈上有自己的空间,并且因为地址与它引用的变量相同.更多关于堆栈与堆.这意味着编译器不会告诉您有一个引用的实际地址.
int x = 0; int &r = x; int *p = &x; int *p2 = &r; assert(p == p2);
您可以指向指向提供额外间接级别的指针的指针.而引用仅提供一个间接层.
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);
指针可以直接分配nullptr
,而引用则不能.如果你努力尝试,并且知道如何,你可以制作一个参考地址nullptr
.同样,如果你努力尝试,你可以引用一个指针,然后该引用可以包含nullptr
.
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
指针可以遍历数组,您可以使用++
指向指针所指向的下一个项目,并+ 4
转到第5个元素.无论指针指向的对象是什么大小.
需要取消引用指针*
以访问它指向的内存位置,而可以直接使用引用.指向类/结构的指针用于->
访问它的成员,而引用使用a .
.
指针是保存内存地址的变量.无论引用如何实现,引用都具有与其引用的项相同的内存地址.
引用不能填充到数组中,而指针可以是(由用户提供@litb)
Const引用可以绑定到临时值.指针不能(不是没有一些间接):
const int &x = int(12); //legal C++ int *y = &int(12); //illegal to dereference a temporary.
这使得const&
在参数列表等中使用更安全.
一个参考,可以看作是一个常量指针自动间接(不要用一个指针指向一个恒定值混淆!),即编译器将应用*
运营商为您服务.
必须使用非null值初始化所有引用,否则编译将失败.获取引用的地址既不可能 - 地址运算符将返回引用值的地址 - 也不可能在引用上进行算术运算.
C程序员可能不喜欢C++引用,因为当间接发生时,或者如果参数通过值或指针传递而不查看函数签名,它将不再明显.
C++程序员可能不喜欢使用指针,因为它们被认为是不安全的 - 虽然引用并不比常量指针更安全,除非在最琐碎的情况下 - 缺乏自动间接的便利性并带有不同的语义内涵.
请考虑C++ FAQ中的以下语句:
即使参考使用底层汇编语言的地址经常被实现,请不要不认为引用作为好笑的看着指针指向的对象.参考是对象.它不是指向对象的指针,也不是对象的副本.这是对象.
但如果参考确实是对象,那么怎么会有悬空参考?在非托管语言中,引用不可能比指针更"安全" - 通常只是不能跨范围边界可靠地别名值!
为什么我认为C++引用很有用来自C背景,C++引用可能看起来像一个有点愚蠢的概念,但是在可能的情况下仍然应该使用它们而不是指针:自动间接是方便的,并且在处理RAII时引用变得特别有用- 但不是因为任何感知的安全性优点,而是因为它们使写作惯用代码不那么尴尬.
RAII是C++的核心概念之一,但它与复制语义非常简单地交互.通过引用传递对象避免了这些问题,因为不涉及复制.如果语言中没有引用,则必须使用指针,这些指针使用起来比较麻烦,因此违反了语言设计原则,即最佳实践解决方案应该比替代方案更容易.
如果你想变得非常迂腐,你可以用引号做一件事,你不能用指针做:延长临时对象的生命周期.在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引用绑定到临时对象,也不能获取其地址.
与流行的观点相反,可能有一个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指针来创建引用.这是实现这一目标的自动化方法.
templateT& 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 尝试将代码移植到另一个平台时暴露未定义的行为.
除了语法糖之外,引用是一个const
指针(不是指向a的指针const
).您必须在声明引用变量时建立它所引用的内容,并且以后不能更改它.
更新:现在我再考虑一下,有一个重要的区别.
可以通过获取其地址并使用const转换来替换const指针的目标.
参考目标不能以任何方式替换UB.
这应该允许编译器对引用进行更多优化.
你忘记了最重要的部分:
使用指针的->
成员访问使用带引用的成员访问.
foo.bar
是显然优于foo->bar
以同样的方式,VI是显然优于Emacs的 :-)
引用与指针非常相似,但它们是专门为优化编译器而设计的.
设计引用使得编译器更容易跟踪哪个引用别名哪个变量.两个主要特征非常重要:没有"参考算术",也没有重新分配参考文献.这些允许编译器在编译时找出哪些引用别名是哪些变量.
允许引用引用没有内存地址的变量,例如编译器选择放入寄存器的变量.如果取局部变量的地址,编译器很难将其放入寄存器中.
举个例子:
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] 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,所以array [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
超出范围.
实际上,引用并不像指针.
编译器保持对变量的"引用",将名称与内存地址相关联; 这是在编译时将任何变量名转换为内存地址的工作.
创建引用时,只告诉编译器为指针变量指定另一个名称; 这就是为什么引用不能"指向null",因为变量不能,也不能.
指针是变量; 它们包含其他变量的地址,或者可以为null.重要的是指针有一个值,而引用只有一个它正在引用的变量.
现在对实际代码的一些解释:
int a = 0; int& b = a;
在这里,您没有创建指向的另一个变量a
; 你只是在保存值的内存内容中添加另一个名称a
.此内存现在有两个名称,a
并且b
可以使用任一名称进行寻址.
void increment(int& n) { n = n + 1; } int a; increment(a);
调用函数时,编译器通常会为要复制的参数生成内存空间.函数签名定义了应该创建的空格,并给出了应该用于这些空间的名称.将参数声明为引用只是告诉编译器使用输入变量内存空间而不是在方法调用期间分配新的内存空间.说你的函数将直接操作在调用范围中声明的变量似乎很奇怪,但是请记住,在执行编译代码时,没有更多的范围; 只有普通的平坦内存,你的功能代码可以操纵任何变量.
现在可能存在编译器在编译时可能无法知道引用的情况,例如使用extern变量时.因此,引用可能会也可能不会被实现为底层代码中的指针.但是在我给你的例子中,它很可能不会用指针实现.
参考永远不可能NULL
.
虽然引用和指针都用于间接访问另一个值,但引用和指针之间存在两个重要区别.第一个是引用始终引用一个对象:在不初始化引用的情况下定义引用是错误的.赋值行为是第二个重要区别:分配给引用会更改引用所绑定的对象; 它不会重新绑定对另一个对象的引用.初始化后,引用始终引用相同的基础对象.
考虑这两个程序片段.在第一个中,我们将一个指针指向另一个:
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引用的值,而不是引用本身.在赋值之后,两个引用仍然引用它们的原始对象,并且这些对象的值现在也是相同的.
如果您不熟悉以抽象甚至学术方式学习计算机语言,那么语义上的差异可能会显得深奥.
在最高级别,引用的想法是它们是透明的"别名".您的计算机可能会使用一个地址来使它们工作,但您不应该担心:您应该将它们视为现有对象的"另一个名称",并且语法反映了这一点.它们比指针更严格,因此当您要创建悬空引用时,编译器可以更可靠地警告您,而不是在创建悬空指针时.
除此之外,指针和引用之间当然存在一些实际差异.使用它们的语法显然是不同的,你不能"重新定位"引用,引用虚无,或指向引用.
引用是另一个变量的别名,而指针保存变量的内存地址.引用通常用作函数参数,以便传递的对象不是副本而是对象本身.
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.
它占用了多少空间并不重要,因为你实际上看不到它将占用的任何空间的任何副作用(不执行代码).
另一方面,引用和指针之间的一个主要区别是分配给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工作的语言机制.
引用不是给某些内存的另一个名称.它是一个不可变的指针,在使用时会自动取消引用.基本上它归结为:
int& j = i;
它在内部成为
int* const j = &i;
这是基于本教程.写的更清楚:
>>> 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 type
)Tom
.也可以忘记这样的语句的术语是创建一个引用Tom
.
在C++中可以引用指针,但反过来不可能意味着指向引用的指针是不可能的.对指针的引用提供了更清晰的语法来修改指针.看看这个例子:
#includeusing 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 "< 并考虑上述程序的C版本.在C中你必须使用指向指针(多个间接),它会导致混乱,程序可能看起来很复杂.
#include/* 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++:对指针的引用
指针指针和指针指针
正如我所说,指向引用的指针是不可能的.尝试以下程序:
#includeusing namespace std; int main() { int x = 10; int *ptr = &x; int &*ptr1 = ptr; }
17> Aardvark..:我使用引用,除非我需要以下任何一个:
空指针可以用作标记值,通常是避免函数重载或使用bool的廉价方法.
你可以对指针进行算术运算.例如,
p += offset;
您可以编写`&r + offset`,其中`r`被声明为引用
18> FrankHB..:直接答案
C ++中的参考是什么?类型的某些特定实例不是对象类型。
C ++中的指针是什么?类型的某些特定实例是对象类型。
根据对象类型的ISO C ++定义:
一个对象类型是(可能CV -qualified)类型不函数类型,不是引用类型,而不是CV空隙。
可能很重要的一点是要知道,对象类型是C ++中Universe类型的顶级类别。参考也是顶级类别。但是指针不是。
在复合类型的上下文中一起提到了指针和引用。这基本上是由于声明语语法的性质所继承的,该声明语语法是从(并扩展的)C继承的,它没有引用。(此外,自C ++ 11起,引用的声明符不止一种,而指针仍是“ unityped”的:
&
+&&
vs*
。)因此,在这种情况下,用类似C的样式起草“扩展”特定的语言在某种程度上是合理的。(我仍然会争论说,声明符的语法浪费了很多语法表达性,使人类用户和实现都感到沮丧。因此,它们都不具备内置的条件。在新的语言设计中。不过,这是与PL设计完全不同的主题。)否则,将指针与引用一起限定为特定类型的指针就无关紧要了。除了语法相似性以外,它们仅共享很少的公共属性,因此在大多数情况下无需将它们放在一起。
请注意,以上声明仅提及“指针”和“引用”作为类型。关于它们的实例(例如变量),存在一些有趣的问题。还有太多误解。
顶级类别的差异已经可以揭示许多不直接与指针相关的具体差异:
对象类型可以具有顶级
cv
限定符。参考不能。根据抽象机器的语义,对象类型的变量确实会占用存储空间。引用不必占用存储空间(有关详细信息,请参见下面有关误解的部分)。
...
关于引用的其他一些特殊规则:
复合声明符对引用的限制更大。
引用可能会崩溃。
&&
在模板参数推导过程中基于参考折叠的参数特殊规则(作为“转发参考”)允许参数“完美转发”。引用在初始化时有特殊的规则。通过扩展,声明为引用类型的变量的生存期可以与普通对象不同。
顺便说一句,其他
std::initializer_list
一些类似初始化的上下文也遵循一些类似的参考生存期扩展规则。这是另一种蠕虫。...
误解
句法糖
我知道引用是语法糖,因此代码更易于读写。
从技术上讲,这是完全错误的。引用不是C ++中任何其他功能的语法糖,因为在没有任何语义差异的情况下,引用不能被其他功能完全替代。
(类似地,λ-表达 s为未在c任何其他特征++,因为它不能被精确地与像“未指定”属性模拟语法糖所捕获的变量的声明顺序,这可能是重要的,因为这样的变量的初始化顺序可以是重大。)
从严格的意义上讲,C ++只有几种语法糖。一个实例(从C继承)是内置的(非重载)运算符
[]
,它的定义与内置运算符unary*
和binary 完全具有特定组合形式的相同语义属性+
。存储
因此,指针和引用都使用相同数量的内存。
上面的陈述是完全错误的。为避免这种误解,请改用ISO C ++规则:
来自[intro.object] / 1:
...物体在其构造期间,整个生命周期以及破坏期间都占据一个存储区域。...
来自[dcl.ref] / 4:
未确定参考是否需要存储。
请注意,这些是语义属性。
语用学
即使在语言设计的意义上说指针没有足够的资格与引用放在一起,仍然有一些论点使其在其他上下文中(例如,在对参数类型进行选择时)在它们之间进行选择值得商bat。
但这还不是全部。我的意思是,除了要考虑的指针还是引用之外,还有更多的事情需要考虑。
如果您不必坚持这种过度特定的选择,那么在大多数情况下,答案很短:您不必使用指针,而不必使用指针。指针通常很糟糕,因为它们暗示了太多您不期望的事情,并且它们将依赖太多隐式假设,从而破坏了代码的可维护性和(甚至)可移植性。不必要地依赖指针绝对是一种不好的样式,从现代C ++的意义上应该避免这种情况。重新考虑您的目的,您最终会发现在大多数情况下指针是最后一种功能。
有时,语言规则明确要求使用特定类型。如果要使用这些功能,请遵守规则。
拷贝构造函数需要特定类型的简历 -
&
引用类型作为第一个参数的类型。(通常应该是const
合格的。)移动构造函数需要特定类型的简历 -
&&
引用类型作为第一个参数的类型。(通常应该没有限定符。)运算符的特定重载需要引用或非引用类型。例如:
operator=
作为特殊成员函数重载时,需要引用类型类似于copy / move构造函数的第一个参数。后缀
++
需要虚拟int
。...
如果您知道值传递(即使用非引用类型)就足够了,请直接使用它,尤其是在使用支持C ++ 17强制复制省略的实现时。(警告:但是,要详尽地论证这种必要性可能会非常复杂。)
如果要使用所有权来处理某些句柄,请使用
unique_ptr
和shared_ptr
,或者(如果要求它们不透明,也可以单独使用自制的)智能指针,而不要使用原始指针。如果您要在某个范围内进行某些迭代,请使用迭代器(或标准库尚未提供的某些范围),而不要使用原始指针,除非您确信原始指针在非常特定的情况下会做得更好(例如,减少标头依赖性)案件。
如果您知道传递值就足够了,并且想要一些显式的可为空的语义,请使用wrapper之类的
std::optional
,而不要使用原始指针。如果您知道由于上述原因传递值不理想,并且您不希望使用可为空的语义,请使用{lvalue,rvalue,forwarding} -references。
即使您确实想要像传统指针这样的语义,也经常有更合适的东西,例如
observer_ptr
在Library Fundamental TS中。
唯一的例外无法使用当前语言解决:
当您在上面实现智能指针时,您可能必须处理原始指针。
特定的语言互操作例程需要指针,例如
operator new
。(然而,CV -void*
仍然是相当不同的,比普通的对象指针更安全,因为它排除了意外的指针算术,除非你是靠一些非符合扩展上void*
像GNU的。)可以从lambda表达式转换函数指针而无需捕获,而函数引用则不能。对于这种情况,即使您故意不希望使用可为空的值,也必须在非通用代码中使用函数指针。
因此,在实践中,答案是如此明显:如有疑问,请避免使用指针。仅在出于非常明显的原因没有其他更合适的理由时才需要使用指针。除了上面提到的一些例外情况外,此类选择几乎总是不完全是特定于C ++的(但可能是特定于语言实现的)。这样的实例可以是:
您必须使用旧式(C)API。
您必须满足特定C ++实现的ABI要求。
您必须基于特定实现的假设,在运行时与不同的语言实现(包括各种程序集,语言运行时和某些高级客户端语言的FFI)进行互操作。
在某些极端情况下,您必须提高翻译(编译和链接)的效率。
在某些极端情况下,您必须避免符号膨胀。
语言中立警告
如果您通过某些Google搜索结果(不是特定于C ++)来查看问题,则很可能是错误的位置。
在C ++中引用是很“奇怪”,因为它本质上不是一流的:他们将被视为对对象或功能被称为所以他们就没有机会来支持喜欢成为左操作数的一些一流的操作的成员访问运算符独立于所引用对象的类型。其他语言对其参考文献可能有也可能没有类似的限制。
C ++中的引用可能不会保留不同语言之间的含义。例如,引用通常并不暗示像C ++那样的值具有非null属性,因此此类假设可能不适用于某些其他语言(并且您会很容易找到反例,例如Java,C#,...)。
通常,在不同编程语言中的引用之间仍然存在一些公共属性,但让我们将其留给SO中的其他问题。
(附带说明:这个问题可能比涉及任何“ C样”语言的时间要早,例如ALGOL 68 vs. PL / I。)
19> Andrzej..:指针和引用之间存在一个根本区别,我没有看到任何人提到过:引用在函数参数中启用了pass-by-reference语义.指针虽然起初不可见,但它们不会:它们只提供按值传递的语义.这篇文章已经很好地描述了.
问候,&rzej
@Andrzj:这只是我评论中单句的很长版本:**句柄被复制.**
20> kriss..:另一个区别是你可以有一个指向void类型的指针(它意味着指向任何东西的指针),但禁止引用void.
int a; void * p = &a; // ok void & p = a; // forbidden我不能说我对这种特殊的差异感到非常满意.我更倾向于允许带有地址的含义引用,以及引用的相同行为.它允许使用引用定义一些C库函数的等价物,如memcpy.
21> Tory..:冒着混淆的风险,我想抛出一些输入,我确定它主要取决于编译器如何实现引用,但是在gcc的情况下,引用只能指向堆栈上的变量实际上并不正确,以此为例:
#includeint 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之后?嗯,显然它编译得很好,但是在运行时导致分段错误,因为它不再指向有效变量,我们基本上有一个仍然存在的破坏引用(直到它超出范围),但是没用.
换句话说,引用只不过是一个指针,它将指针机制抽象出来,使它更安全,更容易使用(没有偶然的指针数学,没有混合'.'和' - >'等),假设你不要像我上面的例子那样尝试任何废话;)
现在无论编译器如何处理引用,它总是会有一些指针,因为引用必须引用特定内存地址的特定变量才能使其按预期工作,没有解决这个问题(因此术语"参考").
唯一要记住引用的主要规则是它们必须在声明时定义(除了标题中的引用外,在这种情况下它必须在构造函数中定义,在它包含的对象之后是构造它来定义它为时已晚).
请记住,我上面的例子就是这样,展示参考资料的例子,你永远不会想要以这些方式使用参考!为了正确使用参考文献,这里已经有很多答案可以解决问题
22> 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); }在内联指针版本1时,许多编译器实际上会强制写入内存(我们正在明确地获取地址).但是,他们会将参考文献保留在更优化的寄存器中.
当然,对于没有内联的函数,指针和引用生成相同的代码,如果它们未被函数修改并返回,则通过值传递内在函数而不是引用更好.
23> Don Wakefiel..:引用的另一个有趣用途是提供用户定义类型的默认参数:
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; }默认flavor使用'绑定const引用到引用的临时'方面.
24> Arlene Batad..:该计划可能有助于理解问题的答案.这是引用"j"的简单程序和指向变量"x"的指针"ptr".
#includeusing 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
25> 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++和标准库的相当大的一部分,因此这是引用的一个主要属性.
26> Arthur Tacca..:指针和引用之间存在非常重要的非技术差异:通过指针传递给函数的参数比通过非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 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
引用,即使函数从不期望anullptr
.也就是说,那些人认为fn3()
不应该允许像上面这样的功能签名.Google的C++风格指南就是一个例子.
27> George R..:也许一些比喻会有所帮助; 在桌面屏幕空间的上下文中 -
引用要求您指定实际窗口.
指针需要屏幕上的一块空间的位置,您确保它将包含该窗口类型的零个或多个实例.
28> 小智..:指针和引用之间的区别
指针可以初始化为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模板的模板时,这种类模板将始终返回引用而不是指针,以便使用operator []轻松读取或分配新值:
Std ::vectorv(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 "="
29> Zorgiev..:区别在于非常量指针变量(不要与指向常量的指针混淆)可能在程序执行期间的某个时间发生变化,需要使用指针语义(&,*)运算符,而引用可以在初始化时设置only(这就是为什么你只能在构造函数初始化列表中设置它们,但不能以其他方式设置它们)并使用普通值访问语义.基本上引用了引用,以支持运算符重载,正如我在一本非常古老的书中所读到的那样.正如有人在这个线程中所说 - 指针可以设置为0或任何你想要的值.0(NULL,nullptr)表示指针初始化为nothing.取消引用空指针是一个错误.但实际上指针可能包含一个不指向某个正确内存位置的值.引用依次尝试不允许用户初始化对无法引用的引用的引用,因为您始终为其提供正确类型的rvalue.尽管有很多方法可以将引用变量初始化为错误的内存位置 - 但是最好不要深入研究细节.在机器级别,指针和引用均匀地工作 - 通过指针.让我们说必要的参考文献是语法糖.rvalue引用与此不同 - 它们自然是堆栈/堆对象.