友元件 Friend

類別優點 中,我們講到類別讓我們可以將細節給「包起來」,不讓外界看到。程式碼可以做到這點很大一部分歸功於關鍵字 publicprivate 的幫助。

但是當我們一定要取得另一個類別內被保護的資訊時該怎麼辦呢?這章我們就要來討論這個問題!

什麼問題

開頭說到,當我們一定要取得另一個類別中被保護的資訊時,該怎麼辦呢?

什麼情況會需要取得另一個類別中被保護的資訊呢?

我們來舉一個例子:

class Circle
{
    int r{5};
public:
    float getArea()
    {
        return r * r * 3.14;
    }
};

int main() {
    Circle circle;
    
    // 如何取得 circle 的半徑

    return 0;
}

這是一個非常簡單粗糙的例子,但可以很好的說明我們遇到的問題。

在這個例子中,我們有一個 circle 物件,我們可以取得這個圓的面積,但無法取得它的半徑。

你可能會說有圓面積就可以算半徑啦!我說了這是一個非常粗糙的例子,我們假設我們很笨不知道圓的面積公式 XD。

這樣我們要怎麼取得圓的半徑呢?

有幾個方法可以解決:

  1. 去問寫 Circle 類別的人,他怎麼計算圓的面積,然後再自己反推算出半徑。
  2. 拜託寫 Circle 類別的人,寫另一個函數讓你可以取得半徑資訊,比如說:
class Circle
{
    int r{5};
public:
    float getArea()
    {
        return r * r * 3.14;
    }

    int getRadius()
    {
        return r;
    }
};

這樣我們就可以利用 circle.getRadius() 取得半徑資訊。

但這個方法非常不切實際,因為代表這個類別要因為你一個人的需求就去做更動,那如果別人有別的需求,是不是要創造另一個不同的函數。

那有什麼比較有效率的方法嗎?

Friend 是什麼

關鍵字 friend 就是這個問題的解答!也是我們這篇的重點。

在類別的內部,我們可以使用 friend 關鍵字,告訴編譯器另一個類別或函式現在是朋友!

在 C++ 中,當一個類別或函式被當作是朋友,那就等於它被授予對另一個類別的 privateprotected 成員的完全存取權。

通過這種方式,一個類別可以有選擇性地給其他類別或函式對其成員的完全訪問權限,而不影響其他任何東西。

在日常生活中,就像是只有你授予是朋友的人可以看你的日記本,其他只要不是你的朋友,都沒辦法看你的日記本內容!

友函數

前面我們說到我們可以將一個類別或函數宣告為朋友,被宣告為朋友的函數我們叫它友函數,英文叫 Friend Function。

友函數是一個可以存取類別的 privateprotected 成員,就像它是那個類別的成員函數一樣。除了這點,友函數其實就是一個普通的函數。

現在我們來看看該如何宣告一個友函數:

class Circle
{
    int r{5};
public:
    float getArea()
    {
        return r * r * 3.14;
    }
    
    // 宣告函數 getRadius 為類別 Circle 的朋友
    friend int getRadius(Circle circle);
};

// 函數定義
int getRadius(Circle circle)
{
    // 現在這個函數是類別 Circle 的朋友了
    // 所以可以取得 r 的值!
    return circle.r;
}

int main() {
    Circle circle;
    
    std::cout << "半徑:" << getRadius(circle) << std::endl;

    return 0;
}

在這個例子中,我們將 getRadius(Circle circle) 設為 Circle 類別的朋友,這樣一來,這個函式就有資格取得該類別中的任何資訊了!

值得注意的是,我們必須 Circle 實體物件以參數的方式傳進函式,因為這個函式並非原類別的成員函式,所以不能直接取用資訊。

我們也可以將函式定義寫在類別內,比如說:

class Circle
{
    int r{5};
public:
    float getArea()
    {
        return r * r * 3.14;
    }
    
    friend int getRadius(Circle circle)
    {
        return circle.r;
    }
};

int main() {
    Circle circle;
    
    std::cout << "半徑:" << getRadius(circle) << std::endl;

    return 0;
}

但是要記得,雖然整個 getRadius() 函式寫在類別中,但不代表這個函式就是 Circle 這個類別的成員函式!

友類別

另一個可以被宣告為朋友的就是類別了!英文叫做 Friend Class。

和友函數一樣,友類別是一個可以存取其他類別的 privateprotected 成員。除了這點,友類別其實就是一個普通的類別。

現在我們來看看該如何宣告一個友類別:

class Circle
{
    int r{5};
public:
    float getArea()
    {
        return r * r * 3.14;
    }

    // 宣告類別 Data 為 Circle 的朋友
    friend class Data;
};

class Data
{
    int v;
public:
    void getCircleRadius(Circle circle)
    {
        // 因為是朋友
        // 所以可以取得 Circle 的 r!
        v = circle.r;
        std::cout << "v 的值:" << v << std::endl;
    }
};

int main() {
    Circle circle;
    Data data;
    data.getCircleRadius(circle);

    return 0;
}

這裡我們創建了一個正常的類別 Data,然後在 Circle 類別中宣告 Data 為它的朋友。

這樣一來,我們就可以在 Data 中取用 Circle 的所有資訊了!

值得注意的是,這樣的朋友關係並非雙向的!

在這個例子裡 Data 是 Circle 的朋友,不代表 CircleData 的朋友!也就是說除非在 Data 類別中也定義 Circle 是友類別,不然 Circle 無法取得 Data 中的資料!

什麼時候該使用 Friend

其實,如果能夠不使用 friend,就不要使用。因為這在一定程度上依舊破壞了類別的好處:資料封裝。

我們在一開始講解遇到的問題時,我們說到如果想要取得半徑資訊,我們可以拜託寫 Circle 類別的人,寫另一個函數讓我們可以取得半徑資訊。

在那裡我們說不切實際,但其實在現實世界中,像這樣的關鍵資料,寫程式碼的人基本上都會寫一個函式讓他人可以取得資料。

這樣的函數我們叫做 getter,另一個函式 setter 也是同樣的概念,如果想讀更多可以看這篇關於 getter and setter 的文章。

如果需要大量對外開放資料,我們可能才會使用到 friend class 去將大把資料封裝起來給另一個類別使用。

總而言之,如果能不使用 friend,就盡量不使用 friend!

總結

這章我們了解了什麼是 Friend,它的作用以及實際例子。

它在一定程度上破壞了資料封裝性,但不代表它是萬惡的!有些時候它是非常好用的!

但如果能用一個正常的類別或是函式做到你想做的,那就不要用 Friend!

那這章就到這邊啦!我們下一章見~