• 文章
  • 何時按值、引用傳遞引數
釋出
2010 年 2 月 27 日(最後更新:2010 年 3 月 3 日)

何時按值、引用和指標傳遞引數

評分:4.5/5 (1196 票)
*****
我們偶爾會收到詢問引用和指標之間區別的帖子,
以及在函式呼叫中,何時按引用、指標或值傳遞引數。
以及在函式呼叫中,何時按引用、指標或值傳遞引數。

引用和指標的區別
指標變數是儲存其他變數記憶體地址的變數。
該“目標”變數可以命名,也可以不命名。
例如:

1
2
3
int i;
int* pInt = &i;  // pInt "points to" i
int* pInt2 = new int;  // pInt2 "points to" an unnamed int. 


引用變數是“引用”另一個已命名或未命名變數的變數。
已命名或未命名變數。例如

1
2
3
4
5
6
7
8
void foo( const std::string& str ) {}

std::string s1;
std::string& s1ref = s1;   // s1ref "refers" to s1

// Here, we construct an unnamed, temporary string object to call foo.
// foo's "str" parameter now "refers to" this unnamed object.
foo( std::string( "Hello World" ) );


指標有三個關鍵屬性,使它們與引用不同。
與引用不同。

1. 您使用指標語法訪問指標“指向”的值。
2. 您可以將指標重定向以使其指向不同的“目標”變數。
3. 您可以使指標不指向任何內容(即,空指標)。

例子

1
2
3
4
5
6
7
int i, j;
int* pInt = &i;  // pInt "points to" i
*pInt = 42;  // This assigns the variable pointed to by pInt to 42
             // So in other words, since pInt points to i, i now has
             // the value 42.
pInt = &j;   // This makes pInt now point to j instead of i.
pInt = NULL; // This makes pInt point to nothing. 


請注意,在指標變數之前使用星號如何訪問
所指向的值。這稱為解引用運算子,這個名字有點不幸,因為該語言也
支援引用,並且解引用運算子與引用無關。
與引用無關。

現在說說引用。引用有幾個關鍵特性
它們與指標不同

1. 引用必須在例項化時進行初始化。
2. 引用必須始終“引用”一個已命名或未命名變數
(也就是說,您不能有一個引用變數不引用任何內容,這相當於一個空指標)。
(也就是說,您不能有一個引用變數不引用任何內容,這相當於一個空指標)。
3. 一旦引用被設定為引用某個特定變數,您就無法在它的生命週期內“重新繫結”它來引用不同的變數。
就無法在它的生命週期內“重新繫結”它來引用不同的變數。
變數。
4. 您使用正常的“值”語法訪問被引用的值。

我們來看一些例子

1
2
3
4
5
int i = 20, j = 10;
int& iref = i;    // Instantiate iref and make it refer to i
iref = 42;        // Changes the value of i to 42
iref = j;         // Changes the value of i to 10 (the value of j)
iref = NULL;      // Changes the value of i to 0. 


所以看起來引用比指標更受限制,因為
指標的三個特性中有兩個在引用中不可用。
引用。但事實上,這些限制往往使程式設計
更容易.

首先,在編寫泛型模板程式碼時,您不能輕易編寫
一個可以在值、引用和指標上操作的單個模板函式,因為要訪問指標“指向”的值需要
指標,因為要訪問指標“指向”的值需要
一個星號,而訪問一個普通值不需要星號。
訪問引用的值與訪問一個普通值的方式相同
普通值——不需要星號。所以編寫可以處理
值和引用的模板很容易。這是一個真實的例子

1
2
3
4
5
6
7
template< typename T >
void my_swap( T& t1, T& t2 ) 
{
    T tmp( t1 );
    t1 = t2;
    t2 = tmp;
}


上述函式在這些情況下效果很好

1
2
3
4
5
int i = 42, j = 10;
int& iref = i, jref = j;

my_swap( i, j );          // Sets i = 10 and j = 42
my_swap( iref, jref );    // Swaps i and j right back 


但是,如果您期望以下程式碼將 i 設定為 10,j 設定為 42,那麼它不會執行您想要的操作。
到 10,j 到 42,那麼這並不能達到您的目的

1
2
3
4
int i = 42, j = 10;
int* pi = &i, *pj = &j;

my_swap( pi, pj ); // sets pi = &j and pj = &i 


為什麼?因為您需要解引用指標才能獲取
指向的值,而上面的模板函式沒有
其中有一個星號。

如果您想讓它正常工作,您必須這樣寫

1
2
3
4
5
6
7
template< typename T >
void my_ptr_swap( T* t1, T* t2 )  // There are other ways to declare this
{
    T tmp( *t1 );
    *t1 = *t2;
    *t2 = tmp;
}


現在在上面的例子中,您將使用 my_ptr_swap( pi, pj ); 來交換 pi 和 pj 所指向的值。就我個人而言,我認為這個
這個解決方案很糟糕,原因有三。首先,我必須記住兩個函式名,而不是一個:my_ptr_swap 和 my_swap。
名稱,而不是一個:my_ptr_swap 和 my_swap。其次,my_ptr_swap 是
比 my_swap 更難理解,儘管它們有相同的行數
的程式碼,並且有效地做相同的事情,但涉及額外的
解引用。(我寫的時候差點把函式實現錯了)。第三,空指標!如果一個或兩個
指標你傳遞給 my_ptr_swap 是 NULL?沒什麼好事。實際上,如果
指標您傳遞給 my_ptr_swap 是 NULL?沒什麼好事。實際上,如果
我想讓 my_ptr_swap 健壯,避免崩潰,我必須這樣寫

1
2
3
4
5
6
7
8
9
10
template< typename T >
void my_ptr_swap( T* t1, T* t2 )  // There are other ways to declare this
{
    if( t1 != NULL && t2 != NULL )
    {
        T tmp( *t1 );
        *t1 = *t2;
        *t2 = tmp;
    }
}


但我想,這也不是一個很好的解決方案,因為現在
my_ptr_swap 的呼叫者不能 100% 確定函式做了什麼,
除非他們重複 if() 檢查

1
2
3
4
if( pi != NULL && pj != NULL )
    my_ptr_swap( pi, pj );
else
    std::cout << "Uh oh, my_ptr_swap won't do anything!" << std::endl;


但是重複檢查使得 my_ptr_swap 內部的檢查變得有點
毫無意義。但另一方面,函式應該始終驗證其
引數。一個難題。也許應該有一個返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
template< typename T >
bool my_ptr_swap( T* t1, T* t2 )  // There are other ways to declare this
{
    if( t1 != NULL && t2 != NULL )
    {
        T tmp( *t1 );
        *t1 = *t2;
        *t2 = tmp;
        return true;
    }

    return false;  // false means function didn't swap anything
}


現在我可以寫了
1
2
if( my_ptr_swap( pi, pj ) == false )
    std::cout << "Uh oh, my_ptr_swap won't do anything!" << std::endl;


這肯定更好。但這只是在您的程式中引入了一個額外的錯誤
您需要思考和處理的分支。如果 my_ptr_swap 失敗了怎麼辦?我該怎麼辦? 在大多數微不足道的應用程式中,
錯誤處理,即使做了,也很容易。但在大型應用程式中
您需要執行 5 個步驟的序列,每個步驟都可能失敗
意味著您必須考慮所有以下錯誤分支

1. 如果操作 #1 失敗怎麼辦?
2. 如果操作 #2 失敗怎麼辦?我是否回滾操作 #1?如果
回滾操作 #1 失敗了呢?
3. 如果操作 #3 失敗怎麼辦?我是否回滾 #1 和 #2?如果
回滾操作 #2 成功但 #1 失敗了怎麼辦?如果回滾
回滾操作 #2 失敗了呢?
4. 如果操作 #4 失敗怎麼辦?.... 等等 ...
5. 如果操作 #5 失敗怎麼辦?.... 等等 ...

有數量驚人的失敗情況需要思考和測試。因為事情很快變得複雜,大多數程式設計師
只處理一個故障;雙重故障通常處理得不太
優雅(程式以某種方式中止)。

在我看來,由於錯誤處理很容易主導設計
和實現工作量,程式設計師應該努力不要
在可以輕鬆避免的地方人為地引入錯誤分支。

最普遍的錯誤情況之一是 NULL 指標。
指標。進入引用。

(未完待續)



引用第 2 個關鍵特性,引自上文

2. 引用必須始終“引用”一個已命名或未命名變數
(也就是說,您不能有一個引用變數不引用任何內容,這相當於一個空指標)。
(也就是說,您不能有一個引用變數不引用任何內容,這相當於一個空指標)。

這就是引用優於指標的第二個原因
沒有空指標檢查!它一次性消除了所有 C++ 程式設計中最常見的錯誤情況之一!
它一次性消除了所有 C++ 程式設計中最常見的錯誤情況之一!

話雖如此,指標並非總能避免。當您動態分配記憶體時,
您動態分配記憶體時,指標必須參與。但是我們可以
透過一些明智的程式設計來緩解這個問題。

假設我想對一個 int 型別的 std::vector 進行氣泡排序。好吧,這是一個糟糕的例子,因為
vector<> 已經有一個排序方法,STL 也有,但請讓我幽默一下。
一種方法是

1
2
3
4
5
6
7
8
9
std::vector<int>  v;  
// assume v is filled out with values

// This is REALLY suboptimal, but I'm writing this without testing it, and I 
// want to ensure I get it right:
for( size_t i = 0; i < v.size() - 1; ++i )
    for( size_t j = 0; j < v.size() - 1; ++ )
        if( v[ i ] < v[ j ] )
            my_ptr_swap( &v[ i ], &v[ j ] );


學生們被教導,當函式需要修改其引數時,應該
按指標傳遞。這對於 Pascal 等不支援引用的語言來說很棒,
但在 C++ 中,您有另一個選擇:按引用傳遞。事實上,如果我用 my_swap( v[ i ], v[ j ] ); 替換 my_ptr_swap 呼叫,它仍然有效。而且,
my_ptr_swap 呼叫,my_swap( v[ i ], v[ j ] ); 它仍然有效。而且,
我消除了指標的使用。

這讓我想到....

何時按值、按引用和按指標傳遞引數
在大學裡,學生們被教導有兩種情況應該按指標傳遞

1. 如果函式需要修改其引數;
2. 如果將變數複製到堆疊以將其傳遞給函式是昂貴的。

實際上,這都不是傳遞指標的好理由。事實上,
它們都是按引用傳遞的絕佳理由。在第一種情況下,您將
按非 const 引用傳遞。在第二種情況下,您將按 const 引用傳遞。
常數性超出本討論的範圍。如果您不熟悉它,
const 引用基本上是隻讀變數,非 const 引用是
讀寫變數。

但是,有沒有理由按指標傳遞呢?有*。有時,您確實
有一個可選引數。以 C 執行時庫中一個糟糕的例子為例,
time() 函式宣告為

time_t time( time_t* tm );
如果您將非 NULL 指標傳遞給 time(),它會將當前時間寫入 tm 指向的變數,
除了返回當前時間之外,還會將當前時間寫入 tm 指向的變數。如果您傳遞 NULL
指標,那麼它就不做那個。換句話說,tm 本質上是一個可選的
引數。在某些情況下,傳遞 NULL 指標可能會導致函式做
不同的事情。例如,pthread_create() 接受一個執行緒屬性物件的指標。
屬性物件。如果傳遞 NULL,它將使用新執行緒的預設屬性。
如果傳遞非 NULL 指標,它將從引數中獲取屬性。

請注意,在我給出的兩個案例中,NULL 都是程式設計師預期且合法的有用值。
一個傳遞給 my_ptr_swap 的空指標是意外的,並且被認為是呼叫者的程式設計錯誤。
一個傳遞給 my_ptr_swap 的空指標是意外的,並且被認為是呼叫者的程式設計錯誤。

因此,總結一下

1. 當函式不希望修改引數且值易於複製時,按值傳遞(int、double、char、bool 等簡單型別。std::string、
值易於複製(int、double、char、bool 等簡單型別。std::string、
std::vector 和所有其他 STL 容器都不是簡單型別。)

2. 當值複製成本高昂,且函式不希望修改指向的值,並且 NULL 是函式處理的有效預期值時,按 const 指標傳遞。
不希望修改指向的值,並且 NULL 是有效、預期且函式處理的值。
函式處理。

3. 當值複製成本高昂,且函式希望修改指向的值,並且 NULL 是函式處理的有效預期值時,按非 const 指標傳遞。
希望修改指向的值,並且 NULL 是函式處理的有效、預期值。
函式處理。

4. 當值複製成本高昂,且函式不希望修改引用的值,並且如果使用指標,NULL 將不是有效值時,按 const 引用傳遞。
不希望修改引用的值,並且如果使用指標,NULL 將不是有效值。
使用指標。

5. 當值複製成本高昂,且函式希望修改引用的值,並且如果使用指標,NULL 將不是有效值時,按非 const 引用傳遞。
修改引用的值,並且如果使用指標,NULL 將不是有效值。
而是使用。

6. 編寫模板函式時,沒有明確的答案,因為有一些權衡
需要考慮,這超出了本討論的範圍,但足以說明
大多數模板函式按值或 (const) 引用接受引數,但是
因為迭代器語法與指標相似(星號用於“解引用”),任何
接受迭代器作為引數的模板函式預設也會接受指標
(並且不檢查 NULL,因為 NULL 迭代器的概念有不同的語法)。