20

是否可以创建一个 C++ 头文件 (.h) 来声明一个类及其公共方法,但不定义该类中的私有成员?我发现有几页说您应该在头文件中声明该类及其所有成员,然后在您的 cpp 文件中单独定义方法。我问是因为我想要一个在 Win32 DLL 中定义的类,并且我希望它被正确封装:该类的内部实现可能会更改,包括其成员,但这些更改不应影响使用该类的代码.

我想如果我有这个,那么编译器就不可能提前知道我的对象的大小。但这应该没问题,只要编译器足够聪明,可以使用构造函数,并且只传递指向内存中存储对象的位置的指针,并且永远不要让我运行“sizeof(MyClass)”。

更新: 感谢所有回答的人!似乎 pimpl 成语是实现我所说的一个好方法。我要做类似的事情:

我的 Win32 DLL 文件将有一堆单独的函数,如下所示:

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

这是微软编写 DLL 文件的典型方式,所以我认为这可能是有充分理由的。

但我想利用 C++ 对类的良好语法,所以我将编写一个包装类来包装所有这些函数。它将有一个成员,即“void * pimpl”。这个包装类将非常简单,我不妨声明它并在头文件中定义它。但据我所知,这个包装类除了让 C++ 代码看起来很漂亮之外真的没有其他目的。

4

8 回答 8

34

我认为您正在寻找的是一种叫做“pimpl idiom”的东西。要了解这是如何工作的,您需要了解在 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 将 CThing 的方法定义为对 impl 的传递:

// 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。自我删除的东西。

另外,请记住,这种方法会带来一些烦恼。调试可能有点烦人(额外的重定向级别以逐步执行)。创建一个类也有很多开销。如果你为每节课都这样做,你会厌倦所有的打字:)。

于 2009-04-22T19:42:42.370 回答
13

使用pimpl成语。

于 2009-04-22T19:35:37.707 回答
7

pimpl习惯用法为你的类添加了一个 void* 私有数据成员,如果你需要一些快速和肮脏的东西,这是一种有用的技术。然而,它有它的缺点。其中主要是它使得在抽象类型上使用多态变得困难。有时您可能需要一个抽象基类和该基类的子类,收集指向向量中所有不同类型的指针并调用它们的方法。此外,如果 pimpl 习惯用法的目的是隐藏类的实现细节,那么它几乎成功了:指针本身就是一个实现细节。也许是一个不透明的实现细节。但仍然是一个实现细节。

存在 pimpl 习惯用法的替代方法,可用于从接口中删除所有实现细节,同时提供可在需要时多态使用的基本类型。

在您的 DLL 的头文件(客户端代码中的#included)中创建一个抽象类,其中只有公共方法和概念,这些方法和概念指示如何实例化该类(例如,公共工厂方法和克隆方法):

狗窝

/****************************************************************
 ***
 ***    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*
    };
};

...Animal 是一个抽象基类,因此无法实例化;不需要声明私有 ctor。虚拟 dtor 的存在确保如果有人deletes an Animal*,也会调用正确的子类的 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 的代码调用头文件中声明的方法来使用它。这是一个示例驱动程序:

主.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;
}
于 2009-04-22T21:53:49.090 回答
3

谷歌“疙瘩成语”或“处理 C++”。

于 2009-04-22T19:34:54.587 回答
3

是的,这可能是一件值得做的事情。一种简单的方法是使实现类从头文件中定义的类派生。

缺点是编译器不知道如何构造你的类,所以你需要某种工厂方法来获取类的实例。堆栈上不可能有本地实例。

于 2009-04-22T19:35:35.367 回答
2

您必须在标头中声明所有成员,以便编译器知道对象有多大等等。

但是你可以通过使用一个接口来解决这个问题:

分机:

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

诠释.h:

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

诠释.cpp:

  void ExtClassImpl::func1(int xy)
  {
    ...
  }
  int ExtClassImpl::func2(XYClass&param)
  {
    ...
  }
于 2009-04-23T22:19:01.163 回答
0

是否可以创建一个 C++ 头文件 (.h) 来声明一个类及其公共方法,但不声明该类中的私有成员?

最接近的答案是 PIMPL 成语。

请参阅Herb Sutter的 The Fast Pimpl Idiom

IMO Pimpl 在开发的初始阶段非常有用,您的头文件将多次更改。Pimpl 有它的成本,因为它在堆上分配\释放内部对象。

于 2009-04-22T19:38:03.643 回答
-1

查看C++ 中的The Handle-Body Idiom 类

于 2009-04-22T19:38:00.120 回答