使用类作为基类

Use a class as a BaseClass

本文关键字:基类      更新时间:2023-10-16

我有:

class BASE{
public:
    virtual void func1() = 0;
};

然后我有一些派生类,例如:

class Derived1 : public BASE{
public:
    void func1() {...implementation....}
};
class Derived2 : public BASE{
    public:
        void func1() {...implementation....}
    };

总的来说,我想做一些类似的事情(伪代码(:

if ( cond ){
   BASE b := new Derived1
}
else{
   BASE b := new Derived2
}
b.func1();

因此,调用的 func1(( 函数是专门用于派生类的函数。

我尝试做:

BASE b = new Derived1();

但是编译器抱怨。

C++有可能做到这一点吗?如何?

显然你已经习惯了垃圾收集的OO语言,比如Java。要很好地回答这个问题,从这个角度来看可能是件好事。

当你写东西时

Base b = new Derived1();

在 Java 中,会发生以下情况:

  1. 指向泛型Base对象的指针"&b"(我称之为β(在堆栈上分配
  2. 一个新的Derived1对象(我称之为d(被分配到堆上
  3. β设置为指向Derived对象。

在Java中你很容易摆脱这种情况的原因是有一个垃圾收集器。只要β在堆栈上并指向d,这就不会产生任何影响,因为这样 GC 就知道d仍然可以访问并且可能正在使用中。但是一旦没有指针再引用d(例如,因为你声明b的函数离开了范围(,GC就被允许释放d占用的空间。很容易,但是垃圾收集有几个缺点,这是我们C++

不希望的。

所以在C++,如果你做类似的事情

Base* b = new Derived1();

直接对应于 Java 版本,您有一个问题:当b离开范围时,不再引用d但它仍然位于堆上!您需要自己删除它

delete b;

(请注意,这有很大的优势,您可以准确地确定它在哪个点被删除,而垃圾收集器可能会让它无用地放置很长时间,并且仅在您开始耗尽内存时才擦除它(。但再一次,这还不够:与垃圾收集器不同,您的程序不会自动知道b指向Derived1对象而不是Base对象,因此delete会删除认为它正在处理Base。但是很容易解决这个问题:在Base类中包含虚拟析构函数,例如

class Base{
 public:
  virtual void func1() = 0;
  virtual ~Base() {}  
};


现在,手动删除所有内容的需求显然有点危险:如果您忘记这样做,您就会有内存泄漏,即只要您的程序运行,该对象就永远不会从堆中删除。出于这个原因,应该使用智能指针而不是普通的 C 样式指针,这会在离开范围时自动删除它们指向的对象。在 C++11 中,这些在标准库(标头 <memory> (中定义。你只是做

std::unique_ptr<Base> b(new Derived1());

现在,当b离开作用域时,Derived1对象会自动删除。

在所有这些示例中,调用虚拟函数的工作方式相同:

b->func1();

使用指针(或引用(,否则您的对象将只复制到普通的 BASE 实例中:

BASE* b = new Derived1;
b->func1();
// later...
delete b;

另外,不要忘记在这种情况下使您的析构函数成为虚拟的。

由于整个新/删除的事情只是为了让多态性工作有点麻烦,所以现在人们倾向于使用智能指针:

std::shared_ptr<BASE> b=std::make_shared<Derived1>();
b->func1();

有两种方法可以做到这一点:

  • 使用基类类型的指针:

    Base *b = new Derived1;

  • 如果要创建派生的复制构造,请使用基类类型的 const 引用:

    Base const& b = Derived;

  • 如果要分配在其他地方创建的派生类对象,请使用基类类型的非 const 引用:

    Derived d; Base& b = d;

尼特斯:

  • 为了继承,您需要在类定义中指定以下内容:

    class Derived1 : public Base {

  • 函数需要具有返回值。

    class B{ public: virtual void func() = 0; };

您正在尝试分配指向普通对象的指针。

new Derived1()

此表达式返回一个指针。您要么必须BASE成为指针(BASE *(,要么只是在堆栈上创建它(最好是Base b;(。但在这种情况下,您需要指针(或引用(在类之间进行转换。

此外,您不能在 if-else 语句中创建对象,因为当您以后要使用它时,它将超出范围。

您可以执行以下操作:

BASE *b;
if (cond)
    b = new Derived1();
else
    b = new Derived2();
b->func1();

当然,你必须记得事后delete你的指针。考虑改用智能指针。

指针(及其陷阱(的使用已经涵盖,所以我将向您展示如何在没有动态内存分配的情况下使用多态性,这可能证明我是一个异端,甚至考虑它。

首先,让我们看看你的原始代码,修补后编译

void foo(bool const cond) {
    Base* b = 0;
    if (cond) { b = new Derived1(); }
    else      { b = new Derived2(); }
    b->func1();
    delete b; // do not forget to release acquired memory
}

假设您有一个用于Basevirtual析构函数,这工作正常。

程序员进化的下一个合乎逻辑的步骤是使用智能指针来避免编写delete(delete仅供初学者和专家库编写者使用(:

void foo(bool const cond) {
    std::unique_ptr<Base> b;
    if (cond) { b.reset(new Derived1()); }
    else      { b.reset(new Derived2()); }
    b->func1();
}

当然,它仍然需要一个virtual析构函数来Base

让我们意识到这个函数做两件事:

  • 在给定条件的情况下选择一个类
  • 对生成的实例执行一些工作

我们可以分解它,例如通过提取构建工作:

std::unique_ptr<Base> build(bool const cond) {
    if (cond) { return { new Derived1() }; }
    return { new Derived2() };
}
void foo(bool const cond) {
    std::unique_ptr<Base> b = build(cond);
    b->func1();
}

这是大多数人所做的。

我声称还有另一种可能性,我们可以隔离实际工作,而不是隔离构建:

void dowork(Base& b) { b.func1(); /* and perhaps other things */ }
void foo(bool const cond) {
    std::unique_ptr<Base> b(cond ? new Derived1() : new Derived2());
    work(*b);
}

我们实际上可以更进一步:

void foo(bool const cond) {
    if (cond) {
        Derived1 d;
        work(d);
    } else {
        Derived2 d;
        work(d);
    }
}

多态性不需要动态内存分配。

你知道最后一个例子有什么好玩的:

  • 在没有 C++11 移动语义的情况下,它工作得很好
  • 它不需要在Base中具有virtual析构函数

你忘了吗?

class Derived1 : public BASE
{
};

至少我在你的代码中看不到它 - 派生的 BASE 类

如果函数签名相同,也许在这种情况下接口声明会更合适......然后让每个类实现接口,并将变量声明为你声明的接口类型...