muduo網路庫學習筆記(0):物件導向程式設計風格和基於物件程式設計風格的比較

li27z發表於2016-07-25

我們通過對一個執行緒類的封裝來比較這二者的不同。

物件導向程式設計風格

使用物件導向程式設計風格封裝執行緒類,一般這麼做:寫一個Thread base class,含有(純)虛擬函式Thread::run(),然後應用程式派生一個derived class,覆寫run()。程式裡的每一種執行緒對應一個Thread的派生類。

類圖如下:
這裡寫圖片描述

程式碼如下:

檔名:Thread.h -- 標頭檔案

#ifndef _THREAD_H_
#define _THREAD_H_

#include <pthread.h>

// 執行緒類,是一個抽象基類
class Thread
{
public:
    Thread();  // 建構函式
    virtual ~Thread();  // 基類的解構函式宣告為虛解構函式,保證派生類的解構函式會被呼叫  

    void Start();  // 建立執行緒
    void Join();  // 等待執行緒結束

    void SetAutoDelete(bool autoDelete);  // 設定自動銷燬標識

private:
    static void* ThreadRoutine(void* arg);  // 執行緒的入口函式
    virtual void Run() = 0;  // 真正的執行緒執行體,宣告為純虛擬函式,作為一個介面,不同的派生類有不同的實現
    pthread_t threadId_;  // 執行緒ID
    bool autoDelete_;  // 自動銷燬標識
};

#endif // _THREAD_H_
檔名:Thread.cpp -- 原始檔

#include "Thread.h"
#include <iostream>
using namespace std;

// 建構函式
Thread::Thread() : autoDelete_(false)
{
    cout << "Thread ..." << endl;
}


// 解構函式
Thread::~Thread()
{
    cout << "~Thread ..." << endl;
}

// 執行緒建立函式,呼叫pthread_create()
void Thread::Start()
{
    pthread_create(&threadId_, NULL, ThreadRoutine, this);  // 將this指標作為引數傳遞給執行緒執行函式
}

// 等待執行緒結束函式,呼叫pthread_join()
void Thread::Join()
{
    pthread_join(threadId_, NULL);
}

// 執行緒的入口函式,宣告為靜態,可通過類名直接呼叫
void* Thread::ThreadRoutine(void* arg)
{
    Thread* thread = static_cast<Thread*>(arg);  // pthread_create()傳遞的arg即this指標,指向派生類物件
    thread->Run();  // 基類指標呼叫派生類所實現的虛擬函式,用到了虛擬函式的多型
    if (thread->autoDelete_)
        delete thread;
    return NULL;
}

// 設定自動銷燬標識函式
void Thread::SetAutoDelete(bool autoDelete)
{
    autoDelete_ = autoDelete;
}
檔名:Thread_test.cpp -- 測試:實現派生類TestThread,繼承自Thread基類

#include "Thread.h"
#include <unistd.h>
#include <iostream>
using namespace std;

class TestThread : public Thread
{
public:
    TestThread(int count) : count_(count)
    {
        cout << "TestThread ..." << endl;
    }

    ~TestThread()
    {
        cout << "~TestThread ..." << endl;
    }

private:
    // 派生類的執行緒執行方法
    void Run()
    {
        while(count_--)
        {
            cout << "this is a test ..." << endl;
            sleep(1);
        }
    }

    int count_;
};

int main(void)
{
    TestThread* t2 = new TestThread(5);
    t2->SetAutoDelete(true);  // 設定自動銷燬標識,執行緒執行完畢,執行緒物件自動銷燬
    t2->Start();
    t2->Join();

    for ( ; ; )
        pause();

    return 0;
}

程式說明:
1.Thread類作為抽象類,是不能例項化物件的。TestThread類是派生類,是具體類。

2.因為Thread類作為多型基類,該類的解構函式必須是一個純虛解構函式,否則會導致派生類的資源釋放不完全。

3.執行緒的相關函式:
包含標頭檔案:#include <pthread.h>

// 建立執行緒
int pthread_create(pthread_t *tidp, const pthread_attr_t *attr,
(void*)(*start_rtn)(void*), void *arg); 

引數:
第一個引數為指向執行緒識別符號的指標;
第二個引數用來設定執行緒屬性;
第三個引數是執行緒執行函式的起始地址;
第四個引數是執行函式的引數。

返回值:若成功則返回0,否則返回出錯編號。


// 等待一個執行緒的結束
int pthread_join(pthread_t thread, void **retval); 


引數:
第一個引數為被等待的執行緒識別符號,第二個引數為一個使用者定義的指標,它可以用來儲存被等待執行緒的返回值。這個函式是一個執行緒阻塞的函式,呼叫它的函式將一直阻塞到被等待的執行緒結束為止,當函式返回時,被等待執行緒的資源被收回。

返回值:如果執行成功,將返回0,如果失敗則返回一個錯誤號。

4.執行緒物件的生命週期和執行緒的生命週期是不一樣的。執行緒物件的銷燬一般是程式結束的時候,設定autoDelete_標誌,使執行緒執行完畢,執行緒物件自動銷燬。(需動態建立物件)

從上述過程中我們可以看出:物件導向的程式設計風格主要還是圍繞物件導向的三大特點“封裝、繼承、多型”來展開的——通過封裝隱藏實現細節,只暴露需要對外提供的屬性或方法;通過繼承的特性來提高程式的可複用性;定義抽象基類(純虛擬函式),讓其派生類繼承並實現它的介面方法。

基於物件的程式設計風格

“物件導向”和“基於物件”都實現了“封裝”的概念,但是物件導向實現了“繼承和多型”,而“基於物件”是使用物件,無法利用現有的物件模板產生新的物件型別繼而產生新的物件,沒有實現“繼承和多型”。

基於物件的程式設計風格要藉助boost bind/function來實現,用它來代替虛擬函式作為類的(回撥)介面。它的出現替代了STL中的mem_fun、ptr_fun、bind1st、bin2nd等函式,其主要作用是使一種型別的函式介面轉換成另一種型別的函式介面,且boost bind/function的通用性更好。

首先來了解一下boost bind/function,一個簡單的使用示例如下:

#include <iostream>

// 需包含對應的標頭檔案
#include <boost/function.hpp>
#include <boost/bind.hpp>

using namespace std;

class Foo
{
public:
    // 類的非靜態成員函式
    void memberFunc(double d, int i, int j)
    {
        cout << d << endl; 
        cout << i << endl;    
        cout << j << endl; 
    }
};

int main()
{
    Foo foo;

    // boost::function + boost::bind 把一個類成員函式適配成為一個返回值型別為void,接收一個int型別引數的函式:void(int)
    // 對於非靜態成員函式,bind的實參表依次為: 指向成員函式的指標, 繫結到this的物件, n個引數
    // 如果我們不打算向成員函式繫結第2個引數,這裡使用 _1 來佔位,同理下邊還會用到 _2 _3 這樣的佔位符.
    boost::function<void (int)> fp = boost::bind(&Foo::memberFunc, &foo, 0.5, _1, 10);

    fp(100);  // 呼叫foo::memberFunc(0.5, 100, 10),列印0.5,100,10

    // 這裡我們用到了boost::ref,bind庫可以搭配ref庫使用,ref庫包裝了物件的引用,可以讓bind儲存物件引用的拷貝,從而降低了拷貝的代價
    boost::function<void (int, int)> fp2 = boost::bind(&Foo::memberFunc, boost::ref(foo), 0.5, _1, _2);  

    fp2(100, 200);  // 呼叫foo::memberFunc(0.5, 100, 200),列印0.5,100,200

    return 0;
}

下面進入正題——使用基於物件的程式設計風格來封裝一個執行緒類,藉助boost::function + boost::bind,我們可以這麼做:令Thread是一個具體類,其建構函式接受ThreadFunc物件,應用程式只需提供一個能轉換為ThreadFunc的物件(可以是函式),即可建立一份Thread實體,然後呼叫Thread::start()即可。

類圖如下:
這裡寫圖片描述

程式碼如下:

檔名:Thread.h

#ifndef _THREAD_H_
#define _THREAD_H_

#include <pthread.h>
#include <boost/function.hpp>

class Thread
{
public:
    typedef boost::function<void ()> ThreadFunc;  // 定義執行緒執行函式的型別
    explicit Thread(const ThreadFunc& func);  // explicit建構函式用來防止隱式轉換

    void Start();
    void Join();

    void SetAutoDelete(bool autoDelete);

private:
    static void* ThreadRoutine(void* arg);
    void Run();
    ThreadFunc func_;
    pthread_t threadId_;
    bool autoDelete_;
};

#endif // _THREAD_H_
檔名:Thread.cpp

#include "Thread.h"
#include <iostream>
using namespace std;

// 建構函式
Thread::Thread(const ThreadFunc& func) : func_(func), autoDelete_(false)
{
}

// 建立執行緒
void Thread::Start()
{
    pthread_create(&threadId_, NULL, ThreadRoutine, this);
}

// 等待執行緒結束
void Thread::Join()
{
    pthread_join(threadId_, NULL);
}

// 執行緒入口函式
void* Thread::ThreadRoutine(void* arg)
{
    Thread* thread = static_cast<Thread*>(arg);
    thread->Run();
    if (thread->autoDelete_)
        delete thread;
    return NULL;
}

// 設定自動銷燬標識
void Thread::SetAutoDelete(bool autoDelete)
{
    autoDelete_ = autoDelete;
}

// Run方法直接呼叫func_()
void Thread::Run()
{
    func_();
}
檔名:Thread_test.cpp

#include "Thread.h"
#include <boost/bind.hpp>
#include <unistd.h>
#include <iostream>
using namespace std;

class Foo
{
public:
    Foo(int count) : count_(count)
    {
    }

    void MemberFun()
    {
        while (count_--)
        {
            cout<<"this is a test ..."<<endl;
            sleep(1);
        }
    }

    void MemberFun2(int x)
    {
        while (count_--)
        {
            cout<<"x="<<x<<" this is a test2 ..."<<endl;
            sleep(1);
        }
    }

    int count_;
};

void ThreadFunc()
{
    cout<<"ThreadFunc ..."<<endl;
}

void ThreadFunc2(int count)
{
    while (count--)
    {
        cout<<"ThreadFunc2 ..."<<endl;
        sleep(1);
    }
}

int main()
{
    // 呼叫普通函式
    Thread t1(ThreadFunc);
    Thread t2(boost::bind(ThreadFunc2, 3));
    // 呼叫成員函式
    Foo foo(3);
    Thread t3(boost::bind(&Foo::MemberFun, &foo));
    Foo foo2(3);
    Thread t4(boost::bind(&Foo::MemberFun2, &foo2, 1000));

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();

    return 0;
}

可見,基於物件的程式設計風格不暴露抽象類,只使用具體類。

小結

重新審視“物件導向”:“繼承與多型”的特性為程式開發帶來了靈活性,同時,我們也要注意到“虛擬函式作為介面的弊端——一旦釋出,不能修改”、“基於繼承的設計會帶來強耦合性”等問題,所以,究竟是採用“物件導向”還是“基於物件”的設計值得我們在開發時深入思考。

相關文章