有一个简单的测试程序:
// Example program
#include <iostream>
#include <string>
using namespace std;
class a1 {
public:
void printid() { cout << endl << "this is a1 id"; }
};
class a2 : public a1 {
public:
void printid() { cout << endl << "this is a2 id"; };
};
int main() {
a1 aa1;
a2 aa2;
a1* aa1_ptr = &aa1;
a2* aa2_ptr = &aa2;
a1* aa1_ptr_derived = (a1*)aa2_ptr;
aa1.printid();
aa2.printid();
aa1_ptr->printid();
aa2_ptr->printid();
aa1_ptr_derived->printid();
return 0;
}
这个程序的输出是:
this is a1 id
this is a2 id
this is a1 id
this is a2 id
this is a1 id
如果您将 printid() 函数设为虚拟,则结果会发生显着变化。
即,如果程序是:
// Example program
#include <iostream>
#include <string>
using namespace std;
class a1 {
public:
virtual void printid() { cout << endl << "this is a1 id"; }
};
class a2 : public a1 {
public:
virtual void printid() { cout << endl << "this is a2 id"; };
};
int main() {
a1 aa1;
a2 aa2;
a1* aa1_ptr = &aa1;
a2* aa2_ptr = &aa2;
a1* aa1_ptr_derived = (a1*)aa2_ptr;
aa1.printid();
aa2.printid();
aa1_ptr->printid();
aa2_ptr->printid();
aa1_ptr_derived->printid();
return 0;
}
这个程序的输出是:
this is a1 id
this is a2 id
this is a1 id
this is a2 id
this is a2 id
也就是说,当我尝试从通过将派生指针转换为基类获得的基类指针调用虚函数时,将调用派生类虚函数。
问题:
- 这样对吗?毕竟,当我在基类指针上调用 printid() 时,我得到“这是 a1 id”。而当我从同一个指向基类的指针调用 printid(),但通过从派生类强制转换获得时,由于某种原因,“这是 a2 id”被调用了吗?
UPD1:
对不起,同事们,我彻底钝化了。我不会删除问题,让它挂在学生的恐惧。只是通常在设计时,我从下往上查看层次结构,并希望获得指向基类的指针的多态行为。他确实明白了。然后出于某种原因,我从上到下查看了层次结构,并出于某种原因决定在降低层次结构时,我应该接收基类的所有属性。
您在第二种情况下看到的行为是虚函数的重点。这就是它们的设计目的。所以不清楚为什么你对观察到的行为感到惊讶。
通过定义虚函数,调用中特定函数的选择是根据调用中使用的对象的动态类型进行的。因为 在通话中
指针
aa1_ptr_derived实际上指向一个类型的对象a2,然后从类中调用该函数a2。这就是多态的全部思想。形式上,在 C++ 语言中,只有一种方法可以“抑制”多态性:在调用中指定一个限定的方法名
(加上构造函数和析构函数中虚拟调用的特性)。
是的,这就是它的本意。
这是通过虚函数表实现的。调用虚函数时,通常通过表中的数字来调用。表的地址存储在对象内部。但是表绑定到一个类,而不是一个对象。
编译器是做什么的?如果他知道这是一个常规函数,他会立即替换它的地址。如果他看到这是一个虚函数,那么他会根据对象的地址计算表的地址(通常只是 +4 或类似的值)。然后在收到的表地址中找到所需的虚函数按编号起作用,就会有它的地址。
将指针转换为祖先并不会改变计算表地址的本质。其实我们有。
PS 在很多情况下,编译器可以确定被调用的函数等等。并且不使用虚函数表。