標籤:

設計C++函數傳參時如何決定使用指針還是引用?

單就函數傳參來說,引用能辦到的事情指針一定能辦到;但存在一些場合只能用指針(比如指針的指針)不能用引用。那麼設計參數類型時,使用指針和引用的原則是什麼?如何使帶給使用者的surprise最少?


如果語義是 pass-by-reference,那麼一律用指針。當然目前 C++ 里沒有必要用 raw pointer ,除非是老舊的 APIs 。這樣在 calling site 可以明確的看出語義。

如果是 pass-by-value 語義,可以在情況允許的時候用 const 來避免拷貝。把 shared_ptr 作為參數傳遞的時候,也可以用 const shared_ptr 來避免無謂的計數。僅此而已。C++ reference 本來就是為了 overload 的美觀設計的語法糖,用在這裡做一點點優化也算恰如其分。


正常來說,標準答案肯定是傳引用,看了下大多數回答,果然如此。

但我在這裡要唱個小小的反調……

理論上能引用就引用,安全無害,還不怕NULL。

如果要修改輸入參數,就用常規引用;輸入參數不做修改,就常量引用。

但令我詫異的是,Qt傳遞可修改參數用的是指針

一開始我也覺得大失水準,不像是Qt那麼優雅的框架應有的表現。

但用多了後,觀感就完全不同了,主要有如下兩點:

  • 指針可以設置默認參數,某些場合有奇效。

// DOM解析方法,為方便閱讀,做了下換行排版
bool QDomDocument::setContent(
const QByteArray data,
bool namespaceProcessing,
QString *errorMsg = Q_NULLPTR,
int *errorLine = Q_NULLPTR,
int *errorColumn = Q_NULLPTR
)

如上述代碼,在不需要使用錯誤信息時,最後三個參數都可以不用輸入。

  • 指針傳參可讀性更強

int a = 0;
int b = 0;
someFunc_whichMayModifySomeInput_byReference(a, b);
someFunc_whichMayModifySomeInput_byPointer(a, b);

如上述代碼,兩個函數中的第二個參數都是可修改參數,此時傳指針的寫法一目了然,而傳引用的寫法,不追蹤定義到函數簽名的話,閱讀代碼時很容易搞錯。

雖然傳指針有遇到NULL的風險,對使用者的心智負擔比傳引用高吧……但因為NULL問題太典型了,所以幾乎所有使用者都會注意規避的。

並且,如果通過設置編碼規範,利用取址操作符將棧對象轉換為指針進行傳遞的話,既享受了指針傳參的靈活性和可讀性優勢,又具有和傳引用同樣的安全性,豈不美哉?

============================================

對於傳指針和傳引用如何權衡, @陳宇 的答案寫的很完美了知乎用戶:設計C++函數傳參時如何決定使用指針還是引用?

我只補充一點——只要傳遞的是非const指針,那一定要在API注釋/文檔中註明該參數的生命周期

Qt文檔的寫法是這樣的:

The ownership of item is transferred to the layout, and it"s the layout"s responsibility to delete it.

QAbstractItemView does not take ownership of delegate.

The ownership of the style object is not transferred.

The ownership of action is not transferred to this QWidget.

對於使用者而言,針對不同的生命周期情況,參數的傳遞方式也不同:

  • 如果會修改聲明周期,建議使用者傳遞堆對象指針。
  • 如果不會修改聲明周期,建議使用者以方式傳遞棧對象地址。


個人習慣一般不用引用…


我的風格是:除了邏輯上要處理可能為空的情況,一律用引用。(即指針與nullable reference等價)

因為其它情況用指針還要多寫句assert(p != nullptr);

至於說引用容易被誤認為是傳值的說法,我覺得如果不是C語言根深蒂固的話其實也好接受,畢竟你在諸如std::swap的時候不也是司空見慣嗎……


取決於是強迫調用者還是被調用者來處理空引用。 後者就可以允許用指針了。


這個問題問得好。該用引用還是指針,要分情況來看。

1. 函數不是構造函數,且參數是只讀:用const引用。

如果函數參數是只讀的,且不是構造函數,const引用最好。原因有二:

  1. 如果用指針,用戶使用時可能會傳nullptr作為參數。用引用的話,用戶基本不可能犯這種錯誤。
  2. const reference可以接受匿名變數作為參數。比如,

int say_hello(const std::string name) {
std::cout &<&< "hello " &<&< name &<&< std::endl; } // 然後你可以這樣調用 say_hello("world");

這裡"world"被隱式構造為一個匿名的std::string變數。而const reference可以接受匿名變數。如果name被定義為指針或者普通引用都不行。

2. 函數不是構造函數,且參數是作為輸出(out)參數:用指針。

這種情況用指針最好。因為用戶在傳指針的時候需要用到取地址操作符(),這樣代碼看起來更易懂。比如:

void copy1(const std::string a, std::string *b);
void copy2(const std::string a, std::string b);

copy1(foo, bar);
copy2(foo, bar);

這兩個copy函數,很明顯第一種一眼就能看出來是把foo複製到bar。如果是第二種就必須得看函數定義才能知道。

3. 函數不是構造函數,且不是只讀,但也不是輸出參數:用引用。

這種情況用引用比用指針好是引用可以防止不小心傳nullptr這種情況。這種參數多半是類似stream這種,雖然只是「讀」,但還是會產生副作用,改變變數的狀態。我個人很討厭用起來必須改變狀態的類型,因為對於函數式編程和並行程序非常不友好。所以我都盡量不定義這樣的類型(除了負責IO的類)。

4. 函數是構造器,但參數並不參與組成構造類的一部分,那就參考以上幾點。

如果參數不參與構造的話其實跟不是構造函數的情況一樣。

以上只是比較簡單的情況,下面才是比較有趣的部分。也就是如果函數是構造器的話,該傳什麼類型的參數。

5. 函數是構造器,且參數參與構造,且參數是可移動的類型:傳值

這種情況直接傳值是最好的。比如:

class person {
public:
person(std::string name) : name_{std::move(name)} {}
private:
std::string name_;
};

傳值比傳const引用好處在於,取決於用戶是否需要保留原參,可以省略複製。比如,

Person p{"Jack"}; // 創建一個臨時變數,如果一個move;多半情況下編譯器會直接創建在Person里
std::string name{"John"};
Person p1{name}; // 這種情況跟用const引用一樣,都是一次copy
Person p2{std::move(name)}; // 因為被轉化成了右值,name會被直接移動到p2里,省掉一次copy

當然,如果string是像這個例子中這麼小的話,move和copy開銷是一樣的,因為small string optimization。但如果參數是長的string,或者類似於vector或者map這種heap allocated變數的話,move就比copy開銷小非常多。

5. 函數是構造器,且參數參與構造,但構造的類只保留參數的引用:傳shared_ptr或者自定義指針

首先我非常不提倡這種類型,因為如果這樣定義,用戶就必須保證name的生命周期比person長。但只看構造器的簽名的話又沒辦法看出來這點。用起來非常麻煩,每次都需要查文檔或者讀代碼才能知道用name構建了person之後能不能銷毀name。比如如果你這樣定義這個類:

class person {
public:
person(const family_tree family) : family_{name} {
family_-&>add(this);
}
private:
const family_tree *family_;
};

family_tree family{...};
person p{family}; //只看這兩行代碼你能知道可不可以return p嗎?

我一般只有當類是作為函數類型用的時候才會定義這種類型。其他情況,我一般會用shared_ptr作為參數,然後類里也保存shared_ptr作為成員。比如像這樣,

class person {
public:
person(std::shared_ptr& family) : family_{std::move(family)} {}
private:
std::shared_ptr& family_;
};

但這樣如果family是stack上的變數且我可以保證family的生命周期比person長怎麼辦?其實很容易,這樣就行了:

int main() {
family_tree family{...};
person p{shared_ptr&(family, [](family_tree *) {})};
...
}


如果你的目的是給使用者surprise最少,那麼可以考慮僅僅在const的情況下用引用。需要修改其內容的情況下一律用指針。

原因在於在調用函數時引用被偽裝成與值傳遞相同的形式, f(a,b)這樣的形式你其實一眼看不出傳的是值還是引用,這容易引起誤解,以為這是傳值,從而認為該調用不會修改變數的值。

而指針就不存在這個問題,一個明確的符號顯式的表明了該對象可能會被修改 f(a,b) 這樣的格式是一個明顯的信號,不會造成誤解。至於const引用則與常量基本等價,所以認為他是值還是引用都沒關係。

如果僅僅是擔心在函數內星號用得太多,可以在函數頭把指針賦值給一個引用類型的局部變數。

對於函數參數而言,我個人是只用const引用,不使用非const的引用的。非const的引用我只在函數內部的局部變數中使用。


一般來說,不能放nullptr的,就用引用。因為強行把nullptr穿進去要寫*static_cast&(nullptr),顯得很傻逼。除此之外沒有什麼區別了。

當然不排除有的人還可以強行寫一個nullref,可以變成任何*static_cast&(nullptr)(逃


這是個編碼風格問題,容易引起爭論。

只講一個我個人喜歡的慣例。

傳入的是數組的,用指針:void sendChicken(Chick* chicken);

傳入的內容會被修改的,用指針:bool peekValueIfExists(int* outValue); // 用的時候 peek(v);

有可能傳入空指針的,用指針:void setName(const char* nameUseDefaultIfNull);

這以外,臨時借用,不會改變對象生命周期的,用引用。

可能改變對象生命期的,用指針。比如,oldBastard.setPistol(p); //


看題主的需求是寫tokenlizer,建議索引用下標吧——做成成員變數共享。

輸入的string也可以用const引用的方式保存在對象里。

這樣就不用把下標傳來傳去來記錄當前掃描位置了,記得檢查越界就行——反正也不會多線程解析,如果非要每次都傳過去的話,怎麼寫都彆扭——我踩過這坑。一開始是改成全局變數,然後發現直接裝類里不就完了嗎。

畢竟最終返回的結果是vector&這種形式再交給parser。


個人使用一套傳參方式來明確語義:

不可空的入參:const lvalue ref OR by value (如果容易複製)

可空的入參:const pointer

出參:pointer

入出參:lvalue ref (並且參數名寫的比較明確這是個又入又出的參數。一般不怎麼用這個,容易驚喜)

普通構造函數里肯定需要copy一份的那種入參:by value然後move之

規定必須用右值引用的地方,比如拷貝構造函數:rvalue ref


我自己的習慣:

簡單的pod類型直接傳值。

如果要修改變數,為了最小驚訝,一般用指針。

immutable的對象用const T。

nullable的必須用指針。

標準庫相關的設施,為了風格統一,可能考慮non-const reference.


問題描述有一處問題請注意:指針的指針也可以使用指針的引用代替。。

首先,我們來了解一下指針和引用的區別:

一、引用不可以為空,但指針可以為空。定義一個引用的時候,必須初始化。因此如果你有一個變數是用於指向另一個對象,但是它可能為空,這時你應該使用指針;如果變數總是指向一個對象,你的設計不允許變數為空,這時你應該使用引用。

結論一:如果輸入的對象可以為null,或者欲輸出的對象用戶可以不使用,使用指針傳參,其餘統統用引用。

二、引用不可以改變指向,指針可以。

結論二:由於是為了傳參數,能不能改變指向一點關係都沒有。所以該傳引用還是要傳引用。

三、數組一般與首元素的指針對應而非首元素引用。

結論三:由於C風格的影響,傳數組還是傳指針。(但傳首元素引用是完全正確的)


舉幾個例子

1、被引用的變數只需要在函數里使用,並且只讀,那麼用常引用是最好的了。

最典型的是拷貝構造函數

myClass::myClass (const myClass anotherClass);
myClass::importData (const std::vector& data);
//我只要一次性讀取數據就行了,函數結束時這個引用也結束

2、如果這個 「引用」 要在對象生存期內一直保存,那麼盡量用指針。

比如說Qt里的 QWidget類對象一般都要指定一個父對象 parent(當父對象被析構的時候,所有子對象都被自動析構),存一個指針而不是存引用。這個指針可以空,也可以改變

class QWidget { //假設自己寫一個QWidget
public:
QWidget (QWidget *parent = NULL):parentWidget (parent){/**/}
QWidget* parent () const {return parentWidget;}
private:
QWidget *parentWidget; //而不是 QWidget parentWidget;
}

3、如果函數返回一個很大的對象,可以在函數外構造出來,然後作為引用參數。這樣可以節省拷貝的時間。

std::vector& calculation ();
//返回一個數組 std::vector&,需要拷貝一次。如果數組很大,就會拖慢速度。

void calculation (std::vector& veryBigArray);
//把用於存結果的數組的引用當參數傳進函數,然後在函數里直接使用,無需拷貝。

void calculation (std::vector& *veryBigArray);
//這樣也可以,但是必須先檢查 veryBigArray 是不是空指針。

4、如果函數可能需要反饋一個狀態,而返回值已經被佔用了,那麼可以用指針。(也可以用引用)

QString 里的 toDouble 函數可以把一個浮點格式的字元串轉化為相應的浮點值並返回。

可是我怎麼才能知道轉換成功了沒有?(比如輸入的格式是不是正確)

double QString::toDouble (bool *ok = Q_NULLPTR);
//"ok"用來記錄狀態。注意它有一個默認值,即空指針。
//用戶如果不關心轉換成功與否,那麼這個參數可以留空。

double QString::toDuble (bool ok);
//當然也可以,但是這樣的話 ok 就不能留空


這是一個好問題,由這個問題可以引申出一個非常好的設計技巧,下面我來介紹。

在決定使用指針還是引用之前,需要明白引用和指針的區別,正因為它們有不同,這才會決定在什麼情況下使用它們,下面我列出最主要的區別

1 指針的值可以為空,但是引用的值不能為空,並且引用在定義的時候必須初始化

2 指針可以再次指向其它對象,而引用不能再指向其它對象

針對這兩個區別,其實就可以推斷出一種類的設計技巧,在類的屬性設計中,我將屬性分成了兩類:

  • 基本屬性,使用比較基本的數據類型來定義。
  • 擴展屬性,使用指針來指向一個對象,初始化時賦值為空。

下面我來舉個例子,來具體解釋。

在Qt中有一個控制項叫QLineEdit,行編輯器

在正常情況下,用戶可以向編輯器中輸入任意文字,但有時候要求行編輯器只接收數字,那麼有的人可能會想到繼承QLineEdit,重寫過濾函數,這是一種解決方案,但並不好,比較好的做法就是為編輯器安裝一個驗證器(validator),也就是說,添加一個驗證器指針的成員變數,我把這個成員變數稱之為擴展屬性,使用指針指向該驗證器,並且初始化為空,在需要的時候調用安裝函數。

class QLineEdit
{
public:
QLineEdit();
~QLineEdit();

//基本屬性設置
QString text() const;
setText( const QString );

//擴展屬性設置
void setValidator( QValidator * );//安裝函數,如果參數為空,則刪除驗證器
QValidator * validator() const;
private:
QString m_text; //基本屬性:行編輯器顯示的字元串
QValidator * m_validator;//擴展屬性:驗證器
};

問1:為什麼成員變數m_validator是指針而不是引用?

答:如果m_validator為引用,那麼必須要在初始化列表中為m_validator綁定對象,一旦綁定,不能更改,當我的行編輯器想要從驗證數字變成驗證只能輸入字母時便無法實現,但是使用指針就可以完美實現,當我調用setValidator(QValidator *)時,便可以為QLineEdit安裝不同的驗證器。

問2:為什麼不使用繼承做一個只能輸入數字的行編輯器?

答:在Qt的界面控制項中還有

QComboBox(下拉框編輯器)

QSpinBox(跳步編輯器)

這些控制項也需要用到驗證器,如果每個都繼承,那代碼會很冗餘,而直接編寫一個QValidator類作為這些控制項的擴展屬性會是一個不錯的選擇,還有一個問題是如果使用繼承實現的話,那現在需求n種驗證器,那是否會編寫n個繼承類呢。

所以使用擴展屬性,並且定義為指針是比較好的選擇。

既然成員變數使用的是指針,那麼我的函數參數就必須使用指針。

其實,擴展屬性就相當於給一輛車裝個行車記錄儀。

那麼再來看看什麼時候使用引用,在成員變數是基本屬性時,它對應的成員函數參數就使用引用。

原因:

1 使用指針可能會有誤操作,釋放崩潰的問題,而使用引用卻可以避免,也不需要關心釋放問題。

2 使用引用要比指針速度更快。

以上是成員函數參數的問題,對於普通函數也同樣適用。

總結:

1 如果參數跟基本屬性有關係,那就使用引用,如果參數跟擴展屬性有關係,那就使用指針。

2 在指針的優勢沒有明顯高過引用的地方就使用引用,畢竟指針存在釋放危險。

廣告一下我的公眾號:小豆君,只要關注,便可加入小豆君為大家創建的C++Qt交流群,方便討論學習。


見《More Effective C++》條款1

當你知道你指向某個東西,而且不會改變指向其他東西,或是當你實現一個操作符而其他語法需求無法由points達成,你就應該選擇references。任何其他時候,請採用points.


一般能用引用就用引用。

引用相比指針多了很多限制,例如定義既初始化,引用必定有效,無法獲得原數據地址等。

當需要操作裸指針時才用指針作為參數,但是一般都不必要。


能傳引用就傳引用吧。

不然函數體裡面*多的話打得你會吐的。


一個很簡單的情況,如果你的實參是值,而你又要改變實參的值時,那就傳引用。引用安全一點兒


盡量傳引用,能避開指針就避開。

指針的缺點在於容易引發內存泄露或者多次釋放導致的程序中斷,而引用除了個別情況下不能使用以外可以安全的對對象/變數進行直接操作,畢竟它只是個別名嘛。

一般對對象不進行寫操作的就用const 引用類型若要進行修改就不加const限定。

通常情況下使用數組的時候會傳指針的拷貝,保證原數組首元素不變,否則很容易發生內存泄露的問題。

想到其他情況再更吧~


推薦閱讀:

C++的編譯單元要知道所有的實現?
switch語句中,case的後面為什麼必須是常量?
C/C++ 中怎樣優雅的寫多判斷 if 語句?
C語言的宏定義和C++的內聯函數有什麼意義?
C++ 類當中為什麼要有private?

TAG:C | 指針 |