單例模式(Singleton Pattern)

實現單例模式的思路是:一個類別只有一個實例,並提供全域點存取此實例。

有些物件只需要一個實例,比如設定和登入的物件、和驅動程式溝通的物件、執行緒池等,單例模式可以在任何地方存取這個實體,像全域變數一樣方便,且需要時才建立物件,不像全域變數程式一開始就建好物件,造成可能的資源浪費。以下我們示範單例模式的使用,由於一次只會有一個實例產生,所以兩個instance有相同的位址,以下為程式碼。

#include <iostream>
#include <string>
using namespace std; 

class Singleton {
private:
    static Singleton *m_instance;
    Singleton(){}
public:
    static Singleton *getInstance(){
        if(m_instance==NULL){
            m_instance = new Singleton();
        }
        return m_instance;
    }
};
Singleton* Singleton::m_instance = NULL;

int main(){
    Singleton *instance1 = Singleton::getInstance();
    Singleton *instance2 = Singleton::getInstance();
    int addr1 = (int)instance1;
    int addr2 = (int)instance2;
    delete instance1;
    return 0;
}

單例模式在多執行緒的應用場合下要小心,當唯一實例尚未創建時,如果有兩個執行緒同時調用創建方法,可能各自創建了一個實例,這樣就有兩個實例被構造出來,違反了單例模式的原則,有以下三種解決方式:
1.對getInstance()這個函式同步化,不過會造成程式執行效率降低,如果需要頻繁運作此函式,可能需要重新考慮。
2.率先建立實例,而不用拖延實體化的作法,如以下所示:

class Singleton {
private:
    static Singleton *m_instance = new Singleton();
    Singleton(){}
public:
    static Singleton *getInstance(){
        return m_instance;
    }
};

3.利用雙重檢查上鎖,在getInstance()中減少使用同步化,檢查實例是否已經建立,沒建立的話才進行同步化,如此一來只有第一次會同步化,如以下所示:

class Singleton {
private:
    //volatile不會對m_instance進行編譯器最佳化,確保多執行緒處理m_instance是正確的
    volatile static Singleton *m_instance;
    Singleton(){}
public:
    static Singleton *getInstance(){
        if(m_instance==NULL){
            //下面進行同步化
            m_instance = new Singleton();
        }
        return m_instance;
    }
};

簡單工廠模式(Simple Factory Pattern)

簡單工廠模式算是最基本的工廠模式,經常使用到,也可以把這當成是一種編成習慣,我們用披薩店的情境來加以說明。

假設有一間披薩店,賣各式各樣的披薩,如果有人點了夏威夷披薩,就準備夏威夷披薩,送入烤箱烘烤,然後拿給消費者,假如有人點鮪魚披薩,就準備鮪魚披薩,接著烘烤並拿給消費者。

#include <iostream>
#include <string>
using namespace std;
 
class Pizza{
public:
    string m_name;
    void bake(){cout << m_name << "進行烘烤" << endl;}
    void deliver(){cout << m_name << "進行運送" << endl;}
};
class HawaiiPizza : public Pizza{
public:
    HawaiiPizza(){m_name="Hawaii";}
};
class TunaPizza : public Pizza{
public:
    TunaPizza(){m_name="Tuna";}
};

Pizza *orderPizza(string name){
    Pizza *ret;
    if(name=="Hawaii"){
        ret = new HawaiiPizza();
    }
    if(name=="Tuna"){
        ret = new TunaPizza();
    }
    ret->bake();
    ret->deliver();
    return ret;
}

int main() { 
    Pizza *myHawaiiPizza = orderPizza("Hawaii");
    Pizza *myTunaPizza = orderPizza("Tuna");

    delete myHawaiiPizza;
    delete myTunaPizza;
    return 0; 
}

在上面的程式範例中,orderPizza()函式內部依參數來實體化類別,所以當增加口味更改時,修改orderPizza()函式是不可避免的,但我們已經知道會修改甚麼地方,可以將會變動的地方取出並封裝,下面使用PizzaFactory類別來處理披薩的產生,而PizzaStore類別負責披薩產生的後續處理。

#include <iostream>
#include <string>
using namespace std; 

class Pizza{
public:
    string m_name;
    void bake(){cout << m_name << "進行烘烤" << endl;}
    void delever(){cout << m_name << "進行運送" << endl;}
};
class HawaiiPizza : public Pizza{
public:
    HawaiiPizza(){m_name="Hawaii pizza";}
};
class TunaPizza : public Pizza{
public:
    TunaPizza(){m_name="Tuna pizza";}
};

class PizzaFactory{
public:
    Pizza *createPizza(string name);
};
Pizza *PizzaFactory::createPizza(string name){
    Pizza *ret;
    if(name=="Hawaii"){
        ret = new HawaiiPizza();
    }
    if(name=="Tuna"){
        ret = new TunaPizza();
    }
    return ret;
}

class PizzaStore{
private:
    PizzaFactory m_PizzaFactory;
public:
    PizzaStore(PizzaFactory factory){m_PizzaFactory=factory;}
    Pizza *orderPizza(string name);
};
Pizza *PizzaStore::orderPizza(string name){
    Pizza *ret = m_PizzaFactory.createPizza(name);
    ret->bake();
    ret->deliver();
    return ret;
}

int main() { 
    PizzaFactory myPizzaFactory;
    PizzaStore myPizzaStore(myPizzaFactory);
    Pizza *myHawaiiPizza = myPizzaStore.orderPizza("Hawaii");
    Pizza *myTunaPizza = myPizzaStore.orderPizza("Tuna");

    delete myHawaiiPizza;
    delete myTunaPizza;
    return 0; 
}

上述例子用單一工廠,產生所有的可能類別實例,下面用類似的方式,但是讓每個產品都有各自的工廠,當產品有新增時,上述例子必須要修改工廠方法,下面方法則是增加一個新的工廠子類別,如此較符合開放封閉原則,但有會產生大量工廠子類別的缺點。

我們用HawaiiPizzaFactory和TunaPizzaFactory類別取代原本的PizzaFactory的功能,以下為程式碼。

#include <iostream>
#include <string>
using namespace std; 

class Pizza{
public:
    string m_name;
    void bake(){cout << m_name << "進行烘烤" << endl;}
    void delever(){cout << m_name << "進行運送" << endl;}
};
class HawaiiPizza : public Pizza{
public:
    HawaiiPizza(){m_name="Hawaii pizza";}
};
class TunaPizza : public Pizza{
public:
    TunaPizza(){m_name="Tuna pizza";}
};

class PizzaFactory{
public:
    virtual Pizza *createPizza() = 0;
};
class HawaiiPizzaFactory : public PizzaFactory{
public:
    Pizza *createPizza();
};
class TunaPizzaFactory : public PizzaFactory{
public:
    Pizza *createPizza();
};
Pizza *HawaiiPizzaFactory::createPizza(){
    Pizza *ret = new HawaiiPizza();
    return ret;
}
Pizza *TunaPizzaFactory::createPizza(){
    Pizza *ret = new TunaPizza();
    return ret;
}
class PizzaStore{
private:
    PizzaFactory *m_PizzaFactory;
public:
    PizzaStore(PizzaFactory *factory){m_PizzaFactory=factory;}
    void setFlavor(PizzaFactory *factory){m_PizzaFactory=factory;}
    Pizza *orderPizza();
};
Pizza *PizzaStore::orderPizza(){
    Pizza *ret = m_PizzaFactory->createPizza();
    ret->bake();
    ret->ship();
    return ret;
}

int main() { 
    HawaiiPizzaFactory myHawaiiFactory;
    PizzaStore myPizzaStore(&myHawaiiFactory);
    Pizza *myHawaiiPizza = myPizzaStore.orderPizza();

    TunaPizzaFactory myTunaFactory;
    myPizzaStore.setFlavor(&myTunaFactory);
    Pizza *myTunaPizza = myPizzaStore.orderPizza();

    delete myHawaiiPizza;
    delete myTunaPizza;
    return 0; 
}

裝飾者模式(Decorator Pattern)

裝飾者模式:動態的將責任加諸於物件上,想要擴充功能,裝飾者提供有別於繼承的另一個選擇。

我們用個人化披薩來解釋裝飾者模式,假使餐廳販售某種消費者自製披薩,消費者選擇披薩的樣式,之後從眾多配料中選幾種自己喜歡的,我們的目標就是當消費者選好披薩時,可以得知披薩的價錢和名稱,價錢是樣式和選擇配料價錢的總和,名稱是樣式加上挑選的配料名稱。

由於配料眾多,所以可能選到普通披薩、番茄青醬披薩、洋蔥蛋青醬披薩等,假使我們要為每種可能性定一個類別,類別數目會多到難以撰寫,且容易有遺漏,以下為其中三種類別。

class Pizza{
public:
    virtual int getPrice() = 0;
    virtual void getName() = 0;
};
class NormalPizza : public Pizza{
    int getPrice(){return 15;}
    void getName(){cout << "普通披薩" << endl;}
};
class TomatoPestoPizza: public Pizza{
    int getPrice(){return 50;}
    void getName(){cout << "番茄青醬披薩" << endl;}
};
class OnionEggPestoPizza: public Pizza{
    int getPrice(){return 55;}
    void getName(){cout << "洋蔥蛋青醬披薩" << endl;}
};

我們可以改變上述的架構,披薩類別包含所有配料,以及價錢和命名的方式,披薩子類別為各種樣式的披薩,實例決定是否要某個配料,最後經由選擇的配料,得到價錢和名稱,這個方法較第一個好,但是假使消費者想要配料加倍(比如雙份洋蔥),或是需要更彈性的命名方式,都會讓我們不好更改,以下為程式碼。

#include<iostream>
#include<string>
using namespace std; 

class Pizza{
protected:
    bool m_onion;
    bool m_egg;
    bool m_tomate;
    int m_basicPrice;
    string m_basicName;
public:
    Pizza();
    void addOnion(){m_onion=true;}
    void addEgg(){m_egg=true;}
    void addTomate(){m_tomate=true;}
    int getPrice();
    string getName();
};
class NormalPizza : public Pizza{
public:
    NormalPizza();
};
class PestoPizza: public Pizza{
public:
    PestoPizza();
};

Pizza::Pizza(){
    m_onion = false;
    m_egg = false;
    m_tomate = false;
    m_basicPrice = 0;
    m_basicName = "";
}
int Pizza::getPrice(){
    int ret = m_basicPrice;
    if(m_onion){
        ret += 10;
    }
    if(m_egg){
        ret += 10;
    }
    if(m_tomate){
        ret += 15;
    }
    return ret;
}
string Pizza::getName(){
    string ret = m_basicName;
    if(m_onion){
        ret = "洋蔥" + ret;
    }
    if(m_egg){
        ret = "蛋" + ret;
    }
    if(m_tomate){
        ret = "番茄" + ret;
    }
    return ret;
}
NormalPizza::NormalPizza(){
    m_basicPrice = 15;
    m_basicName = "普通披薩";
}
PestoPizza::PestoPizza(){
    m_basicPrice = 30;
    m_basicName = "青醬披薩";
}
int main() { 
    PestoPizza myPizza;
    myPizza.addTomate();
    myPizza.addOnion();
    int price = myPizza.getPrice();
    string name = myPizza.getName();
    return 0; 
}

最後看看裝飾者模式,我們對所有口味定義一個類別,這些類別都繼承Pizza類別,將所有配料定義一個類別,這些類別都繼承Topping類別,假使我們選擇普通披薩,希望添加洋蔥在這個披薩上,就建立NormalPizza和Onion實例,並把NormalPizza包在Onion內,假使還需要來點番茄,就建立了Tomato實例,並將以上兩個物件包含在內。

假使我們選擇完成,口味和附加上的配料都包含getPrice()方法,透過層層包裝,getPrice()方法從目前價錢加上該配料的價錢後回傳,從外而內呼叫,最後得到總價錢,名稱也是用同樣的概念得到,以下為程式碼。

#include<iostream>
#include<string>
using namespace std; 

class Pizza{
protected:
    int m_price;
    string m_name;
public:
    Pizza(){m_price=0; m_name="";}
    virtual int getPrice(){return m_price;}
    virtual string getName(){return m_name;}
};
class NormalPizza : public Pizza{
public:
    NormalPizza(){m_price=15; m_name="普通披薩";}
};
class PestoPizza: public Pizza{
public:
    PestoPizza(){m_price=30; m_name="青醬披薩";}
};

class Topping : public Pizza{
};
class Onion : public Topping{
public:
    Pizza *m_pizza;
    Onion(Pizza *pizza){
        m_pizza = pizza;
    }
    int getPrice(){return 10 + m_pizza->getPrice();}
    string getName(){return "洋蔥" + m_pizza->getName();}
};
class Egg : public Topping{
public:
    Pizza *m_pizza;
    Egg(Pizza *pizza){
        m_pizza = pizza;
    }
    int getPrice(){return 10 + m_pizza->getPrice();}
    string getName(){return "蛋" + m_pizza->getName();}
};
class Tomato : public Topping{
public:
    Pizza *m_pizza;
    Tomato(Pizza *pizza){
        m_pizza = pizza;
    }
    int getPrice(){return 15 + m_pizza->getPrice();}
    string getName(){return "番茄" + m_pizza->getName();}
};
int main() { 
    Pizza *myPizza = newPestoPizza();
    Pizza *temp = myPizza;
    myPizza = &Onion(myPizza);
    myPizza = &Tomato(myPizza);
    int price = myPizza->getPrice();
    string name = myPizza->getName();
    delete temp;
    return 0; 
}

觀察者模式(Observer Pattern)

觀察者模式:在物件之間定義一個主題和多個觀察者,當主題的狀態改變,觀察者都會收到通知,並自行更新。

用訂報紙的例子來說明觀察者模式,報紙由報社生產,當我們想要訂閱時會跟報社說要訂報紙,當我們不想訂的時候就跟報社說不繼續訂了,報社會主動發送最新的新聞給所有訂閱者,在這個例子裡,把報社當成主題,客戶當成觀察者,而主題 + 觀察者 = 觀察者模式。

實作上我們定義Subject類別,要求繼承的類別必需實作出註冊、移除、通知方法,定義Observer類別,要求繼承的類別必須實作更新方法,當有新的觀察者想要加入此主題時,主題呼叫registerObserver()方法把此觀察者加入名單,如果某個觀察者想退出這個主題時,主題呼叫removeObserver()將此觀察者從名單中移除,如果Subject有新消息要通知名單上的觀察者時,主題呼叫notifyObservers()方法,此方法會依序執行所有觀察者的update()方法,觀察者模式將主題與觀察者兩者的相依性鬆綁,主題不知道觀察者實際上如何更新,而觀察者也不知道有那些其他的觀察者,以及主題如何註冊、移除觀察者的。

#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;

class Observer{
public:
    virtual void update(string message) = 0;
};
class Subject{
public:
    virtual void registerObserver(Observer *) = 0;
    virtual void removeObserver(Observer *) = 0;
    virtual void notifyObservers(string message) = 0;
};

class NewsOffice : public Subject{
private:
    list m_observesList;
public:
    void registerObserver(Observer *observer);
    void removeObserver(Observer *observer);
    void notifyObservers(string message);
};
class Customer : public Observer{
private:
    string m_name;
public: 
    Customer(string name){m_name = name;}
    void update(string message);
};

void NewsOffice::registerObserver(Observer *observer){
    m_observesList.push_back(observer);
}
void NewsOffice::removeObserver(Observer *observer){
    list::iterator itr = find(m_observesList.begin(), m_observesList.end(), observer);
    if (itr != m_observesList.end()){
        m_observesList.remove(*itr);
    }
}
void NewsOffice::notifyObservers(string message){
    list::iterator listBegin = m_observesList.begin();
    list::iterator listEnd = m_observesList.end();
    while (listBegin != listEnd){
        (*listBegin)->update(message);
        listBegin++;
    }
}
void Customer::update(string message){
    cout << m_name << "收到" << message << endl;
}
int main() {
    NewsOffice office;
    Observer *bill = new Customer("Bill");
    Observer *mike = new Customer("Mike");
    office.registerObserver(bill);
    office.registerObserver(mike);
    office.notifyObservers("一封訊息");

    office.removeObserver(bill);
    office.notifyObservers("一封新訊息");
    delete bill;
    delete mike;
    system("PAUSE");
    return 0;
}

策略模式(Strategy Pattern)

策略模式的原則:
1.將變動部分封裝起來。
2.針對介面寫程式,不是針對實踐寫程式。
3.多用合成,少用繼承。

假如我們開發一款角色扮演遊戲,遊戲每個角色都有攻擊、防禦的動作,我們可以定義一個包含以上行為的父類別Character,再透過繼承讓所有子類別,也就是我們實際的腳色,都可以使用到這些方法,以下為程式碼。

#include<iostream>
using namespace std;

class Character{
public:
    void attack(){cout << "進行攻擊" << endl;}
    void defend(){cout << "進行防禦" << endl;}
};
class Warrior : public Character{
};
class Magician : public Character{
};

int main() {
    Warrior myWarrior;
    Magician myMagician;
    myWarrior.attack();
    myMagician.defend();
    return 0;
}

經由繼承讓子類別可以去呼叫父類別擁有的方法,但是日子久了,假使遊戲需要更多種不同的玩法,必須增加攻擊方式和飛行行為,我們在父類別加入fly()方法,但這時候問題來了,我們讓龍騎士能夠飛行,但讓戰士飛行有點不合邏輯,因為戰士也繼承Character類別的飛行,要處理這種情況可以Override父類別的fly()方法,以下為程式碼。

#include<iostream>
using namespace std;

class Character{
public:
    virtual void attack(){cout << "進行攻擊" << endl;}
    void defend(){cout << "進行防禦" << endl;}
    virtual void fly(){cout << "進行飛行" << endl;}
};
class Warrior : public Character{
public:
    void attack(){cout << "進行普通攻擊" << endl;}
    void fly(){cout << "不進行飛行" << endl;}
};
class Magician : public Character{
public:
    void attack(){cout << "進行魔法攻擊" << endl;}
    void fly(){cout << "不進行飛行" << endl;}
};
class DragonRider : public Character{
public:
    void attack(){cout << "進行普通攻擊" << endl;}
};

int main() {
    Warrior myWarrior;
    Magician myMagician;
    DragonRider myDragonRider;
    myWarrior.attack();
    myMagician.attack();
    myDragonRider.fly();
    return 0;
}

這樣的做法其實不恰當,在目前的架構下加入新的行為時,每個角色都必須去實作此方法,當角色一多將會變得非常麻煩,而且如果攻擊或防禦每多一種變化就去增加一種實作方式,這樣會讓系統變的難以維護,這時可以改用策略模式。

原本戰士類別必須知道attack()方法的實際內容,修改後我們增加一個AttackBehavior類別,之後交給AttackBehavior類別去實際處理攻擊行為,把Warrior類別與攻擊方式的耦合降低,如此一來,戰士類別不用管attack()方法實際如何做,反正由AttackBehavior類別去處理,而且透過這個方式,我們可以在執行階段動態的改變攻擊行為,以下為程式碼。

#include<iostream>
using namespace std;

class AttackBehavior{
public:
    virtual void attack(){ cout << "進行攻擊" << endl; }
};
class DefendBehavior{
public:
    void defend(){ cout << "進行防禦" << endl; }
};
class FlyBehavior{
public:
    virtual void fly(){ cout << "進行飛行" << endl; }
};
class NormalAttack : public AttackBehavior{
public:
    void attack(){ cout << "進行普通攻擊" << endl; }
};
class MagicAttack : public AttackBehavior{
public:
    void attack(){ cout << "進行魔法攻擊" << endl; }
};
class FlyWithWing : public FlyBehavior{
public:
    void fly(){ cout << "進行飛行" << endl; }
};
class FlyNoWay : public FlyBehavior{
public:
    void fly(){ cout << "不進行飛行" << endl; }
};

class Character{
protected:
    AttackBehavior *m_attackBehavior;
    DefendBehavior *m_defendBehavior;
    FlyBehavior *m_flyBehavior;
public:
    Character(AttackBehavior *, DefendBehavior *, FlyBehavior *);
    void performAttack(){ m_attackBehavior->attack(); }
    void performDefend(){ m_defendBehavior->defend(); }
    void performFly(){ m_flyBehavior->fly(); }
    void setAttackMode(AttackBehavior *atk){ m_attackBehavior = atk; }
    void setFlyMode(FlyBehavior *fly){ m_flyBehavior = fly; }
};
Character::Character(AttackBehavior *attack, DefendBehavior *defend, FlyBehavior *fly){
    m_attackBehavior = attack;
    m_defendBehavior = defend;
    m_flyBehavior = fly;
}
class Warrior : public Character{
public:
    Warrior(AttackBehavior *attack, DefendBehavior *defend, FlyBehavior *fly) : Character(attack, defend, fly){}
};

class Magician : public Character{
public:
    Magician(AttackBehavior *attack, DefendBehavior *defend, FlyBehavior *fly) : Character(attack, defend, fly){}
};
class DragonRider : public Character{
public:
    DragonRider(AttackBehavior *attack, DefendBehavior *defend, FlyBehavior *fly) : Character(attack, defend, fly){}
};

int main() {
    AttackBehavior *attack = new NormalAttack();
    DefendBehavior *defend = new DefendBehavior();
    FlyBehavior *fly1 = new FlyNoWay();
    FlyBehavior *fly2 = new FlyWithWing();
    Warrior myWarrior(attack, defend, fly1);
    DragonRider myDragonRider(attack, defend, fly2);
    myWarrior.performAttack();
    myDragonRider.performFly();

    AttackBehavior *attack2 = new MagicAttack();
    myDragonRider.setAttackMode(attack2);
    myDragonRider.performAttack();

    delete attack;
    delete defend;
    delete fly1;
    delete fly2;
    delete attack2;
    system("PAUSE");
    return 0;
}

二進位檔案操作(read、write)

在寫入或讀取檔案時,也可以用read()與write()函式以區塊的方式寫入,這兩個函式通常用二進位模式來操作檔案,函式型態如下,num是寫入資料的位元組數目:
istream &read(char buf, streamsize num);
ostream &write(const char
buf, streamsize num);

下面程式將陣列資料寫入檔案,然後再將資料讀出,.dat檔案格式是我們隨意寫的格式,有沒有寫或名稱為何都不影響結果:

#include <fstream> 
using namespace std;

int main() {
    ofstream fout("test.dat", ios::out | ios::binary);
    if (!fout) {
        return 1;
    }

    int arr[3] = {1,2,3};
    fout.write((char*)arr, sizeof(arr));
    fout.close();

    ifstream fin("test.dat", ios::in | ios::binary);
    if (!fin) {
        return 1;
    }

    int arr2[3] = {0,0,0};
    fin.read((char*)arr2, sizeof(arr2));
    fin.close();

    return 0;
}

檔案操作(ifstream、fstream、getline)

C++與檔案處理相關的有ifstream、ofstream及fstream這三個串流,使用時必須先函入fstream標頭,ifstream物件處理檔案輸入,ofstream物件處理檔案輸出,fstream物件處理檔案輸入輸出,建立串流物件之後,可以使用open()函式來連結串流,以下為讀入一個txt檔,並將檔案內容寫到另一個txt檔上。

#include<iostream>
#include<fstream>
using namespace std; 

int main(){ 
    ifstream fin;
    ofstream fout;
    fin.open("src.txt");     
    fout.open("dst.txt");   

    if(!fin){ 
        return 1; 
    } 

    char ch; 
    //用!fin.eof()來判斷,最後會多讀一次
    while(fin.peek()!=EOF) {   
        fin.get(ch); 
        fout.put(ch); 
    } 

    fin.close(); 
    fout.close();
    return 0; 
}

我們也可以一次讀取一行,下面程式從檔案一次讀取一行,接著再將檔案一行一行輸出到txt檔上。

#include <string>
#include <vector>
#include <fstream>
using namespace std;

int main(){
    ifstream in("test.txt");
    string inputStr;
    vector inputContent;
    while(getline(in, inputStr)){
        inputContent.push_back(inputStr);
    }
    in.close();

    ofstream out("output.txt");
    for(int i=0; i < inputContent.size(); i++){
        out << inputContent[i] << endl;
    }
    out.close();

    return 0;
}