c++學習筆記(四)

StaDark發表於2024-07-25

目錄
  • 類 & 物件
    • 封裝
      • 訪問許可權
    • 類的建構函式&解構函式
      • 建構函式的分類及呼叫
        • 複製建構函式的呼叫時機
        • 建構函式呼叫規則
      • 深複製與淺複製
      • 初始化列表
      • 類物件作為類成員
      • 靜態成員
    • C++物件模型和this指標
      • 成員變數和成員函式分開儲存
      • this指標概念
      • 空指標訪問成員函式
      • const修飾成員函式
    • 友元
      • 全域性函式做友元
      • 類做友元
      • 成員函式做友元
    • 運算子過載
      • 加號運算子過載
      • 左移運算子過載
      • 遞增 / 遞減運算子過載
      • 賦值運算子過載
      • 關係運算子過載
      • 函式呼叫運算子過載
    • 繼承
      • 基本語法
      • 繼承方式
      • 構造和析構順序
      • 繼承同名成員處理方式
      • 多繼承
      • 菱形繼承
    • 多型
      • 多型的基本概念
      • 多型案例(一)——計算器類
      • 純虛擬函式和抽象類
      • 多型案例(二)——飲品類
      • 虛析構和純虛析構
      • 多型案例(三)—— 電腦組裝

類 & 物件

c++物件導向的三大特徵為:封裝、繼承、多型

封裝

封裝的意義:

  • 將屬性和行為作為一個整體,表現生活中的事物(資料成員和方法)
  • 將屬性和行為加以許可權控制(訪問修飾符)

類中的屬性和行為我們一般稱為成員,屬性為成員屬性,行為是成員方法

img

這裡以盒子為例,定義一個類,並宣告兩個物件

class Box
{
    //訪問許可權
    //公共許可權
    public:
    //資料成員
    	double lenth;	//長
    	double breadth; //寬
    	double height;  //高
    //方法
    //求盒子體積
    	double cal_Volume()
        {
            return lenth * breadth * height;
        }
};

int main()
{
    Box box1;	// 宣告 box1,型別為 Box
    Box box2;	// 宣告 box2,型別為 Box
    //物件box1和box2都有他們各自的成員
}

訪問許可權

類在設計時,可以把屬性和行為放在不同的許可權下來加以控制

訪問許可權有三種:

  1. public 公共許可權

    其成員類內可以訪問,類外可以訪問

  2. protected 保護許可權

    其成員類內可以訪問,類外不可以訪問,但在子類(派生類)中可以訪問

  3. private(預設) 私有許可權

    其成員類內可以訪問,類外不可以訪問,不可檢視

class Person
{
    public:
    	String name;
    
    protected:
    	int age;
    
    int id_card;//預設為private
    
    public:
    	func()
        {
            name = "alen";
            age = 26;
            id_card = 114514;
        }
};

int main()
{
    Person p;
    p.name = "walker";
    p.age = 23;//報錯,類外不能訪問
    p.id_card = 1919810;//報錯,類外不能訪問
}

struct和class區別

兩者的唯一區別在於預設的訪問許可權

  • struct預設許可權為公共

  • class預設許可權為私有

  • class C1
    {
        int a;  //預設是私有許可權 private
    };
    
    struct C2
    {
        int a;  //預設是公共許可權 public
    };
    

    成員屬性設定為私有

    優點:

    1. 將所有成員屬性設定為私有,可以自己控制讀寫許可權
    2. 對於寫許可權,我們可以檢測資料的有效性
class Person
{
    public:
    	//設定名字
    	void setName(string name)
        {
            m_Name = name;
        }
    	
    	//設定偶像
    	void setIdol(string idol)
        {
            m_Idol = idol;
        }
    
    	//設定年齡
    	void setAge(int age)
        {
            if (age < 0 || age > 150)
            {
                cout<<"年齡"<<age<<"輸入有誤"<<endl;
                return;
            }
            m_Age = age;
        }
    
    	string getName()
        {
            return m_Name;
        }
    
    	int getAge()
        {
            return m_Age;
        }
    
    private:
    	string m_Name;//姓名 可讀可寫
    	int m_Age = 18;//年齡 只讀	(當可寫的時候,年齡限制在0-150之間)
    	string m_Idol;//偶像 只寫
    
};

類的建構函式&解構函式

c++中的建構函式解構函式的作用是對物件的初始化清理

物件的初始化和清理工作是強制的,如果我們沒有提供構造和解構函式的話,編譯器將自動提供空實現的構造和解構函式

  • 建構函式:主要作用在於建立物件時為物件的成員屬性賦值,建構函式由編譯器自動呼叫,無需手動呼叫
  • 解構函式:主要作用在於物件銷燬前系統自動呼叫,執行一些清理工作

構建函式語法(內聯):類名( ) { }

  1. 沒有返回值,也不用寫void
  2. 函式名稱與類名相同
  3. 建構函式可以有引數,因此可以發生過載
  4. 程式在呼叫物件的時候會自動呼叫構造,無須手動呼叫,而且只會呼叫一次

解構函式語法(內聯):~類名( ) { }

  1. 沒有返回值,也不用寫void
  2. 函式名稱與類名相同,在名稱前加上符號 ~
  3. 解構函式不可以有引數,因此不可以發生過載
  4. 程式在物件銷燬前會自動呼叫析構,無須手動呼叫,而且只會呼叫一次
class Person
{
public:
    //建構函式	進行初始化操作
    Person()
    {
        cout<<"Person建構函式的呼叫"<<endl;
    }
    
	~Person()
    {
        cout<<"Person解構函式的呼叫"<<endl;
    }
};

void test01()
{
    Person p;	//在棧上的資料,p建立時執行建構函式,test01執行完畢後釋放這個物件,物件的解構函式被呼叫
}

int main()
{
    test01();
    return 0;
}

建構函式的分類及呼叫

兩種分類方式:

  • 按引數分為:有參構造和無參構造(預設)
  • 按型別分為:普通構造和複製構造

三種呼叫方式:

  • 括號法
  • 顯示法
  • 隱式轉換法
class Person
{
public:
    //普通構造
    Person()
    {
        cout<<"Person無參函式的呼叫"<<endl;
    }
	Person(int a)
    {
        age = a;
        cout<<"Person有參函式的呼叫"<<endl;
    }
    //複製構造
    Person(const Person &p)
    {
        //將傳入物件的所有屬性,複製到當前物件上
        age = p.age;
    }
    
    
    
    
    ~Person()
    {
        cout<<"Person解構函式的呼叫"<<endl;
    }
    
private:
    int age;
};

void test01()
{
	//括號法
    Person p1;//預設建構函式呼叫
    Person p2(10);//有參建構函式
    Person p3(p2);//複製建構函式
    
    /*
    注意事項:
    呼叫預設建構函式的時候,不要加()
    因為這句程式碼“  Person p1();  ”編譯器會認為是一個函式宣告,不會認為是在建立物件
    */
    
    //顯示法
    Person p4;
    Person p5 = Person(10); //有參
    Person p6 = Person(p5); //複製
    
    Person(10); //匿名物件 特點:當前行執行結束後,系統會立即回收掉匿名物件
    
    /*
    注意事項2:
    不要用複製建構函式初始化匿名物件
    Person(p3) 會被視為物件p3的重定義
    */
    //隱式轉換法
    Person p7 = 10; //有參	相當於 Person p7 = Person(10)
    Person p8 = p7; //複製
}

int main()
{
    test01();
	return 0;
}

複製建構函式的呼叫時機

c++中複製建構函式呼叫實機通常有三種情況

  • 使用一個已經建立完畢的物件來初始化一個新物件
  • 值傳遞的方式給函式引數傳值(值傳遞本質是複製一個臨時的副本來傳遞,因此會呼叫複製建構函式)
  • 以值方式返回區域性物件(同上)

建構函式呼叫規則

預設情況下,c++編譯器至少給一個類新增3個函式

  1. 預設建構函式(無參,函式體為空)
  2. 預設解構函式(無參,函式體為空)
  3. 預設複製函式,對屬性進行值複製

建構函式呼叫規則如下:

  • 如果使用者定義有參建構函式,c++不再提供預設無參構造,但是會提供預設複製構造
  • 如果使用者定義複製建構函式,c++不會再提供其他建構函式

深複製與淺複製

淺複製(預設):簡單的賦值複製操作

深複製:在堆區重新申請空間,進行複製操作

淺複製可能帶來的問題是堆區的記憶體重複釋放

class Person
{
    Person(const Person &p)
    {
        m_Age = p.m_Age;
        m_Height = p.m_Height;	//編譯器預設的淺複製(單純的值複製)
    }
    
        Person(const Person &p)
    {
        m_Age = p.m_Age;
        m_Height = new int(*p.m_Height);	//深複製操作,另外開闢一片堆記憶體進行複製
    }
};

初始化列表

作用:c++提供了初始化列表語法,用來初始化屬性

語法:建構函式( ) : 屬性(值1), 屬性(值2)...{ }

class Person
{
    //傳統初始化操作
    Person(int a, int b, int c)
    {
        m_A = a;
        m_B = b;
        m_C = c;
    }
    
    //初始化列表初始化屬性
    Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
    {
        
    }
    private:
    	int m_A;
    	int m_B;
    	int m_C;
}

類物件作為類成員

c++類中的成員可以是另一個類的物件,我們稱該成員為 物件成員

class A
{
    
};

class B
{
	A a;
}

B類中有物件A作為成員,A


靜態成員

靜態成員就是在成員變數和成員函式前加上關鍵字static,稱為靜態成員

靜態成員分為:

  • 靜態成員變數

    • 所有物件共享同一份資料
    • 在編譯階段分配記憶體
    • 類內宣告,類外初始化
  • 靜態成員函式

    • 所有物件共享一個函式
    • 靜態成員函式只能訪問靜態成員變數
class Person
{
public:   
    //靜態成員變數
    //編譯階段就分配記憶體
    static int m_A;
private:
    static int m_B;//靜態成員變數也有訪問許可權
};
//類內宣告,類外初始化操作
int Person:: m_A = 100;

void test01()
{
    Person p;
    cout<< p.m_A <<endl;//100
    Person p2;
    p2.m_A = 200;
    cout<< p.m_A <<endl;//200
    //所有物件都共享同一份資料
}

void test02()
{
    //靜態成員變數不專屬於某個物件,所有物件都共享同一份資料
    //因此靜態成員變數有兩種訪問方式:
    //1.透過物件進行訪問
    Person p;
    cout<< p.m_A <<endl;
    //2.透過類名進行訪問
    cout<< Person::m_A<<endl;
}
class Person
{
public:
    static void func()
    {
        cout <<"靜態成員函式呼叫"<< endl;
    }
};

void test01()
{
    //兩種訪問方式
    //透過物件訪問
    Person p;
    p.func();
    //透過類名訪問
    Person::func();
}

C++物件模型和this指標

成員變數和成員函式分開儲存

在c++中,類內的成員變數和成員函式分開儲存

只有非靜態成員變數才屬於類的物件上

class Person
{

};

class Human
{
public:
    int m_A;//非靜態成員變數	屬於類的物件上的資料
    static int m_B;//靜態成員變數	不屬於類物件上
    void func(){}//非靜態成員函式	不屬於類物件上
    static void func2(){}//靜態成員函式	不屬於類物件上
};

void test01()
{
    Person p;
    //空物件佔用記憶體空間為:1 (byte)
    //C++編譯器會給每個空物件也分配一個位元組空間,是為了區分空物件佔記憶體的位置
    //每個空物件也應該有一個獨一無二的記憶體地址
    cout << "size of p = " << sizeof(p) << endl;
}

void test02()
{
    Human p1;
    //佔4 (byte) -> 非靜態成員變數(int)所佔的記憶體
    //因為靜態成員變數、非靜態成員函式、靜態成員函式不在類的物件上所以分開儲存
    cout << "size of p = " << sizeof(p1) << endl;
}

this指標概念

c++中成員變數和成員函式是分開儲存的

每一個非靜態成員函式只會產生一份函式例項,也就是說多個同型別的物件會公用同一份程式碼

那麼問題是:這一塊程式碼是如何區分哪個物件呼叫自己的呢?

c++透過提供特殊的物件指標,this指標來解決上述問題。this指標指向被呼叫的成員函式所屬的物件

this指標是隱含每一個非靜態成員函式內的一種指標

this指標不需要定義,直接使用即可

this指標的本質是指標常量,指標的指向是不可修改的

this指標的用途:

  • 當形參和成員變數同名時,可用this指標來區分
  • 在類的非靜態成員函式中返回物件本身,可使用 return *this
class Person
{
public:
    Person(int age)
    {
        age = age;//形參和成員屬性名字相同,編譯器誤以為他倆是同一個東西
    	
        //this指標指向的是被呼叫的成員函式(這裡呼叫的是建構函式)所屬的物件(這裡的物件是p1)
        this->age = age;//使用this指標可解決名稱衝突
    }
    
    //要返回本體的話須加引用
    Person& PersonAddAge(Person &p)
    {
        this->age += p.age;
        //this指向p2的指標,而*this指向的就是p2的本身
        return *this;
    }
    
    int age;//應該使用別的規範名稱,如m_Age
};
//1.解決名稱衝突
void test01()
{
    Person p1(18);
    cout << "p1的年齡是" << p1.age << endl;//並非18
}
//2.返回物件本身用*this
void test02()
{
    Person p1(10);
    
    Person p2(10);
    //返回值為物件本身,即p2 = p2.PersonAddAge(p1)
    //鏈式程式設計思想
    p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
    cout << "p2的年齡是" << p2.age << endl;//
}


空指標訪問成員函式

c++中空指標也是可以呼叫成員函式的,但是也要注意有沒有用到this指標

如果用到this指標,需要加以判斷保證程式碼的健壯性

//空指標呼叫成員函式
class Person
{
public:
    void showClassName()
    {
        cout << "this is Person class" << endl;
    }
    
    void showPersonAge()
    {
        //報錯的原因是傳入的指標為NULL
        /*解決措施:
        if (this == NULL)
        {
        	return;
        }
        */
        cout << "age = " << this->m_Age << endl;
    }
    
    int m_Age;
};

void test01
{
    Person * p = null;
    
    p->showClassName();//不報錯
    
    p->showPersonAge();//報錯,因為呼叫了成員屬性,但指標為空指標
}

const修飾成員函式

常函式:

  • 成員函式後加const後我們稱這個函式為常函式
  • 常函式內不可以修改成員屬性
  • 成員屬性宣告時加關鍵字mutable後,在常函式中依然可以修改

常物件:

  • 宣告物件前加const稱該物件為常物件
  • 常物件只能呼叫常函式
//常函式
class Person
{
public:
    
    //在成員函式後面加const,修飾的是this指向,讓this指標指向的值也無法修改
    void showPerson() const
    {
        //this->m_A = 100;	不可修改
        this->m_B = 100;
    }
    
    int m_A;
    mutable int m_B;//特殊變數,即使在常函式中,也可以修改這個變數
}

void test02()
{
    const Person p; //在物件前加const,變為常物件
    //p.m_A = 100; 報錯
    p.m_B = 100; //m_B是特殊值,在常物件中可修改
    //常物件只能呼叫常函式
    p.showPerson();
    //p.func();	//報錯	常物件不可以呼叫普通成員函式,因為普通成員函式可以修改屬性
}

友元

生活中,家裡(class)有客廳(public)也有臥室(private),客廳所有客人都能進,但是臥室除了好友之外的普通客人不能進

在程式裡,有些私有屬性也想讓類外特殊的一些函式或者類進行訪問,就需要用到友元的技術

友元的目的就是讓一個函式或者類訪問另一個類中私有成員

友元的關鍵詞為friend

友元的三種實現

  • 全域性函式做友元
  • 類做友元
  • 成員函式做友元

全域性函式做友元

class Building
{
    //友元宣告
    friend void goodFriend(Building &building);
public:
    Building()
    {
        m_SittingRoom = "客廳";
        m_BedRoom = "臥室";
    }
    
public:
    string m_SittingRoom;
    
private:
    string m_BedRoom;
};

//全域性函式
void goodFriend(Building &building)
{
    cout << "猴米全域性函式正在訪問:"<< building->m_BedRoom/*private*/ <<endl;
}

void test01()
{
    Building building;
    goodFriend(&building);//成功訪問
}

類做友元

class Building;
class GoodFriend
{
public:
    GoodFriend();
    
    void visit(); //參觀函式	訪問Building類中元素
    
    Building * building;
};

class Building
{
    //友元宣告
    friend class GoodFriend;
    
public:
    Building();
public:
    string m_SittingRoom;
    
private:   
    string m_BedRoom;
};

//類外寫成員函式
Building::Building()
{
        m_SittingRoom = "客廳";
        m_BedRoom = "臥室";
}
GoodFriend::GoodFriend()
{
    //建立建築物物件
	building = new Building;
}

void GoodFriend::visit()
{
    cout << "猴米類正在訪問:" << building->m_BedRoom << endl;
}

void test01()
{
    GoodFriend gf;
    gf.visit();//呼叫成功
}

成員函式做友元

class Building;
class GoodFriend
{
public:
    GoodFriend();
    
    void visit01(); //讓visit01函式可以訪問Building類的私有成員
    void visit02(); //讓visit02函式不可以訪問Building類的私有成員
    
    Building * building;
};

class Building
{
    //友元宣告
    friend void GoodFriend::visit01();
    
public:
    Building();
public:
    string m_SittingRoom;
    
private:   
    string m_BedRoom;
};

//類外寫成員函式
Building::Building()
{
        m_SittingRoom = "客廳";
        m_BedRoom = "臥室";
}
GoodFriend::GoodFriend()
{
    //建立建築物物件
	building = new Building;
}

void GoodFriend::visit01()
{
    cout << "猴米類正在訪問:" << building->m_BedRoom << endl;
}

void GoodFriend::visit01()
{
    cout << "猴米類正在訪問:" << building->m_BedRoom << endl;//報錯
}


運算子過載

運算子過載的概念:對已有的運算子重新進行定義,賦予其另一種功能,以適應不同的資料型別


加號運算子過載

作用:實現兩個自定義資料型別相加的運算

Tips:

  • 對於內建的資料型別的表示式的運算子是不可能改變的
  • 不要濫用運算子過載
class Person
{
public:
    
    //成員函式過載+號
    Person operator+(Person &p)
    {
        Person temp;
        temp.m_A = this->m_A + p.m_A;
        temp.m_B = this->m_B + p.m_B;
        return temp;
    }
    
    int m_A;
    int m_B;
};

//全域性函式過載+號
Person operator+(Person &p1, Person &p2)
{
	Person temp;
	temp.m_A = this->m_A + p.m_A;
	temp.m_B = this->m_B + p.m_B;
	return temp;
}

void test01
{
    Person p1;
    p1.m_A = 10;
    p1.m_B = 10;
    Person P2;
    p2.m_A = 10;
    p2.m_B = 10;
    
    //成員函式本質呼叫
    //Person p3 = p1.operator+(p2);
    Person p3 = p1 + p2;//簡化
    
    cout << "p3.m_A = " << p3.m_A << endl;
    cout << "p3.m_B = " << p3.m_B << endl;
}

左移運算子過載

作用:可以輸出自定義資料型別

class Person
{
    friend ostream& operator<<(ostream &cout,Person &p);
public:
    Person(int a, int b)
    {
        m_A = a;
        m_B = b;
    }
private:
    //不會利用成員函式過載<<運算子,因為無法實現cout在左側
    int m_A;
    int m_B;
};

//只能利用全域性函式過載左移運算子
ostream& operator<<(ostream &cout,Person &p)
{
    cout << "m_A = " << p.m_A << " m_B = " << p.m_B;
    return cout;
}

void test01()
{
    Person p(10,10);

    cout << p << endl;
}
int main() {
    cout << "Hello, World!" << endl;
    test01();
    return 0;
}

遞增 / 遞減運算子過載

作用:透過過載遞增 / 遞減運算子,實現自己的整形資料

//過載遞增運算子

//自定義整型
class MyInteger
{
    friend ostream & operator<<(ostream& cout, MyInteger myint);
public:
    MyInteger()
    {
        m_Num = 0;
    }

//過載前置++運算子
//返回引用是為了一直對一個資料進行遞增操作
    MyInteger& operator++()
    {
        //先進行++運算
        m_Num++;

        //再將自身返回
        return *this;
    }

//過載後置++運算子
//int代表佔位引數,可用於區分前置和後置遞增
    MyInteger operator++(int)
    {
        //先 記錄當時結果
        MyInteger temp = *this;
        //後 遞增
        m_Num++;
        //最後將記錄結果做返回
        return temp;
    }

    //過載前置--運算子
//返回引用是為了一直對一個資料進行遞減操作
    MyInteger& operator--()
    {
        //先進行--運算
        m_Num--;

        //再將自身返回
        return *this;
    }

//過載後置--運算子
//int代表佔位引數,可用於區分前置和後置遞減
    MyInteger operator--(int)
    {
        //先 記錄當時結果
        MyInteger temp = *this;
        //後 遞減
        m_Num--;
        //最後將記錄結果做返回
        return temp;
    }

private:
    int m_Num;
};

ostream & operator<<(ostream& cout, MyInteger myint)
{
    cout << myint.m_Num;
    return cout;
}


void test01()
{
    MyInteger myint;		    //0
    cout << ++myint << endl;	//1
    cout << myint++ << endl;	//1
    cout << myint << endl;		//2
    
    cout << --myint << endl;	//1
    cout << myint-- << endl;	//1
    cout << myint << endl;		//0

}

賦值運算子過載

c++編譯器至少給一個類新增4個函式

  1. 預設建構函式(無參,函式體為空)
  2. 預設解構函式(無參,函式體為空)
  3. 預設複製建構函式,對屬性進行值複製
  4. 賦值運算子operator=,對屬性進行值複製

如果類中有屬性指向堆區,做賦值操作時也會出現深淺複製問題

//賦值運算子過載

class Person
{
public:

    Person(int age)
    {
        m_Age = new int(age);
    }

    ~Person()
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
    }

    //過載 賦值運算子
    Person& operator=(Person &p)
    {
        //應該先判斷是否有屬性在堆區,如有,應先釋放乾淨再進行深複製
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }

        //深複製
        m_Age = new int(*p.m_Age);

        //返回物件本身
        return *this;
    }

    int *m_Age;
};

void test01()
{
    Person p1(10);
    Person p2(20);
    Person p3(30);

    p3 = p2 = p1;    //賦值操作

    cout << "p1's age = " << *p1.m_Age << endl;

    cout << "p2's age = " << *p2.m_Age << endl;

    cout << "p3's age = " << *p3.m_Age << endl;
}

關係運算子過載

作用:過載關係運算子,可以讓兩個自定義型別物件進行對比操作

//關係運算子過載

class Person
{
public:

    Person(string name, int age)
    {
        m_Name = name;
        m_Age = age;
    }

    //過載==號
    bool operator==(Person &p)
    {
        if (this->m_Name == p.m_Name)
        {
            return true;
        }
        return false;
    }

    //過載!=號
    bool operator!=(Person &p)
    {
        if (this->m_Name != p.m_Name)
        {
            return true;
        }
        return false;
    }

    string m_Name;
    int m_Age;
};

void test01()
{
    Person p1("Tom",18);
    Person p2("Tom",18);

    if (p1 == p2)
    {
        cout << "p1 == p2" << endl;
    } else{
        cout << "p1 != p2" << endl;
    }
}

函式呼叫運算子過載

  • 函式呼叫運算子()也可以過載
  • 與哦於過載後使用的方式非常像函式的呼叫,因此稱為仿函式
  • 仿函式沒有固定寫法,非常靈活
//函式呼叫運算子過載
class MyPrint
{
public:

    //過載函式呼叫運算子
    void operator()(string test)
    {
        cout << test <<endl;
    }
};

class MyAdd
{
public:
    int operator()(int num1, int num2)
    {
        return num1 + num2;
    }
};
void test01()
{
    MyPrint myPrint;
    myPrint("hello world");

    //匿名物件呼叫
    int a = MyAdd()(10,20);
    cout << "a = " << a << endl;
}

繼承

繼承是物件導向三大特性之一

繼承允許我們依據另一個類來定義一個類,有重用程式碼功能和提高執行效率的效果。

當建立一個類時,不必重新編寫新的資料成員和成員函式,只需指定新建的類繼承了一個已有的類的成員即可。這個已有的類稱為基類,新建的類稱為派生類。也可以稱為父類子類

img

基本語法

class 子類 : 繼承方式 父類

class A : public B

以上圖為例

// 基類
class Animal {
public:    
    void eat()
    {
        cout << "吃!" << endl;
    }
    void sleep()
    {
        cout << "zzzZZZ" << endl;
    }
    
    string name;
    int age;
};


//派生類
class Dog : public Animal {
public:	
    void bark()
    {
        cout << "汪汪汪!" << endl;
    }
};

子類中的成員,包含兩大部分:

  • 從父類繼承過來的
  • 自己增加的成員

從父類繼承過來的表現其共性,而新增的成員體現了其個性。

一個子類繼承了所有的基類方法,但下列情況除外:

  • 父類的構造函式、解構函式和複製構造函式。
  • 父類的過載運算子
  • 父類的友元函式

繼承方式

繼承方式一共有三種:

  • 公共繼承(public):當一個類派生自公有基類時,基類的公有成員也是派生類的公有成員,基類的保護成員也是派生類的保護成員,基類的私有成員不能直接被派生類訪問,但是可以透過呼叫基類的公有保護成員來訪問。
  • 保護繼承(protected):當一個類派生自保護基類時,基類的公有保護成員將成為派生類的保護成員。
  • 私有繼承(private):當一個類派生自私有基類時,基類的公有保護成員將成為派生類的私有成員。

我們幾乎不使用 protectedprivate 繼承,通常使用 public 繼承。

構造和析構順序

在繼承後,派生類也會繼承基類的建構函式與解構函式

那麼他們之間的呼叫順序如下:

父類構造——子類構造——子類析構——父類析構

繼承同名成員處理方式

當子類與父類出現同名的成員時:

  • 如訪問子類同名成員,直接訪問即可
  • 如訪問父類同名成員,需要加作用域(類名::)
class Base{
public:
    Base()
    {
        m_A = 100;
    }
    
    void func()
    {
        cout << "Base" << endl;
    }
    
    int m_A;
};

class Son:public Base{
public:
    Son()
    {
        m_A = 200;
    }
    
    void func()
    {
        cout << "Son" << endl;
    }
    
    int m_A;
};

//同名資料成員處理
void test01()
{
    Son s;
    cout << "Son下  m_A = " << s.m_A << endl;//200
    cout << "Base下 m_A = " << s.Base::m_A << endl;//100
}
//同名成員函式處理
void test02()
{
    Son s;
    
    s.func();//Son
    s.Base::func();//Base
}

總結:

  • 子類物件可以直接訪問到子類中同名成員
  • 子類物件加作用域可以訪問到父類同名函式
  • 當子類和父類擁有同名的成員函式,子類會隱藏父類中同名成員函式,但加作用域可以訪問到

多繼承

多繼承即一個子類可以有多個父類,它繼承了多個父類的特性。

C++ 類可以從多個類繼承成員,語法如下:

class <派生類名>:<繼承方式1><基類名1>,<繼承方式2><基類名2>,…
{
<派生類類體>
};

c++實際開發中不建議使用多繼承,因為可能會引發多個父類中有同名成員導致作用域管理混亂

示例如下:

// 形狀基類 Shape
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};
 
// 刷漆基類 PaintCost
class PaintCost 
{
   public:
      int getCost(int area)
      {
         return area * 70;
      }
};
 
// 派生類
class Rectangle: public Shape, public PaintCost
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};
 
int main(void)
{
   Rectangle Rect;
   int area;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
 
   area = Rect.getArea();
   
   // 輸出物件的面積
   cout << "Total area: " << Rect.getArea() << endl;
 
   // 輸出總花費
   cout << "Total paint cost: $" << Rect.getCost(area) << endl;
 
   return 0;
}

菱形繼承

菱形繼承概念:

  • 兩個派生類繼承同一個基類
  • 又有某個類同時繼承著兩個派生類
  • 這種繼承被稱為菱形繼承,或者鑽石繼承

典型的菱形繼承案例:

菱形繼承問題:

  1. 老虎繼承了動物的資料,獅子同樣也繼承了動物的資料;當獅虎獸使用資料時,就會產生二義性
    • 當菱形繼承,兩個父類擁有相同資料時,需要加以作用域區分
  2. 獅虎獸繼承自動物的資料繼承了兩份,但我們應該清楚,這份資料我們只需要一份即可,導致資源浪費
    • 利用虛繼承來解決(在繼承方式前加上關鍵字virtual:class Tiger : virtual public Animal,基類Animal變成虛基類)

多型

多型的基本概念

多型是c++物件導向的三大特性之一

多型按字面的意思就是多種形態。當類之間存在層次結構,並且類之間是透過繼承關聯時,就會用到多型。呼叫成員函式時,會根據呼叫函式的物件的型別來執行不同的函式。

多型分為兩類:

  • 靜態多型:函式過載和運算子過載屬於靜態多型,服用函式名
  • 動態多型:派生類和虛擬函式來實現執行時的多型

靜態多型和動態多型區別:

  • 靜態多型的函式地址早繫結 —— 編譯階段確定函式地址
  • 動態多型的函式地址晚繫結 —— 執行階段確定函式地址
class Animal
{
public:
    void speak()
    {
        cout<<"動物在說話"<<endl;
    }
};

class Cat :public Animal
{
public:
    void speak()
    {
        cout<<"meow,meow,meow"<<endl;
    }
};

//執行說話的函式
//地址早繫結	在編譯階段確定函式地址
//如果想執行讓貓說話,那麼這個函式地址就不能提前繫結,需要晚繫結
void doSpeak(Animal &animal)
{
    animal.speak();
}

void test01()
{
    Cat cat;
    doSpeak(cat);//動物在說話
}
class Animal
{
public:
    //虛擬函式	實現晚繫結
    virtual void speak()
    {
        cout<<"動物在說話"<<endl;
    }
};

class Cat :public Animal
{
public:
    //重寫	函式返回值型別、函式名、引數列表完全相同
    void speak()
    {
        cout<<"meow,meow,meow"<<endl;
    }
};

//執行說話的函式
void doSpeak(Animal &animal)
{
    animal.speak();
}

void test01()
{
    Cat cat;
    doSpeak(cat);//meow
}

總結:

動態多型滿足條件:

  1. 有繼承關係
  2. 子類重寫父類的虛擬函式

多型使用條件

  • 父類指標或引用指向子類物件

多型案例(一)——計算器類

案例描述:

分別用普通寫法和多型技術,設計實現兩個運算元進行的計算器類

多型的優點:

  • 程式碼組織結構清晰
  • 可讀性強
  • 利於前期和後期的擴充套件以及維護
//普通實現
class Calculator
{
public:
    Calculator(int num1, int num2)
    {
        m_Num1 = num1;
        m_Num2 = num2;
    }

    int getResult(string oper)
    {
        if (oper == "+") return m_Num1 + m_Num2;
        else if (oper == "-") return m_Num1 - m_Num2;
        else if (oper == "*") return m_Num1 * m_Num2;
        //(普通寫法下)如果想擴充套件新功能,就需要修改原始碼
        //在真實開發中,我們提倡開閉原則:對擴充套件進行開放,對修改進行關閉
    }


    int m_Num1;//運算元1
    int m_Num2;//運算元2
};


void test01()
{
    //建立一個計算器物件
    Calculator c(10,10);

    cout << c.m_Num1 << "+" << c.m_Num2 << "=" << c.getResult("+") << endl;
    cout << c.m_Num1 << "-" << c.m_Num2 << "=" << c.getResult("-") << endl;
    cout << c.m_Num1 << "*" << c.m_Num2 << "=" << c.getResult("*") << endl;
}

//利用多型實現計算器
//實現計算器抽象類
class AbstractCalculator
{
public:

    virtual int getResult()
    {
        return 0;
    }

    int m_Num1;//運算元1
    int m_Num2;//運算元2
};

//加法計算器類
class AddCaculator :public AbstractCalculator
{
public:
    virtual int getResult()
    {
        return m_Num1 + m_Num2;
    }
};

//減法計算器類
class SubCaculator :public AbstractCalculator
{
public:
    virtual int getResult()
    {
        return m_Num1 - m_Num2;
    }
};

//乘法計算器類
class MulCaculator :public AbstractCalculator
{
public:
    virtual int getResult()
    {
        return m_Num1 * m_Num2;
    }
};

void test02()
{
    //多型使用條件:父類指標/引用指向子類物件

    //加法運算
    AbstractCalculator * abc = new AddCaculator;//父類指標
    abc->m_Num1 = 100;
    abc->m_Num2 = 100;

    cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;
    //用完後記得銷燬
    delete abc;

    //減法運算
    abc = new SubCaculator;
    abc->m_Num1 = 100;
    abc->m_Num2 = 100;

    cout << abc->m_Num1 << "-" << abc->m_Num2 << "=" << abc->getResult() << endl;
    //用完後記得銷燬
    delete abc;

    //乘法運算
    abc = new MulCaculator;
    abc->m_Num1 = 100;
    abc->m_Num2 = 100;

    cout << abc->m_Num1 << "*" << abc->m_Num2 << "=" << abc->getResult() << endl;
    //用完後記得銷燬
    delete abc;
}

純虛擬函式和抽象類

在多型中,通常父類中虛擬函式的實現是無意義的,主要是呼叫子類重寫的內容(例如計算器類案例中的抽象計算器類)

因此可以將虛擬函式改為純虛擬函式

純虛擬函式語法:virtual 返回值型別 函式名(引數列表)= 0;

當類中有了純虛擬函式,這個類也成為抽象類

抽象類特點:

  • 無法例項化物件
  • 子類必須重寫抽象類中的純虛擬函式,否則也屬於抽象類
class AbstractCalculator//有純虛擬函式的類就是抽象類
{
public:
	//純虛擬函式
    virtual int getResult() = 0;

    int m_Num1;//運算元1
    int m_Num2;//運算元2
};

//加法計算器類
class AddCalculator : public AbstractCalculator
{
public:
    virtual int getResult()
    {
        return m_Num1 + m_Num2;
    }
};

void test01()
{
    //多型使用條件:父類指標/引用指向子類物件

    //加法運算
    AbstractCalculator * abc = new AddCalculator;//父類指標
    abc->m_Num1 = 100;
    abc->m_Num2 = 100;

    cout << abc->m_Num1 << "+" << abc->m_Num2 << "=" << abc->getResult() << endl;
    //用完後記得銷燬
    delete abc;

    AbstractCalculator abc;//報錯	抽象類無法例項化物件
    new AbstractCalculator;//報錯 堆區或棧區都不行
}

多型案例(二)——飲品類

案例描述:

製作飲品的大致流程為:煮水 - 沖泡 - 倒入杯中 - 加入輔料

利用多型技術實現本案例,提供抽象製作飲品基類,提供子類製作咖啡和茶葉

class AbstractDrinking
{
public:
    //煮水
    virtual void Boil() = 0;
    //沖泡
    virtual void Brew() = 0;
    //倒入杯中
    virtual void PourInCup() = 0;
    //加入輔料
    virtual void PutSth() = 0;
    //製作飲品
    void makeDrink()
    {
        Boil();
        Brew();
        PourInCup();
        PutSth();
    }
};

//製作咖啡
class Coffee :public AbstractDrinking
{
    //煮水
    virtual void Boil(){
        cout << "用阿拉斯加山脈泉水煮水" << endl;
    }
    //沖泡
    virtual void Brew(){
        cout << "沖泡咖啡" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "倒入咖啡杯中" << endl;
    }
    //加入輔料
    virtual void PutSth(){
        cout << "加入方糖和牛奶" << endl;
    }
};

//製作茶葉
class Tea :public AbstractDrinking
{
    //煮水
    virtual void Boil(){
        cout << "用長白山泉水煮水" << endl;
    }
    //沖泡
    virtual void Brew(){
        cout << "沖泡茶葉" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "倒入茶杯中" << endl;
    }
    //加入輔料
    virtual void PutSth(){
        cout << "加入茉莉花" << endl;
    }
};
//製作函式
void doWork(AbstractDrinking * abs)
{
    abs->makeDrink();
    delete abs; //釋放堆區資料
}

void test01()
{
    //製作咖啡
    doWork(new Coffee);
    cout << "----------------" << endl;
    //製作茶葉
    doWork(new Tea);
}

虛析構和純虛析構

多型使用時,如果子類中有屬性開闢到堆區,那麼父類指標在釋放時無法呼叫到子類的析構程式碼

解決方式:將父類中的解構函式改為虛析構或者純虛析構

虛析構或者純虛析構共性:

  • 可以解決父類指標釋放子類物件
  • 都需要有具體的函式實現

虛析構或者純虛析構區別:

  • 如果是純虛析構,該類屬於抽象類,無法例項化物件

虛析構語法:

virtual ~類名( ) { }

純虛析構語法:

virtual ~類名( ) = 0 ;

類名::~類名(){ }

總結:

  1. 虛析構或純虛析構就是用來解決父類指標釋放子類物件
  2. 如果子類中沒有堆區資料,可以不寫為虛析構或純虛析構
  3. 擁有純虛解構函式的類也屬於抽象類

多型案例(三)—— 電腦組裝

案例描述:

電腦主要組成部分為CPU(計算)、顯示卡(顯示)和記憶體條(儲存)

將每個零件封裝成抽象基類,並且提供不同的廠商生產的不同的零件,例如Intel和AMD廠商

建立電腦類提供讓電腦工作的函式,並且呼叫每個零件工作的介面

測試時組裝三臺不同的電腦進行工作

//抽象類
//CPU
class CPU
{
public:
    //抽象計算函式
    virtual void Calculate() = 0;
};
//GPU
class GPU
{
public:
    //抽象顯示函式
    virtual void Display() = 0;
};
//記憶體條
class Memory
{
public:
    //抽象儲存函式
    virtual void Storage() = 0;
};

//電腦類
class Computer
{
public:
    Computer(CPU * cpu, GPU * gpu, Memory * mem)
    {
        m_cpu = cpu;
        m_gpu = gpu;
        m_mem = mem;
    }
    //提供工作的函式
    void work()
    {
        //呼叫零件介面
        m_cpu->Calculate();
        m_gpu->Display();
        m_mem->Storage();
    }
    //提供解構函式來釋放3個建立在堆區的電腦零件
    ~Computer()
    {
        if (m_cpu != NULL)
        {
            delete m_cpu;
            m_cpu = NULL;
        }
        if (m_gpu != NULL)
        {
            delete m_gpu;
            m_gpu = NULL;
        }
        if (m_mem != NULL)
        {
            delete m_mem;
            m_mem = NULL;
        }
    }

private:
    CPU * m_cpu;//CPU的零件指標
    GPU * m_gpu;//GPU的零件指標
    Memory * m_mem;//記憶體條的零件指標
};

//具體廠商零件
//Intel
class IntelCPU :public CPU
{
public:
    virtual void Calculate()
    {
        cout << "Intel CPU is working!" << endl;
    }
};

class IntelGPU :public GPU
{
public:
    virtual void Display()
    {
        cout << "Intel GPU is working!" << endl;
    }
};

class IntelMemory :public Memory
{
public:
    virtual void Storage()
    {
        cout << "Intel Memory is working!" << endl;
    }
};

//AMD
class AMDCPU :public CPU
{
public:
    virtual void Calculate()
    {
        cout << "AMD CPU is working!" << endl;
    }
};

class AMDGPU :public GPU
{
public:
    virtual void Display()
    {
        cout << "AMD GPU is working!" << endl;
    }
};

class AMDMemory :public Memory
{
public:
    virtual void Storage()
    {
        cout << "AMD Memory is working!" << endl;
    }
};

void test01()
{
    //no.1 pc
    CPU * intelCpu = new IntelCPU;
    GPU * intelGpu = new IntelGPU;
    Memory * intelMem = new IntelMemory;

    Computer * pc1 = new Computer(intelCpu,intelGpu,intelMem);
    pc1->work();
    delete pc1;

    //no.2 pc
    CPU * amdCpu = new AMDCPU;
    GPU * amdGpu = new AMDGPU;
    Memory * amdMem = new AMDMemory;

    Computer * pc2 = new Computer(amdCpu,amdGpu,amdMem);
    pc1->work();
    delete pc2;
}

相關文章