《C++ Primer》學習筆記(七):類

定義抽象數據類型

設計Sales_data類

Sales_data 的接口應該包含以下操作:

  • 一個 isbn 成員函數,用於返回對象的 ISBN 編號
  • 一個 combine 成員函數,用於將一個 Sales_data 對象加到另一個對象上
  • 一個 add 的函數,執行兩個 Sales_data 對象的加法
  • 一個 read 函數,將數據從 istream 讀入到 Sales_data 對象中
  • 一個 print 函數, 將 Sales_data 對象的值輸出到 ostream

定義改進的Sales_data類

成員函數(member function) 的聲明必須在類的內部,定義則既可以在類的內部也可以在類的外部。
作爲接口組成部分的非成員函數,它們的定義和聲明都在類的外部。

struct Sales_data
{
    // 新成員:關於Sales_data對象的操作
    std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&);
    double avg_price() const; //返回售出書籍的平均價格
    // 數據成員
    std::string bookNo; //表示ISBN編號
    unsigned units_sold = 0; //表示這本書的銷量
    double revenue = 0.0; //這本書的總銷售收入
};
// Sales_data的非成員接口函數
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

注意:定義在類內部的函數是隱式的inline函數

成員函數通過一個名爲 this的額外的隱式參數來訪問調用它的對象。this 參數是一個常量指針,被初始化爲調用該函數的對象地址,不允許改變 this中保存的地址。

例如一個名爲total的對象,如果調用total.isbn(),則編譯器負責把total的地址傳給isbn()的隱式形參this,可以等價地認爲編譯器將該調用重寫成了如下形式:

//僞代碼:用於說明調用成員函數的實際執行過程
Sales_data::isbn(&total)

其中,調用Sales_dataisbn成員時傳入了total的地址。

默認情況下,this 的類型是指向類類型非常量版本的常量指針。例如在Sales_data成員函數中,this的類型是Sales_data *constthis也遵循初始化規則,所以默認不能把 this綁定到一個常量對象上,即不能在常量對象上調用普通的成員函數。

由於this是隱式的,想要將this聲明爲指向常量的指針就需要額外的處理方式:const成員函數。在成員函數的參數列表後面添加關鍵字 const,表示 this 是一個指向常量的指針。這類成員函數被稱作常量成員函數(const member function) 。可以將isbn的函數體想象成如下的形式:

// 僞代碼,說明隱式的this指針是如何使用的
// 下面的代碼是非法的:因爲我們不能顯式地定義自己的this指針
// 謹記此處的this是一個指向常量的指針,因爲isbn是一個常量成員
std::string Sales_data::isbn(const Sales_data *const this)
{ return this->isbn; }

注意:常量對象,以及常量對象的引用或指針都只能調用常量成員函數

編譯器分兩步處理類:首先編譯成員的聲明 ,然後才輪到成員函數體(如果有的話)。因此,成員函數體可以隨意使用類中的其他成員而無需在意這些成員出現的次序。

可以定義一個返回this對象的函數:

Sales_data& Sales_data::combine(const Sales_data &rhs)
{
    units_sold += rhs.units_sold;   // 把rhs的成員加到this對象的成員上
    revenue += rhs.revenue;
    return *this;       			// 返回調用該函數的對象
}

定義類相關的非成員函數

常常會爲類定義一些輔助函數,比如addread等。儘管這些函數定義的操作從概念上來說屬於類的接口的組成部分,但它們實際上並不屬於類本身。

一般來說,如果非成員函數是類接口的組成部分,則這些函數的聲明應該與類在同一個頭文件內。

// 輸入的交易信息包括ISBN、售出總數和售出價格
istream &read(istream &is, Sales_data &item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
    os << item.isbn() << " " << item.units_sold << " "
        << item.revenue << " " << item.avg_price();
    return os;
}
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)
{
	Sales_data sum = lhs; 	// 把lhs的數據成員拷貝給sum
	sum.combine(rhs); 		// 把rhs的數據成員加到sum當中
	return sum;
}

IO類屬於不能被拷貝的類型,因此我們只能通過引用來傳遞它們。

構造函數

類通過一個或幾個特殊的成員函數來控制其對象的初始化操作,這些函數被稱作 構造函數(constructor)。構造函數的任務是初始化類對象的數據成員,只要類的對象被創建,就會執行構造函數。構造函數沒有返回類型。

構造函數的名字和類名相同,和其他函數不一樣的是,構造函數沒有返回類型,且不能被聲明爲const 函數。當我們創建一個類的const對象時,直到構造函數完成初始化過程,對象才能真正取得其”常量“屬性。因此構造函數在const對象的構造過程中可以向其寫值。

類通過一個特殊的構造函數來控制默認初始化過料, 這個函數叫做 默認構造函數(default constructor)。默認構造函數無須任何實參。如果我們的類沒有顯式的定義構造函數,那麼編譯器就會爲我們隱式地定義一個默認構造函數。

如果一個類包含有內置類型或者複合類型的成員,則只有當這些成員全都被賦予了類內的初始值時,這個類才適合於使用合成的默認構造函數

如果類中包含一個其他類類型的成員且這個成員的類型沒有默認構造函數,那麼編譯器將無法初始化該成員。對於這樣的類來說,必須自定義默認構造函數。

在C++11中,如果類需要默認的函數行爲,可以通過在參數列表後面添加 =default來要求編譯器生成構造函數。其中 =default既可以和函數聲明一起出現在類的內部,也可以作爲定義出現在類的外部。和其他函數一樣,如果 =default在類的內部,則默認構造函數是內聯的。

Sales_data() = default;

上面的默認構造函數之所以對Sales_data有效,是因爲我們爲內置類型的數據成員提供了初始值。如果你的編譯器不支持類內初始值,那麼你的默認構造函數就應該使用構造函數初始值列表類初始化類的每個成員。

構造函數初始值列表(constructor initializer list) 負責爲新創建對象的一個或幾個數據成員賦初始值。形式是每個成員名字後面緊跟括號括起來的(或者在花括號內的)成員初始值,不同成員的初始值通過逗號分隔。

Sales_data(const std::string &s): bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):
    bookNo(s), units_sold(n), revenue(p*n) { }

拷貝、賦值和析構

編譯器能合成拷貝、賦值和析構函數,但是對於某些類來說,合成的版本無法正常工作。特別是,當類需要分配類對象之外的資源時,合成的版本通常會失效。管理動態內存的類通常不能依賴於上述操作的合成版本。

訪問控制與封裝

class Sales_data 
{
public: 	// 添加了訪問說明符
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p):
    bookNo(s), units_sold(n), revenue(p*n) { }
    Sales_data(const std::string &s): bookNo(s) { }
    Sales_data(std::istream&);
    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data&);
    
private: 	// 添加了訪問說明符
    double avg_price() const
    	{ return units_sold ? revenue/units_sold : 0; }   
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
  • 定義在 public 說明符之後的成員在整個程序內都可以被訪問。public 成員定義類的接口。
  • 定義在 private 說明符之後的成員可以被類的成員函數訪問,但是不能被使用該類的代碼訪問。private 部分封裝了類的實現細節。

使用classstruct定義類的唯一區別就是默認的訪問權限。

友元

類可以允許其他類或函數訪問它的非公有成員,方法是使用關鍵字 friend將其他類或函數聲明爲它的 友元(friend)

class Sales_data 
{
// 爲Sales_data的非成員函數所做的友元聲明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
// 其他成員及訪問說明符與之前一致
public:
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p):
    	bookNo(s), units_sold(n), revenue(p*n) { }
    Sales_data(const std::string &s): bookNo(s) { }
    Sales_data(std::istream&);
    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data&);
private:
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
// Sales_data接口的非成員組成部分的聲明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);

友元聲明只能出現在類定義的內部,但是在類內出現的具體位置不限,但是一般最好在類定義的開頭或者結尾集中聲明。友元不是類的成員也不受它所在區域訪問控制級別的約束。

友元聲明僅僅指定了訪問權限,而並非一個通常意義上的函數聲明。如果希望類的用戶能調用某個友元函數,就必須在友元聲明之外再專門對函數進行一次聲明(部分編譯器沒有該限制,但是即使編譯器沒有該限制,最好也還是提供一個獨立的函數聲明,以提高可移植性)。

除了普通函數,類還可以把其他類或其他類的成員函數聲明爲友元。此外, 友元函數能定義在類的內部, 這樣的函數是隱式內聯的。友元類的成員函數可以訪問此類包括非公有成員在內的所有成員。

class Screen 
{
    // Window_mgr的成員可以訪問Screen類的私有部分
    friend class Window_mgr;
    // Screen類的剩餘部分
};

要想令某個成員函數作爲友元,必須仔細組織程序的結構以滿足聲明和定義的彼此依賴關係。例如對於:

class Screen
{
	//Window_mgr::clear必須在Screen類之前被聲明
	friend void Window_mgr::clear(ScreenIndex);
	//Screen類的剩餘部分
}

必須按照如下方式設計程序:

  • 首先定義Window_mgr類,其中聲明clear函數,但是不能定義它。在clear使用Screen的成員之前必須先聲明Screen
  • 接下來定義Screen,包括對於clear的友元聲明。
  • 最後定義clear,此時它纔可以使用Screen的成員。

友元關係不存在傳遞性。每個類負責控制自己的友元類或友元函數。

友元函數可以直接定義在類的內部,這種函數是隱式內聯的。但是必須在類外部提供相應聲明令函數可見。

把其他類的成員函數聲明爲友元時,必須明確指定該函數所屬的類名。

如果類想把一組重載函數聲明爲友元,需要對這組函數中的每一個分別聲明。

類的其他特性

除了定義數據和函數成員之外,類還可以自定義某種類型在類中的別名。由類定義的類型名字和其他成員一樣存在訪問限制,可以是public或者private中的一種。

class Screen 
{
public:
	typedef std::string::size_type pos;
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};
// *******************

class Screen 
{
public:
    // 使用類型別名等價地聲明一個類型名字
    using pos = std::string::size_type;
    // 其他成員與之前的版本一致
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};

與普通成員不同,定義類型的成員必須先定義後使用。因此類型成員通常出現在類開始的地方

定義在類內部的成員函數是自動內聯的。如果需要顯式聲明內聯成員函數,建議只在類外部定義的位置說明 inline,這樣可以使類更容易理解。和我們在頭文件中定義inline函數的原因一樣,inline成員函數也應該與相應的類定義在同一個頭文件中。

如果我們希望在一個類的const成員函數中仍能夠修改類的某個數據成員,可以通過在變量的聲明中加入mutable關鍵字來做到這一點。

class Screen 
{
public:
    void some_member() const;
private:
    mutable size_t access_ctr;  // 即使在一個const對象內也能被修改
    // 其他成員與之前的版本一致
};
void Screen::some_member() const
{
    ++access_ctr;   // 保存一個計數值,用於記錄成員函數被調用的次數
    // 該成員需要完成的其他工作
}

當我們提供一個類內初始值時,必須以符號=或者花括號表示。

一個const成員函數如果以引用的形式返回*this,那麼它的返回類型將是常量引用。

通過區分成員函數是否爲 const 的,可以對其進行重載。因爲非常量版本的函數對於常量對象是不可用的,在常量對象上只能調用const版本的函數;在非常量對象上,儘管兩個版本都能調用,但顯然會選擇非常量版本,因爲是一個更好的匹配。

class Screen 
{
public:
    // 根據對象是否是const重載了display函數
    Screen &display(std::ostream &os)
    { do_display(os); return *this; }
    const Screen &display(std::ostream &os) const
    { do_display(os); return *this; }
    
private:
    // 該函數負責顯示Screen的內容
    void do_display(std::ostream &os) const
    { os << contents; }
    // 其他成員與之前的版本一致
};

Screen myScreen(5,3);
const Screen blank(5, 3);
myScreen.set('#').display(cout);    // 調用非常量版本
blank.display(cout);    			// 調用常量版本

構造函數再探

如果成員是 const、引用,或者是某種未定義默認構造函數的類類型,必須在初始值列表中將其初始化。最好令構造函數初始值的順序與成員聲明的順序一致,並且儘量避免使用某些成員初始化其他成員。

class ConstRef
{
public:
    ConstRef(int ii);
private:
    int i;
    const int ci;
    int &ri;
};

// 正確:顯式地初始化引用和const成員
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }

如果一個構造函數爲所有參數都提供了默認實參,則它實際上也定義了默認構造函數。

委託構造函數

委託構造函數使用它所屬類的其他構造函數執行它自己的初始化過程,或者說它把它自己的一些(或者全部)職責委託給了其他構造函數。

class Sales_data
{
public:
    // 非委託構造函數使用對應的實參初始化成員
    Sales_data(std::string s, unsigned cnt, double price):
    	bookNo(s), units_sold(cnt), revenue(cnt*price) {  }
    // 其餘構造函數全都委託給另一個構造函數
    Sales_data():Sales data("", 0, 0) {  }
	Sales_data(std::string s):Sales_data(s, 0, 0) {  }
	Sales_data(std::istream &is):Sales data() { read(is, *this); }
    // 其他成員與之前的版本一致
}

默認構造函數的作用

對象被默認初始化或者值初始化時自動執行默認構造函數。

默認初始化在以下情況下發生

  • 在塊作用域內不使用初始值定義非靜態變量或數組。
  • 類本身含有類類型的成員且使用合成默認構造函數。
  • 類類型的成員沒有在構造函數初始值列表中顯式初始化。

值初始化在以下情況下發生

  • 數組初始化時提供的初始值數量少於數組大小。
  • 不使用初始值定義局部靜態變量。
  • 通過 T() 形式(T 爲類型)的表達式顯式地請求值初始化。

如果定義了其他構造函數,那麼最好也提供一個默認構造函數

Sales_data obj();   // 錯誤:聲明瞭一個函數而非對象
Sales_data obj2;    // 正確:obj2是一個對象而非函數

隱式的類類型轉換

如果構造函數只接受一個實參,則它實際上定義了轉換爲此類類型的隱式轉換機制。這種構造函數被稱爲轉換構造函數(converting constructor)。

Sales_data類中,接受string的構造函數和接受istream的構造函數分別定義了stringistream兩種類型向Sales_data隱式轉換的規則。也就是說,在需要使用Sales_data的地方,可以使用string或者istream作爲替代:

string null_book = "9-999-99999-9";
// 構造一個臨時的Sales_data對象
// 該對象的units_sold和revenue等於0,bookNo等於null_book
item.combine(null_book);
//編譯器用給定的string自動創建了一個Sales_data對象,新生成的這個(臨時)
//Sales_data對象被傳遞給combine。因爲combine的參數是一個常量引用,所
//以可以給該參數傳遞一個臨時量。

編譯器只允許一步類類型轉換,下面的代碼隱式地使用了兩種轉換規則,所以是錯誤的:

// 錯誤:需要用戶定義的兩種轉換。
// (1) 把"9-999-99999-9"轉換成string
// (2) 再把這個(臨時的)string轉換成Sales_data
item.combine("9-999-99999-9");
// 正確:顯式地轉換成string,隱式地轉換成Sales_data
item.combine(string("9-999-99999-9"));
// 正確: 隱式地轉換成string,顯式地轉換成Sales_data
item.combine(Sales_data("9-999-99999-9"));

可以通過將構造函數聲明爲 explicit的來抑制構造函數定義的隱式轉換。關鍵字explicit只對一個實參的構造函數有效,因爲有多個實參的構造函數不能用於執行隱式轉換,所以無須將這些構造函數指定爲explicit的。

只需要在類內聲明構造函數時使用關鍵字explicit,在類外部定義時不應重複

class Sales_data
{
public:
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p):
        bookNo(s), units_sold(n), revenue(p*n) { }
    explicit Sales_data(const std::string &s): bookNo(s) { }
    explicit Sales_data(std::istream&);
    // 其他成員與之前的版本一致
};
Sales_data item1 (null_book);   // 正確: 直接初始化
// 錯誤:不能將explicit構造函數用於拷貝形式的初始化過程
Sales_data item2 = null_book;
//執行拷貝初始化時(使用 =)會發生隱式轉換,所以 explicit 構造函數只能用於直接初始化。

我們用過的一些標準庫中的類含有單參數的構造函數:

  • 接受一個單參數的const char *string構造函數不是explicit的。
  • 接受一個容量參數的vector構造函數是explicit的。

聚合類

聚合類滿足如下條件:

  • 所有成員都是 public 的。
  • 沒有定義任何構造函數。
  • 沒有類內初始值。
  • 沒有基類。
  • 沒有虛函數。
struct Data
{
    int ival;
    string s;
};

可以使用一個用花括號包圍的成員初始值列表初始化聚合類的數據成員。初始值順序必須與聲明順序一致。如果初始值列表中的元素個數少於類的成員個數,則靠後的成員被值初始化。初始值列表的元素個數絕對不能超過類的成員數量。

類的靜態成員

有時候類需要它的一些成員與類本身直接相關,而不是與類的各個對象保持關聯。我們可以通過在成員的聲明前加上關鍵字static使得其與類關聯在一起。舉個例子,定義一個類,用於表示銀行的賬戶記錄:

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();
};

類的靜態成員存在於任何對象之外,對象中不包含與靜態成員相關的數據。
類似的, 靜態成員函數也不與任何對象綁定在一起,它們不包含this指針。

由於靜態成員不與任何對象綁定,因此靜態成員函數不能聲明爲 const的(函數聲明爲const實際上是用於修飾this的底層const屬性,因爲沒有this指針,所以也不能聲明函數爲const),也不能在靜態成員函數內使用 this指針。

可以通過作用域運算符直接訪問靜態成員,也可以使用類的對象、引用或者指針來訪問靜態成員:

double r;
r = Account::rate(); // 使用作用城運算符訪問靜態成員

Account ac1;
Account *ac2 = &ac1;
// 調用靜態成員函數rate的等價形式
r = ac1.rate(); 	// 通過Account的對象或引用
r = ac2->rate(); 	// 通過指向Account對象的指針

class Account
{
public:
	//成員函數不同通過作用域運算符就能直接使用靜態成員
    void calculate() { amount += amount * interestRate; }
private:
    static double interestRate;
    // 其他成員與之前的版本一致
};

在類外部定義靜態成員時,不能重複 static關鍵字,其只能用於類內部的聲明語句。

由於靜態數據成員不屬於類的任何一個對象,因此它們並不是在創建類對象時被定義的。通常情況下,不應該在類內部初始化靜態成員。而必須在類外部定義並初始化每個靜態成員。一個靜態成員只能被定義一次。一旦它被定義,就會一直存在於程序的整個生命週期中。

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

要想確保對象只定義一次,最好的辦法是把靜態數據成員的定義與其他非內聯函數的定義放在同一個文件中

儘管在通常情況下,不應該在類內部初始化靜態成員。但是可以爲靜態成員提供 const 整數類型的類內初始值,不過要求靜態成員必須是字面值常量類型的 constexpr。即使一個常量靜態數據成員在類內部被初始化了,通常情況下也應該在類的外部定義一下該成員。

class Account
{
public:
    static double rate() { return interestRate; }
    static void rate(double);
private:
    static constexpr int period = 30;  // period是常量表達式
    double daily_tbl[period];
};

特別的, 靜態數據成員的類型可以就是它所屬的類類型。而非靜態數據成員則受到限制,只能聲明成它所屬類的指針或引用:

class Bar
{
public:
	// ...
private:
    static Bar mem1;   	// 正確:靜態成員可以是不完全類型
    Bar *mem2;    		// 正確:指針成員可以是不完全類型
    Bar mem3;   		// 錯誤:數據成員必須是完全類型
};

靜態成員和普通成員的另外一個區別是我們可以使用靜態成員作爲默認實參。

class Screen
{
public:
    // bkground表示一個在類中稍後定義的靜態成員
    Screen& clear(char = bkground);
private:
    static const char bkground;
};

練習

  1. 在網站 http://www.informit.com/title/032174113 上,第1章的代碼目錄包含了頭文件 Sales_item.h。將它拷貝到你自己的工作目錄中。用它編寫一個程序,讀取一組書籍銷售記錄,將每條記錄打印到標準輸出上。

Sales_item.h

/*
* This file contains code from "C++ Primer, Fifth Edition", by Stanley B.
* Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the
* copyright and warranty notices given in that book:
*
* "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo."
*
*
* "The authors and publisher have taken care in the preparation of this book,
* but make no expressed or implied warranty of any kind and assume no
* responsibility for errors or omissions. No liability is assumed for
* incidental or consequential damages in connection with or arising out of the
* use of the information or programs contained herein."
*
* Permission is granted for this code to be used for educational purposes in
* association with the book, given proper citation if and when posted or
* reproduced.Any commercial use of this code requires the explicit written
* permission of the publisher, Addison-Wesley Professional, a division of
* Pearson Education, Inc. Send your request for permission, stating clearly
* what code you would like to use, and in what specific way, to the following
* address:
*
*     Pearson Education, Inc.
*     Rights and Permissions Department
*     One Lake Street
*     Upper Saddle River, NJ  07458
*     Fax: (201) 236-3290
*/

/* This file defines the Sales_item class used in chapter 1.
* The code used in this file will be explained in
* Chapter 7 (Classes) and Chapter 14 (Overloaded Operators)
* Readers shouldn't try to understand the code in this file
* until they have read those chapters.
*/

#ifndef SALESITEM_H
// we're here only if SALESITEM_H has not yet been defined 
#define SALESITEM_H

#include "Version_test.h" 

// Definition of Sales_item class and related functions goes here
#include <iostream>
#include <string>

class Sales_item {
	// these declarations are explained section 7.2.1, p. 270 
	// and in chapter 14, pages 557, 558, 561
	friend std::istream& operator>>(std::istream&, Sales_item&);
	friend std::ostream& operator<<(std::ostream&, const Sales_item&);
	friend bool operator<(const Sales_item&, const Sales_item&);
	friend bool
		operator==(const Sales_item&, const Sales_item&);
public:
	// constructors are explained in section 7.1.4, pages 262 - 265
	// default constructor needed to initialize members of built-in type
#if defined(IN_CLASS_INITS) && defined(DEFAULT_FCNS)
	Sales_item() = default;
#else
	Sales_item() : units_sold(0), revenue(0.0) { }
#endif
	Sales_item(const std::string &book) :
		bookNo(book), units_sold(0), revenue(0.0) { }
	Sales_item(std::istream &is) { is >> *this; }
public:
	// operations on Sales_item objects
	// member binary operator: left-hand operand bound to implicit this pointer
	Sales_item& operator+=(const Sales_item&);

	// operations on Sales_item objects
	std::string isbn() const { return bookNo; }
	double avg_price() const;
	// private members as before
private:
	std::string bookNo;      // implicitly initialized to the empty string
#ifdef IN_CLASS_INITS
	unsigned units_sold = 0; // explicitly initialized
	double revenue = 0.0;
#else
	unsigned units_sold;
	double revenue;
#endif
};

// used in chapter 10
inline
bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs)
{
	return lhs.isbn() == rhs.isbn();
}

// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);

inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
	// must be made a friend of Sales_item
	return lhs.units_sold == rhs.units_sold &&
		lhs.revenue == rhs.revenue &&
		lhs.isbn() == rhs.isbn();
}

inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
	return !(lhs == rhs); // != defined in terms of operator==
}

// assumes that both objects refer to the same ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}

// assumes that both objects refer to the same ISBN
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
	Sales_item ret(lhs);  // copy (|lhs|) into a local object that we'll return
	ret += rhs;           // add in the contents of (|rhs|) 
	return ret;           // return (|ret|) by value
}

std::istream&
operator>>(std::istream& in, Sales_item& s)
{
	double price;
	in >> s.bookNo >> s.units_sold >> price;
	// check that the inputs succeeded
	if (in)
		s.revenue = s.units_sold * price;
	else
		s = Sales_item();  // input failed: reset object to default state
	return in;
}

std::ostream&
operator<<(std::ostream& out, const Sales_item& s)
{
	out << s.isbn() << " " << s.units_sold << " "
		<< s.revenue << " " << s.avg_price();
	return out;
}

double Sales_item::avg_price() const
{
	if (units_sold)
		return revenue / units_sold;
	else
		return 0;
}
#endif

Version_test.h

/*
* This file contains code from "C++ Primer, Fifth Edition", by Stanley B.
* Lippman, Josee Lajoie, and Barbara E. Moo, and is covered under the
* copyright and warranty notices given in that book:
*
* "Copyright (c) 2013 by Objectwrite, Inc., Josee Lajoie, and Barbara E. Moo."
*
*
* "The authors and publisher have taken care in the preparation of this book,
* but make no expressed or implied warranty of any kind and assume no
* responsibility for errors or omissions. No liability is assumed for
* incidental or consequential damages in connection with or arising out of the
* use of the information or programs contained herein."
*
* Permission is granted for this code to be used for educational purposes in
* association with the book, given proper citation if and when posted or
* reproduced. Any commercial use of this code requires the explicit written
* permission of the publisher, Addison-Wesley Professional, a division of
* Pearson Education, Inc. Send your request for permission, stating clearly
* what code you would like to use, and in what specific way, to the following
* address:
*
* 	Pearson Education, Inc.
* 	Rights and Permissions Department
* 	One Lake Street
* 	Upper Saddle River, NJ  07458
* 	Fax: (201) 236-3290
*/

#ifndef VERSION_TEST_H
#define VERSION_TEST_H

/* As of the first printing of C++ Primer, 5th Edition (July 2012),
* the Microsoft Complier did not yet support a number of C++ 11 features.
*
* The code we distribute contains both normal C++ code and
* workarounds for missing features.  We use a series of CPP variables to
* determine whether a given features is implemented in a given release
* of the MS compiler.  The base version we used to test the code in the book
* is Compiler Version 17.00.50522.1 for x86.
*
* When new releases are available we will update this file which will
* #define the features implmented in that release.
*/

#if _MSC_FULL_VER == 170050522 || _MSC_FULL_VER == 170050727 
// base version, future releases will #define those features as they are
// implemented by Microsoft

/* Code in this delivery use the following variables to control compilation

Variable tests           C++ 11 Feature
CONSTEXPR_VARS            constexpr variables
CONSTEXPR_FCNS            constexpr functions
CONSTEXPR_CTORS           constexpr constructors and other member functions
DEFAULT_FCNS              = default
DELETED_FCNS              = delete
FUNC_CPP                  __func__ local static
FUNCTION_PTRMEM           function template with pointer to member function
IN_CLASS_INITS            in class initializers
INITIALIZER_LIST          library initializer_list<T> template
LIST_INIT                 list initialization of ordinary variables
LROUND                    lround function in cmath
NOEXCEPT                  noexcept specifier and noexcept operator
SIZEOF_MEMBER             sizeof class_name::member_name
TEMPLATE_FCN_DEFAULT_ARGS default template arguments for function templates
TYPE_ALIAS_DECLS          type alias declarations
UNION_CLASS_MEMS          unions members that have constructors or copy control
VARIADICS                 variadic templates
*/
#endif  // ends compiler version check

#ifndef LROUND
inline long lround(double d)
{
	return (d >= 0) ? long(d + 0.5) : long(d - 0.5);
}
#endif

#endif  // ends header guard
#include <iostream>
#include "Sales_item.h"

int main()
{
	Sales_item book;
	std::cout << "Please enter the sales record:" << std::endl;
	while (std::cin >> book){
		std::cout << "ISBN, sales numbers, sales volume and average selling price are respectively "
			<< book << std::endl;
	}

	system("pause");
	return 0;
}

在這裏插入圖片描述

  1. 編寫一個名爲 Person 的類,使其表示人員的姓名和地址。使用 string 對象存放這些元素。在你的Person類中提供一些操作使其能夠返回姓名和地址。添加讀取和打印 Person 對象的操作。併爲你的 Person 類添加正確的構造函數。
#ifndef SALESITEM_H
#define SALESITEM_H

#include <string>

class Person{
    friend std::istream& operator>>(std::istream&, Person&);
    friend std::ostream& operator<<(std::ostream&, const Person&);

private:
    std::string name; // 姓名
    std::string address; //地址

public:
    std::string get_name() const {return name;}
    std::string get_address() const {return address;}

    //構造函數
    Person() = default;
    Person(const std::string& in_name, const  std::string& in_address):name(in_name), address(in_address) {}
    Person(std::istream &in) {in >> *this;}


};

std::istream& operator>>(std::istream& in, Person& p)
{
    in >> p.name >> p.address;

    if(!in){//input failed: reset object to default state
        p = Person();
    }

    return in;
}

std::ostream& operator<<(std::ostream& out, const Person& p)
{
    out << p.name << p.address;
    return out;
}

#endif
  1. 編寫你自己的 Screen 類型。給你的Screen類添加三個構造函數:一個默認構造函數;另一個構造函數接受寬和高的值,然後將 contents初始化成給定數量的空白;第三個構造函數接受寬和高的值以及一個字符,該字符作爲初始化後屏幕的內容。給你自己的Screen 類添加 movesetdisplay函數。
#include <string>
#include <iostream>

class Screen {
public:
	typedef std::string::size_type pos;
#if defined(IN_CLASS_INITS) && defined(DEFAULT_FCNS)
	Screen() = default;  // needed because Screen has another constructor
#else
	Screen(): cursor(0), height(0), width(0) { }
#endif
	// cursor initialized to 0 by its in-class initializer
	Screen(pos ht, pos wd, char c) : height(ht), width(wd),
		contents(ht * wd, c) { }
	
	Screen(pos ht = 0, pos wd = 0) :
		cursor(0), height(ht), width(wd), contents(ht * wd, ' ') { }

	char get() const              // get the character at the cursor
	{
		return contents[cursor];
	}       // implicitly inline

	inline char get(pos ht, pos wd) const; // explicitly inline
	Screen &clear(char = bkground);
private:
	static const char bkground = ' ';
public:
	Screen &move(pos r, pos c);      // can be made inline later
	Screen &set(char);
	Screen &set(pos, pos, char);
	// other members as before
	// display overloaded on whether the object is const or not
	Screen &display(std::ostream &os)
	{
		do_display(os); return *this;
	}
    
	const Screen &display(std::ostream &os) const
	{
		do_display(os); return *this;
	}
private:
	// function to do the work of displaying a Screen
	void do_display(std::ostream &os) const { os << contents; }
	// other members as before
private:
#ifdef IN_CLASS_INITS
	pos cursor = 0;
	pos height = 0, width = 0;
#else
	pos cursor;
	pos height, width;
#endif
	std::string contents;
};

Screen &Screen::clear(char c)
{
	contents = std::string(height*width, c);
	return *this;
}

inline                   // we can specify inline on the definition
Screen &Screen::move(pos r, pos c)
{
	pos row = r * width; // compute the row location
	cursor = row + c;    // move cursor to the column within that row
	return *this;        // return this object as an lvalue
}

char Screen::get(pos r, pos c) const // declared as inline in the class
{
	pos row = r * width;      // compute row location
	return contents[row + c]; // return character at the given column
}

inline Screen &Screen::set(char c)
{
	contents[cursor] = c; // set the new value at the current cursor location
	return *this;         // return this object as an lvalue
}
inline Screen &Screen::set(pos r, pos col, char ch)
{
	contents[r*width + col] = ch;  // set specified location to given value
	return *this;                  // return this object as an lvalue
}
  1. 定義一對類 XY,其中 X 包含一個指向 Y 的指針,而Y 包含一個類型爲 X 的對象。
class X;//前向聲明

class Y{
    X* x;	
};

class X{
    Y y;
};

X的聲明稱爲前向聲明,它向程序中引入了名字 X 並且指明 X是一種類類型。對於類型 X 來說,此時我們已知它是一個類類型,但是不清楚它到底包含哪些成員,所以它是一個不完全類型。我們可以定義指向不完全類型的指針,但是無法創建不完全類型的對象。

  1. 解釋下面代碼的含義,說明其中的 TypeinitVal分別使用了哪個定義。如果代碼存在錯誤,嘗試修改它。
typedef string Type;
Type initVal(); 
class Exercise {
public:
    typedef double Type;
    Type setVal(Type);
    Type initVal(); 
private:
    int val;
};
Type Exercise::setVal(Type parm) { 
    val = parm + initVal();     
    return val;
}
  • Exercise類的內部,函數 setvalinitval 用到的 Type都是 Exercise內部聲明的類型別名,對應的實際類型是double
  • Exercise 類的外部,定義 Exercise::setVal函數時形參類型 Type用的是 Exercise 內部定義的別名,對應 double;返回類型 Type用的是全局作用域的別名,對應 string。使用的 initVal 函數是 Exercise類內定義的版本。

編譯上述程序時在 setVal 的定義處發生錯誤,此處定義的函數形參類型是 double、返回值類型是 string,而類內聲明的同名函數形參類型是 double、返回值類型也是 double,二者無法匹配。

爲了使得setVal的定義和類內的聲明的返回值類型匹配,需要添加作用域運算符:

//在返回值前添加作用域運算符
Exercise::Type Exercise::setVal(Type parm){
	val=parm+initVal();		//此處使用的是類內的initVal函數
	return val;
}
  1. 如果接受string的構造函數和接受istream&的構造函數都使用默認實參,這種行爲合法嗎?如果不,爲什麼?

不合法。當一個構造函數的全部實參都設置了默認參數時,這個構造函數就同時具備了默認構造函數的功能。那麼如果接受string的構造函數和接受istream&的構造函數都使用默認實參,並且用戶不提供任何實參創建該類的對象時,就會出現二義性,編譯器無法判斷哪個構造函數更好。

  1. 下面的靜態數據成員的聲明和定義有錯誤嗎?請解釋原因。
//example.h
class Example {
public:
	static double rate = 6.5;
	static const int vecSize = 20;
	static vector<double> vec(vecSize);
};

//example.c
#include "example.h"
double Example::rate;
vector<double> Example::vec;

C++ 類中的static成員的初始化和特點

類中各種類型成員的初始化方式

  • 引用和常量:必須通過構造函數的參數列表的方式初始化。
  • 靜態變量: 必須在類定義體外進行初始化與定義
  • 靜態常量:可以直接在類中初始化,也可以在類中聲明,在類定義體外進行定義

在類的內部,ratevec 的初始化是錯誤的,因爲除了靜態常量成員之外,其他靜態成員不能在類的內部初始化。

另外,example.c 文件的兩條語句也是錯誤的,因爲在這裏我們必須給出靜態成員的初始值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章