在不处理指针时调用子类(虚拟)函数(后期绑定)

Posted

技术标签:

【中文标题】在不处理指针时调用子类(虚拟)函数(后期绑定)【英文标题】:Calling subclass (virtual) function (late binding), when not dealing with pointers 【发布时间】:2017-09-28 04:32:14 【问题描述】:

查看答案Why do we need virtual functions in C++,他们表明虚函数允许后期绑定,这导致在被向下转换时能够调用子类函数。

为了完整起见,我将相关代码包含在内。

class Animal

 public:
   virtual void eat()  
       std::cout << "I'm eating generic food.";
   
;
class Cat : public Animal

public:
  void eat()  std::cout << "I'm eating a rat."; 
;

现在考虑以下两个函数

void func(Animal *xyz)  xyz->eat(); 
void func2(Animal xyz)  xyz.eat(); 

我们看到调用 func 结果

Animal *animal = new Animal;
Cat *cat = new Cat;
func(animal); // outputs: "I'm eating generic food."
func(cat);    // outputs: "I'm eating a rat."

调用 func2 时会导致

Animal animal;
Cat cat;
func2(animal); // outputs: "I'm eating generic food."
func2(cat);    // outputs: "I'm eating generic food."

我的问题是:

我怎样才能使接收参数的函数(不是指针)指向子类的实例,将使用覆盖的方法?换句话说,func2如何导致“我正在吃老鼠。”。此外,我想了解为什么会出现这种差异。提前谢谢你。

【问题讨论】:

【参考方案1】:

我怎样才能使接收参数的函数(不是指针)指向子类的实例,将使用覆盖的方法?换句话说,func2怎么会导致“我在吃老鼠”。

您可以使用引用而不是指针。

void func2(Animal& xyz)  xyz.eat(); 

然后,使用

Animal animal;
Cat cat;
func2(animal);
func2(cat); 

会如您所愿。

此外,我想了解为什么会出现这种差异。

这是由对象切片引起的。请参阅 What is object slicing? 以了解什么是对象切片以及它如何在您的代码中发挥作用。

【讨论】:

感谢您的回答。是否可以以这种方式重新编写代码,从而不需要引用?我正在处理需要一般行为的 2 个类。 @LeastSquaresWonderer,要获得动态调度,或者你称之为后期绑定,你需要传递一个指针或一个引用。如果传递对象,则无法获得动态调度。阅读对象切片以了解原因。【参考方案2】:

C++ 中的整个虚函数机制是基于根据调用中使用的对象的动态(“实际”)类型来选择要调用的实际函数的思想。在这个函数中

void func2(Animal xyz)  xyz.eat(); 

xyz 对象的类型是Animal。它被明确硬编码为Animal。它的静态类型是Animal。它的动态类型是Animal。在所有方面都是Animal。您自己要求编译器通过使用上述参数声明来做到这一点。

这意味着xyz.eat(); 调用将始终调用Animal::eat()。没有办法解决它。没有办法让它调用Cat::eat(),因为这里没有Cat 对象。通过将Cat 作为实际参数传递,您只是要求编译器从该Cat 生成Animal xyz,然后忽略原始Cat。 (这就是通常所说的“切片”。)所有后续工作都使用Animal xyz 完成。

【讨论】:

【参考方案3】:

解释是func2 函数与您传递给它的对象一起工作。该函数有自己的参数变量Animal xyx,它是从您的animalcat 变量构造的。所以在func2 中你总是有一个基类对象可以处理,因此函数总是调用通用答案。

void func2(Animal xyz)  // uses its own Animal xyz, created on call

    xyz.eat();          // uses a local xyz object of Animal class with its generic functions


Cat cat;
func2(cat);       // here a new Animal xyz is created from the cat

您必须将指向实际对象的指针或引用作为参数传递给函数,以允许函数访问对象的特定重写虚函数。像这样:

void func2(Animal& xyz)   // uses a reference to the argument object

    xyz.eat();            // the actual argument is used with its overridden functions


Cat cat;
func2(cat);               // the cat is passed to the callee

【讨论】:

【参考方案4】:

您可以使用值语义来实现您的类。然后你不需要指针或引用。请参阅 Sean Parent 的“Better Code: Runtime Polymorphism”,这可能会让您大吃一惊。 Here's the code。观看演讲,了解他如何使用值语义来避免不良数据共享并实现令人印象深刻的多态撤消系统。这是一个带注释的示例:

// A generic draw function that any type that streams to std::ostream can use
template <typename T>
void draw(const T& x, ostream& out, size_t position)
 
  out << string(position, ' ') << x << endl; 


// A class that can hold anything with value semantics
// -- note: no virtual functions and no inheritance!
class object_t 
    // ... see the talk for the details here ...
    // This is where the magic happens
;

// Define a vector of our object_t to be a document
using document_t = vector<object_t>;

// Overload the draw() function for document - just iterate
// through the vector and call draw() on each item in it
void draw(const document_t& x, ostream& out, size_t position)

    out << string(position, ' ') << "<document>" << endl;
    for (auto& e : x) draw(e, out, position + 2);
    out << string(position, ' ') << "</document>" << endl;


// Define my own class that the code above knows nothing 
// about and that doesn't inherit from object_t
class my_class_t 
    /* ... */
;

// Overload draw for it
void draw(const my_class_t&, ostream& out, size_t position)
 out << string(position, ' ') << "my_class_t" << endl; 

// Use all this stuff
int main()

    document_t document; // just a vector!

    // Add some objects that don't inherit from object_t!
    document.emplace_back(0);
    document.emplace_back(string("Hello!"));

    // Show what we've got so far
    draw(document, cout, 0);

    // Add some more stuff
    document.emplace_back(document); // Add a whole copy of the current doc!
    document.emplace_back(my_class_t()); // Add an arbitrary type that doesn't inherit from object_t!

    draw(document, cout, 0);

打印出来:

<document>
  0
  Hello!
</document>
<document>
  0
  Hello!
  <document>
    0
    Hello!
  </document>
  my_class_t
</document>

看到它在 Wandbox 上运行。

再看一遍——你只是得到了多态行为,而不必显式地从基类继承。(提示:他使用重载作为主要机制而不是继承。)

另请参阅描述此方法的 blog post。

【讨论】:

以上是关于在不处理指针时调用子类(虚拟)函数(后期绑定)的主要内容,如果未能解决你的问题,请参考以下文章

从指向对象的指针调用成员函数指针时调用错误的函数

如何能避免在调用子类对象的虚函数时调用父类的虚函数呢?

深入C++对象模型&虚函数表

C++的虚函数表

C++的虚函数表

VUE之自定义指令