这个问题其实可以在C++之父的著作《The C++ Programming Language》中找到答案:

7.7 References

A pointer allows us to pass potentially large amounts of data around at low cost: instead of copying the data we simply pass its address as a pointer value. The type of the pointer determines what can be done to the data through the pointer. Using a pointer differs from using the name of an object in a few ways:

? We use a different syntax, for example, ?p instead of obj and p?&>m rather than obj.m.? We can make a pointer point to different objects at different times.? We must be more careful when using pointers than when using an object directly: a pointer may be a nullptr or point to an object that wasnt the one we expected.

These differences can be annoying; for example, some programmers find f(x) ugly compared to f(x). Worse, managing pointer variables with varying values and protecting code against the possibility of nullptr can be a significant burden. Finally, when we want to overload an operator, say +, we want to write x+y rather than x+y. The language mechanism addressing these problems is called a reference. Like a pointer, a reference is an alias for an object, is usually implemented to hold a machine address of an object, and does not impose performance overhead compared to pointers, but it differs from a pointer in that:

? You access a reference with exactly the same syntax as the name of an object.? A reference always refers to the object to which it was initialized.? There is no "null reference", and we may assume that a reference refers to an object

下面是机翻(稍微人为修正了一点):

指针允许我们以低成本传递可能是大量的数据:我们不复制数据,而是简单地将其地址作为指针值传递。指针的类型决定了可以通过指针对数据做什么。使用指针与使用对象名称有几个不同之处:

  1. 我们使用不同的语法,例如,用p代替obj,用p-&>m代替obj.m。
  2. 我们可以让指针在不同的时间指向不同的对象。
  3. 与直接使用对象相比,我们在使用指针时必须更加小心:指针可能是nullptr或者指向一个不是我们所期望的对象。

这些差异可能令人讨厌。例如,一些程序员认为f(x)比f(x)难看。更糟糕的是,管理可能具有不同值的指针变数和保护代码免受nullptr的影响可能是一个很大的负担。最后,当我们想重载一个运算符,比如说+的时候,我们想写成x+y而不是x+y。解决这些问题的语言机制就是引用。与指针类似,引用是对象的别名,通常用于保存对象的机器地址,与指针相比不会增加性能开销,但它与指针的不同之处在于:

  1. 您可以使用与对象名称完全相同的语法来访问引用。
  2. 引用总是引用初始化它的对象。
  3. 没有「空引用」,我们可以假定引用引用了一个对象。


从实际写代码的角度,针对两点优势举例。

  1. 引用是个还不错的语法糖在某些场合替代指针,比如传参:

struct Data {
int id;
std::string name;
};

void changeDataName(Data data, const std::string name) { data.name = name; }

void changeDataName2(Data* data, const std::string name) {
// TODO: 检查 data 非 null,比如
// assert(data);
data-&>name = name;
}

调用:

Data data;
changeDataName(data, "hello");
changeDataName2(data, "world");

使用引用的简洁性一目了然,另一方面,引用必然非空(除非使用者故意 * 解引用某个空指针),也能够表明这个方法不支持传入空指针,避免大量没必要的空指针检查。

而对于可能为空指针的传参,使用 std::optional(C++ 17)比传裸指针更好。

2. 引用是一个更好的 const 指针,比如现在有个第三方库的类 Foo,那么如果要使用指针包装的时候:

class FooProxy {
public:
FooProxy(Foo* foo): foo_(foo) {}
// 其他方法...
private:
Foo* foo_;
};

实际上这个代理设计如果没有暴露出介面修改 foo_ 指向的 Foo 对象时,那么 foo_ 应该改成 Foo* const 类型,来避免后来的维护者没有理解这个代理的设计初衷,而误增加了一个方法修改 foo_ 指向的对象。

此时,用引用更适合:

class FooProxy {
public:
FooProxy(Foo foo): foo_(foo) {}
// 其他方法...
private:
Foo foo_;
};

除非你要对这个 FooProxy 的设计就是,它可以修改指向对象,亦或者允许代理在指向对象之前就构造(比如预先建立有初始容量的 std::vector&),此时无法使用引用,只能使用指针:

class FooProxy {
public:
FooProxy() = default;
FooProxy(Foo* foo): foo_(foo) {}
void setFoo(Foo* foo) { foo_ = foo; };
// 其他方法...
private:
Foo* foo_;
};

但这种设计需要考虑是否合理,比如多线程使用 FooProxy 时,如果是原先的设计(不可修改引用的对象),如果 Foo 有一些线程安全的方法,那么直接包装就是线程安全的。但如果允许改变 foo_,那么必须再加一个锁,保护 setFoo 和这些方法。


  1. 解决空指针问题,引用必须初始化,不存在空引用。
  2. 语法上更友好,不需要取址和解引用,代码更易读。


引用使用更自然

不过的有一个问题,传指针的话,调用的时候一般会有xx形式,看代码的人一看这个,就会敏感的注意到可能是out类型

引用就没有这个功能,只能你查看函数原型。这对从c语言半道出家的c++程序员可能容易出问题


其实引用确实可以完全用指针来代替,但是就像汇编可以实现所有面向对象的语言可以实现的功能但是你不能说汇编是面向对象的一样,引用和指针从概念上是不一样的。

引用更容易写出错误更少的代码,更容易达到「好的代码不是没有明显的错误,而是明显没有错误」的境界。


推荐阅读:
相关文章