可以重新分配指针: int x = 5;整数 y = 6;诠释* p; p = &x; p = &y; *p = 10;断言(x == 5);断言(y == 10);引用不能重新绑定,必须在初始化时绑定:int x = 5;整数 y = 6;诠释&q; // 错误 int &r = x;指针变量有它自己的标识:可以用一元 & 运算符获取的独特的、可见的内存地址和可以用 sizeof 运算符测量的一定量的空间。在引用上使用这些运算符会返回一个与引用绑定的值相对应的值;引用自己的地址和大小是不可见的。由于引用以这种方式假定原始变量的身份,因此可以方便地将引用视为同一变量的另一个名称。诠释 x = 0; int &r = x; int *p = &x; int *p2 = &r;断言(p == p2); // &x == &r 断言(&p != &p2);您可以将任意嵌套的指针指向提供额外间接级别的指针。引用仅提供一级间接。诠释 x = 0;整数 y = 0; int *p = &x;整数 *q = &y; int **pp = &p; **pp = 2; pp = &q; // *pp 现在是 q **pp = 4;断言(y == 4);断言(x == 2);可以为指针分配 nullptr,而引用必须绑定到现有对象。如果你足够努力,你可以绑定一个对 nullptr 的引用,但这是未定义的,并且不会表现得一致。 /* 下面的代码是未定义的;你的编译器可能会以不同的方式优化它,发出警告,或者完全拒绝编译它 */ int &r = *static_cast
什么是 C++ 参考(针对 C 程序员)
reference 可以被认为是具有自动间接寻址的 常量指针(不要与指向常量值的指针混淆!),即编译器将应用 {1 } 运算符为您服务。
所有引用都必须用非空值初始化,否则编译将失败。既不可能获得引用的地址——地址运算符将返回被引用值的地址——也不可能对引用进行算术运算。
C 程序员可能不喜欢 C++ 引用,因为当间接发生或如果参数通过值或指针传递而不查看函数签名时,它将不再明显。
C++ 程序员可能不喜欢使用指针,因为它们被认为是不安全的——尽管引用实际上并不比常量指针更安全,除非在最微不足道的情况下——缺乏自动间接的便利性并带有不同的语义内涵。
考虑 C++ FAQ 中的以下语句:
尽管引用通常使用底层汇编语言中的地址来实现,但请不要将引用视为指向对象的有趣指针。引用是对象。它不是指向对象的指针,也不是对象的副本。它是对象。
但如果引用真的是对象,怎么会有悬空引用呢?在非托管语言中,引用不可能比指针更“安全”——通常没有一种方法可以跨范围边界可靠地对值进行别名!
为什么我认为 C++ 参考很有用
来自 C 背景,C++ 引用可能看起来有点愚蠢,但仍应尽可能使用它们而不是指针:自动间接 方便,并且引用在处理 {1 } - 但不是因为任何感知到的安全优势,而是因为它们使编写惯用代码变得不那么尴尬。
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 &
绑定的事实延长了对象的生命周期,并且仅当引用超出范围时,实际引用类型的析构函数(与引用类型相比,这可能是base) 被调用。由于它是参考,因此不会在两者之间进行切片。
Animal x = fast ? getHare() : getTortoise()
,那么 x
将面临经典的切片问题,而 Animal& x = ...
将正常工作。
除了语法糖,引用是一个 const
指针(not 指向 const
的指针)。您必须在声明引用变量时确定它所引用的内容,并且以后不能更改它。
更新:现在我想多了,有一个重要的区别。
const 指针的目标可以通过获取其地址并使用 const 强制转换来替换。
引用的目标不能以任何方式替换 UB。
这应该允许编译器对引用进行更多优化。
T* const
(这恰好从您的代码中消除了很多 * 和 & )。
int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);
可以。
与流行的观点相反,可以有一个为 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 的 Exposing undefined behavior when trying to port code to another platform。
你忘记了最重要的部分:
带有指针的成员访问使用 ->
带有引用的成员访问使用 .
foo.bar
明显优于 foo->bar
,就像 vi 明显优于 Emacs :-)
->
为指针引用所提供的,就像指针本身一样。
.
和 ->
与 vi 和 emacs 有关系 :)
.
比使用 ->
更好,但就像 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];
}
}
一个优化编译器可能会意识到我们正在访问大量的 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
超出范围。
maybeModify
不获取与 x
相关的任何地址比证明一堆指针算术不发生要容易得多。
void maybeModify(int& x) { 1[&x]++; }
的评论标记为过时,上面的其他评论正在讨论
实际上,引用并不像指针。
编译器保持对变量的“引用”,将名称与内存地址相关联;这就是编译时将任何变量名转换为内存地址的工作。
创建引用时,您只需告诉编译器您为指针变量分配了另一个名称;这就是为什么引用不能“指向 null”,因为变量不能是,也不是。
指针是变量;它们包含一些其他变量的地址,或者可以为空。重要的是指针有一个值,而引用只有一个它正在引用的变量。
现在对实际代码进行一些解释:
int a = 0;
int& b = a;
在这里,您没有创建另一个指向 a
的变量;您只是在保存 a
值的内存内容中添加另一个名称。该内存现在有两个名称,a
和 b
,可以使用任一名称对其进行寻址。
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
调用函数时,编译器通常会为要复制到的参数生成内存空间。函数签名定义了应该创建的空间,并给出了这些空间应该使用的名称。将参数声明为引用只是告诉编译器使用输入变量内存空间,而不是在方法调用期间分配新的内存空间。说您的函数将直接操作在调用范围内声明的变量可能看起来很奇怪,但请记住,在执行编译后的代码时,没有更多的范围;只有普通的平坦内存,您的函数代码可以操纵任何变量。
现在可能有一些情况,您的编译器在编译时可能无法知道引用,例如使用 extern 变量时。因此,引用可能会也可能不会被实现为底层代码中的指针。但是在我给你的例子中,它很可能不会用指针来实现。
引用永远不能是 NULL
。
void Foo::bar() { virtual_baz(); }
这样的段错误代码。如果您不知道引用可能为 null,则无法将 null 追溯到其来源。
int &r=*p;
是未定义的行为。那时,您没有“指向 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.
直接回答
C++ 中的引用是什么?一些不是对象类型的特定类型实例。
C++中的指针是什么?一些特定的类型实例,它是对象类型。
从 the ISO C++ definition of object type:
对象类型是(可能是 cv 限定的)类型,它不是函数类型,不是引用类型,也不是 cv void。
重要的是要知道,对象类型是 C++ 中类型宇宙的顶级类别。参考也是顶级类别。但指针不是。
指针和引用一起提到in the context of compound type。这基本上是由于从 C 继承(和扩展)的声明符语法的性质,它没有引用。 (此外,自 C++ 11 以来有不止一种引用声明符,而指针仍然是“单一类型的”:&
+&&
vs. *
。)因此,通过“扩展”来起草一种特定的语言在这种情况下,C 的风格是有些合理的。 (我仍然会争辩说,声明符的语法大量浪费了语法表达能力,使人类用户和实现都感到沮丧。因此,它们都没有资格内置 em> 在一种新的语言设计中。不过,这是关于 PL 设计的完全不同的主题。)
否则,指针可以被限定为具有引用的特定类型的类型是无关紧要的。除了语法相似性之外,它们只是共享太少的共同属性,因此在大多数情况下不需要将它们放在一起。
请注意,上面的陈述仅提及“指针”和“引用”作为类型。关于它们的实例(如变量)有一些感兴趣的问题。也有太多的误解。
顶级类别的差异已经可以揭示许多与指针无关的具体差异:
对象类型可以具有顶级 cv 限定符。引用不能。
根据抽象机器语义,对象类型的变量确实占用了存储空间。参考不必占用存储空间(详见下文关于误解的部分)。
...
还有一些关于引用的特殊规则:
复合声明器对引用的限制更大。
引用可以折叠。在模板参数推导过程中,基于引用折叠的 && 参数(作为“转发引用”)的特殊规则允许参数的“完美转发”。
在模板参数推导过程中,基于引用折叠的 && 参数(作为“转发引用”)的特殊规则允许参数的“完美转发”。
引用在初始化中有特殊的规则。声明为引用类型的变量的生命周期可以通过扩展与普通对象不同。顺便说一句,其他一些上下文(例如涉及 std::initializer_list 的初始化)遵循一些类似的引用生命周期扩展规则。这是另一罐蠕虫。
顺便说一句,其他一些上下文(例如涉及 std::initializer_list 的初始化)遵循一些类似的引用生命周期扩展规则。这是另一罐蠕虫。
...
误解
语法糖
我知道引用是语法糖,所以代码更容易读写。
从技术上讲,这是完全错误的。引用不是 C++ 中任何其他特性的语法糖,因为在没有任何语义差异的情况下,它们不能被其他特性完全替换。
(类似地,lambda-expressions 不是 C++ 中任何其他功能的句法糖,因为它不能用像 the declaration order of the captured variables 这样的“未指定”属性精确模拟,这可能是很重要,因为这些变量的初始化顺序可能很重要。)
从严格意义上讲,C++ 只有几种语法糖。一个实例是(继承自 C)内置(非重载)运算符 []
,即 is defined exactly having same semantic properties of specific forms of combination over built-in operator unary *
and binary +
。
贮存
因此,指针和引用都使用相同数量的内存。
上面的说法是完全错误的。为避免此类误解,请查看 ISO C++ 规则:
... 一个对象在其建造期间、其整个生命周期和其销毁期间占据一个存储区域。 ...
从 [dcl.ref]/4:
未指定引用是否需要存储。
请注意,这些是语义属性。
语用学
即使在语言设计的意义上,指针还不足以与引用放在一起,但仍有一些论据使得在其他一些上下文中在它们之间做出选择是有争议的,例如,在选择参数类型时。
但这还不是全部。我的意思是,除了指针与引用之外,您还需要考虑更多的事情。
如果您不必坚持这种过于具体的选择,那么在大多数情况下答案很简短:您没有必要使用指针,所以您不需要。指针通常已经够糟糕了,因为它们暗示了太多你不期望的东西,并且它们将依赖于太多隐含的假设,从而破坏了代码的可维护性和(甚至)可移植性。不必要地依赖指针绝对是一种不好的风格,在现代 C++ 的意义上应该避免。重新考虑你的目的,你最终会发现在大多数情况下指针是最后排序的特征。
有时,语言规则明确要求使用特定类型。如果您想使用这些功能,请遵守规则。复制构造函数需要特定类型的 cv-& 引用类型作为第一个参数类型。 (通常它应该是 const 限定的。)移动构造函数需要特定类型的 cv-&& 引用类型作为第一个参数类型。 (通常不应该有限定符。)运算符的特定重载需要引用或非引用类型。例如:作为特殊成员函数的重载 operator= 需要类似于复制/移动构造函数的第一个参数的引用类型。 Postfix ++ 需要虚拟 int。 ...
复制构造函数需要特定类型的 cv-& 引用类型作为第一个参数类型。 (通常它应该是 const 限定的。)
移动构造函数需要特定类型的 cv-&& 引用类型作为第一个参数类型。 (通常不应该有限定词。)
运算符的特定重载需要引用或非引用类型。例如:作为特殊成员函数的重载 operator= 需要类似于复制/移动构造函数的第一个参数的引用类型。 Postfix ++ 需要虚拟 int。 ...
重载 operator= 作为特殊成员函数需要类似于复制/移动构造函数的第一个参数的引用类型。
Postfix ++ 需要虚拟 int。
...
如果您知道按值传递(即使用非引用类型)就足够了,请直接使用它,尤其是在使用支持 C++17 强制复制省略的实现时。 (警告:然而,详尽地推理必要性可能非常复杂。)
如果您想使用所有权操作某些句柄,请使用诸如 unique_ptr 和 shared_ptr 之类的智能指针(如果您要求它们不透明,甚至可以自己使用自制指针),而不是原始指针。
如果您正在对某个范围进行一些迭代,请使用迭代器(或标准库尚未提供的某些范围),而不是原始指针,除非您确信原始指针在非常具体的情况下会做得更好(例如,对于更少的标头依赖性)案例。
如果您知道按值传递就足够了,并且您想要一些明确的可为空语义,请使用 std::optional 之类的包装器,而不是原始指针。
如果您知道由于上述原因传递值并不理想,并且您不想要可为空的语义,请使用 {lvalue, rvalue, forwarding}-references。
即使您确实需要像传统指针这样的语义,通常也有更合适的东西,例如 Library Fundamental TS 中的observer_ptr。
唯一的例外不能用当前语言解决:
当您在上面实现智能指针时,您可能必须处理原始指针。
特定的语言互操作例程需要指针,例如 operator new。 (然而,与普通对象指针相比,cv-void* 仍然完全不同且更安全,因为它排除了意外的指针算术,除非您依赖 void* 上的一些不符合标准的扩展,例如 GNU。)
函数指针可以从 lambda 表达式转换而无需捕获,而函数引用则不能。对于这种情况,您必须在非泛型代码中使用函数指针,即使您故意不想要可为空的值。
所以,在实践中,答案是如此明显:当有疑问时,避免指针。只有当有非常明确的理由认为没有其他方法更合适时,您才必须使用指针。除了上面提到的一些例外情况,这些选择几乎总是不是纯粹的 C++ 特定的(但可能是特定于语言实现的)。此类实例可以是:
您必须服务于旧式 (C) API。
您必须满足特定 C++ 实现的 ABI 要求。
您必须根据特定实现的假设在运行时与不同的语言实现(包括各种程序集、语言运行时和一些高级客户端语言的 FFI)进行互操作。
在某些极端情况下,您必须提高翻译(编译和链接)的效率。
在某些极端情况下,您必须避免符号膨胀。
语言中立性警告
如果您通过some Google search result (not specific to C++)来查看问题,这很可能是错误的地方。
C++ 中的引用非常“奇怪”,因为它本质上不是一流的:they will be treated as the objects or the functions being referred to,因此它们没有机会支持一些一流的操作,例如独立于被引用对象的类型作为 the member access operator 的左操作数.其他语言可能对它们的引用有类似的限制,也可能没有。
C++ 中的引用可能不会保留不同语言的含义。例如,引用通常不会像 C++ 中的值那样暗示值的非空属性,因此此类假设在某些其他语言中可能不起作用(您会很容易找到反例,例如 Java、C#...)。
一般来说,不同编程语言的引用之间仍然可以有一些共同的属性,但是让我们把它留给 SO 中的其他一些问题。
(附带说明:这个问题可能比涉及任何“类 C”语言(如 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 工作的语言机制。
这是基于 tutorial。写的更清楚了:
>>> 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
的引用。
nullptr
初始化引用?你真的读过这个线程的任何其他部分,或者......?
引用不是给某些记忆的另一个名称。它是一个不可变的指针,在使用时会自动取消引用。基本上可以归结为:
int& j = i;
它在内部变成
int* const j = &i;
const
指针的有效方式。这种灵活性并不能证明引用和指针之间存在差异。
在 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;
}
有关指针引用的更多信息,请访问以下内容:
C++:对指针的引用
指针对指针和引用指针
正如我所说,指向引用的指针是不可能的。试试下面的程序:
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int *ptr = &x;
int &*ptr1 = ptr;
}
我没有看到有人提到过指针和引用之间的一个根本区别:引用在函数参数中启用了按引用传递的语义。指针,虽然一开始不可见,但不可见:它们只提供按值传递的语义。这在 this article 中有很好的描述。
问候, &rzej
除非我需要以下任何一种,否则我会使用参考:
空指针可以用作标记值,这通常是避免函数重载或使用布尔值的廉价方法。
您可以对指针进行算术运算。例如,p += 偏移量;
r
被声明为引用的地方编写 &r + offset
冒着增加混乱的风险,我想输入一些输入,我确信这主要取决于编译器如何实现引用,但在 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 类型的指针(它意味着指向任何东西的指针),但禁止对 void 的引用。
int a;
void * p = &a; // ok
void & p = a; // forbidden
我不能说我对这种特殊的差异感到非常满意。我更希望它被允许对具有地址的任何内容进行有意义的引用,否则引用的行为相同。它将允许使用引用定义一些 C 库函数的等价物,例如 memcpy。
此外,作为内联函数参数的引用的处理方式可能与指针不同。
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
左值。考虑语句 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's C++ style guidelines 就是一个例子。
也许一些隐喻会有所帮助;在您的桌面屏幕空间的上下文中 -
引用要求您指定一个实际的窗口。
指针需要屏幕上一块空间的位置,以确保它将包含该窗口类型的零个或多个实例。
引用是一个 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 上,如果函数可以被内联,则编译器会在内联调用中对其进行优化,因为动态范围是已知的,但在函数定义中,参数总是作为指针解引用(期望变量的地址为由引用引用)其中它可能被另一个翻译单元使用并且编译器不知道动态范围,除非该函数当然被声明为静态函数,否则它不能在翻译单元之外使用,然后只要它没有在函数中通过引用修改,它就按值传递,然后它将传递您正在传递的引用所引用的变量的地址,并且在 -Ofast 上,这将在寄存器中传递并且如果调用约定中有足够的易失性寄存器,则远离堆栈。
指针和引用的区别
可以将指针初始化为 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 模板这样的模板时,这种类模板总是会返回一个引用,而不是一个指针,以便于使用运算符 [] 轻松读取或分配新值:
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& i = 0
。
关于引用和指针的一些关键相关细节
指针
指针变量使用一元后缀声明符运算符 * 声明
指针对象被分配一个地址值,例如,通过分配给数组对象,使用 & 一元前缀运算符的对象的地址,或分配给另一个指针对象的值
一个指针可以被重新分配任意次数,指向不同的对象
指针是保存分配地址的变量。它在内存中占用的存储空间等于目标机器架构的地址大小
例如,可以通过增量或加法运算符对指针进行数学操作。因此,可以使用指针等进行迭代。
要获取或设置指针引用的对象的内容,必须使用一元前缀运算符 * 来取消引用它
参考
引用必须在声明时进行初始化。
使用一元后缀声明符运算符 & 声明引用。
初始化引用时,使用它们将直接引用的对象的名称,而不需要一元前缀运算符 &
一旦初始化,引用就不能通过赋值或算术操作指向其他东西
无需取消引用即可获取或设置它所引用的对象的内容
对引用的赋值操作操作的是它指向的对象的内容(在初始化之后),而不是引用本身(不会改变它指向的位置)
对引用的算术运算操作的是它指向的对象的内容,而不是引用本身(不会改变它指向的位置)
在几乎所有的实现中,引用实际上存储为被引用对象的内存中的地址。因此,它在内存中占用的存储空间等于目标机器架构的地址大小,就像指针对象一样
尽管指针和引用以几乎相同的方式“在幕后”实现,但编译器对它们的处理方式不同,从而导致上述所有差异。
文章
我最近写的一篇文章比我在这里展示的要详细得多,应该对这个问题很有帮助,尤其是关于记忆中的事情是如何发生的:
Arrays, Pointers and References Under the Hood In-Depth Article