纯粹的虚拟C++奇怪的行为

Weird behaviour C++ pure virtuals

本文关键字:C++ 虚拟 纯粹      更新时间:2023-10-16

纯虚拟类C++怪异行为

请帮助血腥的C++初学者更好地理解纯虚拟课程。

我尝试了一个带有C++虚拟的简单示例,但不确定结果。 例如,如果我在另一种编程语言中尝试相同的方法,例如,输出将是

期望/预期输出

1 -> 推文
2 -> 推文

但是,这里的输出是

实际输出

1 -> 喵
2 -> 推特

为什么? 似乎类动物的运算符=没有效果。 这是因为类动物的标准运算符=被称为什么都不做吗? 如何在不使用指针的情况下实现类似于 java 的行为? 这可能吗?

下面是一个尽可能简化的代码示例:

法典

#include <string>
#include <iostream>
using namespace std;
class Animal
{
public:
virtual string test() const = 0;
};
class Cat : public Animal
{
public:
virtual string test() const
{
return "Meow";
}
};
class Bird : public Animal
{
public:
virtual string test() const
{
return "Tweet";
}
};
void test_method(Animal &a)
{
Bird b;
a = b;
cout << "1 -> " << a.test() << endl;
cout << "2 -> " << b.test() << endl;
}
int main(int args, char** argv)
{
Cat c;
Animal &a = c;
test_method(a);
return 0;
}

显然,您对a = b作业的行为有一些不切实际的期望。

您的a = b赋值只是将数据从Bird对象的子对象Animalb复制到a引用Cat对象的子对象Animal。由于Animal根本没有数据,因此a = b是一个空操作,一个无操作。它根本不做任何事情。

但即使它确实做了一些事情,它也只会复制数据字段。它无法复制对象的多态标识。无法更改C++中对象的多态标识。无法更改对象的类型。无论您做什么,Cat将始终保持Cat

您编写的代码可以缩短为

Cat a;
Bird b;
(Animal &) a = b; // Same as `(Animal &) a = (Animal &) b`
a.test(); // still a `Cat`
b.test(); // still a `Bird`

你以一种更模糊的方式做了同样的事情。

在C++引用不能被回弹;它们总是引用你创建它们的变量。在这种情况下,a是对c的引用,因此它是并且将永远是一个Cat

重新分配引用时,不会重新绑定引用 - 而是在分配引用引用的基础变量。因此,赋值a = bc = b相同。

您是所谓的对象切片的受害者。C++ 在处理对象的方式上与 Java 和 C# 不同。例如,在 C# 中,当您创建新实例时,每个用户定义类型都作为引用进行管理,并且只有基元类型和结构作为值进行处理,这意味着在 Java 或 C# 中,当您分配对象时,您只分配引用,例如:

对象 a = 新对象(); 对象 b = a;

将导致 a 和 b 都指向同一个对象(我们分配 a 时创建的那个)。

在C++,故事是不同的。 您可以在堆或堆栈中创建对象的实例。 并且您可以通过引用、指针或值传递所述对象。

如果你分配引用或指针,它的行为将类似于C#和Java。但是,如果按值分配对象,即分配实际对象而不是指针或引用,则将创建该对象的新副本。默认情况下,每个用户在C++中定义类型都是可复制的。

当涉及继承和多态性时,此复制行为会产生问题,因为将子类型复制到父类型时,将创建的副本将仅包含子类型中父类型的部分信息,从而丢失您可能具有的任何多态性。

在您的示例中,当您将 Cat 对象复制到 Animal 对象时,只复制 cat 的 Animal 部分,这就是您失去多态性的原因,虚拟表不再存在。如果你的基类以任何方式是抽象的,这甚至是不可能的。

如果要保留多态性,解决方案是通过指针或引用而不是按值传递对象。您可以在堆中创建对象并分配该指针,您可以在堆栈中获取对象的地址并断言该指针,或者您可以只获取对象的引用并分配它。

这里要吸取的教训是,永远不要通过具有任何类型的多态性的值对象传递或赋值,否则您最终会对其进行切片。

试试这个。

#include <string>
#include <iostream>
using namespace std;
class Animal
{
public:
virtual string test() const = 0;
};
class Cat : public Animal
{
public:
virtual string test() const
{
return "Meow";
}
};
class Bird : public Animal
{
public:
virtual string test() const
{
return "Tweet";
}
};
void test_method(Animal *a)
{
Bird *b = new Bird();
a = b;
cout << "1 -> " << a->test() << endl;
cout << "2 -> " << b->test() << endl;
free(a);
}
int main(int args, char** argv)
{
Cat *c = new Cat();
Animal *a = c;
test_method(a);
free(c);
return 0;
}

删除a = b,你会得到"喵",然后是"推文"...

以更通用的态度:

首先,为泛型类定义一个泛型接口(在此特定示例中Animal)。

然后,您可以将此接口与任何子类实例一起使用(在此特定示例中为CatBird)。

每个实例都将根据您的特定实现"操作"。

您在函数test_method中的错误是使用了子类的实例,而没有通过泛型类(带有引用或指针)引用它。

为了将其更改为Animal实例的通用函数,您可以执行以下操作:

void test_method(Animal &a)
{
cout << a.test() << endl;
}