C++類的總結

類的兩項基本能力:

(1) 數據抽象,即定義數據成員和函數成員的能力。
(2) 封裝,即保護類的成員不被隨意訪問的能力。通過將類的實現細節設爲private,我們就能完成類的封裝。類可以將其他類或者函數設爲友元,這樣它們就能訪問類的非公有成員了。

1. 如何設計一個類,如何定義類及類的成員

(1)類可以在它的第一個訪問說明符之前定義成員,對這種成員的訪問權限依賴於類定義的方式。如果使用struct關鍵字,則定義在第一個訪問說明符之前的成員是public的;相反,如果使用class關鍵字,則這些成員是private的。

// 使用struct關鍵字定義類
struct 類名 {
// 訪問說明符之前的行爲或屬性默認是public的
public:
    //公有的行爲或屬性  
private:
    //私有的行爲或屬性
};

// 使用class關鍵字定義類
class 類名 {
// 訪問說明符之前的行爲或屬性默認是private的
public:
    //公有的行爲或屬性  
private:
    //私有的行爲或屬性
};

Note: 類定義結束後的那個分號不能省略。

(2)儘管所有成員函數都必須在類的內部聲明,但是成員函數體可以定義在類內也可以定義在類外。當我們在類的外部定義成員函數時,成員函數的定義必須與它的聲明匹配。也就是說,返回類型、參數列表和函數名都得與類內部的聲明保持一致。如果成員被聲明成常量成員函數,那麼它的定義也必須在參數列表後明確指定 const 屬性。同時,類外部定義的成員名字必須包含它所屬的類名。

class Sales_data {
// 友元聲明,後面會講
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
public:
    // 構造函數,採用參數初始化表
    Sales_data(): units_sold(0), revenue(0.0) { 
    }
    Sales_data(const std::string &s): 
           bookNo(s), units_sold(0), revenue(0.0) {
    }
    Sales_data(const std::string &s, unsigned n, double p):
           bookNo(s), units_sold(n), revenue(p*n) {
    }
    // 成員函數的聲明
    Sales_data(const std::string &book, const unsigned num,
              const double sellp, const double salep);
    Sales_data(std::istream &is);
    // 類的內部定義成員函數
    std::string isbn() const {
        return bookNo; 
    }
    Sales_data &combine(const Sales_data &rhs) {
        units_sold += rhs.units_sold;   // 把rhs的成員加到this對象的成員上
        revenue += rhs.revenue;         
        return *this;                   //返回調用該函數的對象
    }
    double avg_price() const {
        if (units_sold) {
            return revenue/units_sold;
        } else {
            return 0;
        }
    }
private:
    std::string bookNo;         // 書籍編號,隱式初始化爲空串
    unsigned units_sold = 0;    // 銷售量,顯式初始化爲0
    double sellingprice = 0.0;  // 原始價格
    double saleprice = 0.0;     // 實售價格
    double discount = 0.0;      // 折扣
    double revenue;             // 收入
};
// 類的外部定義成員函數,在類的內部已經做過聲明
Sales_data::Sales_data(std::istream &is) {
    read(is, *this);    // read函數的作用是從is中讀取一條交易信息然後
                        // 存入this對象中
}

Sales_data::Sales_data(const std::string &book, const unsigned num, const double sellp, const double salep) {
    bookNo = book;
    units_sold = num;
    sellingprice = sellp;
    saleprice = salep;
    if (sellingprice == 0) {
        discount = saleprice / sellingprice;        // 計算實際折扣
    }
}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
    Sales_data sum = lhs;
    sum.combine(rhs);
    return sum;
}

std::istream &read(std::istream &is, Sales_data &item) {
    is >> item.bookNo >> item.units_sold >> item.sellingprice >> item.saleprice;
    return is;
}

std::ostream &print(std::ostream &os, const Sales_data &item) {
    os << item.isbn() << " " << item.units_sold << " " << item.sellingprice 
    << " " << item.saleprice << " " << item.discount;
    return os;
}

(3)令成員作爲內聯函數: 在類中,常有一些規模較小的函數適用於被聲明成內聯函數,定義在類內部的成員函數是自動 inline 的。在類外定義內聯函數時,可以在類的內部把 inline 作爲聲明的一部分顯式地聲明成員函數,在類的外部用 inline 關鍵字修飾函數的定義:

inline
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
    return lhs.isbn() < rhs.isbn();
}

Note: 雖然在聲明和定義的地方同時說明 inline 是合法的,但最好只在類外部定義的地方說明 inline,這樣可以使類更容易理解。

內聯函數作用: 內聯函數在編譯的時候將不進行函數調用,編譯器將內聯函數的代碼粘貼在調用(形式上調用)處,可以提高效率。內聯函數只能是代碼很少很簡單的函數,如果一個很大很複雜的函數即使被設爲內聯,編譯器也將自動設置該函數爲非內聯。

(4)重載成員函數: 如果同一作用域的幾個函數名字相同但形參列表不同,我們稱之爲重載(overloaded)函數。和非成員函數一樣,成員函數也可以被重載,只要函數之間在參數的數量和/或類型上有所區別就行。

// 構造函數的重載
Sales_data(){}
Sales_data(std::istream &is){};
Sales_data(const std::string &s){}
Sales_data(const std::string &s, unsigned n, double p){}

Note: 不能通過函數的返回值類型不同來定義重載函數,只能通過形參列表的不同。

2. 類成員的訪問權限

C++通過 public、protected、private 三個關鍵字來控制成員變量和成員函數的訪問權限,它們分別表示公有的、受保護的、私有的,被稱爲成員訪問限定符。所謂訪問權限,就是你能不能操作該類中的成員。
在類的內部(定義類的代碼內部),無論成員被聲明爲 public、protected 還是 private,都是可以互相訪問的,沒有訪問權限的限制。
在類的外部(定義類的代碼之外),只能通過對象訪問成員,並且通過對象只能訪問 public 屬性的成員,不能訪問 private、protected 屬性的成員。

Note: C++ 中的 public、private、protected 只能修飾類的成員,不能修飾類,C++中的類沒有公有私有之分。

3. 構造函數和析構函數

(1)構造函數: 在C++中,有一種特殊的成員函數,它的名字和類名相同,沒有返回值,不需要用戶顯式調用(用戶也不能調用),而是在創建對象時自動執行。這種特殊的成員函數就是構造函數(Constructor)。
(2)構造函數的重載: 和普通成員函數一樣,構造函數是允許重載的。一個類可以有多個重載的構造函數,創建對象時根據傳遞的實參來判斷調用哪一個構造函數。構造函數的調用是強制性的,一旦在類中定義了構造函數,那麼創建對象時就一定要調用,不調用是錯誤的。如果有多個重載的構造函數,那麼創建對象時提供的實參必須和其中的一個構造函數匹配;反過來說,創建對象時只有一個構造函數會被調用。
(3)析構函數: 析構函數(Destructor)是一種特殊的成員函數,沒有返回值,不需要程序員顯式調用(程序員也沒法顯式調用),而是在銷燬對象時自動執行。構造函數的名字和類名相同,而析構函數的名字是在類名前面加一個~符號。

注意:析構函數沒有參數,不能被重載,因此一個類只能有一個析構函數。如果用戶沒有定義,編譯器會自動生成一個默認的析構函數。

// constructor.cpp
#include <iostream>
#include <string>
using namespace std;

class Sales_data {
public:
    // 構造函數
    Sales_data(): units_sold(0), revenue(0.0) {
        cout << "constructor1 executed" << endl;
    }
    // 構造函數的重載
    Sales_data(const std::string &s): 
           bookNo(s), units_sold(0), revenue(0.0) {
        cout << "constructor2 executed" << endl;
    }
    // 析構函數
    ~Sales_data() {
        cout << "destructor executed" << endl;
    }
private:
    std::string bookNo;     
    unsigned units_sold;  
    double revenue;         
};

int main(int argc, char *argv[])
{
    Sales_data sale1; 
    Sales_data sale2("978-7-121-15535-2");
    return 0;
}

這裏寫圖片描述

由運行結果知,創建對象時系統會自動調用構造函數進行初始化工作,程序即將結束時系統會自動調用析構函數進行清理工作。

4. 如何聲明並使用友元

(1)類可以允許其他類或者函數訪問它的非公有成員,方法是令其他類或者函數成爲它的友元(friend)。如果類想把一個函數作爲它的友元,只需要增加一條 friend 關鍵字開始的函數聲明語句即可。

friend Sales_data add(const Sales_data&, const Sales_data&);

友元聲明只能出現在類定義的內部,但是在類內出現的具體位置不限。友元不是類的成員也不受它所在區域訪問控制級別的約束。

(2)類還可以把其他的類定義成友元,也可以把其他類(之前定義過的)的成員函數定義成友元。此外,友元函數能定義在類的內部,這樣的函數是隱式內聯的。

class Screen {
// Window_mgr的成員可以訪問Screen類的私有部分
friend class Window_mg;
private:
    unsigned height = 0, width = 0;
    unsigned cursor = 0;
    string contents;
public:
    Screen() = default;         // 默認構造函數
    Screen(unsigned ht, unsigned wd, char c) : height(ht), width(wd),
        contents(ht * wd, c) {  }
};

class Window_mgr {
public:
    // 窗口中每個屏幕的編號
    using ScreenIndex = std::vector<Screen>::size_type;
    // 按照編號將指定的Screen重置爲空白
    void clear(ScreenIndex);
private:
    std::vector<Screen> screens{Screen(24, 80, ' ')};
};

void Window_mgr::clear(ScreenIndex i) {
    // s是一個Screen的引用,指向我們想清空的那個屏幕
    Screen &s = screens[i];
    // 將那個選定的Screen重置爲空白
    s.contents = string(s.height * s.width, ' ');
} 

如果一個類指定了友元類,則友元類的成員函數可以訪問此類包括非公有成員在內的所有成員。首先把s定義成screens vector中第i個位置上的Screen的引用,隨後利用Screen的height和width成員計算出一個新的string對象,並令其含有若干個空白字符,最後我們把這個含有很多空白的字符串賦給contents成員。如果clear不是Screen的友元,上面的代碼將無法通過編譯,因爲此時clear將不能訪問Screen的height、width和contents成員。而當Screen將Window_mgr指定爲其友元之後,Screen的所有成員對於Window_mgr就都變成可見的了。

Note: 友元關係不具有傳遞性,也就是說,如果Window_mgr有它自己的友元,則這些友元並不能理所當然地具有訪問Screen的特權,每個類負責控制自己的友元類或友元函數。

使用友元的利弊:當非成員函數確實需要訪問類的私有成員時,我們可以把它聲明成該類的友元。此時,友元可以“工作在類的內部”,像類的成員一樣訪問類的所有數據和函數。但是一旦使用不慎(比如隨意設定友元),就有可能破壞類的封裝性。

5. 類的靜態成員和靜態函數

有時候類需要它的一些成員與類本身直接相關,而不是與類的各個對象保持關聯。例如,一個銀行賬戶類可能需要一個數據成員來表示當前的基準利率。在此例中,我們希望利率與類關聯,而非與類的每個對象關聯。從實現效率的角度來看,沒必要每個對象都存儲利率信息。而且更加重要的是,一旦利率浮動,我們希望所有的對象都能使用新值。

聲明靜態成員
我們通過在成員的聲明之前加上關鍵字 static 使得其與類關聯在一起。和其他成員一樣靜態成員可以是 public 的或 private 的。靜態數據成員的類型可以是常量、引用、指針、類類型等。

class Account {
public:
    void calculate() {
        amount += amount * interestRate;
    }
    static double rate() {
        return interestRate;
    }
    static void rate(double);
private:
    std::string owner;
    double amount;
    static double interestRate;
    static double initRate();
};

static 成員變量屬於類,不屬於某個具體的對象,即使創建多個對象,也只爲靜態成員變量分配一份內存,所有對象使用的都是這份內存中的數據。當某個對象修改了靜態成員變量,也會影響到其他對象。

static 成員變量既可以通過對象來訪問,也可以通過類來訪問。

//通過類類訪問 static 成員變量
Account::interestRate = 2.735;
//通過對象來訪問 static 成員變量
Account account("hp", 10000);
account.interestRate = 2.735;
//通過對象指針來訪問 static 成員變量
Account *pa = new Account("hp", 10000);
pa -> interestRate = 2.735;

因爲靜態數據成員不屬於類的任何一個對象,所以它們並不是在創建類的對象時被定義的。這意味着它們並不是由類的構造函數初始化的。而且一般來說,我們不能在類的內部初始化靜態成員。相反地,必須在類的外部定義和初始化每個靜態成員。一個靜態數據成員只能定義一次,並且它將一直存在於程序的整個生命週期中。

我們定義靜態數據成員的方式和在類的外部定義成員函數差不多。我們需要指定對象的類型名,然後是類名、作用域運算符以及成員自己的名字:

// 定義並初始化一個靜態成員
double Account::interestRate = initRate();

這條語句定義了名爲 interestRate 的對象,該對象是類 Account 的靜態成員,其類型是 double。從類名開始,這條定義語句的剩餘部分就都位於類的作用域之內了。因此,我們可以直接使用 initRate 函數。注意,雖然 initRate 是私有的,我們也能用它初始化 interestRate。和其他成員的定義一樣,interestRate 的定義也可以訪問類的私有成員。

Note: static 成員變量的內存既不是在聲明類時分配,也不是在創建對象時分配,而是在(類外)初始化時分配。反過來說,沒有在類外初始化的 static 成員變量不能使用。static 成員變量不佔用對象的內存,而是在所有對象之外開闢內存,即使不創建對象也可以訪問。具體來說,static 成員變量和普通的 static 變量類似,都在內存分區中的全局數據區分配內存。

使用 static 成員變量而不是全局變量的優點:
(1) static 成員的名字是在類的作用域中,因此可以避免與其他類的成員或全局對象名字衝突。
(2) 可以實施封裝。static 成員可以是私有成員,而全局對象不可以。
(3) 通過閱讀程序容易看出 static 成員是與特定類關聯的,這種可見性可清晰地顯示程序員的意圖。

定義靜態成員函數
在類的內部聲明函數時需要添加 static 關鍵字,但是在類外部定義函數時就不需要了。
static 成員函數是類的組成部分但不是任何對象的組成部分,它有以下幾個特點:
(1) static 函數沒有 this 指針。
(2) static 成員函數不能被聲明爲 const (將成員函數聲明爲 const 就是承諾不會修改該函數所屬的對象)。
(3) static 成員函數也不能被聲明爲虛函數。

和其他的成員函數一樣,我們既可以在類的內部也可以在類的外部定義靜態成員函數。當在類的外部定義靜態成員時,不能重複 static 關鍵字,該關鍵字只出現在類內部的聲明語句:

void Account::rate(double newRate) {
    interestRate = newRate;
}

靜態成員函數與普通成員函數的根本區別:
(1)普通成員函數有 this 指針,可以訪問類中的任意成員。
(2)而靜態成員函數沒有 this 指針,只能訪問靜態成員(包括靜態成員變量和靜態成員函數)。
Note: 在C++中,靜態成員函數的主要目的是訪問靜態成員,靜態成員函數可以通過類來調用(一般都是這樣做),也可以通過對象來調用。

發佈了48 篇原創文章 · 獲贊 141 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章