Part I: The Basics
Chapter 7. Classes
作用域與定義在類外部的成員
函數的返回型別通常出現在函數名字的前面。當一個成員函數定義在類的外部時,返回型別中使用的名字是在類的作用域外部。因此,返回型別必須指明它是哪個類的成員。
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;
// ...
};
建議:型別名的定義通常應該出現在類的開始。這樣,任何使用這個型別的成員都出現在型別名定義之後。
成員定義中的普通塊作用域的名字查詢
成員函數體內使用的名字按如下方式解析:
如果沒有在建構函式初始值列表顯式初始化一個成員,那麼在建構函式體開始執行之前,該成員預設初始化。
// 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 建構函式的標準庫類
聚合類 (aggregate class) 給使用者直接存取其成員的許可權,具有特殊的初始化語法方式。聚合類需滿足下述條件:
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 個明顯的缺點:
數據成員都是字面值常數型別的聚合類是字面值常數類 (literal class)。如果非聚合類滿足下面 下麪的條件,也是字面值常數類:
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;
類有時需要一些成員與類相關聯,而不是與類型別的單個物件相關聯。
宣告 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 成員可以使用的方式,而普通成員不能使用
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
};
class Screen {
public:
// bkground refers to the static member
// declared later in the class definition
Screen& clear(char = bkground);
private:
static const char bkground;
};