2009-04-22 42 views
19

是否可以創建一個C++頭文件(.h)來聲明一個類及其公有方法,但是沒有定義該類中的私有成員?我發現一個few pages,說你應該在頭文件中聲明該類及其所有成員,然後在你的cpp文件中分別定義這些方法。我問,因爲我想有一個在Win32 DLL中定義的類,並且我希望它能夠被正確封裝:該類的內部實現可能會更改,包括其成員,但這些更改不應該影響使用該類的代碼。聲明一個類和方法但不是成員的C++頭文件?

我想,如果我有這個,那麼它會使編譯器不可能提前知道我的對象的大小。但是這應該沒問題,只要編譯器足夠聰明就可以使用構造函數,並且只需傳遞指向存儲對象的內存位置的指針,並且永遠不要讓我運行「sizeof(MyClass)」。

更新:感謝所有回答的人!這似乎是pimpl成語是實現我所說的一個好方法。我會做同樣的事情:

我的Win32 DLL文件就會有這樣的一羣獨立的功能:

void * __stdcall DogCreate(); 
int __stdcall DogGetWeight(void * this); 
void __stdcall DogSetWeight(void * this, int weight); 

這是微軟自己寫的DLL文件的典型方式,所以我覺得有這可能是很好的理由。

但我想利用C++對類有很好的語法,所以我將編寫一個包裝類來包裝所有這些函數。它將有一個成員,這將是「無效* pimpl」。這個包裝類將非常簡單,我不妨將它聲明並在頭文件中定義它。但是這個包裝類除了讓C++代碼看起來非常漂亮外,沒有任何其他目的。

回答

33

我覺得你在找什麼叫做「pimpl成語」。要理解這是如何工作的,你需要明白,在C++中你可以轉發聲明類似的東西。

class CWidget; // Widget will exist sometime in the future 
CWidget* aWidget; // An address (integer) to something that 
        // isn't defined *yet* 

// later on define CWidget to be something concrete 
class CWidget 
{ 
    // methods and such 
}; 

因此,轉發聲明意味着承諾以後完全聲明一個類型。它的說法是:「這個東西叫做CWidget,我保證,我稍後會告訴你更多關於它的事情。」

前向聲明的規則說你可以定義一個指針或者一個已經前向聲明的引用。這是因爲指針和引用實際上只是地址 - 這是一個這個尚未定義的東西。能夠聲明一個指向某個東西的指針而沒有完全聲明它是很方便的,原因很多。

它在這裏很有用,因爲你可以使用這個來隱藏一些類的內部使用「pimpl」方法。 Pimpl的意思是「指向執行」。因此,而不是「小部件」,你有一個類是實際的實現。您在頭文件中聲明的類只是CImpl類的傳遞。下面是它如何工作的:

// Thing.h 

class CThing 
{ 
public: 
    // CThings methods and constructors... 
    CThing(); 
    void DoSomething(); 
    int GetSomething(); 
    ~CThing(); 
private: 
    // CThing store's a pointer to some implementation class to 
    // be defined later 
    class CImpl;  // forward declaration to CImpl 
    CImpl* m_pimpl; // pointer to my implementation 
}; 

Thing.cpp有定義爲道直通到IMPL CThing的方法:

// Fully define Impl 
class CThing::CImpl 
{ 
private: 
    // all variables 
public: 
    // methods inlined 
    CImpl() 
    { 
      // constructor 
    } 

    void DoSomething() 
    { 
      // actual code that does something 
    } 
    //etc for all methods  
}; 

// CThing methods are just pass-throughs 
CThing::CThing() : m_pimpl(new CThing::CImpl()); 
{ 
} 

CThing::~CThing() 
{ 
    delete m_pimpl; 
} 

int CThing::GetSomething() 
{ 
    return m_pimpl->GetSomething(); 
} 

void CThing::DoSomething() 
{ 
    m_impl->DoSomething(); 
} 

田田!你已經隱藏了你的cpp中的所有細節,你的頭文件是一個非常整齊的方法列表。這是一件好事。您可能會看到與上述模板不同的唯一情況是人們可能會使用boost :: shared_ptr <>或其他智能指針作爲impl。一些刪除自己的東西。

此外,請記住這種方法帶來一些煩惱。調試可能有點煩人(額外級別的重定向步驟)。它也是創建一個班級的很多開銷。如果你爲每個班級都這樣做,你會厭倦了所有的打字:)。

3

Google「pimple idiom」或「handle C++」。

+0

拼寫更好的'pimpl'(用於'私人執行')? – 2009-04-22 19:41:32

3

是的,這可能是一件值得期待的事情。一種簡單的方法是使實現類從頭中定義的類派生。

缺點是編譯器不會知道如何構造你的類,所以你需要某種工廠方法來獲得類的實例。在堆棧上存在本地實例是不可能的。

-1

檢查出類The Handle-Body成語在C++

+1

您的鏈接現在已損壞。 – 2011-06-13 09:20:20

0

是否有可能使一個C++頭 文件(.h)中聲明一個類, 它的公共方法,但不 聲明私有那個 類的成員?

最近的答案是PIMPL成語。

從Herb Sutter提交這個The Fast Pimpl Idiom

IMO Pimpl在您的頭文件將要改變很多次的開發初始階段非常有用。 Pimpl由於其在堆中分配內部對象而分配成本。

7

pimpl idiom增加了一個void *私有數據成員給你的類,如果你需要快速的&髒東西,這是一個有用的技巧。但是它有缺點。其中主要是它使抽象類型上使用多態性變得困難。有時你可能需要一個抽象基類和該基類的子類,收集指向矢量中所有不同類型的指針並調用它們的方法。另外,如果pimpl習慣用法的目的是隱藏類的實現細節,那麼它只有差不多成功:指針本身是一個實現細節。也許是一個不透明的實現細節。但是,一個實現細節。

存在一種替代pimpl習語的方法,可用於從接口中刪除所有實現細節,同時提供可根據需要多形地使用的基本類型。

在您的DLL的頭文件(一個由客戶端代碼執行#included)創建一個唯一的公共方法和概念的抽象類,它決定了類是如何被實例化(例如,公共工廠方法&克隆方法):

kennel.h

/**************************************************************** 
*** 
*** The declaration of the kennel namespace & its members 
*** would typically be in a header file. 
***/ 

// Provide an abstract interface class which clients will have pointers to. 
// Do not permit client code to instantiate this class directly. 

namespace kennel 
{ 
    class Animal 
    { 
    public: 
     // factory method 
     static Animal* createDog(); // factory method 
     static Animal* createCat(); // factory method 

     virtual Animal* clone() const = 0; // creates a duplicate object 
     virtual string speak() const = 0; // says something this animal might say 
     virtual unsigned long serialNumber() const = 0; // returns a bit of state data 
     virtual string name() const = 0; // retuyrns this animal's name 
     virtual string type() const = 0; // returns the type of animal this is 

     virtual ~Animal() {}; // ensures the correct subclass' dtor is called when deleteing an Animal* 
    }; 
}; 

...動物是abstract base class,因此不能被實例化;沒有私營公司需要申報。虛擬dtor的存在確保瞭如果某人deleteAnimal*,那麼也會調用適當的子類「dtor」。

爲了實現基類型的不同子類(例如,狗&貓),您需要在DLL中聲明實現級類。這些類最終來自您在頭文件中聲明的抽象基類,而工廠方法實際上會實例化這些子類中的一個。

dll.cpp:

/**************************************************************** 
*** 
*** The code that follows implements the interface 
*** declared above, and would typically be in a cc 
*** file. 
***/ 

// Implementation of the Animal abstract interface 
// this implementation includes several features 
// found in real code: 
//  Each animal type has it's own properties/behavior (speak) 
//  Each instance has it's own member data (name) 
//  All Animals share some common properties/data (serial number) 
// 

namespace 
{ 
    // AnimalImpl provides properties & data that are shared by 
    // all Animals (serial number, clone) 
    class AnimalImpl : public kennel::Animal  
    { 
    public: 
     unsigned long serialNumber() const; 
     string type() const; 

    protected: 
     AnimalImpl(); 
     AnimalImpl(const AnimalImpl& rhs); 
     virtual ~AnimalImpl(); 
    private: 
     unsigned long serial_;    // each Animal has its own serial number 
     static unsigned long lastSerial_; // this increments every time an AnimalImpl is created 
    }; 

    class Dog : public AnimalImpl 
    { 
    public: 
     kennel::Animal* clone() const { Dog* copy = new Dog(*this); return copy;} 
     std::string speak() const { return "Woof!"; } 
     std::string name() const { return name_; } 

     Dog(const char* name) : name_(name) {}; 
     virtual ~Dog() { cout << type() << " #" << serialNumber() << " is napping..." << endl; } 
    protected: 
     Dog(const Dog& rhs) : AnimalImpl(rhs), name_(rhs.name_) {}; 

    private: 
     std::string name_; 
    }; 

    class Cat : public AnimalImpl 
    { 
    public: 
     kennel::Animal* clone() const { Cat* copy = new Cat(*this); return copy;} 
     std::string speak() const { return "Meow!"; } 
     std::string name() const { return name_; } 

     Cat(const char* name) : name_(name) {}; 
     virtual ~Cat() { cout << type() << " #" << serialNumber() << " escaped!" << endl; } 
    protected: 
     Cat(const Cat& rhs) : AnimalImpl(rhs), name_(rhs.name_) {}; 

    private: 
     std::string name_; 
    }; 
}; 

unsigned long AnimalImpl::lastSerial_ = 0; 


// Implementation of interface-level functions 
// In this case, just the factory functions. 
kennel::Animal* kennel::Animal::createDog() 
{ 
    static const char* name [] = {"Kita", "Duffy", "Fido", "Bowser", "Spot", "Snoopy", "Smkoky"}; 
    static const size_t numNames = sizeof(name)/sizeof(name[0]); 

    size_t ix = rand()/(RAND_MAX/numNames); 

    Dog* ret = new Dog(name[ix]); 
    return ret; 
} 

kennel::Animal* kennel::Animal::createCat() 
{ 
    static const char* name [] = {"Murpyhy", "Jasmine", "Spike", "Heathcliff", "Jerry", "Garfield"}; 
    static const size_t numNames = sizeof(name)/sizeof(name[0]); 

    size_t ix = rand()/(RAND_MAX/numNames); 

    Cat* ret = new Cat(name[ix]); 
    return ret; 
} 


// Implementation of base implementation class 
AnimalImpl::AnimalImpl() 
: serial_(++lastSerial_) 
{ 
}; 

AnimalImpl::AnimalImpl(const AnimalImpl& rhs) 
: serial_(rhs.serial_) 
{ 
}; 

AnimalImpl::~AnimalImpl() 
{ 
}; 

unsigned long AnimalImpl::serialNumber() const 
{ 
    return serial_; 
} 

string AnimalImpl::type() const 
{ 
    if(dynamic_cast<const Dog*>(this)) 
     return "Dog"; 
    if(dynamic_cast<const Cat*>(this)) 
     return "Cat"; 
    else 
     return "Alien"; 
} 

現在,你必須在頭&定義的接口實現的細節完全分隔出來,其中客戶端代碼無法看到它。您可以通過調用鏈接到您的DLL的代碼在頭文件中聲明的方法來使用它。下面是一個示例驅動程序:

main.cpp中:

std::string dump(const kennel::Animal* animal) 
{ 
    stringstream ss; 
    ss << animal->type() << " #" << animal->serialNumber() << " says '" << animal->speak() << "'" << endl; 
    return ss.str(); 
} 

template<class T> void del_ptr(T* p) 
{ 
    delete p; 
} 

int main() 
{ 
    srand((unsigned) time(0)); 

    // start up a new farm 
    typedef vector<kennel::Animal*> Animals; 
    Animals farm; 

    // add 20 animals to the farm 
    for(size_t n = 0; n < 20; ++n) 
    { 
     bool makeDog = rand()/(RAND_MAX/2) != 0; 
     if(makeDog) 
      farm.push_back(kennel::Animal::createDog()); 
     else 
      farm.push_back(kennel::Animal::createCat()); 
    } 

    // list all the animals in the farm to the console 
    transform(farm.begin(), farm.end(), ostream_iterator<string>(cout, ""), dump); 

    // deallocate all the animals in the farm 
    for_each(farm.begin(), farm.end(), del_ptr<kennel::Animal>); 

    return 0; 
} 
+2

One nit - 沒有必要將pimpl指針作爲void *。所有需要做的事情就是讓公共頭部轉發引用pimpl類。 – 2009-04-23 00:21:17

2

你必須所有成員聲明在標題所以編譯器知道有多大的對象等。

但是你可以通過使用一個接口解決這個問題:

ext.h:

class ExtClass 
{ 
public: 
    virtual void func1(int xy) = 0; 
    virtual int func2(XYClass &param) = 0; 
}; 

int.h:

class ExtClassImpl : public ExtClass 
{ 
public: 
    void func1(int xy); 
    int func2(XYClass&param); 
}; 

int.cpp:

void ExtClassImpl::func1(int xy) 
    { 
    ... 
    } 
    int ExtClassImpl::func2(XYClass&param) 
    { 
    ... 
    }