1. 程式人生 > >類--其他特性,作用域,建構函式,靜態成員

類--其他特性,作用域,建構函式,靜態成員

一、類的其他特性

1、類成員再探

1)定義一個型別成員

  除了定義資料和函式成員之外,類還可以自定義某種型別在類中的別名。由類定義的型別名字和其他成員一樣存在訪問限制,可以是public或者private中的一種。用來定義類型別名的成員必須先定義後使用,因此,型別成員通常出現在類開始的地方。

 1 // 一個視窗類
 2 class Screen
 3 {
 4 public:
 5     typedef std::string::size_type pos;
 6 private:
 7     pos m_cursor = 0; // 游標位置
 8     pos m_height = 0
, m_width = 0; 9 std::string m_contents; 10 };
View Code

2)令成員作為行內函數

  定義在類內部的成員函式是自動inline的。我們可以在類內部把inline作為宣告的一部分顯示地宣告成員函式,同樣的,也能在類的外部用inline關鍵字修飾函式的定義。

 1 #ifndef SCREEN_H
 2 #define SCREEN_H
 3 
 4 #include <string>
 5 
 6 // 一個視窗類
 7 class Screen
 8 {
 9 public:
10     typedef std::string
::size_type pos; 11 Screen() = default; 12 Screen(pos ht, pos wd, char c) :m_height(ht), m_width(wd), m_contents(ht*wd, c){} 13 char get() const { return m_contents[m_cursor]; } // 隱式內聯 14 inline char get(pos r, pos c) const; // 顯示內聯 15 Screen &move(pos r, pos c); // 能在之後被設為內聯 16 private
: 17 pos m_cursor = 0; // 游標位置 18 pos m_height = 0, m_width = 0; 19 std::string m_contents; 20 }; 21 22 #endif
View Code
 1 #include "Screen.h"
 2 
 3 inline Screen &Screen::move(pos r, pos c) // 可在函式的定義處指定inline
 4 {
 5     pos row = r * m_width;
 6     m_cursor = row + c;
 7     return *this;
 8 }
 9 
10 char Screen::get(pos r, pos c) const // 在類的內部宣告成inline
11 {
12     pos row = r * c;
13     return m_contents[row + c];
14 }
View Code

我們無須在宣告和定義的地方同時說明inline,但這麼做是合法的。不過最好只在類外部定義的地方說明inline,這樣更容易理解。

3)可變資料成員

  有時會發生一種情況,希望能修改類的某個資料成員,即使是在一個const成員函式內。可以通過在變數的宣告中加入mutable關鍵字做到。

  一個可變資料成員永遠不會是const,即使它是const物件的成員。因此一個const成員函式可以修改一個可變成員的值

 1 class Screen
 2 {
 3 public:
 4     void some_member() const;
 5     void print() const{
 6         std::cout << m_access_ctr << std::endl;
 7     }
 8 private:
 9     mutable size_t m_access_ctr = 0; // 即使在一個const物件內也能被修改
10 };
11 void Screen::some_member()const
12 {
13     ++m_access_ctr;
14 }
View Code

 

2、類型別

  即使兩個類的成員列表完全一致,它們也是不同的型別。對於一個類來說,它的成員和其他任何類(或者任何其他作用域)的成員都不是一回事。

1)類的宣告

  就像函式的宣告和定義分離開來一樣,我們也能僅宣告類而暫時不定義它:

1 class Screen;

這種宣告有時被稱作前向宣告,它向程式中引入了名字Screen並且指明Screen是一種類型別。對於型別Screen來說,在它宣告之後定義之前是一個不完全型別,也就是說,此時我們已知Screen是一個類型別,但是不清楚它到底包含哪些成員。

  不完全型別只能在非常有限的情況下使用:可以定義指向這種型別的指標或引用,也可以宣告(但是不能定義)以不完全型別作為引數或者返回型別的函式。

  因為只有當類全部完成後類才算被定義,所以一個類的成員不能是類自己。然而,一旦一個類的名字出現後,它就被認為是宣告過了(但尚未定義),因此類允許包含指向它自身型別的引用或指標。

 3、友元

1)友元類

  如果一個類指定了友元類,則友元類的成員函式可以訪問此類包括非公有成員在內的所有成員。友元關係不具有傳遞性。

 1 class Screen
 2 {
 3     friend class Window_mgr; // 友元類
 4 public:
 5     typedef std::string::size_type pos;
 6     Screen() = default;
 7     Screen(pos ht, pos wd, char c) :m_height(ht), m_width(wd), m_contents(ht*wd, c){}
 8 private:
 9     pos m_cursor = 0; // 游標位置
10     pos m_height = 0, m_width = 0;
11     std::string m_contents;
12 };
13 
14 class Window_mgr
15 {
16 public:
17     using ScreenIndex = std::vector<Screen>::size_type;
18     Window_mgr() :m_screens(10, Screen()){}
19     void clear(ScreenIndex);
20 private:
21     std::vector<Screen> m_screens;
22 };
23 
24 void Window_mgr::clear(ScreenIndex i)
25 {
26     Screen &s = m_screens[i];
27     s.m_contents = std::string(s.m_height*s.m_width, ' ');
28 }
View Code

2)成員函式作為友元

  當把一個成員函式宣告成為友元時,我們必須明確指出該成員函式屬於哪一個類:

 1 class Screen;
 2 class Window_mgr
 3 {
 4 public:
 5     using ScreenIndex = std::vector<Screen>::size_type;
 6     void clear(ScreenIndex);
 7     Window_mgr();
 8 private:
 9     std::vector<Screen> m_screens;
10 };
11 
12 // 一個視窗類
13 class Screen
14 {
15     friend void Window_mgr::clear(Window_mgr::ScreenIndex); // 友元類
16 public:
17     typedef std::string::size_type pos;
18     Screen() = default;
19     Screen(pos ht, pos wd, char c) :m_height(ht), m_width(wd), m_contents(ht*wd, c){}
20 private:
21     pos m_cursor = 0; // 游標位置
22     pos m_height = 0, m_width = 0;
23     std::string m_contents;
24 };
25 
26 Window_mgr::Window_mgr(){
27     m_screens = std::vector<Screen>(10, Screen());
28 }
29 
30 void Window_mgr::clear(ScreenIndex i)
31 {
32     Screen &s = m_screens[i];
33     s.m_contents = std::string(s.m_height*s.m_width, ' ');
34 }
View Code

注意上述宣告出現的位置。

3)函式過載和友元

  儘管過載函式的名字相同,但它們仍然是不同的函式。因此,如果一個類想把一組過載函式宣告成它的友元,它需要對這組函式中的每一個分別宣告。

4)友元宣告和作用域

  類和非成員函式的宣告不是必須在它們的友元宣告之前。當一個名字第一次出現在一個友元宣告中時,我們隱式地假定該名字在當前作用域中是可見的。然而,友元本身不一定真的宣告在當前作用域中。

  甚至就算在類的內部定義該函式,我們也必須在類的外部提供相應的宣告從而使得函式可見。換句話說,即使我們僅僅是用宣告友元的類的成員呼叫該友元函式,它也必須是被宣告過的。

 

二、類的作用域

1、作用域和定義在類外部的成員

  一個類就是一個作用域的事實能夠很好地解釋為什麼當我們在類的外部定義成員函式時必須同時提供類名和函式名。在類的外部,成員的名字被隱藏起來了。

  一旦遇到了類名,定義的剩餘部分就在類的作用域之內了,這裡的剩餘部分包括引數列表和函式體。因此,我們可以直接使用類的其他成員而無須再次授權了。

  函式的返回型別通常出現在函式名之前。因此,當成員函式定義在類的外部時,返回型別中使用的都位於類的作用之外。這時,返回型別必須指明它是哪個類的成員。

2、名字查詢與類的作用域

  名字查詢的過程:

a、首先,在名字所在的快中尋找其宣告語句,只考慮在名字的使用之前出現的宣告。

b、如果沒找到,繼續查詢外層作用域。

c、如果最終沒有找到匹配的宣告,則程式報錯。

 

  對於定義在類內部的成員函式來說,解析其中名字的方式與上述的查詢規則有所區別,類的定義分兩步處理:

a、首先,編譯成員的宣告。

b、直到類全部可見後才編譯函式體。

  這種兩階段的處理方式只適用於成員函式中使用的名字。宣告中使用的名字,包括返回型別或者引數列表中使用的名字,都必須在編譯前確保可見。如果某個成員的宣告使用了類中尚未出現的名字,則編譯器將會在定義該類的作用域中繼續查詢。

  

  一般來說,內層作用域可以重新定義外層作用域中的名字,即使該名字已經在內層作用域中使用過。然而在類中,如果成員使用了外層作用域中的某個名字,而該名字代表一種型別,則類不能在之後重新定義該名字。

  

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

a、首先,在成員函式內查詢該名字的宣告。和前面一樣,只有在函式使用之前出現的宣告才被考慮。

b、如果在成員函式內沒有找到,則在類中繼續查詢,這時所有的成員都可以被考慮。

c、如果類內也沒找到該名字的宣告,在成員函式定義之前的外部作用域繼續查詢。

 

三、建構函式再探

1、建構函式初始值列表

  如果沒有在建構函式的初始值列表中顯示地初始化成員,則該成員將在建構函式體之前執行預設初始化

  在建構函式體內執行的是成員的賦值,有時我們可以忽略資料成員初始化和賦值之間的差異,但並非總能這樣。如果成員是const、引用,或者屬於某種未提供預設建構函式的類型別,我們必須通過建構函式初始值列表為這些成員提供初始值。

  在建構函式初始值列表中每個成員只能出現一次。成員的初始化順序與它們在類定義中的出現順序一致,建構函式初始值列表中初始值的前後位置關係不會影響實際的初始化順序。

2、委託建構函式

  C++11新標準擴充套件了建構函式初始值的功能,使得我們可以定義所謂的委託建構函式。一個委託建構函式使用它所屬類的其他建構函式執行它自己的初始化過程,或者說它把它自己的一些(或者全部)職責委託給了其他建構函式。

  和其他建構函式一樣,一個委託建構函式也有一個成員初始值的列表和一個函式體。在委託建構函式內,成員初始值列表只有一個唯一的入口,就是類名本身。和其他成員初始值列表一樣,類名後面緊跟圓括號後面括起來的引數列表,引數列表必須與類中另外一個建構函式匹配。

  當一個建構函式委託給另一個建構函式時,受委託的建構函式的初始值列表和函式體被依次執行。如果受委託的建構函式體包含程式碼的話,將先執行這些程式碼,然後控制權才會交還給委託者的函式體。

 1 #include <iostream>
 2 #include <string>
 3 
 4 class SalesData
 5 {
 6 public:
 7     SalesData(std::string s, unsigned cnt, double price):
 8         m_book_no(s), m_units_sold(cnt), m_revenue(cnt*price){
 9         std::cout << "QAQ" << std::endl;
10     }
11     SalesData() :SalesData("", 0, 0){
12         std::cout << "hello" << std::endl;
13     }
14 
15 private:
16     std::string m_book_no;
17     unsigned m_units_sold = 0;
18     double m_revenue = 0.0;
19 };
20 int main()
21 {
22     SalesData sd;
23     return 0;
24 } 
View Code

3、預設建構函式的作用

  當物件被預設初始化或值初始化時自動執行預設建構函式。

  如果想定義一個使用預設建構函式進行初始化的物件,物件名之後不要帶上空的括號對,帶上空的括號對是定義了一個函式而非物件。

4、隱式的類型別轉換

  如果建構函式只接受一個實參,則它實際上定義了轉換為此類型別的隱式轉換機制,有時我們把這種建構函式稱作轉換建構函式。

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

 1 #include <iostream>
 2 #include <string>
 3 
 4 class SalesData
 5 {
 6 public:
 7     SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){}
 8     SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){}
 9     SalesData(const std::string &s, unsigned n, double p) :
10         m_book_no(s), m_units_sold(n), m_revenue(p * n){}
11 
12     SalesData &combine(const SalesData rhs)
13     {
14         m_units_sold += rhs.m_units_sold;
15         m_revenue += rhs.m_revenue;
16         return *this; // 返回呼叫該函式的物件
17     }
18 private:
19     std::string m_book_no; // 書名
20     unsigned m_units_sold; // 數量
21     double m_revenue; // 總價
22 };
23 int main()
24 {
25     SalesData sd;
26     sd.combine(std::string("998")); // string隱式的轉換成SalesData
27     // sd.combine("998"); // 錯誤,有2步轉換
28     return 0;
29 }
View Code

  在要求隱式轉換的上下文中,我們可以通過將函式宣告成explicit加以阻。關鍵字explicit只對一個實參的建構函式有效。需要多個實參的建構函式不能用於執行隱式轉換,所以無須將這些建構函式指定為explicit的。只能在類年內宣告函式時使用explicit,在類外部定義時不應重複。

 1 #include <iostream>
 2 #include <string>
 3 
 4 class SalesData
 5 {
 6 public:
 7     SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){}
 8     explicit SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){}
 9     SalesData(const std::string &s, unsigned n, double p) :
10         m_book_no(s), m_units_sold(n), m_revenue(p * n){}
11 
12     SalesData &combine(const SalesData rhs)
13     {
14         m_units_sold += rhs.m_units_sold;
15         m_revenue += rhs.m_revenue;
16         return *this; // 返回呼叫該函式的物件
17     }
18 private:
19     std::string m_book_no; // 書名
20     unsigned m_units_sold; // 數量
21     double m_revenue; // 總價
22 };
23 int main()
24 {
25     SalesData sd;
26     sd.combine(std::string("998")); // 錯誤
27     return 0;
28 } 
View Code

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

 1 #include <iostream>
 2 #include <string>
 3 
 4 class SalesData
 5 {
 6 public:
 7     SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){}
 8     explicit SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){}
 9     SalesData(const std::string &s, unsigned n, double p) :
10         m_book_no(s), m_units_sold(n), m_revenue(p * n){}
11 
12     SalesData &combine(const SalesData rhs)
13     {
14         m_units_sold += rhs.m_units_sold;
15         m_revenue += rhs.m_revenue;
16         return *this; // 返回呼叫該函式的物件
17     }
18 private:
19     std::string m_book_no; // 書名
20     unsigned m_units_sold; // 數量
21     double m_revenue; // 總價
22 };
23 int main()
24 {
25     std::string book = "998";
26     SalesData sd(book); // 直接初始化
27     //SalesData sd=book; // 錯誤,拷貝初始化
28     return 0;
29 } 
View Code

  儘管編譯器不會將explicit的建構函式用於隱式轉換過程,但是我們可以使用這樣的建構函式顯示地強制進行轉換:

 1 #include <iostream>
 2 #include <string>
 3 
 4 class SalesData
 5 {
 6 public:
 7     SalesData() :m_book_no(""), m_units_sold(0), m_revenue(0){}
 8     explicit SalesData(const std::string &s) :m_book_no(s), m_units_sold(0), m_revenue(0){}
 9     SalesData(const std::string &s, unsigned n, double p) :
10         m_book_no(s), m_units_sold(n), m_revenue(p * n){}
11 
12     SalesData &combine(const SalesData rhs)
13     {
14         m_units_sold += rhs.m_units_sold;
15         m_revenue += rhs.m_revenue;
16         return *this; // 返回呼叫該函式的物件
17     }
18 private:
19     std::string m_book_no; // 書名
20     unsigned m_units_sold; // 數量
21     double m_revenue; // 總價
22 };
23 int main()
24 {
25     std::string book = "998";
26     SalesData sd;
27     sd.combine(SalesData(book));
28     sd.combine(static_cast<SalesData>(book));
29     return 0;
30 } 
View Code

 5、聚合類

  聚合類使得使用者可以直接訪問其成員,並且具有特殊的初始化語法形式。當一個類滿足以下條件時,我們就說它是聚合的:

a、所有成員都是public的。

b、沒有定義任何建構函式。

c、沒有類內初始值。

d、沒有基類,也沒有虛擬函式。

  我們可以提供一個花括號括起來的成員初始值列表來初始化聚合類的資料成員,初始值的順序必須與宣告的順序一致。與初始化陣列元素的規則一樣,如果初始值列表中的元素個數少於類的成員數量,則靠後的成員被值初始化。

 1 #include <iostream>
 2 #include <string>
 3 
 4 class Data
 5 {
 6 public:
 7     int val;
 8     std::string s;
 9 };
10 int main()
11 {
12     Data d = { 233, "hello" };
13     std::cout << d.val << "," << d.s << std::endl;
14     return 0;
15 }
View Code

6、字面值常量類

  除了算術型別、引用和指標外,某些類也是字面值型別。資料成員都是字面值型別的聚合類是字面值常量類。如果一個類不是聚合類,但它符合下列要求,則它也是一個字面值常量類:

a、資料成員都必須是字面值型別。

b、類必須至少含有一個constexpr建構函式。

c、如果一個數據成員含有類內初始值,則內建型別成員的初始值必須是一條常量表達式;或者如果成員屬於某一種型別,則初始值必須使用成員自己的constexpr建構函式。

d、類必須使用解構函式的預設定義,該成員負責銷燬類的物件。

  儘管建構函式不能是const的,但是字面值常量類的建構函式可以是constexpr函式。事實上,一個字面值常量類必須至少提供一個constexpr建構函式。constexpr建構函式可以宣告成default的形式;否則,constexpr建構函式體一般來說應該是空的。

 

四、類的靜態成員

1、宣告靜態成員

  我們通過在成員的宣告之前加上關鍵字static使得其與類關聯在一起。靜態成員可以是public或private的。靜態資料成員的型別可以是常量、引用、指標、類型別等。

  靜態成員函式不與任何物件綁在一起,它們不包含this指標。因此,靜態成員函式不能宣告成const的,而且我們也不能在static函式體內使用this指標

2、使用靜態成員

  使用作用域運算子直接訪問靜態成員。雖然靜態成員不屬於某個物件,但是我們仍然可以使用類的物件、引用或指標來訪問靜態成員。成員函式不用通過作用域運算子就能直接使用靜態成員。

3、定義靜態成員

  我們既可以在類的內部也可以在類的外部定義靜態成員函式。當在類的外部定義靜態成員時,不能重複static關鍵字,該關鍵字只出現在類內部的宣告語句

  因為靜態資料成員不屬於類的任何一個物件,所以它們並不是在建立類的物件時被定義的。這意味著它們不是由類的建構函式初始化的。而且一般來說,我們不能在類的內部初始化靜態成員。相反的,必須在類的外部定義和初始化每個靜態成員。一個靜態資料成員只能定義一次。

  類似於全域性變數,靜態資料成員定義在任何函式體之外。因此一旦它被定義,就將一直存在於程式的整個生命週期。

 1 #include <iostream>
 2 #include <string>
 3 
 4 class Account
 5 {
 6 public:
 7     void cal()
 8     {
 9         amount += amount * interestRate;
10     }
11     static double rate() { return interestRate; }
12     static void rate(double);
13 private:
14     std::string owner;
15     double amount;
16     static double interestRate;
17     static double initRate(){ return 0.5; }
18 };
19 
20 // 從類名開始,這條語句的剩餘部分就都位於類的作用域之內了。因此我們可以直接使用initRate()。
21 // 注意雖然initRate是私有的,我們也能使用它來初始化interestRate
22 double Account::interestRate = initRate();
23 
24 void Account::rate(double newRate)
25 {
26     interestRate = newRate;
27 }
28 
29 int main()
30 {
31     std::cout << Account::rate() << std::endl;
32     return 0;
33 }
View Code

4、靜態成員的類內初始化

  通常情況下,類的靜態成員不應該在類的內部初始化。然而,我們可以為靜態成員提供const整數型別的類內初始值,不過要求靜態成員必須是字面值常量型別的constexpr。初始值必須是常量表達式,因為這些成員本身就是常量表達式,所以它們能用在所有適合於常量表達式的地方。

1 class Account
2 {
3 private:
4     static constexpr int period = 30;
5     double arr[period];
6 };

  如果在類的內部為靜態成員提供了一個初始值,則成員的定義不能在指定一個初始值了:

1 class Account
2 {
3 private:
4     static constexpr int period = 30;
5     double arr[period];
6 };
7 constexpr int period; // 初始值在類的內部提供

  即使一個常量靜態資料成員在類內部被初始化了,通常情況下也應該在類的外部定義一下該成員。

5、靜態成員能用於某些場景,而普通成員不能

  靜態資料成員的型別可以就是它所屬的類型別。而非靜態資料成員則受到限制,只能宣告成所屬類的指標或引用。

  靜態成員和普通成員的另一個差別是我們可以使用靜態成員作為預設實參。

 1 #include <iostream>
 2 #include <string>
 3 
 4 class Account
 5 {
 6 public:
 7     Account(int v = bg) :val(bg){}
 8     static void show()
 9     {
10         std::cout << "static:mem.val " << mem.val << std::endl;
11         std::cout << "static:bg " << bg << std::endl;
12     }
13     void showVal()
14     {
15         std::cout << val << std::endl;
16     }
17 private:
18     static Account mem;
19     static int bg;
20     int val;
21 };
22 
23 int Account::bg = 998;
24 Account Account::mem = Account(999);
25 
26 int main()
27 {
28     Account ac;
29     ac.show();
30     ac.showVal();
31     return 0;
32 }
View Code