第7章 類
類的基本思想是數據抽象(data abstraction)和封裝(encapsulation)。數據抽象是一種依賴於接口(interface)和實現(implementation)分離的編程及設計技術。類的接口包括用戶所能執行的操作;類的實現包括類的數據成員、負責接口實現的函數體以及其他私有函數。
定義抽象數據類型(Defining Abstract Data Types)
設計Sales_data類(Designing the Sales_data Class)
類的用戶是程序員,而非應用程序的最終使用者。
定義改進的Sales_data類(Defining the Revised Sales_data Class)
成員函數(member function)的聲明必須在類的內部,定義則既可以在類的內部也可以在類的外部。定義在類內部的函數是隱式的內聯函數。
struct Sales_data { // new members: operations on Sales_data objects std::string isbn() const { return bookNo; } Sales_data& combine(const Sales_data&); double avg_price() const; // data members std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; };
成員函數通過一個名爲this
的隱式額外參數來訪問調用它的對象。this
參數是一個常量指針,被初始化爲調用該函數的對象地址。在函數體內可以顯式使用this
指針。
total.isbn() // pseudo-code illustration of how a call to a member function is translated Sales_data::isbn(&total) std::string isbn() const { return this->bookNo; } std::string isbn() const { return bookNo; }
默認情況下,this
的類型是指向類類型非常量版本的常量指針。this
也遵循初始化規則,所以默認不能把this
綁定到一個常量對象上,即不能在常量對象上調用普通的成員函數。
C++允許在成員函數的參數列表後面添加關鍵字const
,表示this
是一個指向常量的指針。使用關鍵字const
的成員函數被稱作常量成員函數(const member function)。
// pseudo-code illustration of how the implicit this pointer is used // this code is illegal: we may not explicitly define the this pointer ourselves // note that this is a pointer to const because isbn is a const member std::string Sales_data::isbn(const Sales_data *const this) { return this->isbn; }
常量對象和指向常量對象的引用或指針都只能調用常量成員函數。
類本身就是一個作用域,成員函數的定義嵌套在類的作用域之內。編譯器處理類時,會先編譯成員聲明,再編譯成員函數體(如果有的話),因此成員函數可以隨意使用類的其他成員而無須在意這些成員的出現順序。
在類的外部定義成員函數時,成員函數的定義必須與它的聲明相匹配。如果成員函數被聲明爲常量成員函數,那麼它的定義也必須在參數列表後面指定const
屬性。同時,類外部定義的成員名字必須包含它所屬的類名。
double Sales_data::avg_price() const { if (units_sold) return revenue / units_sold; else return 0; }
可以定義返回this
對象的成員函數。
Sales_data& Sales_data::combine(const Sales_data &rhs) { units_sold += rhs.units_sold; // add the members of rhs into revenue += rhs.revenue; // the members of 'this' object return *this; // return the object on which the function was called }
定義類相關的非成員函數(Defining Nonmember Class-Related Functions)
類的作者通常會定義一些輔助函數,儘管這些函數從概念上來說屬於類接口的組成部分,但實際上它們並不屬於類本身。
// input transactions contain ISBN, number of copies sold, and sales price 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; }
如果非成員函數是類接口的組成部分,則這些函數的聲明應該與類放在同一個頭文件中。
一般來說,執行輸出任務的函數應該儘量減少對格式的控制。
構造函數(Constructors)
類通過一個或幾個特殊的成員函數來控制其對象的初始化操作,這些函數被稱作構造函數。只要類的對象被創建,就會執行構造函數。
構造函數的名字和類名相同,沒有返回類型,且不能被聲明爲const
函數。構造函數在const
對象的構造過程中可以向其寫值。
struct Sales_data { // constructors added Sales_data() = default; 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) { } Sales_data(std::istream &); // other members as before };
類通過默認構造函數(default constructor)來控制默認初始化過程,默認構造函數無須任何實參。
如果類沒有顯式地定義構造函數,則編譯器會爲類隱式地定義一個默認構造函數,該構造函數也被稱爲合成的默認構造函數(synthesized default constructor)。對於大多數類來說,合成的默認構造函數初始化數據成員的規則如下:
- 如果存在類內初始值,則用它來初始化成員。
- 否則默認初始化該成員。
某些類不能依賴於合成的默認構造函數。
- 只有當類沒有聲明任何構造函數時,編譯器纔會自動生成默認構造函數。一旦類定義了其他構造函數,那麼除非再顯式地定義一個默認的構造函數,否則類將沒有默認構造函數。
- 如果類包含內置類型或者複合類型的成員,則只有當這些成員全部存在類內初始值時,這個類才適合使用合成的默認構造函數。否則用戶在創建類的對象時就可能得到未定義的值。
- 編譯器不能爲某些類合成默認構造函數。例如類中包含一個其他類類型的成員,且該類型沒有默認構造函數,那麼編譯器將無法初始化該成員。
在C++11中,如果類需要默認的函數行爲,可以通過在參數列表後面添加=default
來要求編譯器生成構造函數。其中=default
既可以和函數聲明一起出現在類的內部,也可以作爲定義出現在類的外部。和其他函數一樣,如果=default
在類的內部,則默認構造函數是內聯的。
Sales_data() = default;
構造函數初始值列表(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) { }
當某個數據成員被構造函數初始值列表忽略時,它會以與合成默認構造函數相同的方式隱式初始化。
// has the same behavior as the original constructor defined above Sales_data(const std::string &s): bookNo(s), units_sold(0), revenue(0) { }
構造函數不應該輕易覆蓋掉類內初始值,除非新值與原值不同。如果編譯器不支持類內初始值,則所有構造函數都應該顯式初始化每個內置類型的成員。
拷貝、賦值和析構(Copy、Assignment,and Destruction)
編譯器能合成拷貝、賦值和析構函數,但是對於某些類來說合成的版本無法正常工作。特別是當類需要分配類對象之外的資源時,合成的版本通常會失效。
訪問控制與封裝(Access Control and Encapsulation)
使用訪問說明符(access specifier)可以加強類的封裝性:
- 定義在
public
說明符之後的成員在整個程序內都可以被訪問。public
成員定義類的接口。 - 定義在
private
說明符之後的成員可以被類的成員函數訪問,但是不能被使用該類的代碼訪問。private
部分封裝了類的實現細節。
class Sales_data { public: // access specifier added 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: // access specifier added double avg_price() const { return units_sold ? revenue/units_sold : 0; } std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; };
一個類可以包含零或多個訪問說明符,每個訪問說明符指定了接下來的成員的訪問級別,其有效範圍到出現下一個訪問說明符或類的結尾處爲止。
使用關鍵字struct
定義類時,定義在第一個訪問說明符之前的成員是public
的;而使用關鍵字class
時,這些成員是private
的。二者唯一的區別就是默認訪問權限不同。
友元(Friends)
類可以允許其他類或函數訪問它的非公有成員,方法是使用關鍵字friend
將其他類或函數聲明爲它的友元。
class Sales_data { // friend declarations for nonmember Sales_data operations added 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&); // other members and access specifiers as before 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; }; // declarations for nonmember parts of the Sales_data interface Sales_data add(const Sales_data&, const Sales_data&); std::istream &read(std::istream&, Sales_data&); std::ostream &print(std::ostream&, const Sales_data&);
友元聲明只能出現在類定義的內部,具體位置不限。友元不是類的成員,也不受它所在區域訪問級別的約束。
通常情況下,最好在類定義開始或結束前的位置集中聲明友元。
封裝的好處:
- 確保用戶代碼不會無意間破壞封裝對象的狀態。
- 被封裝的類的具體實現細節可以隨時改變,而無須調整用戶級別的代碼。
友元聲明僅僅指定了訪問權限,而並非一個通常意義上的函數聲明。如果希望類的用戶能調用某個友元函數,就必須在友元聲明之外再專門對函數進行一次聲明(部分編譯器沒有該限制)。
爲了使友元對類的用戶可見,通常會把友元的聲明(類的外部)與類本身放在同一個頭文件中。
類的其他特性(Additional Class Features)
類成員再探(Class Members Revisited)
由類定義的類型名字和其他成員一樣存在訪問限制,可以是public
或private
中的一種。
class Screen { public: // alternative way to declare a type member using a type alias using pos = std::string::size_type; // other members as before };
與普通成員不同,用來定義類型的成員必須先定義後使用。類型成員通常位於類起始處。
定義在類內部的成員函數是自動內聯的。
如果需要顯式聲明內聯成員函數,建議只在類外部定義的位置說明inline
。
inline
成員函數該與類定義在同一個頭文件中。
使用關鍵字mutable
可以聲明可變數據成員(mutable data member)。可變數據成員永遠不會是const
的,即使它在const
對象內。因此const
成員函數可以修改可變成員的值。
class Screen { public: void some_member() const; private: mutable size_t access_ctr; // may change even in a const object // other members as before }; void Screen::some_member() const { ++access_ctr; // keep a count of the calls to any member function // whatever other work this member needs to do }
提供類內初始值時,必須使用=
或花括號形式。
返回*this的成員函數(Functions That Return *this)
const
成員函數如果以引用形式返回*this
,則返回類型是常量引用。
通過區分成員函數是否爲const
的,可以對其進行重載。在常量對象上只能調用const
版本的函數;在非常量對象上,儘管兩個版本都能調用,但會選擇非常量版本。
class Screen { public: // 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 }; Screen myScreen(5,3); const Screen blank(5, 3); myScreen.set('#').display(cout); // calls non const version blank.display(cout); // calls const version
類類型(Class Types)
每個類定義了唯一的類型。即使兩個類的成員列表完全一致,它們也是不同的類型。
可以僅僅聲明一個類而暫時不定義它。這種聲明被稱作前向聲明(forward declaration),用於引入類的名字。在類聲明之後定義之前都是一個不完全類型(incomplete type)。
class Screen; // declaration of the Screen class
可以定義指向不完全類型的指針或引用,也可以聲明(不能定義)以不完全類型作爲參數或返回類型的函數。
只有當類全部完成後纔算被定義,所以一個類的成員類型不能是該類本身。但是一旦類的名字出現,就可以被認爲是聲明過了,因此類可以包含指向它自身類型的引用或指針。
class Link_screen { Screen window; Link_screen *next; Link_screen *prev; };
友元再探(Friendship Revisited)
除了普通函數,類還可以把其他類或其他類的成員函數聲明爲友元。友元類的成員函數可以訪問此類包括非公有成員在內的所有成員。
class Screen { // Window_mgr members can access the private parts of class Screen friend class Window_mgr; // ... rest of the Screen class };
友元函數可以直接定義在類的內部,這種函數是隱式內聯的。但是必須在類外部提供相應聲明令函數可見。
struct X { friend void f() { /* friend function can be defined in the class body */ } X() { f(); } // error: no declaration for f void g(); void h(); }; void X::g() { return f(); } // error: f hasn't been declared void f(); // declares the function defined inside X void X::h() { return f(); } // ok: declaration for f is now in scope
友元關係不存在傳遞性。
把其他類的成員函數聲明爲友元時,必須明確指定該函數所屬的類名。
class Screen { // Window_mgr::clear must have been declared before class Screen friend void Window_mgr::clear(ScreenIndex); // ... rest of the Screen class };
如果類想把一組重載函數聲明爲友元,需要對這組函數中的每一個分別聲明。
類的作用域(Class Scope)
當成員函數定義在類外時,返回類型中使用的名字位於類的作用域之外,此時返回類型必須指明它是哪個類的成員。
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 and Class Scope)
成員函數體直到整個類可見後纔會被處理,因此它能使用類中定義的任何名字。
聲明中使用的名字,包括返回類型或參數列表,都必須確保使用前可見。
如果類的成員使用了外層作用域的某個名字,而該名字表示一種類型,則類不能在之後重新定義該名字。
typedef double Money; class Account { public: Money balance() { return bal; } // uses Money from the outer scop private: typedef double Money; // error: cannot redefine Money Money bal; // ... };
類型名定義通常出現在類起始處,這樣能確保所有使用該類型的成員都位於類型名定義之後。
成員函數中名字的解析順序:
- 在成員函數內查找該名字的聲明,只有在函數使用之前出現的聲明纔會被考慮。
- 如果在成員函數內沒有找到,則會在類內繼續查找,這時會考慮類的所有成員。
- 如果類內也沒有找到,會在成員函數定義之前的作用域查找。
// it is generally a bad idea to use the same name for a parameter and a member int height; // defines a name subsequently used inside Screen class Screen { public: typedef std::string::size_type pos; void dummy_fcn(pos height) { cursor = width * height; // which height? the parameter } private: pos cursor = 0; pos height = 0, width = 0; };
可以通過作用域運算符::
或顯式this
指針來強制訪問被隱藏的類成員。
// bad practice: names local to member functions shouldn't hide member names void Screen::dummy_fcn(pos height) { cursor = width * this->height; // member height // alternative way to indicate the member cursor = width * Screen::height; // member height } // good practice: don't use a member name for a parameter or other local variable void Screen::dummy_fcn(pos ht) { cursor = width * height; // member height }
構造函數再探(Constructors Revisited)
構造函數初始值列表(Constructor Initializer List)
如果沒有在構造函數初始值列表中顯式初始化成員,該成員會在構造函數體之前執行默認初始化。
如果成員是const
、引用,或者是某種未定義默認構造函數的類類型,必須在初始值列表中將其初始化。
class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; }; // ok: explicitly initialize reference and const members ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) { }
最好令構造函數初始值的順序與成員聲明的順序一致,並且儘量避免使用某些成員初始化其他成員。
如果一個構造函數爲所有參數都提供了默認實參,則它實際上也定義了默認構造函數。
委託構造函數(Delegating Constructors)
C++11擴展了構造函數初始值功能,可以定義委託構造函數。委託構造函數使用它所屬類的其他構造函數執行它自己的初始化過程。
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 }
默認構造函數的作用(The Role of the Default Constructor)
當對象被默認初始化或值初始化時會自動執行默認構造函數。
默認初始化的發生情況:
- 在塊作用域內不使用初始值定義非靜態變量或數組。
- 類本身含有類類型的成員且使用合成默認構造函數。
- 類類型的成員沒有在構造函數初始值列表中顯式初始化。
值初始化的發生情況:
- 數組初始化時提供的初始值數量少於數組大小。
- 不使用初始值定義局部靜態變量。
- 通過
T()
形式(T爲類型)的表達式顯式地請求值初始化。
類必須包含一個默認構造函數。
如果想定義一個使用默認構造函數進行初始化的對象,應該去掉對象名後的空括號對。
Sales_data obj(); // oops! declares a function, not an object Sales_data obj2; // ok: obj2 is an object, not a function
隱式的類類型轉換(Implicit Class-Type Conversions)
如果構造函數只接受一個實參,則它實際上定義了轉換爲此類類型的隱式轉換機制。這種構造函數被稱爲轉換構造函數(converting constructor)。
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"); // 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"));
在要求隱式轉換的程序上下文中,可以通過將構造函數聲明爲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 };
explicit
關鍵字只對接受一個實參的構造函數有效。
只能在類內聲明構造函數時使用explicit
關鍵字,在類外定義時不能重複。
執行拷貝初始化時(使用=
)會發生隱式轉換,所以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;
可以使用explicit
構造函數顯式地強制轉換類型。
// 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));
聚合類(Aggregate Classes)
聚合類滿足如下條件:
- 所有成員都是
public
的。 - 沒有定義任何構造函數。
- 沒有類內初始值。
- 沒有基類。
- 沒有虛函數。
struct Data { int ival; string s; };
可以使用一個用花括號包圍的成員初始值列表初始化聚合類的數據成員。初始值順序必須與聲明順序一致。如果初始值列表中的元素個數少於類的成員個數,則靠後的成員被值初始化。
// val1.ival = 0; val1.s = string("Anna") Data val1 = { 0, "Anna" };
字面值常量類(Literal Classes)
數據成員都是字面值類型的聚合類是字面值常量類。或者一個類不是聚合類,但符合下列條件,則也是字面值常量類:
- 數據成員都是字面值類型。
- 類至少含有一個
constexpr
構造函數。 - 如果數據成員含有類內初始值,則內置類型成員的初始值必須是常量表達式。如果成員屬於類類型,則初始值必須使用成員自己的
constexpr
構造函數。 - 類必須使用析構函數的默認定義。
constexpr
構造函數用於生成constexpr
對象以及constexpr
函數的參數或返回類型。
constexpr
構造函數必須初始化所有數據成員,初始值使用constexpr
構造函數或常量表達式。
類的靜態成員(static Class Members)
使用關鍵字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(); };
由於靜態成員不與任何對象綁定,因此靜態成員函數不能聲明爲const
的,也不能在靜態成員函數內使用this
指針。
用戶代碼可以使用作用域運算符訪問靜態成員,也可以通過類對象、引用或指針訪問。類的成員函數可以直接訪問靜態成員。
double r; r = Account::rate(); // access a static member using the scope operator 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 class Account { public: void calculate() { amount += amount * interestRate; } private: static double interestRate; // remaining members as before };
在類外部定義靜態成員時,不能重複static
關鍵字,其只能用於類內部的聲明語句。
由於靜態數據成員不屬於類的任何一個對象,因此它們並不是在創建類對象時被定義的。通常情況下,不應該在類內部初始化靜態成員。而必須在類外部定義並初始化每個靜態成員。一個靜態成員只能被定義一次。一旦它被定義,就會一直存在於程序的整個生命週期中。
// define and initialize a static class member 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 is a constant double daily_tbl[period]; };
靜態數據成員的類型可以是它所屬的類類型。
class Bar { 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; };