指针变量和引用变量之间的区别是什么?
当前回答
引用是常量指针。int*const a=&b与int&a=b相同。这就是为什么没有const引用,因为它已经是const,而const的引用是const int*consta。当使用-O0编译时,编译器将在这两种情况下将b的地址放在堆栈上,并且作为类的成员,它也将出现在堆栈/堆上的对象中,与您声明了常量指针时相同。使用-Ofast,可以免费优化此功能。常量指针和引用都被优化了。
与常量指针不同,无法获取引用本身的地址,因为它将被解释为它引用的变量的地址。因此,在Ofast上,表示引用的常量指针(被引用变量的地址)将始终在堆栈外进行优化,但如果程序绝对需要实际常量指针的地址(指针本身的地址,而不是指针指向的地址),即您打印常量指针的位置,那么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上,传递被引用变量的地址,因此它与通过指针传递相同,即常量指针包含的地址。On Ofast如果函数可以内联,则编译器会在内联调用中对其进行优化,因为动态范围是已知的,但在函数定义中,参数总是作为指针(期望引用引用的变量的地址)被解引用,其中它可能被另一个转换单元使用,而编译器不知道动态范围,当然,除非函数声明为静态函数,否则它不能在转换单元之外使用,然后它通过值传递,只要它没有在函数中通过引用进行修改,那么它将传递您传递的引用所引用的变量的地址,如果调用约定中有足够多的易失性寄存器,则将在一个寄存器中传递,并保持在堆栈之外。
其他回答
我对引用和指针有一个类比,将引用看作对象的另一个名称,将指针看作对象的地址。
// 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
}
引用的另一个有趣用法是提供用户定义类型的默认参数:
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;
}
默认风格使用引用的“bind const reference to a temporary”方面。
直接答案
C++中的引用是什么?不是对象类型的特定类型实例。
C++中的指针是什么?某个特定的对象类型实例。
根据ISO C++对对象类型的定义:
对象类型是一种(可能是cv限定的)类型,它不是函数类型,不是引用类型,也不是cv void。
可能需要知道的是,对象类型是C++中类型宇宙的顶级类别。引用也是一个顶级类别。但指针不是。
指针和引用在复合类型的上下文中一起提到。这基本上是由于从(和扩展的)C继承的声明器语法的性质,它没有引用。(此外,自从C++11以来,有不止一种类型的引用声明器,而指针仍然是“unityped”:&+&&vs.*。)因此,在这种情况下,用类似C风格的“扩展”来起草一种特定于语言的语言是有一定道理的。(我仍然认为,声明器的语法浪费了大量的语法表达能力,使人类用户和实现都感到沮丧。因此,它们都不适合内置于新的语言设计中。不过,这是PL设计的一个完全不同的主题。)
否则,指针可以被限定为具有引用的特定类型是无关紧要的。除了语法相似性之外,它们共享的公共财产太少了,所以在大多数情况下没有必要将它们放在一起。
注意,上面的语句只提到“指针”和“引用”作为类型。关于它们的实例(如变量),有一些有趣的问题。还有太多的误解。
顶级类别的差异已经揭示了许多与指针无关的具体差异:
对象类型可以具有顶级cv限定符。引用不能。根据抽象机器语义,对象类型的变量确实占用了存储空间。引用不必占用存储空间(有关详细信息,请参阅下面的误解部分)。...
关于引用的其他一些特殊规则:
复合声明符对引用的限制更大。引用可以折叠。基于模板参数推导过程中引用折叠的&&参数特殊规则(作为“转发引用”)允许参数的“完美转发”。引用在初始化时有特殊规则。声明为引用类型的变量的生存期可以通过扩展与普通对象不同。顺便说一句,其他一些上下文(如涉及std::initializer_list的初始化)遵循引用生命周期扩展的一些类似规则。这是另一罐蠕虫。...
误解
语法糖
我知道引用是语法糖,所以代码更容易读写。
从技术上讲,这显然是错误的。引用不是C++中任何其他特性的语法糖,因为它们不能被没有任何语义差异的其他特性完全替换。
(类似地,lambda-expressions不是C++中任何其他功能的语法糖,因为它不能用捕获变量的声明顺序这样的“未指定”财产精确模拟,这可能很重要,因为这些变量的初始化顺序可能很重要。)
在严格意义上,C++只有几种语法糖。一个实例是(继承自C)内置(非重载)运算符[],它的定义与内置运算符unary*和binary+的特定组合形式具有相同的语义财产。
存储
因此,指针和引用都使用相同的内存量。
上面的说法完全错误。为了避免这种误解,请查看ISO C++规则:
来自[intro.object]/1:
……一个物体在其建造期间、在其整个生命周期和在其毁灭期间占据一个存储区域。。。
来自[dcl.ref]/4:
未指定引用是否需要存储。
请注意,这些是语义财产。
语用学
即使在语言设计的意义上,指针不足以与引用放在一起,但仍有一些争论使得在某些其他上下文中(例如,在对参数类型进行选择时)在它们之间进行选择是有争议的。
但这并不是全部。我的意思是,你需要考虑的不仅仅是指针和引用。
如果你不必坚持这种过于具体的选择,在大多数情况下,答案很简单:你没有必要使用指针,所以你不需要。指针通常很糟糕,因为它们暗示了太多你不期望的东西,而且它们依赖于太多的隐含假设,破坏了代码的可维护性和(甚至)可移植性。不必要地依赖指针绝对是一种糟糕的风格,在现代C++的意义上应该避免。重新考虑一下你的目的,你最终会发现在大多数情况下,指针是最后一种功能。
有时语言规则明确要求使用特定类型。如果您想使用这些功能,请遵守规则。复制构造函数需要特定类型的cv-引用类型作为第一个参数类型。(通常它应该是常量限定的。)移动构造函数需要特定类型的cv-&&引用类型作为第一个参数类型。(通常不应有限定符。)运算符的特定重载需要引用或非引用类型。例如:重载运算符=作为特殊成员函数需要类似于复制/移动构造函数的第一个参数的引用类型。后缀++需要伪int。...如果您知道传递值(即使用非引用类型)就足够了,请直接使用它,特别是在使用支持C++17强制复制省略的实现时。(警告:然而,详尽地解释必要性可能非常复杂。)如果您想使用所有权操作一些句柄,请使用unique_ptr和shared_ptr之类的智能指针(如果您需要自制指针不透明,甚至可以使用它们),而不是原始指针。如果您在一个范围内进行一些迭代,请使用迭代器(或标准库尚未提供的一些范围),而不是原始指针,除非您确信原始指针在非常特定的情况下会做得更好(例如,对于较少的头部依赖性)。如果您知道通过值传递就足够了,并且需要一些显式的可空语义,请使用包装器(如std::optional),而不是原始指针。如果您知道由于上述原因,传递值并不理想,并且您不希望使用可为null的语义,请使用{lvalue,rvalue,forward}-引用。即使您确实需要像传统指针那样的语义,也通常有更合适的方法,例如库基础TS中的observer_ptr。
在当前语言中无法解决以下唯一的例外:
当您在上面实现智能指针时,可能必须处理原始指针。特定的语言互操作例程需要指针,如运算符new。(然而,cv void*与普通对象指针相比仍有很大的不同和安全性,因为它排除了意外的指针算法,除非您依赖于void*上的一些非一致扩展,如GNU的。)函数指针可以从lambda表达式转换而不需要捕获,而函数引用则不能。对于这种情况,您必须在非泛型代码中使用函数指针,即使您故意不希望值为空。
因此,在实践中,答案是显而易见的:当有疑问时,避免使用指针。只有在有非常明确的理由认为没有其他更合适的时候,才必须使用指针。除了上面提到的一些例外情况外,这些选择几乎总是不是纯C++特定的(但可能是特定于语言实现的)。此类实例可以是:
您必须为旧式(C)API服务。您必须满足特定C++实现的ABI要求。您必须基于特定实现的假设,在运行时与不同的语言实现(包括各种程序集、语言运行时和某些高级客户端语言的FFI)进行互操作。在某些极端情况下,您必须提高翻译(编译和链接)的效率。在某些极端情况下,您必须避免符号膨胀。
语言中立警告
如果你通过谷歌搜索结果(不是C++特有的)看到这个问题,这很可能是错误的地方。
C++中的引用相当“奇怪”,因为它本质上不是一级的:它们将被视为被引用的对象或函数,因此它们没有机会支持一些一级操作,例如独立于被引用对象的类型而成为成员访问运算符的左操作数。其他语言可能对其引用有类似的限制,也可能没有。
C++中的引用可能不会保留不同语言之间的含义。例如,引用通常并不意味着像C++中那样的值具有非空财产,因此这种假设在某些其他语言中可能不起作用(并且很容易找到反例,例如Java、C#…)。
一般来说,在不同编程语言中的引用之间仍然可以有一些常见的财产,但让我们把它留给SO中的其他一些问题。
(附带说明:这个问题可能比任何“类C”语言都要早,比如ALGOL 68与PL/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;
}
有关指针引用的详细信息,请访问以下内容:
C++:指针引用指向指针的指针和指向指针的引用
正如我所说,指向引用的指针是不可能的。尝试以下程序:
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int *ptr = &x;
int &*ptr1 = ptr;
}
简单地说,我们可以说引用是变量的替代名称,指针是保存另一个变量地址的变量。例如
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 */