如何在C++中声明一个抽象类的向量列表?

3

我有几个类继承自一个主类。为了简单起见,我已经过度简化了类定义,使其变得简短并直接到点。

animal.h

所有其他类都从中继承的主类:

class Animal {
protected:
    string name;
public:
    Animal(string name);
    virtual string toString() { return "I am an animal"; }
};

bird.h

class Bird: public Animal {
private:
    bool canFly;
public:
    Bird(string name, bool canFly = true) 
     : Animal(name)   // call the super class constructor with its parameter
    {
        this->canFly = canFly;
    }
    string toString() { return "I am a bird"; }
};

indect.h

class Insect: public Animal {
private:
    int numberOfLegs;
public:
    Insect(string name, int numberOfLegs) : Animal(name) {
        this->numberOfLegs = numberOfLegs;
    }
    string toString() { return "I am an insect."; }
};

现在,我需要声明一个vector<Animal>,它将保存每个继承类的多个实例。

main.cpp

#include <iostream>
#include "animal.h"
#include "bird.h"
#include "insect.h"

// assume that I handled the issue of preventing including a file more than once
// using #ifndef #define and #endif in each header file.

int main() {

    vector<Animal> creatures;

    creatures.push_back(Bird("duck", true));
    creatures.push_back(Bird("penguin", false));
    creatures.push_back(Insect("spider", 8));
    creatures.push_back(Insect("centipede",44));

    // now iterate through the creatures and call their toString()

    for(int i=0; i<creatures.size(); i++) {
        cout << creatures[i].toString() << endl;
    }
}

我期望得到以下输出:

我是一只鸟

我是一只鸟

我是一只昆虫

我是一只昆虫

但是我得到了:

我是一个动物

我是一个动物

我是一个动物

我是一个动物

我知道这与 'vector creatures;' 这行代码有关。它调用了 Animal 的构造函数。但我的意图是告诉编译器,这个'creatures'指向一个由Animal继承的类所构成的数组,可能是'Bird',也可能是'insect' ,重点是:它们都实现了各自独特的‘toString()`版本。
我该怎么做才能声明一个多态对象数组,这些对象都是从同一个祖先类继承而来的?

1
你所面临的是对象切片问题。 - The Philomath
@ThePhilomath,您能否详细说明一下,或者指导我解决这个问题的地方? - Ahmad
问题出在 creatures.push_back(Bird("duck", true)); 上。你正在创建一个 Bird 对象并将其复制到 Animal 对象中。一种方法是动态创建对象,以便使用 vtable 可以解析正确的函数调用。 - The Philomath
2
创建一个指向Animal的指针向量:vector<Animal*> - jignatius
3个回答

5

你不能使用值类型语义(请阅读有关对象切割的文章)。你必须使用指针。

例如:

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Animal
{
 protected:
  std::string name;

 public:
  Animal(std::string name) : name(name)
  {
  }
  virtual std::string toString()
  {
    return "I am an animal";
  }
  virtual ~Animal()
  {
  }
};

class Bird : public Animal
{
 private:
  bool canFly;

 public:
  Bird(std::string name, bool canFly = true) : Animal(name)  // call the super class constructor with its parameter
  {
    this->canFly = canFly;
  }
  std::string toString()
  {
    return "I am a bird";
  }
};

class Insect : public Animal
{
 private:
  int numberOfLegs;

 public:
  Insect(std::string name, int numberOfLegs) : Animal(name)
  {
    this->numberOfLegs = numberOfLegs;
  }
  std::string toString()
  {
    return "I am an insect.";
  }
};

int main()
{
  std::vector<std::unique_ptr<Animal>> creatures;

  creatures.emplace_back(new Bird("duck", true));
  creatures.emplace_back(new Bird("penguin", false));
  creatures.emplace_back(new Insect("spider", 8));
  creatures.emplace_back(new Insect("centipede", 44));

  // now iterate through the creatures and call their toString()

  for (int i = 0; i < creatures.size(); i++)
  {
    std::cout << creatures[i]->toString() << std::endl;
  }
}

输出:

I am a bird
I am a bird
I am an insect.
I am an insect.

我还建议阅读关于Sean parent Run Time Polymorphism的文章。该思想如下:

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Animal
{
 public:
  struct Interface
  {
    virtual std::string toString() const = 0;
    virtual ~Interface()                 = default;
  };
  std::shared_ptr<const Interface> _p;

 public:
  Animal(Interface* p) : _p(p)
  {
  }
  std::string toString() const
  {
    return _p->toString();
  }
};

class Bird : public Animal::Interface
{
 private:
  std::string _name;
  bool        _canFly;

 public:
  Bird(std::string name, bool canFly = true) : _name(name), _canFly(canFly)
  {
  }
  std::string toString() const override
  {
    return "I am a bird";
  }
};

class Insect : public Animal::Interface
{
 private:
  std::string _name;
  int         _numberOfLegs;

 public:
  Insect(std::string name, int numberOfLegs)
      : _name(name), _numberOfLegs(numberOfLegs)
  {
  }
  std::string toString() const override
  {
    return "I am an insect.";
  }
};

int main()
{
  std::vector<Animal> creatures;

  creatures.emplace_back(new Bird("duck", true));
  creatures.emplace_back(new Bird("penguin", false));
  creatures.emplace_back(new Insect("spider", 8));
  creatures.emplace_back(new Insect("centipede", 44));

  // now iterate through the creatures and call their toString()

  for (int i = 0; i < creatures.size(); i++)
  {
    std::cout << creatures[i].toString() << std::endl;
  }
}

3

问题出在 creatures.push_back(Bird("duck", true)); 上。 你正在创建一个 Bird 对象并将其复制到 Animal 对象中。 一种解决方法是动态创建对象,这样正确的函数调用可以使用虚表进行解析。 修改代码中的这一部分,它就能正常工作。

vector<Animal *> creatures;

    creatures.push_back(new Bird("duck", true));
    creatures.push_back(new Bird("penguin", false));
    creatures.push_back(new Insect("spider", 8));
    creatures.push_back(new Insect("centipede",44));

编辑: 确保在creatures超出作用域之前释放内存。


由于指针是使用“new”创建的,因此一旦您完成了向量操作,它们必须使用“delete”进行解除分配。但是,使用像“unique_ptr”或“shared_ptr”这样的智能指针可以避免手动内存管理。 - jignatius
@jignatius 是的,你必须这样做。由于该问题未标记为C++11或更高版本的标签,我无法使用智能指针提出解决方案。 - The Philomath
好的。我只是想向楼主指出这一点。 - jignatius

1

C++对象是具有特定类型的。这与许多语言不同,其中变量始终持有对对象的引用,因此它们可以像容易地持有对派生对象的引用一样持有对基本对象的引用。

如果你将派生类实例复制到基类对象上,你会得到切片:只复制了基类数据,赋值方的类型仍然是基类的类型。

要在C++中实现多态行为,你需要使用std::variant指定允许的可能性,这样对象将持有其中一个选项,并在赋值时在它们之间切换类型,或者你需要使用指向基类的指针,它可以持有任何派生类型的指针,但你必须小心内存泄漏。你确实需要使用std::visitstd::get来检索值。

如果你要使用指针,你应该始终使用std::shared_ptrstd::unique_ptr来管理对象,以避免内存泄漏。

带有variant的代码:

int main() {

    vector<std::variant<Bird,Insect>> creatures;

    creatures.push_back(Bird("duck", true));
    creatures.push_back(Bird("penguin", false));
    creatures.push_back(Insect("spider", 8));
    creatures.push_back(Insect("centipede",44));

    // now iterate through the creatures and call their toString()

    for(int i=0; i<creatures.size(); i++) {
        cout << std::visit([](auto const& creature){
            return creature.toString();
        },creatures[i]) << endl;
    }
}

使用指针的代码:

int main()
{
  std::vector<std::unique_ptr<Animal>> creatures;

  creatures.emplace_back(std::make_unique<Bird>("duck", true));
  creatures.emplace_back(std::make_unique<Bird>("penguin", false));
  creatures.emplace_back(std::make_unique<Insect>("spider", 8));
  creatures.emplace_back(std::make_unique<Insect>("centipede", 44));

  // now iterate through the creatures and call their toString()

  for (int i = 0; i < creatures.size(); i++)
  {
    std::cout << creatures[i]->toString() << std::endl;
  }
}

使用std::shared_ptr编写的代码是等效的:只需在所有地方将unique替换为shared即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接