#include <iostream>
using namespace std;

class A{
public:
    virtual void print(){
        cout << "A" << endl;
    }
};

class B:public A{
public:
    void print(){
        cout << "B" << endl;
    }
};

int main(){
    A b = B();
    b.print(); // 这样可以吗?
	return 0;
}

面试官问我这样可以吗?

我犹豫了好久好久… 也不知道可以不可以。

面试官看我答不出来,然后问我知道静态绑定和动态绑定吗?(这是在提示我了)

可我还是不知道可以还是不可以。😥

这道题他问的很含糊,可以还是不可以?这个”可以“该怎么理解,会不会报错?用对象调用成员函数肯定是不会报错的。

他问的可以还是不可以是指这样调用能不能实现多态。也就是说b.print() 的输出是什么?

当时的我只记得多态要看对象类型,对象是 B 类型,所以应该是可以实现多态的吧。

可实际上的结果是:不能实现多态

image-20220427144624074

回来查了一下什么是静态绑定和动态绑定。

查出来的就是八股文式的定义,但是看的多了也多少理解一点。

了解静态绑定和动态绑定之前,要先了解静态类型和动态类型两个概念。

  1. 静态类型:一个指针(或引用)在程序中被声明的类型,在编译期确定。
  2. 动态类型:一个指针(或引用)所指对象的实际类型,在运行期确定。

以上两个定义是我的个人理解,未必准确!如有错误,欢迎留言指出。

这两个定义我都说的指针(或引用),因为我查到的大多数博客里都是用正面的例子来说明,他们说 A* p = new B();P 的静态类型是 A, 动态类型是 B。

但是对于 A b = B();呢?b 还有没有静态类型和动态类型这一说?我觉得这里面 b 是一个栈区对象,而不是指针,没有所谓的动态类型,它只有一个类型,那就是 A 类型,所以 b 对象调用的是 A 类中的 print 函数。

说完了静态类型和动态类型,接下来说静态绑定和动态绑定。

  1. 静态绑定:又叫早绑定,绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译阶段。
  2. 动态绑定:又叫晚绑定,绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行阶段。

一般情况下,虚函数是动态绑定的,非虚函数是静态绑定的,函数的默认参数也是静态绑定的。

举几个例子:

#include <iostream>
using namespace std;

class A{
public:
    int a;
    A():a(1){}
    virtual void print(){
        cout << "A" << endl;
    }
    void ppp(){
        cout << "A::ppp" << endl;
    }
};

class B:public A{
public:
    B(){a = 2;}
    void print(){
        cout << "B" << endl;
    }
    void ppp(){
        cout << "B::ppp" << endl;
    }
};

int main(){
    A b = B();
    b.print();  // A
    b.ppp();    // A::ppp
    cout << b.a << endl; // 2
    /* 我的理解:
    b 是栈区对象,类型就是声明的类型 A
    所以 b 调用的函数都是执行静态绑定,也就是 A 类中的函数
    那么第一行 A b = B(); 怎么理解呢?这里只是声明了一个临时对象 B,然后调用拷贝赋值函数生成 A 类型的对象 b
    类中没有提供 operator= 函数,那么默认的拷贝赋值就是按位拷贝
    所以第三个打印 b.a 的值是 2
    */
    cout << "------" << endl;
    A* p = &b;         // p 是指针了,所以存在动态类型和静态类型,静态类型就是声明的类型 A,动态类型是对象b 的类型,然后 b的类型也是 A
    p->print();        // A 动态绑定
    p->ppp();          // A::ppp 静态绑定 
    cout << p->a << endl; // 2
    /* 我的理解:
    和上面的区别在于 p 是指针,因此使用 p 调用函数时
    调用虚函数要看 p 所指向对象 b 的类型
    调用非虚函数要看 p 的声明类型
    本例中指向的类型和声明的类型一致,都是 A
    */
    cout << "------" << endl;
    A* q = new B(); // 标准的多态实现
    q->print();     // B 
    q->ppp();       // A::ppp
    cout << q->a << endl; // 2
    /* 我的理解:
    这是标准的多态实现,即父类指针指向子类对象
    虚函数调用子类重写的版本
    非虚函数调用父类版本
    */
    cout << "------" << endl;
    B c;           
    A& r = c;            // 父类引用指向子类对象
    r.print();           // B
    r.ppp();             // A::ppp 
    cout << r.a << endl; // 2
    /* 我的理解:
    这也是标准的多态实现,即父类引用指向子类对象
    虚函数调用子类重写的版本
    非虚函数调用父类版本
    */
    cout << "------" << endl;
    A&& s = move(c);     // 右值引用也可以实现多态
    s.print();           // B
    s.ppp();             // A::ppp
    cout << s.a << endl; // 2
    cout << "------" << endl;
    A x;
    B y;
    cout << x.a << endl;      // 1
    cout << y.a << endl;      // 2
    x = y;          // 这一步主要实现的是成员变量的赋值,调用函数的过程与它无关    
    x.print();                // A
    x.ppp();                  // A::ppp
    cout << x.a << endl;      // 2
    cout << y.a << endl;      // 2
    cout << "------" << endl;
    A* k = nullptr;
    k->ppp(); // 空指针可以调用非虚函数,因为函数地址已经静态绑定
    // k->print(); Segmentation fault     无法调用虚函数
    // cout << k->a << endl; Segmentation fault
	return 0;
}

最后总结一下实现多态的三个必要条件:

  1. 需要有继承关系,子类继承父类。
  2. 子类需要重写父类的虚函数。
  3. 必须存在向上转型:即父类指针或引用指向子类对象。

其余情况均不能实现多态。