指针和引用的区别

指针和引用在 C++ 中都用于间接访问变量,但它们有一些区别:

  1. 指针是一个变量,它保存看另一个变量的内存地址;引用是另一个变量的别名,与原变量共享内存地址。
  2. 指针可以被重复赋值,指向不同的变量;引用在初始化后不能更改,始终指向同一个变量。
  3. 指针可以为 nullptr,表示不指向任何变量;引用必须绑定到一个变量(必须初始化),不能为 nullptr。
  4. 使用指针需要对其进行解引用以获取或修改其指向的变量的值;引用可以直接使用,无需解引用。

下面的示例展示了指针和引用的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

int main() {
int a = 10, b = 20;

int *p = &a;
cout << "Pointer value: " << *p << endl; // 10
p = &b;
cout << "Pointer value: " << *p << endl; // 20
p = nullptr;

int &r = a;
cout << "Reference value: " << r << endl; // 10
// r = &b; // [Error] invalid conversion from 'int*' to 'int' [-fpermissive]
int &rb = b;
r = rb; // 相当于将 b 的值赋给 a,即 a = b;
cout << "Reference value: " << r << endl; // 20

return 0;
}

从汇编底层角度来解释 C++ 中引用的实现机制:指针和引用有什么区别

从汇编看引用和指针

引用是变量的别名,这是 C++ 语法规定的语义,而引用在汇编层面和指针没区别,引用被编译器当作 const 指针来进行操作。

分别用指针和引用来实现函数 swap():

1
2
3
4
5
6
7
8
9
10
11
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
} // 指针版

void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
} // 引用版

gcc -S 输出swap()函数的汇编,指针版和引用版完全一样,以函数调用两个 swap() 也一样。

总结

  1. 引用只是 C++ 语法糖,可以看作编译器自动完成取地址、解引用的常量指针(必须初始化,不可修改)。
  2. 引用区别于指针的特性都是编译器约束完成的,编译成汇编代码就和指针一样。
  3. 由于引用只是指针包装了一下,所以也存在风险,下文有例。
  4. 引用由编译器保证初始化,使用起来较为方便(如不用检查空指针等)。
  5. 尽量用引用代替指针。
  6. 引用没有顶层 const 即 int& const,因为引用本身就不可变,所以再加顶层 const 也没有意义;但是可以有底层 const 即 const int&,这表示引用所引用的对象本身是常量。
  7. 指针既有顶层 const (int* const --指针本身不可变),也有底层 const (const int* --指针所指向的变量不变)。
  8. 有指针引用,是一个引用绑定到指针,但是没有引用指针。显然,因为指针本身也是一个变量,所以可以有指针引用;但很多时候指针存在的意义就是间接改变对象的值,而引用必须初始化且不可再修改,所以不能有引用指针。
  9. 指针和引用的自增、自减含义不同:指针是自增减是指针运算,而引用是代表所指向的对象自增减(语义上引用就是变量的别名,相当于是变量在操作,实际上编译器会自动解引用)。

#总结 -4. 例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

int main() {
int* a = new int;
cout << *a << endl; // -842150451(一个随机值)
cout << a << endl; // 0084DA90(地址)
int& b = *a;
cout << b << endl; // -842150451(一个随机值)
delete a;
// delete 之后实际 a 仍指向原处,并在内存被重新使用前仍保留原值
// a = nullptr; // 比较安全严谨的做法,使 nullptr 指向为空
// cout << *a << endl; // 引发异常
// *a = 10;
// cout << *a << endl;
b = 20;
cout << b << endl; // 20(我们的赋值)
cout << &b << endl; // 0084DA90(地址)
return 0;
}

我们以指针申请内存空间存放一个 int,会输出一个随机值,不同的编译器给的值不一样,猜测是内存被申请前该地址存放的值,或编译器自定义的一个什么值,这无所谓。同时输出这块申请来的内存的地址。

然后通过指针解引用,为引用初始化,再通过指针释放这块内存,这里关于 delete 有问题,正如注释而言,如果delete 之后不写 a = nullptr,在 Dev5.11 中测试仍可通过对指针解引用 *a 来读取或修改这块地址的内容,而写了之后运行程序会在此异常中止。但 Visual Studio 即使不写 a = nullptr,delete 之后对指针 a 试图解引用也会触发异常。当然这不是我们讨论的重点。

关键在于通过指针释放内存后,引用 b 仍然保留地址,且可以正常读取和修改地址处存放的值,并不会触发异常。而此时这里属于已经被释放的内存,如果之后再用,这显然是不安全的。