【问题标题】:Why can't C++ compiler know a pointer is pointing to a derived class?为什么 C++ 编译器不能知道指针指向派生类?
【发布时间】:2020-09-09 06:49:54
【问题描述】:

我刚刚开始学习 C++ 中的 OOP。我想知道为什么需要 virtual 关键字来指示编译器进行后期绑定?为什么编译器在编译时不知道指针指向派生类?

class A { 
    public: int f() { return 'A';}
};

class B : public A {
    public: int f() { return 'B';}
};

int main() {

    A* pa;
    B b;
    pa = &b; 
    cout << pa->f() << endl;
}

【问题讨论】:

  • 您能否展示一个演示您的问题的最小代码示例?
  • 因为这就是 C++ 的设计方式——特别是“不为不使用的东西付费”的理念。在运行时将基类函数的调用解析为派生类重载的成本比静态解析为基类版本的成本更高,因此它不是默认选择(即有必要故意使函数虚拟,而不是所有函数默认为虚拟)。还有其他原因。
  • @CoryKramer 我添加了一个示例。在这种情况下,是否清楚 pa 指向 B 类的对象?感谢您的快速回复!

标签: c++ oop pointers compiler-construction


【解决方案1】:

关于在编译时不知道,通常情况下行为仅在运行时才知道。考虑这个例子

#include <iostream>

struct A {};
struct B : A {};
struct C : A {};

int main()
{
    int x;
    std::cin >> x;
    A* a = x == 1 ? new B : new C;
}

在这个例子中,编译器如何知道a 将指向B* 还是C*?它不能,因为行为取决于运行时值。

【讨论】:

  • 但是如果新的关键字没有被使用呢?就像我给出的例子一样?
  • @VeryConfused:你问了一个一般情况的问题,或者至少,回答的每个人都认为你的意思是一般情况。给定一个非常具体的情况,允许编译器根据语言规则优化,所以给定你的源代码,因为编译器可以证明*pa b,它可以优化一些东西。
  • 知道*pa b,但是,并没有给编译器许可调用B::f:语言规则要求它调用A::f , 所以它会这样做,不管它是否做了这种优化。将virtual 添加到A 类中的定义会更改此规则:现在编译器必须 改为调用B::f
  • 如果我们有更一般的情况——*pa 的底层类型未知,甚至可能不知道——能够,无论出于何种原因——@987654335 @ 关键字或缺少该关键字,使编译器可以选择假设 *pa 具有类型 A(当缺少 virtual 时)或不做出任何此类假设(当 virtual 存在于基类定义中时) .
【解决方案2】:

怎么可能(完全概括)?例如

#include <cstdlib>
struct Parent {};
struct Child : Parent {};

int main()
{
    Parent* p = std::rand() % 2 ? new Parent() : new Child();
}

【讨论】:

  • 好吧,你是说我的例子中的指针 pa 可能指向一个动态创建的对象(即使它不是),所以这就是编译器不调用覆盖的原因?
  • @VeryConfused:我的意思是编译器不可能在编译时知道p 指向什么。
【解决方案3】:

假设你有一个简单的类层次结构

class Animal
{
    // Generic animal attributes and properties
};

class Mammal : public Animal
{
    // Attributes and properties specific to mammals
};

class Fish : public Animal
{
    // Attributes and properties specific to fishes
};

class Cat : public Mammal
{
    // Attributes and properties specific to cats
};

class Shark : public Fish
{
    // Attributes and properties specific to sharks
};

class Hammerhead : public Shark
{
    // Attributes and properties specific to hammerhead sharks
};

[有点啰嗦,但我想让“具体”类彼此远离]

现在假设我们有一个类似的函数

void do_something_with_animals(Animal* animal);

最后让我们调用这个函数:

Fish *my_fish = new Hammerhead;
Mammal* my_cat = new Cat;

do_something_with_animals(my_fish);
do_something_with_animals(my_cat);

现在,如果我们稍微想一想,在 do_something_with_animals 函数中,真的无法确切地知道animal 的参数可能指向什么。是Mammal吗? Fish?特定的Fish 子类型?

如果do_something_with_animals 函数在不同的translation unit 中定义,这对编译器来说更加困难,其中MammalFish 类(或其任何子类)的定义甚至可能没有可用。

【讨论】:

    【解决方案4】:

    virtual 关键字将单个函数标记为后期绑定。这与编译器可以或不知道任何指向对象的指针无关。这是关于传达程序员的意图(“这个函数意味着要被覆盖”)和效率(“这个函数需要启用后期绑定机制”)。

    【讨论】:

      【解决方案5】:

      (我从一些 cmets 的答案开始,但决定我应该写下我自己的答案。)

      我在这里稍微重新排列了您的代码,以便更容易编译和查看输出:

      #include <iostream>
      
      #ifdef V
      #define VIRTUAL virtual
      #else
      #define VIRTUAL /*nothing*/
      #endif
      
      class A { 
          public: VIRTUAL char f() { return 'A';}
      };
      
      class B : public A {
          public: char f() { return 'B';}
      };
      
      int main() {
          A* pa;
          B b;
          pa = &b; 
          std::cout << pa->f() << std::endl;
      }
      

      编译运行显示:

      $ c++ t.cc && ./a.out
      A
      $ c++ -DV t.cc && ./a.out
      B
      

      这表明virtual 关键字改变了程序的行为。这实际上是语言标准要求的。我认为,您的问题最好改写为为什么标准是这样编写的(它有一个更有用的通用答案)而不是编译器可以优化我的代码(它有一个具体但无用的答案:是的,它可以,但仍然需要打印A,而不是B)。

      语言定义并没有禁止编译器执行特殊的优化技巧。相反——尤其是在这种情况下,对于 C++——语言规范专门试图让编译器编写者更容易优化它。这最终给 C++ 程序员带来了更多负担。

      如果 C++ 是一种不同的语言 ...

      您所说的功能,即virtual 关键字,正是因此而存在的。语言可以有不同的定义(和其他一些语言):他们可以说编译器作者不得假设,给定一些有效的A* pa,@ 987654328@ 指向A 类型的一些实际实例。那么:

      std::cout << pa->f() << std::endl;
      

      总是要弄清楚:*pa 的真正底层类型是什么,因此我应该在这里调用什么函数 f

      在这种假设的(非 C++)语言中,1一个优化的编译器可以获取您的代码并构建它以直接调用B::f(),因为pa指向B 类型的实例。但是在同一种语言中,试图进行大量优化的编译器无法pa 的底层类型由编译时无法预测的东西确定的函数做出假设:

      void f(A* pa) {
          std::cout << pa->f() << std::endl;
      }
      
      int main(int argc, char **argv) {
          A a;
          B b;
          f(argc > 1 ? &b : &a);
      }
      
      

      这个程序在没有额外参数的情况下需要打印A,在使用额外的参数调用时需要打印B。因此,如果我们的非 C++ 语言缺少 virtual 关键字,或者将其定义为无操作,则函数 f——它在运行时调用 A::f()B::f()——必须总是弄清楚要调用哪个底层函数。


      1也不是 C。取名 D。也许是 P,来自 BCPL 进程?


      结论

      因为 C++确实virtual 关键字,我们构建的在基类 A 中具有非虚拟 f() 的变体可以通过 假设优化 pa-&gt;f() 调用 pa-&gt;f() 调用 A::f()。因此,优化编译器可以直接将"A\n" 写入std::cout,而不是实际调用A::f()。无论 C++ 编译器是否优化,调用必须产生A而不是B

      插入了virtual 关键字的变体不得假定pa-&gt;f() 调用A::f()。如果它可以优化到足以看到pa-&gt;f() 调用B::f(),因此,在编译时,完全消除调用并让函数写入"B\n",那没关系!如果它不能优化那么多,那也没关系——至少,就语言规范而言。

      作为程序员,您需要了解virtual 关键字,并在您希望编译器强制 选择基于实际运行时类的正确函数,无论编译器是否足够聪明以在编译时执行此操作。如果你想允许并强制编译器每次只使用基类函数,你可以省略virtual关键字。

      【讨论】:

      • 您的回答是最好的,非常感谢您的帮助!!我会投票,但我没有足够的代表。
      猜你喜欢
      • 1970-01-01
      • 2011-03-04
      • 2020-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-23
      • 2011-06-23
      相关资源
      最近更新 更多