1. 程式人生 > >引用的本質分析(四)

引用的本質分析(四)

C++ 引用本質

我們上節講了 C++ 中的引用,那麽我們就來看下引用的本質引用作為變量別名而存在,因此在一些場合可以代替指針。引用相對於指針來說具有更好的可讀性和實用性。註意:函數中的引用參數不需要進行初始化!

下來我們來看看 swap 函數的實現對比,如下

void swap(int* a, int* b)    // 指針形式的
{
    int t = *a;
    *a = *b;
    *b = t;
}

void swap(int& a, int& b)    // 引用形式的
{
    int t = a;
    a = b;
    b = t;
}

那麽這塊就有個特殊的引用,便是 const 引用了。在 C++ 中可以聲明 const 引用,它的格式為 const Type& name = var;const 引用讓變量擁有只讀屬性。當使用常量對 const 引用進行初始化時,C++ 編譯器會為常量值分配空間並將引用作為這段空間的別名。使用常量對 const 引用初始化後將生成一個只讀變量!

下來我們以代碼為例進行分析,看看引用的特殊意義,代碼如下

#include <stdio.h>

void Example()
{
    printf("Example:\n");
    
    int a = 3;
    const int& b = a;
    int* p = (int*)&b;
    
    // b = 5;
    
    *p = 5;
    
    printf("a = %d\n", a);
    printf("b = %d\n", b);
}

void Demo()
{
    printf("Demo:\n");
    
    const int& c = 1;
    int* p = (int*)&c;
    
    // c = 5;
    
    *p = 5;
    
    printf("c = %d\n", c);
}

int main(int argc, char *argv[])
{
    Example();
    
    printf("\n");
    
    Demo();
    
    return 0;
}

我們在 Example 函數中定義了變量 a,用 b const 引用 a,然後用指針 p 指向 b。然後通過指針 p 改變 b 的值,但是這塊 b 是 const 引用,所以不能直接改變 b。我們看看 a 和 b 會是多少。在 Demo 函數中,我們通過 const 引用 c 為 1,並且定義指針 p 指向它。同樣不能直接改變 c,但是可以通過指針 p 來改變它的值。我們先來看看通過指針 p 改變後的值是否為 5 呢?看看編譯結果

技術分享圖片

我們看到值已經都改變了,我們再來去掉第 11 和 26 行的註釋,看看直接改變 const 引用會怎樣?技術分享圖片

我們看到報的都是它們是只讀變量。那麽我們思考下:引用有自己的存儲空間嗎?我們通過程序來看看

#include <stdio.h>

struct test
{
    char& c;
};

int main(int argc, char *argv[])
{
    char c = 'c';
    char& rc = c;
    test r = { c };
    
    printf("sizeof(char&) = %d\n", sizeof(char&));
    printf("sizeof(rc) = %d\n", sizeof(rc));
    
    printf("sizeof(test) = %d\n", sizeof(test));
    printf("sizeof(r.c) = %d\n", sizeof(r.c));
    
    return 0;
}

我們在第 3 行定義了一個結構體變量 test,但它裏面只有一個 char 類型的引用 c。我們來看看這個結構體占用內存嗎?編譯如下

技術分享圖片

我們看到引用本身只占用了一個字節,但是結構體 test 占用了 4 個字節的內存。我們猜想它是不是跟指針有某種聯系呢?其實引用在 C++ 中的內部實現是一個指針常量。關系如下

技術分享圖片

註意:a> C++ 編譯器在編譯過程中用 指針常量 作為引用的內部實現,因此引用所占的空間大小與指針相同;b> 從使用的角度,引用只是一個別名,C++ 為了實用性而隱藏了引用的存儲空間這一細節。下來我們通過一個示例代碼進行說明

#include <stdio.h>

struct TRef
{
    char* before;
    char& ref;
    char* after;
};

int main(int argc, char* argv[])
{
    char a = 'a';
    char& b = a;
    char c = 'c';

    TRef r = {&a, b, &c};

    printf("sizeof(r) = %d\n", sizeof(r));
    printf("sizeof(r.before) = %d\n", sizeof(r.before));
    printf("sizeof(r.after) = %d\n", sizeof(r.after));
    printf("&r.before = %p\n", &r.before);
    printf("&r.after = %p\n", &r.after);

    return 0;
}

我們看到在結構體 TRef 內部只有 3 個成員,兩個指針,一個引用。我們通過打印結構體的大小和它的 before 指針和 after 指針的大小和地址來分別看看中間的引用究竟是什麽

技術分享圖片

我們看到結構體總共占 12 個字節的內存,指針 before 和 after 各占 4 個字節,並且他們的地址相差 8,從而雙重說明了中間的引用占 4 個字節的內存空間,引用便是指向一個地址的。那麽它的本質便是指針了。

那麽為什麽還要弄個引用來代替指針呢?我們知道在 C 語言中,凡是涉及到指針的操作都是容易出 bug 的地方,因此 C++ 設計了引用來在大部分情況下代替指針。從功能性來說,可以滿足大多數的需要使用指針的場合;從安全性來說,可以避免由於操作指針不當而帶來的內存錯誤;從操作性來說,簡單易用,又不失功能強大。下面我們來看看函數返回引用的一個示例

#include <stdio.h>

int& demo()
{
    int d = 0;
    
    printf("demo: d = %d\n", d);
    
    return d;
}

int& func()
{
    static int s = 0;
    
    printf("func: s = %d\n", s);
    
    return s;
}

int main(int argc, char* argv[])
{
    int& rd = demo();
    int& rs = func();
    
    printf("\n");
    printf("main: rd = %d\n", rd);
    printf("main: rs = %d\n", rs);
    printf("\n");
    
    rd = 10;
    rs = 11;
    
    demo();
    func();
    
    printf("\n");
    printf("main: rd = %d\n", rd);
    printf("main: rs = %d\n", rs);
    printf("\n");

    return 0;
}

我們在 demo 函數裏返回了局部變量 d,因此這個肯定會出問題。在 func 函數裏返回的加 static 修飾的變量,因此它是會放在全局數據區,不會出錯。我們在第 23 和 24 行用 demo 和 func 函數進行初始化,因此這會打印出 d = 0 和 s = 0;在第 27 和 28 行打印 rd 和 rs 的值,因為 demo 函數返回之後 d 會丟失,這時 rd 便是一個野指針了。所以 rd 指向的是一個隨機數,但是 rs 還是為 0;第 31 和 32 行分別對 rd 和 rs 進行重新賦值,再次調用 demo 和 func 函數時,d 還是為 0,s 就為 11 了;最後第 38 和 39 行會打印出 rd 為隨機數,rs 為 11。我們來看看編譯結果和我們分析的是否一致

技術分享圖片

我們看到它在編譯的時候都已經報警告了,打印的結果和我們所分析的是一致的。通過對引用本質的學習,總結如下:1、引用作為變量別名而存在旨在代替指針;2、const 引用可以使得變量具有只讀屬性;3、引用在編譯器內部使用指針常量實現,它的最終本質為指針;4、引用可以盡可能的避開內存錯誤


歡迎大家一起來學習 C++ 語言,可以加我QQ:243343083

引用的本質分析(四)