pointers c++參照 reference用法 - C ++中指針變量和引用變量之間有什麼區別?





15 Answers

什麼是C ++參考( 適用於C程序員

引用可以被認為是一個常量指針 (不要與指向常量值的指針混淆!)和自動間接,即編譯器將為您應用*運算符。

必須使用非null值初始化所有引用,否則編譯將失敗。 獲取引用的地址既不可能 - 地址運算符將返回引用值的地址 - 也不可能在引用上進行算術運算。

C程序員可能不喜歡C ++引用,因為當間接發生時,或者如果參數通過值或指針傳遞而不查看函數簽名,它將不再是顯而易見的。

C ++程序員可能不喜歡使用指針,因為它們被認為是不安全的 - 雖然引用並不比常量指針更安全,除了在最微不足道的情況下 - 缺乏自動間接的便利性並帶有不同的語義內涵。

請考慮C ++ FAQ中的以下語句:

儘管引用通常是使用底層彙編語言中的地址實現的,但請不要將引用視為指向對象的有趣外觀指針。 參考對象。 它不是指向對象的指針,也不是對象的副本。 這對象。

但如果參考確實是對象,那麼怎麼會有懸空引用呢? 在非託管語言中,引用不可能比指針更“安全” - 通常只是不能跨範圍邊界可靠地對值進行別名!

為什麼我認為C ++引用很有用

來自C背景,C ++引用可能看起來像一個有點愚蠢的概念,但是在可能的情況下仍然應該使用它們而不是指針:自動間接方便的,並且在處理RAII時引用變得特別有用 - 但不是因為任何感知的安全性優點,而是因為它們使寫作慣用代碼不那麼尷尬。

RAII是C ++的核心概念之一,但它與復制語義非常簡單地交互。 通過引用傳遞對象避免了這些問題,因為不涉及復制。 如果語言中沒有引用,則必須使用指針,這些指針使用起來比較麻煩,因此違反了語言設計原則,即最佳實踐解決方案應該比替代方案更容易。

c++ pointers reference c++-faq

我知道引用是語法糖,因此代碼更容易讀寫。

但有什麼區別?

以下答案和鏈接摘要:

  1. 指針可以重新分配任意次數,而綁定後無法重新分配引用。
  2. 指針可以指向任何地方( NULL ),而引用總是指向一個對象。
  3. 您無法使用指針獲取引用的地址。
  4. 沒有“參考算術”(但是您可以獲取引用所指向的對象的​​地址,並在其上執行指針算法,如&obj + 5 )。

澄清一個誤解:

C ++標準非常謹慎,以避免規定編譯器如何實現引用,但每個C ++編譯器都將引用實現為指針。 也就是說,聲明如下:

int &ri = i;

如果它沒有完全優化 ,則分配與指針相同的存儲量,並將i的地址放入該存儲器中。

因此,指針和引用都使用相同數量的內存。

作為基本規則,

  • 使用函數參數和返回類型中的引用來提供有用的自記錄接口。
  • 使用指針實現算法和數據結構。

有趣的讀物:




與流行的觀點相反,可能有一個NULL引用。

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

當然,使用參考文件要困難得多 - 但是如果你管理它,你會撕掉你的頭髮試圖找到它。 在C ++中,引用本身並不安全!

從技術上講,這是一個無效的引用 ,而不是空引用。 C ++不支持空引用作為您可能在其他語言中找到的概念。 還有其他類型的無效引用。 任何無效引用都會引發未定義行為的幽靈,就像使用無效指針一樣。

在分配給引用之前,實際錯誤是在NULL指針的解引用中。 但是我不知道任何編譯器會在這種情況下產生任何錯誤 - 錯誤會傳播到代碼中的某個點。 這就是讓這個問題如此陰險的原因。 大多數情況下,如果你取消引用一個NULL指針,你就會在那個位置崩潰,並且不需要太多的調試就可以搞清楚。

我上面的例子簡短而且做作。 這是一個更真實的例子。

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

我想重申,獲取空引用的唯一方法是通過格式錯誤的代碼,一旦你擁有它,你就會得到未定義的行為。 檢查空引用是沒有意義的; 例如,您可以嘗試if(&bar==NULL)...但編譯器可能會優化語句不存在! 有效的引用永遠不能為NULL,因此從編譯器的視圖來看,比較總是為false,並且可以將if子句作為死代碼消除 - 這是未定義行為的本質。

避免麻煩的正確方法是避免取消引用NULL指針來創建引用。 這是實現這一目標的自動化方法。

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

對於那些具有更好寫作技巧的人來看這個問題,請參閱Jim Hyslop和Herb Sutter的Null References

有關解除引用空指針的危險的另一個示例,請參閱Raymond Chen 嘗試將代碼移植到另一個平台時暴露未定義的行為




你忘記了最重要的部分:

使用指針的成員訪問->
使用引用的成員訪問.

foo.bar 明顯優於foo->bar ,就像vi 明顯優於Emacs :-)




引用與指針非常相似,但它們經過精心設計,有助於優化編譯器。

  • 設計引用使得編譯器更容易跟踪哪個引用別名哪個變量。 兩個主要特徵非常重要:沒有“參考算術”,也沒有重新分配參考文獻。 這些允許編譯器在編譯時找出哪些引用別名是哪些變量。
  • 允許引用引用沒有內存地址的變量,例如編譯器選擇放入寄存器的變量。 如果你取一個局部變量的地址,編譯器很難把它放在一個寄存器中。

舉個例子:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

優化編譯器可能會意識到我們正在訪問[0]和[1]相當多的一組。 它希望優化算法:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

為了進行這樣的優化,需要證明在調用期間沒有任何東西可以改變數組[1]。 這很容易做到。 我永遠不會少於2,所以array [i]永遠不會引用數組[1]。 maybeModify()被賦予a0作為參考(別名數組[0])。 因為沒有“引用”算法,編譯器只需要證明maybeModify永遠不會得到x的地址,並且它已經證明沒有任何改變數組[1]。

它還必須證明,當我們在a0中有一個臨時寄存器副本時,未來的調用沒有辦法讀/寫[0]。 這通常是微不足道的,因為在很多情況下很明顯,引用永遠不會存儲在像類實例這樣的永久結構中。

現在用指針做同樣的事情

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

行為是一樣的; 只是現在更難以證明maybeModify不會修改數組[1],因為我們已經給它一個指針; 這隻貓已經不在了。 現在它必須做更加困難的證明:對maybeModify進行靜態分析以證明它永遠不會寫入&x + 1.它還必須證明它永遠不會保存可以引用數組[0]的指針,這只是太棘手了。

現代編譯器在靜態分析方面越來越好,但總是很好地幫助它們並使用引用。

當然,除非進行這種巧妙的優化,否則編譯器確實會在需要時將引用轉換為指針。

編輯:發布這個答案五年後,我發現了一個實際的技術差異,其中引用不同於查看相同尋址概念的不同方式。 引用可以以指針不能的方式修改臨時對象的生命週期。

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

通常,臨時對象(例如通過調用createF(5)創建的對象createF(5)在表達式的末尾被銷毀。 但是,通過將該對象綁定到引用, ref ,C ++將延長該臨時對象的生命週期,直到ref超出範圍。




雖然引用和指針都用於間接訪問另一個值,但引用和指針之間存在兩個重要區別。第一個是引用始終引用一個對象:在不初始化引用的情況下定義引用是錯誤的。賦值行為是第二個重要區別:賦值給引用會更改引用所綁定的對象; 它不會重新綁定對另一個對象的引用。初始化後,引用始終引用相同的基礎對象。

考慮這兩個程序片段。在第一個中,我們將一個指針指向另一個:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

在賦值後,ival,pi處理的對象保持不變。賦值會更改pi的值,使其指向不同的對象。現在考慮一個類似的程序,分配兩個引用:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

此賦值更改ival,即ri引用的值,而不是引用本身。在賦值之後,兩個引用仍然引用它們的原始對象,並且這些對象的值現在也是相同的。




引用是另一個變量的別名,而指針保存變量的內存地址。引用通常用作函數參數,以便傳遞的對像不是副本而是對象本身。

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 



這是基於本tutorial。寫的內容更清楚:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

簡單地記住,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

更重要的是,因為我們可以參考幾乎任何指針教程,指針是指針算法支持的對象,它使指針類似於數組。

看下面的陳述,

int Tom(0);
int & alias_Tom = Tom;

alias_Tom可以理解為一個alias of a variable(具有不同的typedef,這是alias of a typeTom。也可以忘記這樣的語句的術語是創建一個引用Tom




在C ++中可以引用指針,但反過來不可能意味著指向引用的指針是不可能的。對指針的引用提供了更清晰的語法來修改指針。看看這個例子:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

並考慮上述程序的C版本。在C中你必須使用指向指針(多個間接),它會導致混亂,程序可能看起來很複雜。

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

有關指針引用的更多信息,請訪問以下內容:

正如我所說,指向引用的指針是不可能的。嘗試以下程序:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}



我使用引用,除非我需要以下任何一個:

  • 空指針可以用作標記值,通常是避免函數重載或使用bool的廉價方法。

  • 你可以對指針進行算術運算。例如,p += offset;




另一個區別是你可以有一個指向void類型的指針(它意味著指向任何東西的指針)但禁止引用void。

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

我不能說我對這種特殊的差異感到非常滿意。我更傾向於允許帶有地址的含義引用,以及引用的相同行為。它允許使用引用定義一些C庫函數的等價物,如memcpy。




此外,作為內聯函數的參數的引用可以與指針不同地處理。

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

在內聯指針版本1時,許多編譯器實際上會強制寫入內存(我們正在明確地獲取地址)。但是,他們會將參考文獻保留在更優化的寄存器中。

當然,對於沒有內聯的函數,指針和引用生成相同的代碼,如果它們未被函數修改並返回,則通過值傳遞內在函數而不是引用更好。




引用的另一個有趣用途是提供用戶定義類型的默認參數:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

默認flavor使用'綁定const引用到引用的臨時'方面。




也許一些比喻會有所幫助; 在桌面屏幕空間的上下文中 -

  • 引用要求您指定實際窗口。
  • 指針需要屏幕上一塊空間的位置,您可以確保它將包含該窗口類型的零個或多個實例。



指針和引用之間存在非常重要的非技術差異:通過指針傳遞給函數的參數比通過非const引用傳遞給函數的參數更加明顯。 例如:

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

回到C,看起來像fn(x)只能通過值傳遞的調用,所以它肯定無法修改x;修改您需要傳遞指針的參數fn(&x)。因此,如果參數之前沒有參數,則&您知道它不會被修改。 (相反,&意味著修改,不是真的,因為你有時必須通過const指針傳遞大的只讀結構。)

有些人認為,在閱讀代碼時,這是一個非常有用的功能,指針參數應始終用於可修改的參數而不是非const引用,即使函數從不期望a nullptr。也就是說,那些人認為fn3()不應該允許像上面這樣的功能簽名。Google的C ++風格指南就是一個例子。




我總是決定this從C ++核心指導原則:

當“無參數”是有效選項時,首選T *而不是T.




Related