1. 程式人生 > >c++ primer 第五版學習筆記-第七章 類

c++ primer 第五版學習筆記-第七章 類

本文為轉載,出處:https://blog.csdn.net/libin1105/article/details/48664019

                                https://blog.csdn.net/sunhero2010/article/details/49798749

                                https://blog.csdn.net/fnoi2014xtx/article/details/78152884

7.1 定義抽象資料型別

1.類的基本思想是資料抽象和封裝,資料抽象是一種依賴於介面和實現分離的程式設計(以及設計)技術。類的介面包括使用者所能執行的操作:類的實現則包括類的資料成員、負責介面實現的函式體以及定義類所需的各種私有函式。封裝實現了類的藉口和實現的分離。

    成員函式的宣告必須在類的內部,它的定義可以在類的內部也可以在類的外部,作為介面組成部分的非成員函式,例如:add,read,print等,宣告和定義都在類的外部。

    定義在類內部的函式,是隱式的inline函式。

    在類的外部定義成員函式時候,成員函式的定義必須與它的宣告匹配,如果成員被宣告成常量成員函式,那麼它的定義也必須在引數列表後明確指定const屬性。同時,類外部定義的成員的名字必須包含它所屬的類名。

    double Sales_data::avg_price() const{...}

類由類成員組成。類成員包括屬性,欄位,成員函式,建構函式,解構函式等組成。

    類設計應該遵從抽象封裝性。

    類抽象性指對於類的使用者來說只需知道類介面即可使用類功能。類的具體實現由設計者負責。即使某個功能發生了變更但由於使用者是以介面方式呼叫類所以使用者程式碼無需做任何修改。

    類封裝性指類使用者只需知道類的功能無需瞭解具體實現。實現程式碼對使用者來說不可見。

    C++類沒有訪問級別限限制,定義類時不能用public 或 private 做修飾。類成員有訪問級別,可以定義 public protect private

 {
    public:      // 類成員只能宣告不允許定義 替換成string name("tom") 會產生編譯錯誤, 資料成員初始化工作在建構函式中執行
      string  name;            
      // 給類定義別名型別成員 index 由於別名要在外部訪問所以一定要定義在 public
      typedef std::string::size_type index;
      // 內部定義的函式,等價於inline
      char get() const { return contents[cursor]; }
      // 內部宣告一個成員函式(無定義),且函式是內聯的inline表示在編譯時該宣告會被替換成定義語句
      inline char get(index ht, index wd) const;
      // 內部宣告一個成員函式(無定義)
     index get_cursor() const;
            // ...
};

// 定義類 Screen 的成員函式 get 具體實現 
char Screen::get(index r, index c) const 
{
     index row = r * width;    // compute the row location
 
     return contents[row + c]; // offset by c to fetch specified character
} 
// 定義類 Screen 的成員函式 get_cursor 具體實現,且是內聯的
inline Screen::index Screen::get_cursor() const 
{ 
     return cursor;
}

注意:類的inline修飾符可以放在類內部申明也可以放在外部定義。一般放在內部宣告便於理解。  

    類定義完畢後一定要加上封號結束符 ;。

    類資料成員只允許宣告不允許定義;

    可以宣告類而不定義它。成為前向宣告又叫不完全類,這樣的類無法定義例項也無法使用成員。一般用來處理類相互依賴的情況。定義了類就能定義類物件:myclass obj; 一定要注意不能是myclass obj() ; 類物件定義時會分配記憶體空間,每個類都有自己的空間相互間不受影響。

2.成員函式通過一個名為this的額外隱式引數來訪問呼叫它的那個物件。當我們呼叫一個成員函式時,用請求該函式的物件地址初始化this

this是一個類的常量指標,在成員函式內部,this儲存該物件的地址,我們不能改變this儲存的地址

struct book
{
    string no;
    string isbn()const{return this->no;}//等價於 return no;
}Book1;

Book1.isbn()
//當我們呼叫這個函式時,this限定為Book1的地址了
//當我們需要使用當前this對應的元素時,用*this返回當前元素的引用

3.C++允許把const關鍵字放在成員函式的引數列表之後,此時,緊跟在引數列表後面的const表示this是一個指向常量的指標。像這樣使用const的成員函式被稱作常量成員函式。

對於成員函式,若定義在類的內部,預設內聯,否則預設不內聯 
在機器執行中,對於Book1.isbn()的呼叫,我們隱式的傳遞了一個this指標,偽程式碼表示如下 
string isbn(book *const this)
若我們需要限定this為底層const,即函式內不修改成員的值,我們在使用const成員函式,偽程式碼表示如下 
string isbn(const book *const this)

    類物件包含一個 this 指標指向自身(當前的例項物件)且無法更改指標指向。在普通的非 const 成員函式中,this 的型別是一個指向類型別的 const 指標。可以改變 this 所指向的值,但不能改變 this 所儲存的地址。在 const 成員函式中,this 的型別是一個指向 const 類型別物件的 const 指標。既不能改變 this 所指向的物件,也不能改變 this 所儲存的地址。

    基於成員函式是否為 const,可以過載一個成員函式;同樣地,基於一個指標形參是否指向 const可以過載一個函式。

class mycls
{
    public:
        mycls(){}; // 想要定義 const mycls a; 必須要顯示定義預設建構函式
        mycls &Get(){ return *this; };
        const mycls &Get() const { return *this; }; // 想如果const函式返回this引用或指標; 必須要返回const指標或引用,因為無法用const物件(this)初始化非const物件。
 
};
 
const mycls b;
mycls b1 = c.Get(); // 呼叫const版Get函式
const mycls b2 = c.Get(); // 呼叫const版Get函式
// b1 b2 定義時會呼叫類的拷貝函式。b1,b2是Get返回值的副本,b1還會將常量副本轉變成變數
 
mycls &b3 = c.Get(); // 錯誤,不能用 const &mycls 初始化 &mycls (指標或者引用型別不能用常量初始化變數)
const mycls &b4 = c.Get();
 
 
mycls a;
mycls a1 = a.Get(); // 呼叫非const版Get函式
const mycls a2 = a.Get(); // 呼叫非const版Get函式

由此可見呼叫那個版本和呼叫物件是否const有關係,const物件會呼叫const版本,非const物件會呼叫非const版本。

引用網上的總結:

     成員函式具有const過載時,類的const物件將呼叫類的const版本成員函式,類的非const物件將呼叫非const版本成員函式。

     如果只有const成員函式,類的非const物件也可以呼叫const成員函式。                          ——這個思路來描述很囧。下同。

     如果只有非const成員函式,類的const物件…額,不能呼叫非const成員函式。                ——其實跟上一句的意思是一樣的:const物件只能呼叫它的const成員函式。

    總的來說,就是當我們呼叫一個成員函式時,編譯器會先檢查函式是否有const過載,如果有,將根據物件的const屬性來決定應該呼叫哪一個函式。如果沒有const過載,只此一家,那當然就呼叫這一個了。這時編譯器亦要檢查函式是不是沒有const屬性而呼叫函式的物件又有const屬性,若如此,亦無法通過編譯。   

   還有一點非常重要,想要定義類的const物件必須顯示定義對應建構函式,無法依賴系統自動分配的建構函式。

istream &read(istream &is,book &rhs)
{
    is>>rhs.no;
    return is;//可以返回讀入是否成功的值
}
ostream &print(ostream &os,book &rhs)
{
    os<<rhs.no;
    return os;
}
//為什麼read,print前面是&,因為IO類屬於不能拷貝的型別,只能通過引用來傳遞它們,呼叫方式如下
read(cin,Book1);
print(cout,Book1);

4.每個類都分別定義了它的物件被初始化的方式,類通過一個或幾個特殊的成員函式來控制其物件的初始化過程,這些函式叫做建構函式。建構函式的任務是初始化類物件的資料成員,無論何時只要類的物件被建立,就會執行建構函式。建構函式的名字和類名相同,建構函式沒有返回型別,不同建構函式必須在引數型別和引數數量上有差別。建構函式不能宣告為const。在const物件的構造過程中可以向其寫值。

struct book
{
    book() = default;//顯式預設建構函式
    book(const string &s):No(s){}//建構函式初始值列表中未出現變數將以預設建構函式方式初始化
    book(const string &s,int p):No(s),price(p){}
    book(istream &);
    string No;
    int price=0;
    //支援先出現建構函式,後出現變數定義
}
book::book(istream &is)//定義book類的成員,名字是book,同名,所以是建構函式。
{
    read(is,*this);//從is中讀取一條交易,存入this物件中
}

5.如果我們的類沒有顯式的定義建構函式,那麼編譯器會為我們隱式的定義一個預設建構函式。預設建構函式無需實參,編譯器建立的建構函式又被稱為合成的預設建構函式。對於大多數類來說,這個合成的預設建構函式將按照如下規則初始化類的資料成員:

如果存在類的初始值,用它來初始化成員

否則,預設初始化該成員

建構函式是特殊的成員函式。在類物件定義時被呼叫。 不能通過定義的類物件呼叫建構函式,建構函式可以定義多個或者說建構函式允許過載。

    如果沒有定義任何建構函式,系統就會給類分配一個無參的預設建構函式,類只要定義了一個建構函式,編譯器也不會再生成預設建構函式。只有當一個類沒有定義建構函式時,編譯器才會自動生成一個預設建構函式

    定義類物件時不能寫成 Sales_item myobj(); 編譯器會理解成:一個返回 Sales_item 型別叫 myobj的函式宣告。 正確寫法是去掉後面的括號。

    建構函式後面不允許定義成 const,這樣定義會產生語法錯誤: Sales_item() const {};

    建構函式在執行時會做類資料成員的初始化工作。從概念上講,可以認為建構函式分兩個階段執行:(1)初始化階段;(2)普通的計算階段。計算階段由建構函式函式體中的所有語句組成。

    不管成員是否在建構函式初始化列表中顯式初始化,類型別的資料成員總是在初始化階段初始化。初始化發生在計算階段開始之前。

class mycls
{
    public:
        mycls()
        {
           age = 12; name = "tom";
         };
 
        mycls(int i):age(i) 
        {
           age = 12 + i; name = "tom";
         };
 
    private:
        int age;
        string name;
};

    mycls obj1 ;使用無參建構函式,雖然建構函式並沒有顯示初始化資料成員但類型別name還是會被初始化成預設值name初始化為"" age未初始化(其值是個隨機數),初始化後建構函式重新賦值,最終age=12, ame = "tom" ;

    mycls obj2(4) ; 用建構函式引數初始化 age = 4, name = "",建構函式重新賦值,最終age=16, name = "tom" ;

    如果資料成員是自定義類型別,如果不顯示初始化則類一定要有預設建構函式否則編譯錯誤,成員被初始化的次序就是定義成員的次序。第一個成員首先被初始化,然後是第二個,依次類推。

    預設情況下可以用單個實參來呼叫的建構函式定義了從形參型別到該類型別的一個隱式轉換。

class mycls
{
    public:
        int i;
        mycls(int i){ };
        explicit mycls(string s){ };
 
};
    mycls obj(2) ; 也可以這樣使用這個建構函式 mycls obj = 2; 這裡做了一個型別轉換,但是這樣的寫法很不直觀。
    可以通過將建構函式宣告為 explicit,(可以阻止不應該允許的經過轉換建構函式進行的隱式轉換的發生。)來防止在需要隱式轉換的上下文中使用建構函式:mycls obj("tom"), 無法用 mycls obj = "tom" 因為轉換被禁止,通常,除非有明顯的理由想要定義隱式轉換,否則,單形參建構函式應該為 explicit。 
explicit 關鍵字只能用於類內部的建構函式宣告上。在類的定義體外部所做的定義上不再重複它。


6.C++11新標準中,如果我們需要預設的行為,那麼可以通過在引數列表後面寫上=default來要求編譯器生成建構函式。其中=default 既可以和宣告一起出現在類的內部,也可以作為定義出現在類的外部。和其他函式一樣,如果=default在類的內部,則預設建構函式是內聯的;如果它在類的外部,則該成員預設情況下不是內聯的。

book() = default;

7.2 訪問控制與封裝

1.C++語言中,我們使用訪問說明符加強類的封裝性:

定義在public說明符之後的成員在整個程式內可被訪問,public成員定義類的介面

定義在private說明符之後的成員可以被類的成員函式訪問,但是不能被使用該類的程式碼訪問,private部分封裝了類的實現細節。

 作為介面的一部分,建構函式和部分成員函式(isbn(),combine)緊跟在public後面,而資料成員和作為實現部分的函式緊跟在private後面。

struct和class可以定義類,區別是預設訪問許可權不一樣。如果是struct,順序是先Public再private。class相反。

2.類可以允許其他類或者函式訪問它的非公有成員,方法是令其他類或者函式成為它的友元。如果類想把一個函式作為它的友元,只需要增加一條以friend關鍵字開始的函式宣告語句即可。友元宣告只能出現在類定義的內部。

std::istream &read(std::istream &,book &);
//友元僅僅指定訪問許可權,並未宣告,需在之前進行宣告
class book
{
    friend std::istream &read(std::istream& ,book &);
    friend class BOOK;//友元類,注意友元關係不具有傳遞性
    friend void BOK::clear(int);//只宣告另一個類中的某個成員函式為友元
    public:

    private:
}

對於B,若想讓A的成員函式f為友元,應當使用以下順序

class B;//宣告B
class A
{
    public:
        void f(B &);//宣告f
};
class B
{
    friend void A::f(B &);//宣告友元
    private: 
        int cnt;
};
void A::f(B &b){printf("%d\n",b.cnt);}//定義f

注意,我們可以在友元函式宣告時定義該函式,但是,此時定義的函式並沒有宣告,對呼叫不可見

class X
{
    friend void f(){/*定義*/}
    X(){f();}//錯誤,未宣告
}
f();//錯誤,未宣告
void f();//宣告f()
f();//正確

// 友元類
class me
{
    friend class he;
    
    private:
        int i;
        string s;
};
 
class he
{
    public:
        void show(me &it)
        {
            cout << it.i << it.s << endl;
        };
};

類he是me的友元類,所以he中可以訪問me的私有成員i和s;

 將類成員作為另一個類的友元函式情況比較複雜,需要用到前面講過的前向宣告(兩個類之間有互相依賴關係)

// 類成員友元
class me; // 先要前向宣告類
 
class he  // 友元類需要目標類做引數由於目標類已宣告所以可以使用類引用或者指標--show(me &it)方法中的引數
{
    public:
        void show(me &it);
};
 
class me // 目標類需要宣告類的的成員show作為自己的友元函式,he在上面做了成員宣告所以成員show(me &it)可用
{
    friend void he::show(me &it);
    
    private:
        int i;
        string s;
};
 
void he::show(me &it) // 友元方法中使用目標類私有成員,目標類上一步定義了私有成員因此這裡成員可用
{
    cout << it.i << it.s << endl;
};


7.3 類的其他特性

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

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

(1)定義型別成員

class screen
{
    public:
        typedef std::string::size_type pos;
    //using pos=std::string::size_type;
    //要求:先定義,後使用    
    private:
        pos cur=0;//當前游標位置
        pos height=0,width=0;
        std::string contents;
}

(2)可變資料成員 
const成員函式中也可以對它進行修改

class screen
{
    public:
        voidsome_member()const;
    private mutableint cnt;
};
void screen::some_member()const{++cnt;}//cnt是可變的,即使some_member是const的,也可以修改它

(3)this的使用方法

class screen
{
    public:
        screen &set(char c){contents[cur]=c;return *this;}
        screen &move(int pos){cur=pos;return *this;}
        //若為const成員函式,則返回的是const screen &型別
        //即我們不能使用以下的呼叫方式
    ...
}MyScreen;

MyScreen.move(10).set('*');

解決方法之一是過載const函式

(4)類的宣告

class screen;//這是前向宣告,我們已知它是類型別,但是不知道它有什麼成員,所以它是不完全型別

我們可以定義指向不完全型別的指標,但是不能建立不完全型別的物件。screen *x//對   screen x//錯

不完全型別只能在以下情況使用:定義該類的指標或引用,宣告(非定義)以該類作為引數或返回型別的函式

7.4 類的作用域

1.名字查詢的過程:

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

如果沒有找到,繼續查詢外層作用域

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

typedef double Money
string bal;
class Account
{
    public:
        Money balance(){return bal;}
    private:
        Money bal;
}
當編譯器看到宣告balance時,首先在Account中找Money的宣告,找不到去外層,然後對於return bal;由於類的成員函式在所有定義後被處理,所以返回的是Money bal

我們可以重定義變數,但我們不能重定義型別

2.類的定義分兩步處理

首先,編譯成員的宣告

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

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

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

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

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

7.5 建構函式再探

1.成員的初始化順序與它們在類定義中的出現順序一致:第一個成員先被初始化,然後第二個,以此類推。建構函式的初始值列表中初始值的前後位置關係不會影響實際的初始化順序。

成員型別初始化

class DY
{
    const int x;
    const int &z;   
    //對於必須初始化的引用與常量型別,我們支援這樣初始化
    DY(int y):x(y),z(x){}
};

初始化順序由定義順序決定,即

class DY
{
    int i;
    int j;
    public: 
        DY(int val):j(val),i(j){}//錯誤,i的值未定義
};

2.一個委託建構函式使用它所屬類的其他建構函式執行它自己的初始化過程,或者說它把它自己的一些職責委託給了其他建構函式。

struct book
{
    int x,y;
    book(int _x,int _y):x(_x),y(_y){/*程式碼1*/}
    book(int _x):book(_x,0){/*程式碼2*/}
    //呼叫第二個建構函式,先執行程式碼1,後執行程式碼2
};
//非委託建構函式 
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);}

如果想定義一個使用預設建構函式進行初始化的物件,正確的方法是去掉物件名後的空括號對:

sala_data  obj;//obj是一個預設初始化的物件。

預設建構函式的作用:

struct B
{
    int y;
    B(int x):y(x){}
};
struct A
{
    B b;
};
A a;//錯誤,建立A的預設建構函式,缺少B::B()

3.在要求隱式轉換的程式上下文中,我們可以通過將建構函式宣告為explicit加以阻止。explicit只能用於直接初始化,不能用於拷貝初始化。

關鍵字explicit只對一個實參的建構函式有效。需要多個實參的建構函式不能用於執行隱式轉換,所以無須將這些建構函式指定為explicit的。只能在類內宣告建構函式時使用explicit關鍵字,在類外部定義時不應重複。

struct book
{
    string isbn;
    book()=default;
    book(string &s):isbn(s){}
    void add(book &rhs)
    {
        ...
    }
}Bk;

Bk.add(string("123456789"));//正確
Bk.add(book("123456789"));//正確
Bk.add("123456789");//錯誤,不允許兩步隱式轉化

//抑制隱式轉化的方法如下
struct book
{
    string isbn;
    book()=default;
    explicit book(string &s):isbn(s){}
    //explicit函式只能用於直接初始化,且只能用於一個實參的函式
    void add(book &rhs)
    {
        ...
    }
}Bk;

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