C ++头文件,声明一个类和方法,但不是成员?

时间:2009-04-22 19:30:39

标签: c++ file class header encapsulation

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

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

更新:感谢大家的回答!似乎pimpl成语是实现我所谈论的好方法。我打算做类似的事情:

我的Win32 DLL文件将包含许多单独的函数,如下所示:

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

这是Microsoft编写DLL文件的典型方式,因此我认为可能有充分的理由。

但是我想利用C ++对类的优秀语法,所以我将编写一个包装类来包装所有这些函数。它将有一个成员,这将是“无效* pimpl”。这个包装器类将非常简单,我可能只是声明它并在头文件中定义它。但是这个包装类除了让C ++代码看起来很漂亮之外别无其他目的。

8 个答案:

答案 0 :(得分: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将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的其他智能指针。有些东西会自行删除。

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

答案 1 :(得分:13)

使用 pimpl 成语。

答案 2 :(得分:7)

pimpl idiom会向您的班级添加一个void *私有数据成员,如果您需要快速和快速的内容,这是一项非常有用的技巧。脏。然而,它有它的缺点。其中主要是它使得在抽象类型上使用多态变得困难。有时您可能需要一个抽象基类和该基类的子类,收集指向向量中所有不同类型的指针并在其上调用方法。另外,如果pimpl习惯用法的目的是隐藏类的实现细节,那么它只有几乎成功:指针本身就是一个实现细节。也许是一个不透明的实现细节。但是仍然是一个实施细节。

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

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

<强> 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,因此无法实例化;不需要声明私人ctor。虚拟dtor的存在可确保如果某人deleteAnimal*,则还会调用正确的子类'dtor。

为了实现基类型的不同子类(例如dog&amp; cats),您将在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";
}

现在你已经在标题&amp;中定义了界面。实现细节完全分离出客户端代码根本看不到的地方。您可以通过从链接到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;
}

答案 3 :(得分:3)

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

答案 4 :(得分:3)

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

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

答案 5 :(得分: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)
  {
    ...
  }

答案 6 :(得分:0)

  

是否可以制作C ++标头   声明类的文件(.h)和   它的公共方法,但没有   声明私人成员   类?

最接近的答案是PIMPL习语。

请参阅Herb Sutter的The Fast Pimpl Idiom

IMO Pimpl在开发的初始阶段非常有用,您的头文件将会多次更改。 Pimpl的成本是由于它在堆上的内部对象的分配\解除分配。

答案 7 :(得分:-1)

在C ++中查看课程The Handle-Body成语