1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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. 动态绑定:又叫晚绑定,绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行阶段。

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

举几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#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. 必须存在向上转型:即父类指针或引用指向子类对象。

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