我知道引用是语法糖,所以代码更容易读写。
但是指针变量和引用变量有什么区别呢?
可以重新分配指针:
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; // error
int &r = x;
指针变量有它自己的标识:一个独特的、可见的内存地址,可以用一元运算符获取&
,一定量的空间可以用该sizeof
运算符测量。在引用上使用这些运算符会返回一个与引用绑定的值相对应的值;引用自己的地址和大小是不可见的。由于引用以这种方式假定原始变量的身份,因此可以方便地将引用视为同一变量的另一个名称。
int x = 0;
int &r = x;
int *p = &x;
int *p2 = &r;
assert(p == p2); // &x == &r
assert(&p != &p2);
您可以将任意嵌套的指针指向提供额外间接级别的指针。引用仅提供一级间接。
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);
可以分配指针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 10
std::cout
<< (&r != nullptr
? "not null" : "null")
<< std::endl;
bool f(int &r) { return &r != nullptr; }
// prints "not null" under GCC 10
std::cout
<< (f(*static_cast<int *>(nullptr))
? "not null" : "null")
<< std::endl;
但是,您可以引用值为 的指针nullptr
。
指针可以遍历数组;您可以使用++
转到指针指向的下一项,并+ 4
转到第 5 个元素。这与指针指向的对象大小无关。
需要取消引用指针*
才能访问它指向的内存位置,而可以直接使用引用。指向类/结构的指针用于->
访问其成员,而引用使用.
.
引用不能放入数组,而指针可以(用户@litb 提到)
常量引用可以绑定到临时对象。指针不能(不是没有一些间接性):
const int &x = int(12); // legal C++
int *y = &int(12); // illegal to take the address of a temporary.
这使得const &
在参数列表等中使用更方便。
可以将引用视为具有自动间接寻址的常量指针(不要与指向常量值的指针混淆!),即编译器将为*
您应用运算符。
所有引用都必须用非空值初始化,否则编译将失败。既不可能获得引用的地址——地址运算符将返回被引用值的地址——也不可能对引用进行算术运算。
C 程序员可能不喜欢 C++ 引用,因为当间接发生或如果参数通过值或指针传递而不查看函数签名时,它将不再明显。
C++ 程序员可能不喜欢使用指针,因为它们被认为是不安全的——尽管引用实际上并不比常量指针更安全,除非在最微不足道的情况下——缺乏自动间接的便利性并带有不同的语义内涵。
考虑C++ FAQ中的以下语句:
尽管引用通常使用底层汇编语言中的地址来实现,但请不要将引用视为指向对象的有趣指针。引用是对象。它不是指向对象的指针,也不是对象的副本。它是对象。
但如果引用真的是对象,怎么会有悬空引用呢?在非托管语言中,引用不可能比指针更“安全”——通常没有一种方法可以跨范围边界可靠地对值进行别名!
来自 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
指针(不是指向a 的指针const
)。您必须在声明引用变量时确定它所引用的内容,并且以后不能更改它。
更新:现在我想多了,有一个重要的区别。
const 指针的目标可以通过获取其地址并使用 const 强制转换来替换。
引用的目标不能以任何方式替换 UB。
这应该允许编译器对引用进行更多优化。
与流行的观点相反,可以有一个为 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在尝试将代码移植到另一个平台时暴露未定义的行为。
引用与指针非常相似,但它们是专门为优化编译器而设计的。
举个例子:
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
超出范围。
实际上,引用并不像指针。
编译器保持对变量的“引用”,将名称与内存地址相关联;这就是编译时将任何变量名转换为内存地址的工作。
创建引用时,您只需告诉编译器您为指针变量分配了另一个名称;这就是为什么引用不能“指向 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.
What is a reference in C++? Some specific instance of type that is not an object type.
What is a pointer in C++? Some specific instance of type that is an object type.
From the ISO C++ definition of object type:
An object type is a (possibly cv-qualified) type that is not a function type, not a reference type, and not cv void.
It may be important to know, object type is a top-level category of the type universe in C++. Reference is also a top-level category. But pointer is not.
Pointers and references are mentioned together in the context of compound type. This is basically due to the nature of the declarator syntax inherited from (and extended) C, which has no references. (Besides, there are more than one kind of declarator of references since C++ 11, while pointers are still "unityped": &
+&&
vs. *
.) So drafting a language specific by "extension" with similar style of C in this context is somewhat reasonable. (I will still argue that the syntax of declarators wastes the syntactic expressiveness a lot, makes both human users and implementations frustrating. Thus, all of them are not qualified to be built-in in a new language design. This is a totally different topic about PL design, though.)
Otherwise, it is insignificant that pointers can be qualified as a specific sorts of types with references together. They simply share too few common properties besides the syntax similarity, so there is no need to put them together in most cases.
Note the statements above only mentions "pointers" and "references" as types. There are some interested questions about their instances (like variables). There also come too many misconceptions.
The differences of the top-level categories can already reveal many concrete differences not tied to pointers directly:
cv
qualifiers. References cannot.A few more special rules on references:
&&
parameters (as the "forwarding references") based on reference collapsing during template parameter deduction allow "perfect forwarding" of parameters.std::initializer_list
follows some similar rules of reference lifetime extension. It is another can of worms.I know references are syntactic sugar, so code is easier to read and write.
Technically, this is plain wrong. References are not syntactic sugar of any other features in C++, because they cannot be exactly replaced by other features without any semantic differences.
(Similarly, lambda-expressions are not syntactic sugar of any other features in C++ because it cannot be precisely simulated with "unspecified" properties like the declaration order of the captured variables, which may be important because the initialization order of such variables can be significant.)
C++ only has a few kinds of syntactic sugars in this strict sense. One instance is (inherited from C) the built-in (non-overloaded) operator []
, which is defined exactly having same semantic properties of specific forms of combination over built-in operator unary *
and binary +
.
So, a pointer and a reference both use the same amount of memory.
The statement above is simply wrong. To avoid such misconceptions, look at the ISO C++ rules instead:
From [intro.object]/1:
... An object occupies a region of storage in its period of construction, throughout its lifetime, and in its period of destruction. ...
From [dcl.ref]/4:
It is unspecified whether or not a reference requires storage.
Note these are semantic properties.
Even that pointers are not qualified enough to be put together with references in the sense of the language design, there are still some arguments making it debatable to make choice between them in some other contexts, for example, when making choices on parameter types.
But this is not the whole story. I mean, there are more things than pointers vs references you have to consider.
If you don't have to stick on such over-specific choices, in most cases the answer is short: you do not have the necessity to use pointers, so you don't. Pointers are usually bad enough because they imply too many things you don't expect and they will rely on too many implicit assumptions undermining the maintainability and (even) portability of the code. Unnecessarily relying on pointers is definitely a bad style and it should be avoided in the sense of modern C++. Reconsider your purpose and you will finally find that pointer is the feature of last sorts in most cases.
&
reference type as the 1st parameter type. (And usually it should be const
qualified.)&&
reference type as the 1st parameter type. (And usually there should be no qualifiers.)operator=
as special member functions requires reference types similar to 1st parameter of copy/move constructors.++
requires dummy int
.unique_ptr
and shared_ptr
(or even with homebrew ones by yourself if you require them to be opaque), rather than raw pointers.std::optional
, rather than raw pointers.observer_ptr
in Library Fundamental TS.The only exceptions cannot be worked around in the current language:
operator new
. (However, cv-void*
is still quite different and safer compared to the ordinary object pointers because it rules out unexpected pointer arithmetics unless you are relying on some non conforming extension on void*
like GNU's.)So, in practice, the answer is so obvious: when in doubt, avoid pointers. You have to use pointers only when there are very explicit reasons that nothing else is more appropriate. Except a few exceptional cases mentioned above, such choices are almost always not purely C++-specific (but likely to be language-implementation-specific). Such instances can be:
If you come to see the question via some Google search result (not specific to C++), this is very likely to be the wrong place.
References in C++ is quite "odd", as it is essentially not first-class: they will be treated as the objects or the functions being referred to so they have no chance to support some first-class operations like being the left operand of the member access operator independently to the type of the referred object. Other languages may or may not have similar restrictions on their references.
References in C++ will likely not preserve the meaning across different languages. For example, references in general do not imply nonnull properties on values like they in C++, so such assumptions may not work in some other languages (and you will find counterexamples quite easily, e.g. Java, C#, ...).
There can still be some common properties among references in different programming languages in general, but let's leave it for some other questions in SO.
(A side note: the question may be significant earlier than any "C-like" languages are involved, like ALGOL 68 vs. PL/I.)
它占用多少空间并不重要,因为您实际上看不到它占用的任何空间的任何副作用(不执行代码)。
另一方面,引用和指针之间的一个主要区别是分配给 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 工作的语言机制。
这是基于教程。写的更清楚了:
>>> 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
.
引用不是给某些记忆的另一个名称。它是一个不可变的指针,在使用时会自动取消引用。基本上可以归结为:
int& j = i;
它在内部变成
int* const j = &i;
在 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;
}
除非我需要以下任何一种,否则我会使用参考:
空指针可以用作标记值,这通常是避免函数重载或使用布尔值的廉价方法。
您可以对指针进行算术运算。例如,p += offset;
我没有看到有人提到过指针和引用之间的一个根本区别:引用在函数参数中启用了按引用传递的语义。指针,虽然一开始不可见,但不可见:它们只提供按值传递的语义。这篇文章已经很好地描述了这一点。
问候, &rzej
另一个区别是您可以拥有指向 void 类型的指针(它意味着指向任何东西的指针),但禁止对 void 的引用。
int a;
void * p = &a; // ok
void & p = a; // forbidden
我不能说我对这种特殊的差异感到非常满意。我更希望它被允许对具有地址的任何内容进行有意义的引用,否则引用的行为相同。它将允许使用引用定义一些 C 库函数的等价物,例如 memcpy。
冒着增加混乱的风险,我想输入一些输入,我确信这主要取决于编译器如何实现引用,但在 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 之后?好吧,显然它编译得很好,但是在运行时会导致分段错误,因为它不再指向一个有效的变量,我们基本上有一个仍然存在的损坏的引用(直到它超出范围),但是没有用。
换句话说,引用只不过是一个指针,它抽象了指针机制,使其更安全、更易于使用(没有意外的指针数学,没有混淆 '.' 和 '->' 等),假设你不要像我上面的例子那样尝试任何废话;)
现在无论编译器如何处理引用,它总会有某种指针,因为引用必须引用特定内存地址的特定变量才能按预期工作,没有解决这个问题(因此术语“参考”)。
对于引用,唯一需要记住的重要规则是它们必须在声明时定义(头文件中的引用除外,在这种情况下,它必须在构造函数中定义,在它包含的对象之后是构建它为时已晚定义它)。
请记住,我上面的示例只是演示什么是引用的示例,您永远不会希望以这些方式使用引用!为了正确使用参考,这里已经有很多答案一针见血
此外,作为内联函数参数的引用的处理方式可能与指针不同。
void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
int testptr=0;
increment(&testptr);
}
void increftest()
{
int testref=0;
increment(testref);
}
许多编译器在内联指针版本时实际上会强制写入内存(我们正在显式获取地址)。但是,他们会将参考留在更优化的寄存器中。
当然,对于没有内联的函数,指针和引用生成相同的代码,如果函数没有修改和返回它们,那么通过值传递内在函数总是比通过引用传递更好。
引用的另一个有趣用途是提供用户定义类型的默认参数:
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 引用绑定到临时”方面。
该程序可能有助于理解问题的答案。这是一个引用“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
我觉得这里还有一点没有涉及。
与指针不同,引用在语法上等同于它们所引用的对象,即任何可以应用于对象的操作都适用于引用,并且具有完全相同的语法(当然,初始化除外)。
虽然这可能看起来很肤浅,但我相信这个属性对于许多 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++ 和标准库的相当一部分功能,因此这是引用的一个主要属性。
指针和引用之间有一个非常重要的非技术差异:通过指针传递给函数的参数比通过非常量引用传递给函数的参数更可见。例如:
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 的 C++ 风格指南就是一个例子。
也许一些隐喻会有所帮助;在您的桌面屏幕空间的上下文中 -
尽管指针和引用以几乎相同的方式“在幕后”实现,但编译器对它们的处理方式不同,从而导致上述所有差异。
我最近写的一篇文章比我在这里展示的要详细得多,应该对这个问题很有帮助,尤其是关于记忆中的事情是如何发生的:
可以将指针初始化为 0,而不能将引用初始化。事实上,引用也必须引用一个对象,但指针可以是空指针:
int* p = 0;
但是我们不能拥有int& p = 0;
and 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 * const a = &b
是一样的int& 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 上,如果函数可以被内联,则编译器会在内联调用中对其进行优化,因为动态范围是已知的,但在函数定义中,参数总是作为指针解引用(期望变量的地址为由引用引用)其中它可能被另一个翻译单元使用并且编译器不知道动态范围,除非该函数当然被声明为静态函数,否则它不能在翻译单元之外使用,然后只要在函数中没有通过引用修改它,它就按值传递,
简单来说,我们可以说引用是变量的替代名称,而指针是保存另一个变量地址的变量。例如
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 */
以下答案和链接的摘要:
NULL
),而引用总是指向一个对象。&obj + 5
)。澄清一个误解:
C++ 标准非常小心地避免规定编译器如何实现引用,但是每个 C++ 编译器都将引用实现为指针。也就是说,声明如下:
int &ri = i;
如果它没有完全优化,则分配与指针相同的存储量,并将地址
i
放入该存储中。
因此,指针和引用都使用相同数量的内存。
作为基本规则,
有趣的阅读:
不同之处在于非常量指针变量(不要与指向常量的指针混淆)可能会在程序执行期间的某个时间更改,需要使用指针语义(&,*)运算符,而可以在初始化时设置引用仅(这就是为什么您只能在构造函数初始化程序列表中设置它们,但不能以其他方式设置它们)并使用普通值访问语义。正如我在一些非常古老的书中所读到的,基本上引入了引用以支持运算符重载。正如有人在这个线程中所说 - 指针可以设置为 0 或您想要的任何值。0(NULL, nullptr) 表示指针初始化为空。取消引用空指针是错误的。但实际上指针可能包含一个不指向某个正确内存位置的值。反过来,引用尝试不允许用户初始化对由于您始终为其提供正确类型的右值而无法引用的事物的引用。尽管有很多方法可以将引用变量初始化到错误的内存位置 - 最好不要深入研究细节。在机器级别上,指针和参考都统一工作 - 通过指针。假设在基本引用中是语法糖。右值引用与此不同 - 它们自然是堆栈/堆对象。尽管有很多方法可以将引用变量初始化到错误的内存位置 - 最好不要深入研究细节。在机器级别上,指针和参考都统一工作 - 通过指针。假设在基本引用中是语法糖。右值引用与此不同 - 它们自然是堆栈/堆对象。尽管有很多方法可以将引用变量初始化到错误的内存位置 - 最好不要深入研究细节。在机器级别上,指针和参考都统一工作 - 通过指针。假设在基本引用中是语法糖。右值引用与此不同 - 它们自然是堆栈/堆对象。
除了这里的所有答案,
您可以使用引用实现运算符重载:
my_point operator+(const my_point& a, const my_point& b)
{
return { a.x + b.x, a.y + b.y };
}
使用参数作为值将创建原始参数的临时副本,并且由于指针算术,使用指针不会调用此函数。
我对引用和指针进行了类比,将引用视为对象的另一个名称,将指针视为对象的地址。
// 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
}
I always decide by this rule from C++ Core Guidelines:
Prefer T* over T& when "no argument" is a valid option
如果您遵循传递给函数的参数的约定,则可以使用引用和指针之间的区别。常量引用用于传递给函数的数据,指针用于传递出函数的数据。in
在其他语言中,您可以使用和之类的关键字明确地对此进行注释out
。在 C++ 中,您可以(按照约定)声明等价物。例如,
void DoSomething(const Foo& thisIsAnInput, Foo* thisIsAnOutput)
{
if (thisIsAnOuput)
*thisIsAnOutput = thisIsAnInput;
}
使用引用作为输入和指针作为输出是Google 风格指南的一部分。
指针是保存另一个变量的内存地址的变量,其中作为引用是现有变量的别名。(已存在变量的另一个名称)
1. 一个指针可以被初始化为:
int b = 15;
int *q = &b;
或者
int *q;
q = &b;
作为参考,
int b=15;
int &c=b;
(在一个步骤中声明和初始化)
您不能像指针一样取消引用引用,当取消引用时会在该位置给出值,
引用和指针都通过地址工作......
所以
你可以这样做
int* val = 0xDEADBEEF;*val 位于 0xDEADBEEF。
你不能这样做 int& val = 1;
*val 是不允许的。
指针(*)的基本含义是“地址处的值”,这意味着您提供的任何地址都会在该地址处提供值。更改地址后,它将给出新值,而引用变量用于引用任何特定变量,并且将来不能更改以引用任何其他变量。
“我知道引用是语法糖,所以代码更容易读写”
这个。引用不是实现指针的另一种方式,尽管它涵盖了一个巨大的指针用例。指针是一种数据类型——通常指向实际值的地址。但是,它可以设置为零,或者使用地址算术等设置在地址之后的几个位置。引用是具有自己值的变量的“语法糖”。
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; -- legal
int **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.