【C++ primer】第7章 類 (2)

2020-08-14 19:09:36

Part I: The Basics
Chapter 7. Classes


7.4 類的作用域

作用域與定義在類外部的成員

函數的返回型別通常出現在函數名字的前面。當一個成員函數定義在類的外部時,返回型別中使用的名字是在類的作用域外部。因此,返回型別必須指明它是哪個類的成員。

class Window_mgr {
public:
	// add a Screen to the window and returns its index
	ScreenIndex addScreen(const Screen&);
	// other members as before
};
// return type is seen before we're in the scope of Window_mgr
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s){
	screens.push_back(s);
	return screens.size() - 1;
}

名字查詢與類的作用域

名字查詢 (name lookup) —— 查詢與所用名字匹配的過程:

  • 首先,在使用的名字的所在塊中尋找這個名字的宣告。只考慮在名字使用之前的宣告。
  • 如果名字沒有找到,查詢外層作用域。
  • 如果沒有找到宣告,程式報錯。

定義在類內的成員函數中的名字的解析方式,與上面的查詢規則有所不同。類定義分兩步處理:

  • 首先,編譯成員的宣告。
  • 直到整個類可見之後,才編譯函數體。

型別名要特殊處理

一般來說,內層作用域可以重新定義外層作用域的名字,即使該名字已在內層作用域中使用過。然而,在類中,如果一個成員使用外層作用域的名字,且這個名字是一個型別,那麼該類隨後不能重新定義該名字:

typedef double Money;
class Account {
public:
	Money balance() { return bal; }  // uses Money from the outer scope
private:
	typedef double Money; // error: cannot redefine Money
	Money bal;
	// ...
};

建議:型別名的定義通常應該出現在類的開始。這樣,任何使用這個型別的成員都出現在型別名定義之後。

成員定義中的普通塊作用域的名字查詢

成員函數體內使用的名字按如下方式解析:

  • 首先,在成員函數中尋找名字的宣告。只考慮函數體內名字使用前的宣告。
  • 如果在成員函數內沒有找到宣告,在類內尋找宣告。考慮類中的所有成員。
  • 如果在類中沒有沒有找到這個名字的宣告,在成員函數定義之前的作用域中尋找宣告。

7.5 建構函式再探

建構函式初始值列表

如果沒有在建構函式初始值列表顯式初始化一個成員,那麼在建構函式體開始執行之前,該成員預設初始化。

// legal but sloppier way to write the Sales_data constructor: no constructor initializers
Sales_data::Sales_data(const string &s, unsigned cnt, double price) {
	bookNo = s;
	units_sold = cnt;
	revenue = cnt * price;
}

上面的程式碼對數據成員完成了賦值操作。

建構函式初始值有時必不可少

有時可以忽略成員的初始化和賦值的區別,但不總是這樣。
如果成員是 const 或參照,必須初始化。
如果成員是一個沒有定義預設建構函式的類型別,必須初始化。

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

// error: ci and ri must be initialized
ConstRef::ConstRef(int ii) {
              // assignments:
	i = ii;   // ok
	ci = ii;  // error: cannot assign to a const
	ri = i;   // error: ri was never initialized
}

// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {  }

建議:使用建構函式初始值。
在很多類中,初始化和賦值的區別關乎底層效率:前者直接初始化數據成員,後者先初始化後賦值。

成員初始化的順序

成員初始化的順序與它們在類定義中出現的順序一致:第一個成員第一個初始化,然後第二個,依此類推。
在建構函式初始值列表中初始值出現的順序不會改變初始化的順序。

class X {
	int i;
	int j;
public:
	// undefined:  i is initialized before  j
	X(int val): j(val), i(j) { }
};

最好將建構函式的初始值的順序寫的與成員宣告的順序一致。如果可能的的話,避免使用成員初始化其他成員。

// In this version, the order in which i and j are initialized doesn’t matter.
X(int val): i(val), j(val) { }

預設實參和建構函式

class Sales_data {
public:
	// defines the default constructor as well as one that takes a string argument
	Sales_data(std::string s = ""): bookNo(s) { }
	// remaining constructors unchanged
	Sales_data(std::string s, unsigned cnt, double rev): bookNo(s), units_sold(cnt), revenue(rev*cnt) { }
	Sales_data(std::istream &is) { read(is, *this); }
	// remaining members as before
};

委託建構函式

C++11標準擴充套件了建構函式初始值的使用,可以定義委託建構函式。
委託建構函式 (delegating constructor) 使用它所屬類的其他建構函式執行它的初始化過程。即是說將自己的一些(或所有)的工作「委託」給其他建構函式。

class Sales_data {
public:
	// nondelegating constructor initializes members from corresponding arguments
	Sales_data(std::string s, unsigned cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt*price) { }
	// remaining constructors all delegate to another constructor
	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); }
	// other members as before
};

接受 istream& 參數的建構函式是建構函式。它委託預設建構函式,預設建構函式又委託接受三個參數的建構函式。一旦這些建構函式完成它們的工作,istream& 建構函式體開始執行。

當一個建構函式委託另一個建構函式時,受委託的建構函式的建構函式初始值列表和函數體都執行。在 Sales_data 類中,受委託的建構函式的函數體是空的。如果這個函數體包含程式碼,那麼這片程式碼會先執行,然後纔將控制權交還給委託函數的函數體。

預設建構函式的作用

定義一個使用預設建構函式進行初始化的物件:

Sales_data obj(); // oops! declares a function, not an object
Sales_data obj2;  // ok: obj2 is an object, not a function

隱式的類型別轉換

可以使用單個實參呼叫的建構函式,定義了從建構函式的形參型別到該類型別的隱式轉換。這類建構函式有時被稱爲轉換建構函式 (converting constructors)。

Sales_data 類中接受一個 string 型別和接受一個 istream 型別的建構函式都定義了從這些型別到 Sales_data 類的隱式轉換。也就是說,在需要使用 Sales_data 型別物件的地方中,可以使用 string 或 istream 物件作爲替代:

string null_book = "9-999-99999-9";
// constructs a temporary Sales_data object
// with units_sold and revenue equal to 0 and bookNo equal to null_book
item.combine(null_book);

只允許一步類型別轉換

編譯器只會自動執行一步自動型別轉換。

// error: requires two user-defined conversions:
//    (1) convert "9-999-99999-9" to string
//    (2) convert that (temporary) string to Sales_data
item.combine("9-999-99999-9");

如果想要完成上述呼叫,可以將字串顯式地轉換成 string 或 Sales_data 物件:

// ok: explicit conversion to string, implicit conversion to Sales_data
item.combine(string("9-999-99999-9"));
// ok: implicit conversion to string, explicit conversion to Sales_data
item.combine(Sales_data("9-999-99999-9"));

類型別轉換不總是有用的

// uses the istream constructor to build an object to pass to combine
item.combine(cin);

上面的隱式轉換執行了 Sales_data 中接受 istream 的建構函式。這個建構函式通過讀取標準輸入建立了一個(臨時的)Sales_data 物件。然後將這個物件傳遞給 combine。
因爲這個物件是臨時的,所以一旦 combine 完成,就無法存取它了。

抑制建構函式的隱式轉換

將建構函式宣告爲 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&);
	// remaining members as before
};

item.combine(null_book);  // error: string constructor is explicit
item.combine(cin);        // error: istream constructor is explicit

explicit 關鍵字只對可以被單個實參呼叫的建構函式有意義。

explicit 關鍵字只能使用在類內的建構函式宣告中。不能在類外部的定義中重複。

// error: explicit allowed only on a constructor declaration in a class header
explicit Sales_data::Sales_data(istream& is) {
	read(is, *this);
}

explicit 建構函式只能用於直接初始化

隱式轉換髮生的一種情況是使用拷貝形式的初始化(使用 =)。

Sales_data item1 (null_book);  // ok: direct initialization
// error: cannot use the copy form of initialization with an explicit constructor
Sales_data item2 = null_book;

顯式使用建構函式進行轉換

// ok: the argument is an explicitly constructed Sales_data object
item.combine(Sales_data(null_book));
// ok: static_cast can use an explicit constructor
item.combine(static_cast<Sales_data>(cin));

第一個呼叫直接使用建構函式。
第二個呼叫使用 static_cast 執行顯式轉換。static_cast 使用 istream 建構函式建立了一個臨時的 Sales_data 物件。

使用 explicit 建構函式的標準庫類

  • 接受單個 const char* 型別的形參的 string 建構函式不是 explicit。
  • 接受一個容量參數的 vector 建構函式是 explicit。

聚合類

聚合類 (aggregate class) 給使用者直接存取其成員的許可權,具有特殊的初始化語法方式。聚合類需滿足下述條件:

  • 所有數據成員是 public。
  • 沒有定義任何建構函式。
  • 沒有類內初始值。
  • 沒有基礎類別或 virtual 函數。
struct Data {
	int ival;
	string s;
};

可以通過提供用花括號括起來的成員初始值列表,來初始化聚合類的數據成員:

// val1.ival = 0; val1.s = string("Anna")
Data val1 = { 0, "Anna" };

初始值的順序必須與宣告中的數據成員的順序一致。

// error: can't use "Anna" to initialize ival, or 1024 to initialize s
Data val2 = { "Anna", 1024 };

與陣列元素的初始化類似,如果初始值列表中的元素個數比類中的成員數量少,則靠後的成員被值初始化。初始值列表中元素的個數不能超多類中的成員數量。

顯式地初始化類型別的物件的成員存在 3 個明顯的缺點:

  • 要求類的所有成員都是 public。
  • 類的使用者(而不是類的作者)需要正確初始化每個物件的每個成員,這給類使用者帶來了負擔。
  • 如果增加或刪除成員,所有的初始化都需要更新。

字面值常數類

數據成員都是字面值常數型別的聚合類是字面值常數類 (literal class)。如果非聚合類滿足下面 下麪的條件,也是字面值常數類:

  • 所有數據成員必須是字面值常數型別。
  • 類必須至少有一個 constexpr 建構函式。
  • 如果一個數據成員有類內初始值,則內建型別的初始值必須是常數表達式;或者如果成員是類型別,初始值必須使用該成員自己的 constexpr 建構函式。
  • 類必須使用解構函式的預設定義。

constexpr 建構函式

儘管建構函式不可能是 const,字面值常數類中的建構函式可以是 constexpr 函數。

constexpr 建構函式可以宣告成 = default(或者宣告成刪除函數)。否則,constexpr 建構函式必須滿足建構函式的要求——意味着它沒有 return 語句,且必須滿足 constexpr 函數的要求——意味着它擁有的唯一可執行語句是 return 語句。因此,constexpr 建構函式體一般是空的。

class Debug {
public:
	constexpr Debug(bool b = true): hw(b), io(b), other(b) { }
	constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) { }
	constexpr bool any() { return hw || io || other; }
	void set_io(bool b) { io = b; }
	void set_hw(bool b) { hw = b; }
	void set_other(bool b) { hw = b; }
private:
	bool hw;    // hardware errors other than IO errors
	bool io;    // IO errors
	bool other; // other errors 
};

constexpr 建構函式必須初始化每個數據成員,初始值必須使用 constexpr 建構函式或者常數表達式。

constexpr 建構函式用於生成 constexpr 物件,或者用於 constexpr 函數的參數或返回型別:

constexpr Debug io_sub(false, true, false);  // debugging IO
if (io_sub.any())  // equivalent to if(true)
	cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false); // no debugging during production
if (prod.any())    // equivalent to if(false)
	cerr << "print an error message" << endl;

7.6 static 類成員

類有時需要一些成員與類相關聯,而不是與類型別的單個物件相關聯。

宣告 static 成員

通過在成員的宣告中加上關鍵字 static 將其與類關聯起來。
static 成員可以是 public 或 private。
static 數據成員的型別可以是 const、參照、陣列、類型別等。

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 數據成員相關聯的數據。
因此,每個 Account 物件包含 2 個數據成員——owner 和 amount。

類似地,static 成員函數不與任何物件系結;它們沒有 this 指針。
因此,static 成員函數不能宣告成 const,也不能在 static 成員函數體內指向 this。這個限制既適用於 this 的顯式使用,也適用於呼叫非靜態成員時 this 的隱式使用。

使用類 static 成員

可以通過使用作用域運算子直接存取 static 成員:

double r;
r = Account::rate(); // access a static member using the scope operator

可以使用該類型別的物件、參照或者指針存取 static 成員:

Account ac1;
Account *ac2 = &ac1;
// equivalent ways to call the static member rate function
r = ac1.rate();      // through an Account object or reference
r = ac2->rate();     // through a pointer to an Account object

成員函數可以直接使用 static 成員,不用通過作用域運算子:

class Account {
public:
	void calculate() { amount += amount * interestRate; }
private:
	static double interestRate;
	// remaining members as before
};

定義 static 成員

可以在類內或類外定義 static 成員函數。當在類的外部定義 static 成員時,不需要重複 static 關鍵字。這個關鍵字只出現在類內的宣告中。

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

因爲 static 數據成員不屬於類型別物件的一部分,所以它們不是在建立類物件時定義的。因此,它們不是由類別建構函式進行初始化。而且,一般來說,不能在類內初始化 static 成員。
必須在類的外部定義和初始化每個 static 數據成員。一個 static 數據成員只能定義一次。

類似於全域性變數,static 數據成員定義在任何函數之外。因此,一旦它們被定義,它們一直存在直到程式完成。

// define and initialize a static class member
double Account::interestRate = initRate();

與其他成員的定義一樣,interestRate 也可以存取類的 private 成員。

Tip: 確保物件只定義一次的最佳方法是,將 static 數據成員的定義與類非內聯成員函數的定義放入同一檔案中。

static 數據成員的類內初始化

通常,類的 static 成員不能在類內初始化。
然而,可以爲具有 const 整型的 static 成員類內初始值,且對於 static 成員,如果它是字面值常數型別的 constexpr,那麼必須爲它提供類內初始化。初始值必須是常數表達式。這樣的成員本身就是常數表達式;它們可以用在需要常數表達式的地方。

class Account {
public:
	static double rate() { return interestRate; }
	static void rate(double);
private:
	static constexpr int period = 30;// period is a constant expression
	double daily_tbl[period];
};

如果僅在編譯器可以替換成員值的情境中使用成員,則無需單獨定義初始化的 const 或 constexpr static。但是,如果在無法替換值的情境中使用該成員,則必須爲該成員定義。
例如,如果 period 只是用於定義 daily_tbl 的維度,那麼就沒有必要在 Account 外部定義 period。但是,如果向接受 const int& 的函數傳遞 Account::period,那麼 period 必須定義。

如果在類內提供了初始值,那麼成員的定義不能在指定一個初始值。

// definition of a static member with no initializer
constexpr int Account::period; // initializer provided in the class definition

即使在類主體中初始化了 const static 數據成員,通常也應在類定義之外定義該成員。

static 成員可以使用的方式,而普通成員不能使用

  • static 數據成員可以有不完全型別。特別地,static 數據成員的型別可以是它所屬的類型別。非static 數據成員則受到限制,只能宣告成它所屬類的物件的指針或參照。
class Bar {
public:
	// ...
private:
	static Bar mem1; // ok: static member can have incomplete type
	Bar *mem2;       // ok: pointer member can have incomplete type
	Bar mem3;        // error: data members must have complete type
};
  • 可以使用 static 成員作爲預設實參。非static 數據成員不能用作預設實參,因爲它的值是其成員的物件的一部分。使用非static 數據成員作爲預設實參不會提供一個物件以便從中獲取成員值,因此會出錯。
class Screen {
public:
	// bkground refers to the static member
	// declared later in the class definition
	Screen& clear(char = bkground);
private:
	static const char bkground;
};

【C++ primer】目錄