我知道引用是语法糖,因此代码更容易读写 .
但有什么区别?
以下答案和链接摘要:
-
指针可以重新分配任意次数,而在绑定后无法重新分配引用 .
-
指针可以指向任何地方(
NULL
),而引用始终指向一个对象 . -
您无法使用指针获取引用的地址 .
-
没有"reference arithmetic"(但是您可以获取引用指向的对象的地址,并在
&obj + 5
上对其执行指针运算) .
澄清一个误解:
C标准非常谨慎,以避免规定编译器如何实现引用,但每个C编译器都将引用实现为指针 . 也就是说,声明如:int&ri = i;
如果它没有完全优化,则分配与指针相同的存储量,并将i的地址放入该存储器中 .
So, a pointer and a reference both use the same amount of memory.
作为基本规则,
-
使用函数参数和返回类型中的引用来提供有用的自我文档接口 .
-
使用指针实现算法和数据结构 .
有趣的读物:
30 回答
与流行的观点相反,可能有一个NULL引用 .
当然,使用参考文件要困难得多 - 但是如果你管理它,你会撕掉你的头发试图找到它 . 参考文献在C中本身并不安全!
从技术上讲,这是 invalid reference ,而不是空引用 . C不支持空引用作为您可能在其他语言中找到的概念 . 还有其他类型的无效引用 . 任何无效的引用都会引发 undefined behavior 的幽灵,就像使用无效指针一样 .
在分配给引用之前,实际错误是在NULL指针的解引用中 . 但是我不知道任何编译器会在这种情况下产生任何错误 - 错误会传播到代码中的某个点 . 这就是让这个问题如此阴险的原因 . 大多数情况下,如果你取消引用一个NULL指针,你就会在那个位置崩溃,并且不需要太多的调试就可以搞清楚 .
我上面的例子简短而且做作 . 这是一个更真实的例子 .
我想重申,获取空引用的唯一方法是通过格式错误的代码,一旦你拥有它,你就会得到未定义的行为 . 它 never 检查空引用是否有意义;例如,您可以尝试
if(&bar==NULL)...
但编译器可能会优化该语句!有效的引用永远不能为NULL,所以从编译器的视图来看,比较总是假的,并且可以自由地将if
子句作为死代码消除 - 这是未定义行为的本质 .避免麻烦的正确方法是避免取消引用NULL指针来创建引用 . 这是实现这一目标的自动化方法 .
对于那些具有更好写作技巧的人来看这个问题,请参阅Jim Hyslop和Herb Sutter的Null References .
有关解除引用空指针的危险的另一个示例,请参阅Raymond Chen的Exposing undefined behavior when trying to port code to another platform .
这是基于tutorial . 写的更清楚:
简单地记住,
更重要的是,因为我们可以参考几乎任何指针教程,指针是指针算法支持的对象,它使指针类似于数组 .
看下面的陈述,
alias_Tom
可以理解为alias of a variable
(与typedef
不同,即alias of a type
)Tom
. 也可以忘记这样的语句的术语是创建Tom
的引用 .此外,作为内联函数的参数的引用可以与指针不同地处理 .
在内联指针版本1时,许多编译器实际上会强制写入内存(我们正在明确地获取地址) . 但是,他们会将参考文献保留在更优化的寄存器中 .
当然,对于没有内联的函数,指针和引用生成相同的代码,如果它们未被函数修改并返回,则通过值传递内在函数而不是引用它总是更好 .
如果你想变得非常迂腐,你可以用引号做一件事,你不能用指针做:延长临时对象的生命周期 . 在C中,如果将const引用绑定到临时对象,则该对象的生命周期将成为引用的生命周期 .
在此示例中,s3_copy复制作为串联结果的临时对象 . 而s3_reference本质上成为临时对象 . 它实际上是对临时对象的引用,该临时对象现在具有与引用相同的生命周期 .
如果您在没有
const
的情况下尝试此操作它应该无法编译 . 您不能将非const引用绑定到临时对象,也不能为此处获取其地址 .指针和引用之间存在非常重要的非技术差异:通过指针传递给函数的参数比通过非const引用传递给函数的参数更加明显 . 例如:
回到C,看起来像
fn(x)
的调用只能通过值传递,所以它绝对不能修改x
;要修改一个参数,你需要传递一个指针fn(&x)
. 因此,如果参数前面没有&
,您就知道它不会被修改 . (相反,&
意味着修改,不是真的,因为你有时必须通过const
指针传递大的只读结构 . )有些人认为,在阅读代码时,这是一个非常有用的功能,指针参数应始终用于可修改的参数而不是非
const
引用,即使函数从不期望nullptr
. 也就是说,那些人认为不应该允许像上面的fn3()
这样的功能签名 . Google's C++ style guidelines就是一个例子 .如果您不熟悉以抽象或甚至学术方式学习计算机语言,那么语义上的差异可能会显得深奥 .
在最高级别,引用的想法是它们是透明的“别名” . 您的计算机可能会使用一个地址来使它们工作,但您不应该担心:您应该将它们视为现有对象的“另一个名称”,并且语法反映了这一点 . 它们比指针更严格,因此当您要创建悬空引用时,编译器可以更可靠地警告您,而不是在您创建悬空指针时 .
除此之外,指针和引用之间当然存在一些实际差异 . 使用它们的语法明显不同,你不能“重新定位”引用,引用虚无,或指向引用 .
实际上,引用并不像指针 .
编译器保持对变量的“引用”,将名称与内存地址相关联;这是在编译时将任何变量名转换为内存地址的工作 .
创建引用时,只告诉编译器为指针变量指定另一个名称;这就是为什么引用不能“指向null”,因为变量不能,也不能 .
指针是变量;它们包含其他变量的地址,或者可以为null . 重要的是指针有一个值,而引用只有一个它引用的变量 .
现在对实际代码的一些解释:
在这里,您不是要创建指向
a
的另一个变量;您只是在保存a
值的内存内容中添加另一个名称 . 此内存现在有两个名称a
和b
,可以使用任一名称进行寻址 .调用函数时,编译器通常会为要复制的参数生成内存空间 . 函数签名定义了应该创建的空格,并给出了应该用于这些空间的名称 . 将参数声明为引用只是告诉编译器使用输入变量内存空间而不是在方法调用期间分配新的内存空间 . 说你的函数将直接操作调用范围中声明的变量似乎很奇怪,但请记住,在执行编译代码时,没有更多的范围;只有普通的平坦内存,你的功能代码可以操纵任何变量 .
现在可能存在编译器在编译时可能无法知道引用的情况,例如使用extern变量时 . 因此,引用可能会也可能不会被实现为底层代码中的指针 . 但是在我给你的例子中,它很可能不会用指针实现 .
区别在于非常量指针变量(不要与指向常量的指针混淆)可能在程序执行期间的某个时间发生变化,需要使用指针语义(&,*)运算符,而初始化时可以设置引用only(这就是为什么你只能在构造函数初始化列表中设置它们,但不能以其他方式设置它们)并使用普通值访问语义 . 基本上引用了引用,以支持运算符重载,正如我在一本非常古老的书中所读到的那样 . 正如有人在这个线程中所说 - 指针可以设置为0或任何你想要的值 . 0(NULL,nullptr)表示指针初始化为nothing . 取消引用空指针是一个错误 . 但实际上指针可能包含一个不指向某个正确内存位置的值 . 引用依次尝试不允许用户初始化对无法引用的引用的引用,因为您始终为其提供正确类型的rvalue . 虽然有很多方法可以做引用变量被初始化为错误的内存位置 - 最好不要深入挖掘细节 . 在机器级别,指针和引用均匀地工作 - 通过指针 . 让我们说必要的参考文献是语法糖 . 右值引用与此不同 - 它们自然是堆栈/堆对象 .
引用不是给予某些内存的另一个名称 . 它是一个不可变的指针,在使用时会自动取消引用 . 基本上它归结为:
它在内部成为
你忘记了最重要的部分:
使用指针访问成员 - 使用
->
带引用的成员访问使用
.
foo.bar
明显优于foo->bar
,就像vi明显优于Emacs :-)引用的另一个有趣用途是提供用户定义类型的默认参数:
默认flavor使用'绑定const引用到引用的临时'方面 .
它占用了多少空间并不重要,因为你实际上看不到它将占用的任何空间的任何副作用(不执行代码) .
另一方面,引用和指针之间的一个主要区别是分配给const引用的临时值存在,直到const引用超出范围 .
例如:
将打印:
这是允许ScopeGuard工作的语言机制 .
我觉得还有另外一点,这里没有涉及 .
与指针不同,引用对于它们引用的对象是 syntactically equivalent ,即可以应用于对象的任何操作都可以用于引用,并且具有完全相同的语法(例外当然是初始化) .
虽然这可能看起来很肤浅,但我相信这个属性对于许多C功能至关重要,例如:
模板 . 由于模板参数是鸭子类型,因此类型的语法属性都很重要,因此通常可以将同一模板与
T
和T&
一起使用 .(或
std::reference_wrapper<T>
仍然依赖于T&
的隐式演员表)涵盖
T&
和T&&
的模板更为常见 .左值 . 考虑语句
str[0] = 'X';
没有引用它只适用于c-strings(char* str
) . 通过引用返回字符允许用户定义的类具有相同的表示法 .复制构造函数 . 从语法上讲,将对象传递给复制构造函数是有意义的,而不是指向对象的指针 . 但是,复制构造函数无法通过值获取对象 - 这将导致对同一复制构造函数的递归调用 . 这使引用成为唯一的选择 .
运算符重载 . 通过引用,可以将间接引入操作员调用 - 例如,
operator+(const T& a, const T& b)
,同时保留相同的中缀表示法 . 这也适用于常规的重载功能 .这些点赋予了C和标准库的相当大的一部分,因此这是引用的一个主要属性 .
什么是C参考(适用于C程序员)
引用可以被认为是一个常量指针(不要与指向常量值的指针混淆!)和自动间接,即编译器将为您应用
*
运算符 .必须使用非null值初始化所有引用,否则编译将失败 . 获取引用的地址既不可能 - 地址运算符将返回引用值的地址 - 也不可能在引用上进行算术运算 .
C程序员可能不喜欢C引用,因为当间接发生时,或者如果参数通过值或指针传递而不查看函数签名,它将不再是显而易见的 .
C程序员可能不喜欢使用指针,因为它们被认为是不安全的 - 虽然引用并不比常量指针更安全,除了在最微不足道的情况下 - 缺乏自动间接的便利性并带有不同的语义内涵 .
请考虑C++ FAQ中的以下语句:
但如果参考确实是对象,那么怎么会有悬空引用呢?在非托管语言中,'s impossible for references to be any '更安全' than pointers - there generally just isn' t可以跨范围边界可靠地别名值!
为什么我认为C引用很有用
来自C背景,C引用可能看起来像一个有点傻的概念,但是应该尽可能使用它们而不是指针:自动间接是方便的,并且引用在处理RAII时变得特别有用 - 但不是因为任何感知的安全性优点,而是因为它们使写作惯用代码不那么尴尬 .
RAII是C的核心概念之一,但它与复制语义非常简单地交互 . 通过引用传递对象避免了这些问题,因为不涉及复制 . 如果语言中没有引用,则必须使用指针,这些指针使用起来比较麻烦,因此违反了语言设计原则,即最佳实践解决方案应该比替代方案更容易 .
虽然引用和指针都用于间接访问另一个值,但引用和指针之间存在两个重要区别 . 第一个是引用始终引用一个对象:在不初始化引用的情况下定义引用是错误的 . 赋值行为是第二个重要区别:赋值给引用会更改引用所绑定的对象;它不会重新绑定对另一个对象的引用 . 初始化后,引用始终引用相同的基础对象 .
考虑这两个程序片段 . 在第一个中,我们将一个指针指向另一个:
在赋值后,ival,pi处理的对象保持不变 . 赋值会更改pi的值,使其指向不同的对象 . 现在考虑一个类似的程序,分配两个引用:
此赋值更改ival,即ri引用的值,而不是引用本身 . 在赋值之后,两个引用仍然引用它们的原始对象,并且这些对象的值现在也是相同的 .
也许一些比喻会有所帮助;在桌面屏幕空间的上下文中 -
引用要求您指定实际窗口 .
指针需要屏幕上一块空间的位置,您确保它将包含该窗口类型的零个或多个实例 .
引用是另一个变量的别名,而指针保存变量的内存地址 . 引用通常用作函数参数,以便传递的对象不是副本而是对象本身 .
引用和指针都可用于更改另一个函数内的一个函数的局部变量 . 当它们作为函数的参数传递或从函数返回时,它们都可用于保存大对象的复制,以获得效率增益 . 尽管有上述相似之处,但引用和指针之间存在以下差异 .
References are less powerful than pointers
1)创建引用后,以后不能引用另一个引用;它不能重新安置 . 这通常用指针完成 .
2)引用不能为NULL . 指针通常为NULL,表示它们没有指向任何有效的东西 .
3)必须在声明时初始化引用 . 指针没有这样的限制
由于上述限制,C中的引用不能用于实现链接列表,树等数据结构 . 在Java中,引用没有上述限制,可用于实现所有数据结构 . 引用在Java中更强大,是Java不需要指针的主要原因 .
References are safer and easier to use:
1)更安全:由于必须初始化引用,因此不太可能存在像野生指针这样的野蛮引用 . 仍然可以使引用不引用有效位置
2)易于使用:引用不需要解除引用操作符来访问该值 . 它们可以像普通变量一样使用 . 只有在申报时才需要'&'运营商 . 此外,可以使用点运算符(' . ')访问对象引用的成员,而不像需要箭头运算符( - >)来访问成员的指针 .
与上述原因一起,很少有地方像复制构造函数参数,其中指针不能使用 . 必须使用引用传递复制构造函数中的参数 . 同样 references must be used for overloading some operators like ++ .
指针和引用之间的差异
指针可以初始化为0而引用不能 . 实际上,引用也必须引用一个对象,但指针可以是空指针:
但我们不能拥有
int& p = 0;
和int& p=5 ;
.事实上,要正确地执行它,我们必须首先声明和定义一个对象,然后我们可以对该对象进行引用,因此前面代码的正确实现将是:
另一个重要的一点是,我们可以在没有初始化的情况下进行指针的声明,但是在引用的情况下不能做这样的事情,它必须总是引用变量或对象 . 但是这样使用指针是有风险的,因此通常我们检查指针是否实际指向某事物 . 在引用的情况下,不需要这样的检查,因为我们已经知道在声明期间引用对象是必需的 .
另一个区别是指针可以指向另一个对象但是引用总是引用同一个对象,让我们举个例子:
另一点:当我们有一个类似STL模板的模板时,这种类模板总会存在返回引用而不是指针,以便使用operator []轻松读取或分配新值:
冒着混淆的风险,我想抛出一些输入,我确定它主要取决于编译器如何实现引用,但是在gcc的情况下,引用只能指向堆栈上的变量实际上并不正确,以此为例:
哪个输出:
如果您注意到甚至内存地址完全相同,则意味着引用成功指向堆上的变量!现在,如果你真的想变得怪异,这也有效:
哪个输出:
因此引用是引擎盖下的一个指针,它们都只是存储一个内存地址,地址所指向的是无关紧要的,如果我调用std :: cout << str_ref;你会怎么想?在调用delete&str_ref之后?好吧,显然它编译得很好,但是在运行时导致分段错误,因为它不再指向有效变量,我们基本上有一个仍然存在的破坏引用(直到它超出范围),但是没用 .
换句话说,引用只不过是一个指针,它将指针机制抽象出来,使其更安全,更容易使用(没有偶然的指针数学,没有混淆' . '和' - >'等),假设你不要像我上面的例子那样尝试任何废话;)
现在 regardless 编译器如何处理引用,它将 always 在引擎盖下有某种指针,因为引用 must 引用特定内存地址的特定变量,以使其按预期工作,没有解决这个问题(因此'reference'一词 .
唯一要记住引用的主要规则是它们必须在声明时定义(除了头中的引用外,在这种情况下它必须在构造函数中定义,在它包含的对象之后是构造它来定义它为时已晚) .
Remember, my examples above are just that, examples demonstrating what a reference is, you would never want to use a reference in those ways! For proper usage of a reference there are plenty of answers on here already that hit the nail on the head
引用与指针非常相似,但它们经过精心设计,有助于优化编译器 .
引用的设计使得编译器更容易跟踪哪个引用别名哪个变量 . 两个主要特征非常重要:没有"reference arithmetic"并且没有重新分配引用 . 这些允许编译器在编译时找出哪些引用别名是哪些变量 .
允许引用引用没有内存地址的变量,例如编译器选择放入寄存器的变量 . 如果你取一个局部变量的地址,编译器很难把它放在一个寄存器中 .
举个例子:
优化编译器可能会意识到我们正在访问[0]和[1]相当多的一组 . 它希望优化算法:
为了进行这样的优化,需要证明在调用期间没有任何东西可以改变数组[1] . 这很容易做到 . 我永远不会少于2,所以array [i]永远不能引用数组[1] . maybeModify()被赋予a0作为参考(别名数组[0]) . 因为没有“引用”算法,编译器只需要证明maybeModify永远不会得到x的地址,并且它已经证明没有任何改变数组[1] .
它还必须证明,当我们在a0中有一个临时寄存器副本时,未来的调用没有办法读/写[0] . 这通常是微不足道的,因为在很多情况下很明显,引用永远不会存储在像类实例这样的永久结构中 .
现在用指针做同样的事情
行为是一样的;只是现在更难以证明maybeModify不会修改数组[1],因为我们已经给它一个指针;这只猫已经不在了 . 现在它必须做更加困难的证明:对maybeModify进行静态分析以证明它永远不会写入&x 1.它还必须证明它永远不会保存一个可以引用数组[0]的指针,这就像棘手 .
现代编译器在静态分析方面越来越好,但总是很好地帮助它们并使用引用 .
当然,除非进行这种巧妙的优化,否则编译器确实会在需要时将引用转换为指针 .
编辑:发布这个答案五年后,我发现了一个实际的技术差异,其中引用不同于查看相同寻址概念的不同方式 . 引用可以以指针不能的方式修改临时对象的生命周期 .
通常,临时对象(例如通过调用
createF(5)
创建的对象)将在表达式的末尾被销毁 . 但是,通过将该对象绑定到引用ref
,C将延长该临时对象的生命周期,直到ref
超出范围 .我总是根据C核心指南中的this规则来决定:
我使用引用,除非我需要以下任何一个:
空指针可以用作标记值,通常是避免函数重载或使用bool的廉价方法 .
你可以对指针进行算术运算 . 例如,
p += offset;
在C中可以引用指针,但反向不可能意味着指向引用的指针是不可能的 . 对指针的引用提供了更清晰的语法来修改指针 . 看看这个例子:
并考虑上述程序的C版本 . 在C中你必须使用指向指针(多个间接),它会导致混乱,程序可能看起来很复杂 .
有关指针引用的更多信息,请访问以下内容:
C++: Reference to Pointer
Pointer-to-Pointer and Reference-to-Pointer
正如我所说,指向引用的指针是不可能的 . 尝试以下程序:
除了语法糖之外,引用是
const
指针(不是指向const
的指针) . 您必须在声明引用变量时确定它所引用的内容,并且以后不能更改它 .更新:现在我再考虑一下,有一个重要的区别 .
const指针的目标可以通过获取其地址并使用const转换来替换 .
参考目标不能以任何方式替换UB .
这应该允许编译器对引用进行更多优化 .
指针和引用之间有一个根本区别,我没有看到任何人提到过:引用在函数参数中启用了引用语法 . 指针虽然起初不可见,但它们不会:它们只提供按值传递的语义 . 这在this article中已经很好地描述了 .
问候,&rzej
引用不能,必须在初始化时分配:
nullptr
,而引用则不能 . 如果你努力尝试,并且知道如何,你可以制作参考地址nullptr
. 同样,如果你努力尝试,你可以引用一个指针,然后该引用可以包含nullptr
.指针可以遍历数组,您可以使用
++
转到指针指向的下一个项目,并使用+ 4
转到第5个元素 . 无论指针指向的对象是什么大小 .需要使用
*
取消引用指针以访问它指向的内存位置,而可以直接使用引用 . 指向类/结构的指针使用->
来访问它的成员,而引用使用.
.指针是一个保存内存地址的变量 . 无论引用如何实现,引用都具有与其引用的项相同的内存地址 .
引用不能填充到数组中,而指针可以是(由用户@litb提及)
Const引用可以绑定到临时对象 . 指针不能(不是没有一些间接):
这使得
const&
更安全,可以在参数列表中使用,等等 .另一个区别是你可以有一个指向void类型的指针(它意味着指向任何东西的指针)但禁止引用void .
我不能说我对这种特殊的差异感到非常满意 . 我更倾向于允许带有地址的含义引用,以及引用的相同行为 . 它允许使用引用定义一些C库函数的等价物,如memcpy .
引用永远不能是
NULL
.该计划可能有助于理解问题的答案 . 这是引用“j”和指向变量“x”的指针“ptr”的简单程序 .
运行程序,看看输出,你会明白 .
另外,请花10分钟观看此视频:https://www.youtube.com/watch?v=rlJrrGV0iOg