【问题标题】:C++ polymorphism of a object in an array数组中对象的 C++ 多态性
【发布时间】:2013-12-24 17:08:45
【问题描述】:

我是一名嵌入式软件工程师,来自位和 C 世界。 在那个世界里,闪存中有数据,用 C 中的 const 表示。RAM 中有数据。 RAM 昂贵且有限,而闪存便宜且足够。此外,由于碎片问题或安全规定,不允许使用 new、delete、malloc 等进行动态内存分配,首选静态设计。

我有大约 2000 个对象,它们具有相似的常量属性但行为不同。 所以对他们来说,我将 Shape Class 定义为一个基类,它包含我的对象的共享属性。为了表示不同的行为,Shape Class 有一个名为 Print() 的抽象方法,它将被父级覆盖。

ShapeList 是重要的部分。它是一个 const 数组,由“const Shapes”组成,因此它们将被链接器放入闪存部分。

下面的程序产生一个输出:

I'm a Shape has 3 dots
I'm a Shape has 4 dots
I'm a Shape has 5 dots

虽然预期的输出是:

I'm a Triangle has 3 dots
I'm a Rectangle has 4 dots
I'm a Pentagon has 5 dots

我需要多态行为。当我打印三角形时,它的行为应该像三角形,而不是形状。我该怎么做?

谢谢。

#include <array>
#include <cstdio>
class Shape
{
    public:
    const int DotCount;
    Shape(const int dot): DotCount(dot) {}
    virtual void Print(void) const; // this is virtual method
};

void Shape::Print(void) const
{
    printf("I'm a Shape has %d dots\n", DotCount);
}

class Triangle: public Shape
{
    public:
    Triangle(void): Shape(3) { }
    void Print(void) const;
};

void Triangle::Print(void) const
{
    printf("I'm a Triangle has %d dots\n", DotCount);
}

class Rectangle: public Shape
{
    public:
    Rectangle(void): Shape(4) { }
    void Print(void) const;
};

void Rectangle::Print(void) const
{
    printf("I'm a Rectangle has %d dots\n", DotCount);
}

class Pentagon: public Shape
{
    public:
    Pentagon(void): Shape(5) { }
    void Print(void) const;
};

void Pentagon::Print(void) const
{
    printf("I'm a Pentagon has %d dots\n", DotCount);
}

const std::array<const Shape, 3> ShapeList = { Triangle(), Rectangle(), Pentagon() };

int main(void)
{
    ShapeList.at(0).Print();
    ShapeList.at(1).Print();
    ShapeList.at(2).Print();
    return(0);
}

更多问题: 今天我意识到虚函数还有另一个问题。当我将任何虚拟函数添加到基类中时,编译器开始忽略“const”指令并将对象自动放置到 RAM 而不是闪存中。我不知道为什么。我已经向 IAR 提出了这个问题。到目前为止我得到的结论是,即使使用堆,ROMable 类也不可能出现多态行为:/

【问题讨论】:

  • 它们必须是指针,否则你会得到对象切片en.wikipedia.org/wiki/Object_slicing
  • 我没有仔细阅读这个问题。 Also, dynamic memory allocation using new, delete, malloc etc is not allowed due to fragmentation problem or safety regulations, static designs are preferred. 根据我的阅读,没有指针或引用是不可能实现多态性的。
  • @juanchopanza:我的问题没有解决方案。
  • 请注意,您不一定需要newmalloc。您只需要将指针存储在数组中。它们指向的对象是否被动态分配是一个正交问题。

标签: c++ arrays polymorphism


【解决方案1】:

此版本不使用动态内存:

Triangle tri;
Rectangle rect;
Pentagon pent;
const std::array<const Shape*, 3> ShapeList {
    &tri, &rect, &pent
};
for (unsigned int i = 0; i < ShapeList.size(); i++)
    ShapeList[i]->Print();

在 C# 等语言中,您可以使用 as 关键字来实现“多态性”。在 C++ 中,它看起来像这样:

    const Triangle* tri = dynamic_cast<const Triangle*>(ShapeList[i]);
    if (tri)
        static_cast<Triangle>(*tri).SomethingSpecial();

如果dynamic_cast返回的指针有效,可以调用Triangle的特殊函数。例如,这将允许您有一个循环遍历 ShapeList 并仅调用 Triangle 方法。如果可以使用异常,请考虑将其包装在 try catch 块中并捕获 std::bad_cast

注意:你需要一个const 指针,因为ShapeList[i] 是常量。 static_cast 是必要的原因是因为您在 const 指针上调用了非常量方法。您可以添加 const 限定符,例如 SomethingSpecial() const,然后只需执行 tri-&gt;SomethingSpecial()。否则,您只需将 const 关闭即可。

例如:

static_cast<Triangle*>(tri)->SomethingSpecial();
// error: static_cast from type 'const Triangle*' to type 'Triangle*' 
// casts away qualifiers

这将起作用:

const_cast<Triangle*>(tri)->SomethingSpecial();

【讨论】:

  • @ChristianSchack 有什么好处?尤其是当子类都需要不同的附加数据时。
  • Himm 这个解决方案在我看来不错,至少比其他解决方案好。请问为什么会崩溃:“const std::array ShapeList = { &Triangle(), &Rectangle(), &Pentagon() };" ?
  • op 表示所有子类的数据都是一样的。所以函数的参数不会改变。只是行为。
  • @Mehmet,因为在这种情况下您正在获取隐藏临时地址,所以崩溃了。临时变量超出范围,导致行为未定义。可能临时是在堆栈上分配的,稍后在执行期间会被覆盖。
  • 请注意 static_cast 是不必要的,因为 tri 已经是 Triangle* 类型。
【解决方案2】:

正如其他人所指出的那样,方便且常用的方式并非如此。修复它会导致代码与目标平台的限制不一致。但是,您可以通过几种不同的方式模拟多态性。

您可以像这样按类型隔离对象:

const Triangle tris[] = {tri1, tri2, ...};
const Rectangle rects[] = {rect1, rect2, ...};
// for correct order, if needed
const Shape * const shapes[] = {&tris[0], &rects[2], &rects[0], ...}:

您仍然需要创建所有方法(对于各种类型的行为不同)virtual,并且您需要为每个对象支付额外的指针(如果计算 vtable 指针,则需要支付两个,这有点不公平)。 您还可以删除所有 virtual 以支持显式标记:

enum ShapeKind { Triangle, Rectangle, Pentagon };
struct Shape {
    ShapeKind kind;
    int sides;
    ...
};

如果各种子类需要非常不同的成员数据,请使用union。 这有许多严格的限制并导致相当丑陋的代码,但可以很好地工作。例如,您需要预先知道您的层次结构,并且子类需要大致相同的大小。请注意,这不一定比 virtual 替代方案快,但是当它适用时,它可以占用更少的空间(一个字节而不是 vtable 指针)和 make introspection leaner

【讨论】:

  • "请注意,这不一定更快" - 是的,但如果是这样的话,对于内联的琐碎函数(最多大约一个数量级),它可能会快得多取决于编译器/CPU 等)....
  • C++17 std::variant 为您提供类型安全的联合。您甚至可以使用std::variant&lt;Shape, Triangle, Rectangle&gt; 并使用std::visit (comes with variant) 根据正确的对象类型进行调度。这应该为您提供与enum ShapeKind + 手动调度相同的功能(和运行时性能)。
【解决方案3】:

您可以将多态性与所有约束一起使用,只需对代码稍作更改:

const Triangle triangle;
const Rectangle rectangle;
const Pentagon pentagon;

const std::array<const Shape*, 3> ShapeList = { &triangle, &rectangle, &pentagon };

【讨论】:

    【解决方案4】:

    任何简单的解决方法是添加一个字符串来定义它是什么类型的形状。

    class Shape
    {
        public:
        const int DotCount;
        const char* shapeType
        Shape(const int dot, const char* type): DotCount(dot), shapeType(type) {}
        void Print(void) const;
    };
    
    void Shape::Print(void) const
    {
        printf("I'm a "); printf(shapeType); printf(" has %d dots\n", DotCount);
    }
    
    class Triangle: public Shape
    {
        public:
        Triangle(void): Shape(3, "Triangle") { }
    };
    

    【讨论】:

    • 好主意。最好将name字段保存在基类中,因为都使用它。
    • 这对我不好。 Shape::Print() 函数将疯狂增长以支持父母的所有行为。此外,所有团队成员每次定义新形状时都必须修改 Shape::Print() 函数具有不同的行为。
    • cplusplus.com/doc/tutorial/polymorphism 您的代码看起来应该可以工作,不过请查看此链接。抱歉,我的回答对您的情况没有帮助。
    • @MehmetFide 为什么会发疯?无论如何它都会使用虚拟表以最终获得名称所以基本不一样?
    • @Itzik984:是的,但虚拟表将由编译器负责。在这里提供的解决方案中,Shape::Print 方法很可能必须包含巨大的 switch-case 来实现依赖于 shapeType 的所有父行为。如果你是一个团队并且有很多程序员在做这件事,这也不是一个好方法。想象一下,你要实现三角形,我会做矩形,其他人做更多。我们每个人都必须同意 shapeType 并且每个人都必须扩展 Shape::Print 方法以支持他的形状行为。有了真正的多态解决方案,每个人都将是独立的。
    【解决方案5】:

    我在 C 语言中找到的另一个无需动态分配的多态动态调度的解决方案是手动传递 vtable 指针,就像 GHC 在 Haskell 中对类型类进行脱糖一样。这种方法在 C++ 中也是合理的,因为它比 C++ 的对象系统所允许的更轻量并且严格来说更通用。

    重载/多态函数需要一个指针,该指针指向参数类型所属的每个类型类的函数指针结构——相等比较、排序等。所以你可能有:

    template<class Container, class Element>
    struct Index {
      size_t (*size)(const Container& self);
      const Element& (*at)(const Container& self, size_t index);
    };
    
    enum Ordering { LT, EQ, GT };
    
    template<class T>
    struct Ord {
      Ordering (*compare)(const T& a, const T& b);
    };
    
    template<class Container, class Element>
    const Element* maximum(
      const Index<Container, Element>& index,
      const Ord<Element>& ord,
      const Container& container) {
    
      const size_t size = index.size(container);
      const Element* max = nullptr;
    
      for (size_t i = 0; i < size; ++i) {
        const Element& current = index.at(container, i);
        if (!max || ord.compare(current, *max) == GT)
          max = &current;
      }
    
      return max;
    
    }
    

    由于类型参数是“幻像类型”而不是用于表示,如果您担心代码大小,链接器应该能够对此类函数进行重复数据删除。类型不安全但可能对编译器更友好的替代方法是使用void*

    在 C++ 中,如果您在编译时知道 vtable 函数,也可以将它们作为模板参数传递——即手动去虚拟化。这允许更多优化(例如内联),但显然不允许动态调度。

    一个警告:由于您没有部分函数应用程序或闭包,您会发现进行部分专业化是一种有趣的体验,例如 Haskell:

    instance (Ord a) => Ord [a] where ...
    

    如果元素a 有排序,则表示事物列表[a] 有排序。

    【讨论】:

      猜你喜欢
      • 2020-11-18
      • 2019-12-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-10
      • 1970-01-01
      • 2015-01-16
      • 1970-01-01
      相关资源
      最近更新 更多