樣板、繼承與多型

專案建置[8]

AaW

目錄

  • Function Template
  • Class Template
  • 繼承關係
  • 虛擬函數

注意事項:

今天教的內容雖然是物件導向的精髓,但屬於一般高中生都不太會用到的毒瘤語法,盡量聽懂就好

(我們資訊老師也不會繼承)

函式模板

Function Template

什麼是模板(template)

寫程式一定會用到變數

而有時你的變數類型不影響程式邏輯

但是 C++ 卻必須指定變數類型

#include <iostream>

int add_int(int a, int b) {
    return a + b;
}

double add_double(double a, double b) {
    return a + b;
}

int main() {
    std::cout << add_int(2, 3) << '\n';        // 5
    std::cout << add_double(4.5, 6.7) << '\n'; // 11.2
    return 0;
}

Template 語法

Template 就是讓你使用「萬用型別」的工具

就好像代數一樣

需要用的時候再代入要用的型別就行了

#include <iostream>

template<typename T> // 設 T = 型別名稱
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add<int>(2, 3) << '\n';        // 5
    std::cout << add<double>(4.5, 6.7) << '\n'; // 11.2
    return 0;
}

Template 多參數

Template 也可以有兩個以上參數

也就是可以指定多個型別

用在不同的地方上

#include <iostream>
#include <string>

template<typename T, typename U>
void a_and_b(T a, U b) {
    std::cout << a << " and " << b << '\n';
}

int main() {
    std::cout << a_and_b<int, char>(3, 'G') << '\n';        // 3 and G
    std::cout << a_and_b<std::string, float>("Hello", 3.14) // Hello and 3.14
    return 0;
}

類別模板

Class Template

類別模板使用情境

Class 當然也可以用 Template

而且應該更有機會用到

C++ 就有一大堆型別都是模板

#include <vector>
#include <pair>
#include <string>

int main() {
    std::vector<int> u = {1, 2, 3};
    std::vector<double> v = {0.1, 0.2, 0.3};
    std::pair<std::string, double> p("Aaron", 12.3);
    std::pair<int, std::string> q(42, "Justin");
    return 0;
}

稱為 STL(Standard Template Library)

類別模板語法

#include <iostream>
template<typename T>
class Number {
private:
    T _value;
public:
    Number(T value) : _value(value) {}
    ~Number() {}
    T get_value() { return this->_value; }
    void set_value(T new_value) { this->_value = new_value; }
    void print() { std::cout << this->_value << '\n'; }
};

int main() {
    Number<int> a(42);
    a.print(); // 42
    a.set_value(13);
    a.print() // 13
    Number<double> b(3.14);
    b.print() // 3.14
    return 0;
}

注意!

我們之前一直有強調

寫 class 時要把宣告與實作分開

原因是實作可以先編譯好

要用時再進行連結,以節省編譯時間

但是 class template 沒辦法這樣做

而這跟 template 的原理有關

Template 原理

Template 的本質其實是一種巨集(字串替換)

它在你要使用這個 class 時

才根據你指定的型別臨時生成定義

反過來說,如果這個 class 還沒被拿來用

那它根本就不存在

也不可能被編譯(因為型別不確定)

對專案架構的影響

一個專案最後打包成的函式庫檔案

就是編譯好的實作內容

但當宣告與實作不能分開時

就只能將兩者都放在一個標頭檔裡

而原本的標頭檔與實作檔的副檔名分別為 .h 與 .cpp

那這種包含實作的標頭檔的副檔名就會用 .hpp

朋友

friend

AaW沒有的東西

如果A把B當朋友

B 就可以取得 A 心中的想法private成員

B是什麼?

B可以是一個函數或是一個class

如果是函數,此函數必須在class內進行friend宣告

 如果是類別,此類別B必須在A內定義友誼關係

friend function 範例

class Phone{
public:
    /*
     * 這邊省略,與上面相同,但不包含 equal
     */
    friend bool equal(Phone& p1, Phone& p2); // 先定義,不實作
private:
    string type; // 隸屬公司
    string phone_number;
};

// 我們自己實作 equal
bool equal(Phone& p1, Phone& p2){
    if(p1.type == p2.type) return true;
    else return false;
}
int main(){
    Phone p1("中華電信", "0912345678");
    Phone p2("台灣大哥大", "0987654321");
    equal(p1, p2);
    return 0;
}

friend class 範例

直接在A的public裡面加上這一行

friend class B;

繼承

又回到了class

  • 繼承是物件導向重要觀念之一
  • 從原本的類別衍生出新的類別
  • 這個新的類別會繼承舊類別所有的成員、特性等等
  • 繼承體系的優點:
    • 維持物件封裝特性
    • 不用修改原類別

基礎類別

base class

衍生類別

derived class

這樣畫箭頭比較直觀

但其實這樣畫才是對的

繼承具體來說會繼承什麼?

成員變數與成員函數

全部的
嗎?

衍生類別的建構子、解構子、朋友、 = 運算子要重新定義

繼承的權限表

  • 還記得 public、protected和public這三種權限嗎
  • base class 在宣告時每個成員都屬於這三種中的一種
  • 而繼承時,也分別有三種模式可以選擇
  • 衍生類別的成員變數又有三種權限
  • 所以......

繼承方式

父類別
成員權限

單一繼承

語法時間

一次只繼承一個父類別

class 衍生類別 : 繼承模式 基礎類別 {
	// 類別的定義
};
#include <iostream>

using namespace std;

class car {
    public:     // 基礎類別中的成員函示宣告為public 
        void go()             
        {
            cout <<"汽車啟動了!"<< endl;
        }
        void stop()   
        {
            cout <<"汽車熄火了!"<<endl;
        }	
};
class freighter: public car
{};   //衍生類別用public繼承
       
int main() {
    freighter ft;
           
    ft.stop();
    cout<<"-------------------------------"<<endl;
    ft.go();   
    cout<<"-------------------------------"<<endl;
    // ft是freighter類別的一個物件
    // 因為繼承關係,可以使用car類別的go()和stop()函數
    return 0;    
}

public繼承範例

權限會保留為原本樣子

#include<iostream>

using namespace std;
	
// 宣告類別student
class student
{
	private:
		int age;
	protected:
		int lang;
	public:
		int math;
		student()  // 建構子
		{
			age=0;
			lang=0;
			math=0;
		}
		void set_age(int a1)
		{
			age=a1;
		}
		void show_age()
		{
			cout << "age=" << age << endl;	
		}
};
	
// 以 private型別自類別student繼承成為新的類別s1
class s1:private student
{
	public:
		void set_lang(int v3)
		{
             
			lang=v3;   // 可直接設定型別為protected的成員資料lang
			
		}
		void set_math(int v4)
		{   

			math=v4;   // 可直接設定型別為public的成員資料math
		}
		void setage(int v5)
		{
			// 因為無法直接存取private型別的成員資料age
			// 必須藉由呼叫set_age成員來設定成員資料age
			set_age(v5); 
		}
		void show_data()
		{
			// 因為無法直接存取private型別的成員資料age,必須
		    // 藉由呼叫show_age()成員函式來取得age值並在螢幕上顯示
			show_age();			// 可直接存取類別student內的protected型別的成員資料lang
			// 可直接存取類別student內的public型別的成員資料math
			cout << "lang=" << lang << endl;
			cout << "math=" << math << endl;
		}
};	
int main()
{
    // 宣告物件obj1
    s1 obj1;
    // 可藉由呼叫setage成員函式來呼叫類別student內public型別的成員函式set_age()
    obj1.setage(35); 
    // 可直接存取類別student內的protected型別的成員資料lang
    obj1.set_lang(100);
    // 可直接存取類別student內的public型別的成員資料math
    obj1.set_math(95);
    obj1.show_data();
		
    return 0;
}

private繼承範例

所有繼承下來的變數外部都無法呼叫

多重繼承

一次繼承很多個乾爹

class 衍生類別 : 繼承模式 基礎類別1,  繼承模式 基礎類別2, 繼承模式 基礎類別3  {
	// 類別的定義
};

繼承的實例

c++ stream類型的繼承

所以

cin 是一個 istream 型態的物件

cout 是一個 ostream 型態的物件

更精確來說,cin 的型態是 istream& (他是一個參考變數)

cout 的型態是 ostream& (他是一個參考變數)

知道這個可以幹嘛

我要來補幾百年前挖的坑了

class myClass{
    public:
    int a;
    int b;
    int c;
    friend std::ostream& operator << (std::ostream&, myClass);
};

// 運算子多載(定義<<運算子左邊為ostream&型態,右邊為myClass型態時要做的事)
std::ostream& operator << (std::ostream& out, const myClass& x) {
    out << x.a << " ";
    out << x.b << " ";
    out << x.c << "\n";
    
    return out; // 將ostream&回傳回去 --> 所以cout可以串連!
}

繼續回到繼承

衍生類別的建構子與解構子

建構時會先呼叫基礎類別的建構子

解構時會先呼叫衍生類別的解構子

#include<iostream>
using namespace std;

// 宣告類別stclass
class stclass
{
    public:
        stclass() // 建構子
        {
            cout << "呼叫基礎類別的建構子" << endl;
        }
        ~stclass() // 解構子
        {
            cout << "呼叫基礎類別的解構子" << endl;
        }
};
    // 宣告類別student,並以public型別自類別stclass繼承
class student :public stclass
{
    public:
        student()  // 建構子
        {
            cout << "呼叫衍生類別的建構子" << endl;
        }
        ~student() // 解構子
        {
               cout << "呼叫衍生類別的解構子" << endl;
        }
};
void call()
{
    student st1;
}
// 主函式
int main()
{

    call();//呼叫宣告物件st1的函數    
        
    return 0;
}

衍生類別的建構子與解構子

#include<iostream>
using namespace std;
// 宣告類別stclass
class stclass
{
    public:
        stclass() // 建構子
        {
            cout << "呼叫基礎類別的建構子" << endl;
        }
        ~stclass() // 解構子
        {
            cout << "呼叫基礎類別的解構子" << endl;
        }
};
    // 宣告類別student,並以public型別自類別stclass繼承
class student :public stclass
{
    public:
        student()  // 建構子
        {
            cout << "呼叫衍生類別的建構子" << endl;
        }
        ~student() // 解構子
        {
               cout << "呼叫衍生類別的解構子" << endl;
        }
};
void call()
{
    student st1;
}
// 主函式
int main()
{
        
    call();//呼叫宣告物件st1的函數    
        
    return 0;
}
呼叫基礎類別的建構子
呼叫衍生類別的建構子
呼叫衍生類別的解構子
呼叫基礎類別的解構子

結果:

多形與虛擬函數

先講一個厲害的東西

然後你有一個A型態的指針變數

A* pt;

那pt可以指向所有A的衍生類別

 

並且用pt->呼叫所有衍生類別繼承A的成員與方法

class A{
public:
    int x;
    void fun() {
        cout << "A's function\n";
    }
};

class B : public A{
    void hahaha() {
        cout << "B's function\n";
    }
};

int main() {
    B bbb; // class B 的變數
    A* pt = &B; // 合法
    cout << pt->x << endl;
    pt->fun();
}

所以啥事多形

我們常常會做一件神奇的事

我們會讓基礎類別和衍生類別有一樣名字的函數

這就叫做多形函數

但是他的實作和功能卻不同

因此你用基礎類別指標呼叫衍生類別時,會根據衍生類別的種類有不同種的結果

要怎麼做到多形函數

#include <iostream>
using namespace std;

class A{
    public:
    void fun() {
        cout << "This is fun in class A\n";
    }
};

class B: public A{
public:
	void fun() {
		 cout << "This is fun in class B\n";
	}
};

int main() {
	A* pt = new B;
	pt->fun(); // 這樣會輸出啥?
}
This is fun in class A

為什麼?

因為fun()已經被繫結到基礎類別的實作了!

所以我要怎麼做呢?

要把基礎類別裡面的函數宣告為虛擬函數!

虛擬函數的宣告方式:使用virtual關鍵字

運輸工具啟動
運輸工具停止
飛行器啟動
飛行器停止
汽車啟動
汽車停止
#include <iostream>

using namespace std;

class vehicle {              
    // 宣告基礎類別vehicle
    public:
        virtual void start() // 成員函數start
        {
            cout << "運輸工具啟動"<< endl;
        }
        virtual void stop()     // 成員函數stop
        {
            cout << "運輸工具停止"<< endl ;
        }
};
class aircraft: public vehicle {   
      // 宣告衍生類別aircraft
    public:
        virtual void start()  // 具有和基礎類別相同名稱的成員函數start
        {
            cout  << "飛行器啟動"<< endl;
        }
       virtual void stop()    // 具有和基礎類別相同名稱的成員函數stop
       {
            cout << "飛行器停止"<< endl;
       }	
};
class car: public vehicle {      
    // 宣告衍生類別car
    public:   
        virtual void start()  // 具有和基礎類別相同名稱的成員函數start
        {
            cout << "汽車啟動"<< endl;
        }
        virtual void stop()   // 具有和基礎類別相同名稱的成員函數stop
        {
            cout << "汽車停止"<< endl;
        }
};
int main()
{
    vehicle* ve = new vehicle();  // 基礎類別的指標
    aircraft af;
    car cr;
    ve->start();  // 呼叫其成員函數start()
    ve->stop();  // 呼叫其成員函數stop()
    delete ve;
    ve = &af;   // 將基礎類別指標指向衍生類別aircraft
    ve->start();  // 呼叫其成員函數start()
    ve->stop();  // 呼叫其成員函數stop()
    ve = &cr;   // 將基礎類別指標指向衍生類別car
    ve->start(); // 呼叫其成員函數start()
    ve->stop();  // 呼叫其成員函數stop()
      
    return 0;
}

純虛擬函數與抽象基礎類別

則此函數稱為純虛擬函數

virtual T funName(T1, T2) = 0;

如果有一個函數在基礎類別裡面不定義實作

在使用時,一定要在衍生類別進行定義

否則會出錯

純虛擬函數與抽象基礎類別

則此類別稱為抽象基礎類別

如果有一個基礎類別包含一個純虛擬函數

對於這一些類別一定要使用繼承體系來重新定義(override)虛擬函數

#include <iostream>

using namespace std;

class vehicle {              
    // 宣告基礎類別vehicle
    public:
        virtual void start()=0; // 純虛擬函數start
        virtual void stop()=0;     // 純虛擬函數stop
};
class aircraft: public vehicle {   
    // 宣告衍生類別aircraft
    public:
        virtual void start()        //宣告同名異式的成員函數start (其virtual關鍵字可省略)
        {
            cout  << "飛行器啟動"<< endl;
        }
        virtual void stop()       //宣告同名異式的成員函數stop (其virtual關鍵字可省略)
        {
            cout << "飛行器停止"<< endl;
        }	
};
class car: public vehicle {      
    // 宣告衍生類別car
    public:   
        virtual void start()       //宣告同名異式的成員函數start (其virtual關鍵字可省略)
        {
            cout << "汽車啟動"<< endl;
        }
        virtual void stop()       //宣告同名異式的成員函數stop (其virtual關鍵字可省略)
        {
            cout << "汽車停止"<< endl;
        }
};
int main()
{
    vehicle* ve ;//宣告基礎類別vehicle指標,抽象基礎類別不可實體化
    aircraft af;
    car cr;
        
    ve = &af;   // 將基礎類別指標指向衍生類別aircraft
    ve->start();  // 呼叫其成員函數start()
    ve->stop();  // 呼叫其成員函數stop()
    ve = &cr;   // 將基礎類別指標指向衍生類別car
    ve->start(); // 呼叫其成員函數start()
    ve->stop();  // 呼叫其成員函數stop()
      
    return 0;
}

噁心到極致

虛擬基礎類別

vehicle

car

aircraft

aircar

當aircar要呼叫vehicle的東西時

會發生錯誤

稱為模稜兩可(ambiguous)

這時候我們要進行虛擬繼承

class Derive: virtual public Base { };
#include <iostream>

using namespace std;

class vehicle {    
    // 宣告基礎類別vehicle
    public:
        void start()
        {
            cout << "運輸工具啟動"<< endl;
        }
        void shutdown()
        { 
            cout << "運輸工具熄火"<< endl;
        }
};
class aircraft: virtual public vehicle 
{      
    // 宣告虛擬基礎類別aircraft
    public:
        void fly()
        {
            cout << "飛行器飛行"<< endl;
        }
       void land()
        {
            cout << "飛行器著陸"<< endl;
        }
};
class car: virtual public vehicle 
{        
    // 宣告虛擬基礎類別car
    public:
        void go()
        {
            cout << "汽車啟動"<< endl;
        }
        void stop()
        {
            cout << "汽車熄火"<< endl;
        }
};
class aircar: public aircraft, public car {};  // 宣告衍生類別aircar

int main()
{
    aircar ac;
    ac.start();     // 衍生函數呼叫上兩層基礎類別vehicle的成員函數start
    ac.go();
    ac.fly();
    ac.land();
    ac.stop();
    ac.shutdown();      // 衍生函數呼叫上兩層基礎類別vehicle的成員函數stop
         
    return 0;
}

嗯,上完了

嗯,上完了

但是還有

嗯,上完了

但是還有

成發啦哈哈

成發的規則:

任何一個c++的程式碼

用到我們上課講過的東西都可以

然後要準備3分鐘左右的介紹