這個問題其實可以在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++程序員可能容易出問題


其實引用確實可以完全用指針來代替,但是就像彙編可以實現所有面向對象的語言可以實現的功能但是你不能說彙編是面向對象的一樣,引用和指針從概念上是不一樣的。

引用更容易寫出錯誤更少的代碼,更容易達到「好的代碼不是沒有明顯的錯誤,而是明顯沒有錯誤」的境界。


推薦閱讀:
相关文章