问题 C ++中指针变量和引用变量之间有什么区别?
我知道引用是语法糖,因此代码更容易读写。
但有什么区别?
以下答案和链接摘要:
- 指针可以重新分配任意次数,而绑定后无法重新分配引用。
- 指针无处可指(
NULL
),而引用总是指一个对象。
- 您无法使用指针获取引用的地址。
- 没有“参考算术”(但你可以获取引用所指向的对象的地址,并在其上执行指针算术,如
&obj + 5
)。
澄清一个误解:
C ++标准非常小心,以避免规定编译器的方式
实现引用,但每个C ++编译器都实现
引用作为指针。也就是说,声明如下:
int &ri = i;
如果它没有完全优化, 分配相同数量的存储空间
作为指针,并放置地址
的 i
进入那个存储区。
因此,指针和引用都使用相同数量的内存。
作为基本规则,
- 使用函数参数和返回类型中的引用来提供有用的自记录接口。
- 使用指针实现算法和数据结构。
有趣的读物:
10856
2017-09-11 20:03
起源
答案:
可以重新分配指针:
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
去第五个元素。无论指针指向的对象是什么大小。
指针需要取消引用 *
访问它指向的内存位置,而引用可以直接使用。指向类/结构的指针使用 ->
访问它的成员,而引用使用a .
。
指针是保存内存地址的变量。无论引用如何实现,引用都具有与其引用的项相同的内存地址。
引用不能填充到数组中,而指针可以是(由用户@litb提及)
Const引用可以绑定到临时值。指针不能(不是没有一些间接):
const int &x = int(12); //legal C++
int *y = &int(12); //illegal to dereference a temporary.
这使得 const&
更安全地用于参数列表等等。
1376
2018-02-27 21:26
什么是C ++参考(对于C程序员)
一个 参考 可以被认为是一个 常量指针 (不要与指向常量值的指针混淆!)与自动间接,即编译器将应用 *
经营者为您服务。
必须使用非null值初始化所有引用,否则编译将失败。获取引用的地址既不可能 - 地址运算符将返回引用值的地址 - 也不可能在引用上进行算术运算。
C程序员可能不喜欢C ++引用,因为当间接发生时,或者如果参数通过值或指针传递而不查看函数签名,它将不再是显而易见的。
C ++程序员可能不喜欢使用指针,因为它们被认为是不安全的 - 虽然引用并不比常量指针更安全,除了在最微不足道的情况下 - 缺乏自动间接的便利性并带有不同的语义内涵。
请考虑以下声明 C ++ FAQ:
即使引用通常使用地址中的地址来实现
基础汇编语言,请做 不 把参考想象成一个
有趣的看指向一个对象。一个参考 是 物体。它是
不是指向对象的指针,也不是对象的副本。它 是 该
目的。
但如果参考 真 是对象,怎么会有悬空参考?在非托管语言中,引用不可能比指针更“安全” - 通常只是不能跨范围边界可靠地对值进行别名!
为什么我认为C ++引用很有用
来自C背景,C ++引用可能看起来像一个有点愚蠢的概念,但是应该尽可能使用它们而不是指针:自动间接 是 方便,参考在处理时变得特别有用 RAII - 但不是因为任何感知到的安全优势,而是因为它们使写作惯用代码不那么尴尬。
RAII是C ++的核心概念之一,但它与复制语义非常简单地交互。通过引用传递对象避免了这些问题,因为不涉及复制。如果语言中没有引用,则必须使用指针,这些指针使用起来比较麻烦,因此违反了语言设计原则,即最佳实践解决方案应该比替代方案更容易。
301
2017-09-11 21:43
如果你想变得非常迂腐,你可以用引号做一件事,你不能用指针做:延长临时对象的生命周期。在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引用绑定到临时对象,也不能为此处获取其地址。
152
2017-09-11 21:06
与流行的观点相反,可能有一个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。
有关解除引用空指针的危险的另一个示例,请参阅 尝试将代码移植到另一个平台时暴露未定义的行为 作者:Raymond Chen。
104
2017-09-11 20:07
除了语法糖,参考是一个 const
指针(不 指向一个 const
)。您必须在声明引用变量时确定它所引用的内容,并且以后不能更改它。
更新:现在我再考虑一下,有一个重要的区别。
const指针的目标可以通过获取其地址并使用const转换来替换。
参考目标不能以任何方式替换UB。
这应该允许编译器对引用进行更多优化。
97
2017-09-11 22:10
你忘记了最重要的部分:
使用指针的成员访问 ->
使用引用的成员访问 .
foo.bar
是 明确地 优于 foo->bar
以同样的方式 六 是 明确地 优于 Emacs的 :-)
96
2017-09-19 12:23
实际上,引用并不像指针。
编译器保持对变量的“引用”,将名称与内存地址相关联;这是在编译时将任何变量名转换为内存地址的工作。
创建引用时,只告诉编译器为指针变量指定另一个名称;这就是为什么引用不能“指向null”,因为变量不能,也不能。
指针是变量;它们包含其他变量的地址,或者可以为null。重要的是指针有一个值,而引用只有一个它正在引用的变量。
现在对实际代码的一些解释:
int a = 0;
int& b = a;
在这里,您不会创建指向的另一个变量 a
;你只是在保存值的内存内容中添加另一个名称 a
。这个记忆现在有两个名字, a
和 b
,它可以使用任一名称来解决。
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
调用函数时,编译器通常会为要复制的参数生成内存空间。函数签名定义了应该创建的空格,并给出了应该用于这些空间的名称。将参数声明为引用只是告诉编译器使用输入变量内存空间而不是在方法调用期间分配新的内存空间。说你的函数将直接操作调用范围中声明的变量似乎很奇怪,但请记住,在执行编译代码时,没有更多的范围;只有普通的平坦内存,你的功能代码可以操纵任何变量。
现在可能存在编译器在编译时可能无法知道引用的情况,例如使用extern变量时。因此,引用可能会也可能不会被实现为底层代码中的指针。但是在我给你的例子中,它很可能不会用指针实现。
57
2017-09-01 03:44
引用与指针非常相似,但它们经过精心设计,有助于优化编译器。
- 设计引用使得编译器更容易跟踪哪个引用别名哪个变量。两个主要特征非常重要:没有“参考算术”,也没有重新分配参考文献。这些允许编译器在编译时找出哪些引用别名是哪些变量。
- 允许引用引用没有内存地址的变量,例如编译器选择放入寄存器的变量。如果你取一个局部变量的地址,编译器很难把它放在一个寄存器中。
举个例子:
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]的指针,这只是太棘手了。
现代编译器在静态分析方面越来越好,但总是很好地帮助它们并使用引用。
当然,除非进行这种巧妙的优化,否则编译器确实会在需要时将引用转换为指针。
50
2017-09-11 20:12
32
2018-05-20 19:26