程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 你好,C++(35)類是如何藏私房錢的?6.2.4 拷貝構造函數,私房錢6.2.4

你好,C++(35)類是如何藏私房錢的?6.2.4 拷貝構造函數,私房錢6.2.4

編輯:C++入門知識

你好,C++(35)類是如何藏私房錢的?6.2.4 拷貝構造函數,私房錢6.2.4


6.2.6  類成員的訪問控制

類成員包括類的成員變量和成員函數,它們分別用來描述類的屬性和行為。而類成員的訪問控制決定了哪些成員是公開的,可以被外界訪問,也可以被自身訪問;哪些成員是私有的,只能在類的內部訪問,外界無法訪問。就像一個人的錢包,只有他自己能動,別人是不能動的。又如同自己藏的私房錢也只有自己知道,對其他人而言,私房錢是完全隱藏的。

大家可能會問,為什麼要對類成員的訪問加以控制,大公無私地誰都可以訪問不是挺好的嗎?這是因為在現實世界中,人們對事物的訪問是受到控制的,我們可以訪問到某些事物,但不可能訪問到任何事物。就如同我們只能知道自己錢包裡有多少錢,而無法知道別人錢包裡有多少錢一樣。這一點反映到C++中,就成了類成員的訪問控制。可以設想這樣的情形:錢包裡的錢只能由自己訪問,別人是無權訪問的。當它成為“人”這個類的一個屬性,用某個成員變量來表示後,其訪問自然也應該受到控制,只能由“人”這個類自身的行為(成員函數)來訪問,對於外界其他函數而言,這個成員變量就被隱藏起來是不可見的,自然也就無法訪問。如果不對訪問加以控制,誰都可以訪問,那就很有可能其他函數(小偷)會錯誤地修改這個本不該它修改的數據,數據安全無法得到保證。換句話說,要想讓我們錢包裡的錢安安穩穩的,要想讓類的成員避免不安全的訪問,我們必須對類成員的訪問加以控制。

在C++中,對類成員的訪問控制是通過設置成員的訪問級別來實現的。按照訪問范圍的大小,訪問級別被分為公有類型(public)、保護類型(protected)和私有類型(private) 三種,如圖6-9所示。

1. 公有類型

公有類型的成員用關鍵字public修飾,在成員變量或者成員函數前加上public關鍵字就表示這是一個公有類型的成員。公有類型的成員的訪問不受限制,在類的內外都可以訪問,但它更多的還是作為類提供給外界訪問的接口,是類跟外界交流的通道,外界通過訪問公有類型的成員完成跟類的交互。例如Teacher類的表示上課行為的GiveLesson()成員函數,這是它向外界提供的服務,應該被外界訪問,所以這個成員函數應該設置為公有類型。

 圖6-9  訪問級別

2. 保護類型

保護類型的成員用關鍵字protected修飾,其聲明格式跟public類型相同。保護類型的成員在類的外部無法訪問,但是可以在類的內部及繼承這個類而得的派生類中訪問,它主要用於將屬性或者方法遺傳給它的下一代子類。例如,對於Teacher類的表示姓名屬性的m_strName成員變量,誰也不願意自己的名字被別人修改,所以應該限制外界對它的訪問;而顯然自己可以修改自己的名字,所以Teacher類內部可以訪問這個成員變量;同時,如果Teacher類有派生類,比如表示大學老師的Lecturer,我們也希望這個成員變量可以遺傳給派生類,使其派生類也擁有這個成員變量,可以對其進行訪問。經過這樣的分析,把m_strName成員變量設置為保護類型最為合適。

3. 私有類型

私有類型的成員用關鍵字private修飾,其聲明格式跟public類型相同。私有類型的成員只能在類的內部被訪問,所有來自外部的訪問都是非法的,這樣就把類的成員完全隱藏在類當中,很好地保護了類中數據和行為的安全。所以,趕快把我們的錢包聲明為私有成員吧,這樣小偷就不會訪問到它了。

這裡需要說明的是,如果class關鍵字定義的類當中類成員沒有顯式地說明它的訪問級別,那麼默認情況下它就是私有類型,外界是無法訪問的,由此可見類其實是非常“自私”的(相反地,由struct定義的類中,默認的訪問級別為公有類型)。

我們把前面例子中的Teacher類根據訪問控制加以改寫,以更加真實地反映現實的情況:

// 進行訪問控制後的Teacher類
class Teacher
{
// 公有類型成員
// 外界通過訪問這些成員跟該類進行交互,獲得類提供的服務
public: // 冒號後的變量或者函數都受到它的修飾
    // 構造函數應該是公有的,這樣外界才可以利用構造函數創建該類的對象
    Teacher(string strName) : m_strName(strName)
    {
        // …
    }

    // 老師要為學生們上課,它應該被外界調用,所以這個成員函數是公有類型的
    void GiveeLesson()
    {
        // 在類的內部,可以訪問自身的保護類型和私有類型成員
        PrepareLesson();  // 先備課,訪問保護類型成員
        cout<<"老師上課。"<<endl;
        m_nWallet += 100;  // 一節課100塊錢,訪問私有類型成員
    }

    // 我們不讓別人修改名字,可總得讓別人知道我們的名字吧,
// 對於只可供外界只讀訪問的成員變量,
// 可以提供一個公有的成員函數供外界對其進行讀取訪問
    string GetName()
    {
        return m_strName;
    }

// 保護類型成員
// 不能被外界訪問,但是可以被自身訪問,也可以遺傳給下一級子類,
// 供下一級子類訪問
protected:
// 只有自己備課,子類也需要備課,所以設置為保護類型
void PrepareLesson()
{
     cout<<"老師備課。"<<endl;
}

// 只有自己可以修改自己的名字,子類也需要這樣的屬性
    string m_strName;
private:    // 私有類型
   int m_nWallet; // 錢包只有自己可以訪問,所以設置為私有類型
};

有了訪問控制之後,現在再對Teacher類對象的成員進行訪問時就需要注意了,我們只能在外界訪問其公有成員,如果試圖訪問其保護類型或者私有類型的成員,就會被毫不留情地拒之門外,吃人家的閉門羹:

int main()
{
    // 創建對象時會調用類的構造函數
    // 在Teacher類中,構造函數是公有類型,所以可以直接調用
    Teacher MrChen("ChenLiangqiao");

    // 外部變量,用於保存從對象獲得的數據
    string strName;
   // 通過類的公有類型成員函數,讀取獲得類中的保護類型的成員變量   
    strName = MrChen.GetName();

    // 錯誤:無法直接訪問類的保護類型和私有類型成員
    // 想改我的名字?先要問我答應不答應
    MrChen.m_strName = "WangGang";
   // 想從我錢包中拿走200塊,那更不行了
    MrChen.m_nWallet -= 200;

    return 0;
}

在主函數中,我們首先創建一個Teacher類對象,這個創建過程會調用它的構造函數,這就要求構造函數是公有類型。在構造函數中,我們會訪問成員變量m_strName,雖然它是保護類型的,但是可以在類自身的構造函數中對其進行修改。另一方面,為了讓外界也能夠安全地讀取該成員變量的值,我們為Teacher類添加了一個公有類型的成員函數GetName(),這樣就可以通過它讓外界訪問類中保護類型的成員獲得必要的數據。除此之外,Teacher類還提供了一個公有類型的GiveLesson()函數,外界可以直接調用這個函數獲得Teacher類提供的上課服務。而在這個公有類型的成員函數內部,我們還訪問了類中的保護類型和私有類型成員。

以上的訪問都是合理合法的,但是,如果想在類的外部直接訪問保護類型或私有類型的成員,編譯器會幫我們檢測出這個非法訪問,產生一個編譯錯誤提示無法訪問保護或私有類型的成員。這樣,有編譯器幫我們看著,小偷就再也別想動我們的錢包了。

通過對類成員的訪問進行控制,起到了很好地保護數據和行為的作用,防止了數據被外界隨意修改,同時也限制了外界使用不合理的行為。如果對象的某些成員變量因為業務邏輯的需要允許外界訪問(比如這裡的m_strName),也建議采用提供公有接口的方法,讓外界通過公有接口來訪問這些成員變量,而不是直接把這些成員變量設置為公有類型。一般情況下,類的成員變量都應該設置為保護或私有類型。

6.2.7  在友元中訪問類的隱藏信息

采用類成員的訪問控制機制之後,很好地實現了數據和行為的隱藏,這種隱藏有效地避免了來自外界的非法訪問,保護了數據和行為的安全。但是,這種嚴格的成員訪問控制是不問緣由的,任何來自外界的對類中隱藏信息(保護或私有類型成員)的訪問都會被拒絕,這樣自然也會將一些合理的訪問擋在門外,給類成員的訪問帶來一些麻煩。例如,有時需要定義某個函數,這個函數不是類的一部分,但又需要頻繁地訪問類的隱藏信息;又或者需要定義某個新的類,因為某種原因,這個類需要訪問另外一個類的隱藏信息,就像現實世界中老婆需要訪問老公的錢包一樣。在這些情況下,我們需要從外界直接訪問類的保護或私有類型成員,但卻被嚴格的成員訪問控制機制擋在了門外。

凡事都有例外。為了給訪問控制機制開個後門,讓外界值得信任的函數或者類能夠訪問某個類的隱藏信息,C++提供了友元機制。它利用“friend”關鍵字,可以將外界的某個函數或者類聲明為類的友元函數或者友元類,兩者統稱為友元。當成為類的友元後,就可以對類的隱藏信息進行訪問了。

1. 友元函數

友元函數實際上是一個定義在類外部的普通函數,它不屬於任何類。當使用“friend”關鍵字在類的定義中加以聲明後,這個函數就成為類的友元函數,之後它就可以不受類成員訪問控制的限制,直接訪問類的隱藏信息。在類中聲明友元函數的語法格式如下:

class 類名
{
    friend 返回值類型 函數名(形式參數列表);
// 類的其他聲明和定義…
};

友元函數的聲明跟類的普通成員函數的聲明是相同的,只不過在函數聲明前加上了friend關鍵字修飾並且定義在類的外部,並不屬於這個類。友元函數的聲明不受訪問控制的影響,既可以放在類的私有部分,也可以放在類的公有部分,它們是沒有區別的。另外,一個函數可以同時是多個類的友元函數,只是需要在各個類中分別聲明。

2. 友元類

跟友元函數相似,友元類是定義在某個類之外的另外一個獨立的普通類。因為需要訪問這個類的隱藏信息,所以利用“friend”關鍵字將其聲明為這個類的友元類,賦予它訪問這個類的隱藏信息的能力。成為這個類的友元類之後,友元類的所有成員函數也就相當於成為了這個類的友元函數,自然也就可以訪問這個類中的隱藏信息。      

在C++中,聲明友元類的語法格式如下:

class 類名
{
    friend class 友元類名;
// 類的其他聲明和定義
};

友元類的聲明跟友元函數的聲明類似,這裡就不再贅述。唯一需要注意的是這裡兩個類之間的相互關系,如果我們希望A類能夠訪問B類的隱藏信息,就在B類中將A類聲明為它的友元類。這就表示A類是B類經過認證後的值得信賴的“朋友”,這樣A類才可以訪問B類的隱藏信息。為了更好地理解友元的作用,還是來看一個實際的例子。假設在之前定義的Teacher類中有一個成員變量m_nSalary記錄了老師的工資信息。工資信息當然是個人隱私需要保護了,所以將其訪問控制級別設置為保護類型,只有自己和派生的子類可以訪問:

class Teacher
{
// …
// 保護類型的工資信息
protected:
    int m_nSalary;
};

將m_nSalary 設置為保護類型,可以很好地保護數據安全。但是,在某些特殊情況下,我們又不得不在外界對其進行訪問。比如,稅務局(用TaxationDep類表示)要來查老師的工資收入,他當然應該有權力也有必要訪問Teacher類中m_nSalary這個保護類型的成員;又或者學校想用AdjustSalary()函數給老師調整工資,老師自然樂意它來訪問m_nSalary這個保護類型的成員。在這種情況下,我們就需要把TaxationDep類和AdjustSalary()函數聲明為Teacher類的友元,讓它們可以訪問Teacher類的隱藏信息:

// 擁有友元的Teacher類
class Teacher
{
    // 聲明TaxationDep類為友元類
    friend class TaxationDep;
    // 聲明AdjustSalary()函數為友元函數
    friend int AdjustSalary(Teacher* teacher);
// 其他類的定義…
protected:
    int m_nSalary; // 保護類型的成員
};

// 在類的外部定義的友元函數
int AdjustSalary(Teacher* teacher)
{
    // 在Teacher類的友元函數中訪問它的保護類型的成員m_nSalary
    if( teacher != nullptr && teacher->m_nSalary < 1000)
   {
        teacher->m_nSalary += 500;  // 漲工資
       return teacher->m_nSalary;
}

    return 0;
}

// 友元類
class TaxationDep
{
// 類的其他定義…
public:
    void CheckSalary( Teacher* teacher )
    {
        // 在Teacher類的友元類中訪問它的保護類型成員m_nSalary
        if(teacher != nullptr && teacher->m_nSalary > 1000)
        {
            cout<<"這位老師應該交稅"<<endl;
      }
    }
};

可以看到,當Teacher類利用“friend”關鍵字將AdjustSalary()函數和TaxationDep類聲明為它的友元之後,在友元中就可以直接訪問它的保護類型的成員m_nSalary,這就相當於為友元打開了一個後門,使其可以翻過訪問控制這道保護牆而直接訪問到類的隱藏信息。

友元雖然能給我們帶來一定的便利,但是“開後門”畢竟不是一件多麼正大光明的事情,在使用友元的時候,還應該注意以下幾點:

l  友元關系不能被繼承。這一點很好理解,我們跟某個類是朋友(即是某個類的友元類),並不表示我們跟這個類的兒子(派生類)同樣是朋友。

l  友元關系是單向的,不具有交換性。比如,TaxationDep類是Teacher類的友元類,稅務官員可以檢查老師的工資,但這並不表示Teacher類也是TaxationDep類的友元類,老師也可以檢查稅務官員的工資。

l  將某個函數或者類聲明為友元,即意味著對方是經過審核認證的,是值得信賴的,所以才授權給它訪問自己的隱藏信息。這也提示我們在將函數或類聲明為友元之前,有必要對其進行一定的審核。只有值得信賴的函數和類才能將其聲明為友元。

友元的使用並沒有破壞封裝

在友元函數或者友元類中,我們可以直接訪問類的保護或私有類型的成員,這個“後門”打開後,很多人擔心這樣會讓類的隱藏信息暴露出來,破壞類的封裝。但事實是,合理地使用友元,不僅不會破壞封裝,反而會增強封裝。

在面向對象的設計中,我們強調的是“高內聚、低耦合”的設計原則。當一個類的不同成員變量擁有兩個不同的生命周期時,為了保持類的“高內聚”,我們經常需要將這些不同生命周期的成員變量分割成兩部分,也就是將一個類分割成兩個類。在這種情況下,被分割的兩部分通常需要直接存取彼此的數據。實現這種情況的最安全途徑就是使這兩個類成為彼此的友元。但是,一些“高手”想當然地認為友元破壞了類的封裝,轉而通過提供公有的 get()和set()成員函數使兩部分可以互相訪問數據。實際上,他們不知道這樣的做法正是破壞了封裝。在大多數情況下,這些 get()和set()成員函數和公有數據一樣差勁:它們僅僅隱藏了私有數據的名稱,而沒有隱藏對私有數據的訪問。

友元的使用,只是向必要的客戶公開類的隱藏數據,比如Teacher類只是向TaxationDep類公開它的隱藏數據,只是讓稅務官可以知道它的工資是多少。這要遠比使用公有的get()/set()成員函數,而讓全世界都可以知道它的工資要安全隱蔽的多。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved