指针变量和引用变量有什么区别?
指针可以重新赋值:
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引用可以绑定到临时值。指针不能(没有一些间接方式):
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 ++的核心概念之一,但它与复制语义的交互方式并不简单。通过引用传递对象避免这些问题,因为没有涉及复制。如果语言中不存在引用,则必须使用指针,这更加繁琐,违反了语言设计原则,即最佳实践解决方案应比替代方案更容易。
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 引用绑定到临时对象,也不能获取它的地址。const &
绑定,对象的生命周期得以延长,只有当引用超出范围时,才会调用_actual_引用类型的析构函数(与可能是基类的引用类型相比)。由于它是一个引用,因此不会在其间发生切片。 - David Rodríguez - dribeasAnimal x = fast ? getHare() : getTortoise()
,那么x
将面临经典的切割问题,而Animal& x = ...
将可以正常工作。 - Arthur TaccaT* const
。 - Carlo Woodint i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);
是可以的。 - curiousguy与普遍观点相反,有可能存在一个为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,因此从编译器的角度来看,比较始终为false,并且可以消除if
子句作为死代码 - 这是未定义行为的本质。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。
另一个例子是当尝试将代码移植到另一个平台时,暴露未定义行为,请注意避免对空指针进行引用。
->
正是提供了对指针引用的对象成员的访问方式,就像对指针本身一样。 - Max Truxa.
和 ->
与 vi 和 emacs 有关系呢 :) (注:.
和 ->
是 C/C++ 等编程语言中用于访问结构体成员和指针成员的运算符,vi 和 emacs 则是两种 Unix/Linux 下的文本编辑器) - artm引用(References)与指针(Pointers)非常相似,但它们是专门为优化编译器而设计的。
举个例子:
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(别名为array [0])。由于没有“引用”算术,编译器只需证明maybeModify永远不会获得x的地址,并且已经证明没有任何东西会更改array [1]。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
超出范围。
maybeModify
没有获取与x
相关的任何地址要容易得多,而不是证明一堆指针算术不会发生。 - Cort Ammonvoid maybeModify(int& x) { 1[&x]++; }
,其他上面的注释正在讨论它。 - Ben Voigt实际上,引用并不像指针。
编译器会对变量保留“引用”,将名称与内存地址关联起来;它的工作是在编译时将任何变量名称转换为内存地址。
当您创建一个引用时,您只是告诉编译器将另一个名称分配给指针变量;这就是为什么引用不能“指向空”,因为变量既不能存在也不能不存在。
指针是变量;它们包含某个其他变量的地址,或者可以为空。重要的是,指针有一个值,而引用只有一个被引用的变量。
现在解释一些真实代码:
int a = 0;
int& b = a;
在这里,您不是创建指向a
的另一个变量;您只是为保存a
的内存内容添加了另一个名称。 这块内存现在有两个名称:a
和b
,可以使用任何一个名称来访问它。
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
调用函数时,编译器通常会生成内存空间以便将参数复制到该空间中。函数签名定义了应该创建的空间,并给出了应使用这些空间的名称。将参数声明为引用只是告诉编译器在方法调用期间使用输入变量的内存空间,而不是分配新的内存空间。可能感觉奇怪的是,您的函数将直接操作在调用作用域中声明的变量,但请记住,在执行编译后的代码时,没有更多的作用域; 只有纯粹的平面内存,您的函数代码可以操作任何变量。
现在可能会有一些情况,在编译时编译器可能无法知道引用的位置,例如当使用外部变量时。因此,在底层代码中,引用可能会或可能不会被实现为指针。但在我给您的示例中,它很可能不会被实现为指针。
引用永远不可能是NULL
。
void Foo::bar() { virtual_baz(); }
这样的代码导致段错误。如果你没有意识到引用可能为空,你就无法将空追溯到其起源。 - cmaster - reinstate monicaint &r=*p;
是未定义行为。此时,你不是拥有一个“指向NULL的引用”,而是拥有一个完全无法推理的程序。 - cdhowie如果你不熟悉以抽象甚至学术的方式研究计算机语言,那么可能会出现一些看似深奥的语义差异。
在最高级别上,引用的想法是它们是透明的“别名”。你的计算机可能使用地址使它们工作,但你不必担心这个问题:你应该把它们想象成为现有对象的“另一个名称”,并且语法反映了这一点。它们比指针更严格,因此编译器可以更可靠地警告你即将创建挂起的引用,而不是即将创建挂起的指针。
除此之外,指针和引用之间当然也存在一些实际差异。使用它们的语法显然是不同的,并且你不能“重新设置”引用,也不能有对无物的引用或指向引用的指针。
int &x = *(int*)0;
,实际上引用确实可以指向NULL。 - Calmarius