天天看點

我的C++實踐(14):限制類對象的個數

    1、允許一個或零個對象: 把所有的構造函數聲明為私有,則不能建立任何對象。如果允許隻建立一個對象,可用一個全局函數(或靜态成員函數)來建立唯一的一個靜态對象,并傳回其引用,為提高效率,可把全局函數聲明為inline。注意這個全局函數要聲明為類的友元函數,因為要使用私有的構造函數。

    例如我們想建立一個列印機對象,但希望實際上隻有一個列印機,可以這樣寫:

//printer.hpp:隻允許建立一個列印機對象,即單例模式

#ifndef PRINTER_HPP

#define PRINTER_HPP

class PrintJob{

public:

PrintJob(const string& whatToPrint){

//...

}

//...

};

class Printer{ //列印機類

private:

//所有構造函數都聲明為私有,則不能作為基類使用

Printer(){

//...

}

Printer(Printer const& rhs){

//...

}

friend Printer& thePrinter();

//...

public:

void submitJob(PrintJob const& job){

//...

}

void reset(){

//...

}

void performSelfTest(){

//...

}

//...

};

inline Printer& thePrinter(){ //擷取唯一的一個列印機對象

static Printer p;

return p;

}

//...

#endif

    這裡我們把thePrinter()實作為一個全局函數,裡面隻建立一個靜态對象,傳回其引用,這樣用戶端代碼中的所有thePrinter()調用都隻使用這個唯一的靜态對象。注意,對構造函數,可以隻聲明為私有,如果不使用它,則可以不提供定義,但對需要被使用的構造函數(因為是私有,隻能在類的成員函數或友元函數中使用)則必須提供定義。當然,我們也可以把thePrinter()實作為Printer類的靜态成員函數,這樣就不需要友元聲明了。但使用全局函數也有優勢,因為在某些類中聲明的靜态對象即使沒用到也會被構造(以及析構)出來,而全局函數裡的靜态對象隻有執行函數時才被建立。

    另一方面,我們有時候會把那個唯一的p對象聲明為全局的靜态對象或Printer類的靜态成員,然後在thePrinter()或Printer::thePrinter()中直接傳回它,這不是一種好的實作方案。我們稱函數内的static對象稱為local static對象,其他的static對象(class作用域、namespace作用域、global作用域、file作用域)稱為non-local static對象。C++隻保證在一個特定編譯單元内的靜态對象的初始化順序,對不同編譯單元内的non-local static對象的初始化次序沒有明确定義。可見這樣的實作方案會使得p對象的初始順序不明确。

    當我們需要用到non-local static對象,又要保證正确的初始化順序時,解決方法恰恰就是将non-local static對象放在一個inline包裝函數中,函數傳回一個引用指向此對象(變成local static對象)。用函數調用來替換直接通路non-local static對象,這時函數内的局部靜态對象就會有确定的初始化順序(在第一次執行函數時初始化)。注意,我們通常在設計時并不建議傳回指向函數内局部對象的reference,但這裡的包裝函數行為單純,隻是包裝一個static對象并傳回其reference,并不做其他工作。當多次調用包裝函數時,reference指向同一個static對象,并且值均相同(包裝函數内并沒有改變對象的成員值),是以這樣實作沒有副作用。

    注意,構造函數聲明為私有的類不能作為基類使用,也不能組合到其他類中來使用。我們可以放寬一點,把構造函數聲明為protected的,這樣類同樣不可以建立對象,但可以被繼承。

    2、允許建立任意數量的對象,但不允許作為基類: 把構造函數聲明為私有,這樣就不允許作為基類了。同時我們提供相應的僞構造函數,用它來建立對象并傳回。例如對有限狀态機類,如下:

//fsa.hpp:有限狀态機類,允許建立任意數量的對象,但不允許作為基類

#ifndef FSA_HPP

#define FSA_HPP

class FSA{ //有限狀态機類

private:

//構造函數聲明為私有

FSA(){

//...

}

FSA(FSA const& rhs){

//...

}

public:

static FSA* makeFSA(){ //僞構造函數

return new FSA();

}

static FSA* makeFSA(FSA const& rhs){ //僞拷貝構造函數

return new FSA(rhs);

}

//...

};

#endif

    用戶端使用這個類時,就必須調用僞構造函數來建立對象。當然,由于對象是動态配置設定的,這時客戶必須自己調用delete來删除對象。

    3、允許對象個數限制在某個給定的值: 使用前面在“模闆與繼承相結合的威力”中介紹的奇異遞歸模闆模式技術,編寫一個具有執行個體計數功能的基類即可,讓需要計數的類繼承這個類。因為類要實作計數功能,是一種“實作”的關系,故最好用私有繼承,表示用這個基類來實作需要的功能。這裡我們做一下修改,當需要計數的類對象個數超過上限時就抛出異常。注意不同的類可以通過特化這個基類的表示上限值的成員,以滿足不同類對象個數的要求。

//objectcounter.hpp:對對象建立進行記數的模闆

#ifndef OBJECT_COUNTER_HPP

#define OBJECT_COUNTER_HPP

#include <cstddef>

template<typename T>

class ObjectCounter{ //用來記錄T型對象構造的總個數

private:

static std::size_t numObjects;//存在對象的個數

static const std::size_t maxObjects; //對象個數的上限,由需要計數的類來指定

void incr(){

if(ObjectCounter<T>::numObjects>=ObjectCounter<T>::maxObjects)

throw TooManyObjects();

++ObjectCounter<T>::numObjects;

}

protected:

ObjectCounter(){ //預設構造函數

incr();

}

ObjectCounter(ObjectCounter<T> const& rhs){ //拷貝構造函數

incr();

}

~ObjectCounter(){ //析構函數

--ObjectCounter<T>::numObjects;

}

public:

class TooManyObjects{ }; //異常類

static std::size_t objectCount(){ //傳回目前對象個數

return ObjectCounter<T>::numObjects;

}

};

//static成員變量必須在類外進行定義,由于是非const的,是以也要類外初始化

template<typename T>

std::size_t ObjectCounter<T>::numObjects=0;

#endif

//mystring.hpp:使用了對象計數的MyString類

#ifndef MYSTRING_HPP

#define MYSTRING_HPP

#include "objectcounter.hpp"

class MyString : private ObjectCounter<MyString>{

public:

//由于是私有繼承,要用using把objectCount函數變成公有

using ObjectCounter<MyString>::objectCount;

//...

};

//特化ObjectCounter的maxObjects成員,值為3,以限制MyString類對象的個數,

//這個語句必須加入到MyString類的實作檔案中

template<>

const std::size_t ObjectCounter<MyString>::maxObjects=3;

#endif

//countprinter.hpp:使用了對象計數的Printer類

#ifndef PRINTER_HPP

#define PRINTER_HPP

#include "objectcounter.hpp"

class Printer : private ObjectCounter<Printer>{

public:

//由于是私有繼承,要用using把objectCount函數變成公有

using ObjectCounter<Printer>::objectCount;

//...

};

//對Printer,對象個數特化為值4

template<>

const std::size_t ObjectCounter<Printer>::maxObjects=4;

#endif

//countertest.cpp:用戶端代碼,對對象計數進行測試

#include <iostream>

#include "mystring.hpp"

#include "countprinter.hpp"

int main(){

MyString s1,s2;

std::cout<<"number of MyString: "

<<MyString::objectCount()<<std::endl;

std::cout<<"number of MyString: "

<<s1.objectCount()<<std::endl;

s1.~MyString(); //減少一個對象

std::cout<<"number of MyString: "

<<MyString::objectCount()<<std::endl;

try{

MyString s3,s4,s5; //對象個數超過上限,會抛出出異常

}catch(ObjectCounter<MyString>::TooManyObjects& e){

std::cout<<"Too many MyString objects."<<std::endl;

}

Printer t1,t2,t3;

std::cout<<"number of Printer: "

<<Printer::objectCount()<<std::endl;

std::cout<<"number of Printer: "

<<t1.objectCount()<<std::endl;

Printer t4,t5; //對象個數超過上限,會抛出出異常,終止程式

return 0;

}

    這裡ObjectCounter<T>模闆用于計數,它需要作為基類,成員名的前面最好加類作用域符(也可用this->),使它變成依賴型名稱。表示對象個數上限值的maxObjects成員是一個靜态常量,它并沒有初始化。我們必須在需要計數的類MyString、Printer中指定它的值。不同類的對象個數上限可能不同,是以我們隻要特化maxObjects這個成員并指定需要的值即可(類模闆可以隻特化某個成員)。在MyString、Printer這些類中,由于是私有繼承,是以要用using聲明把objectCount函數變成公有,這樣就可以用它來擷取目前對象的個數。