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;
}

面试官问我这样可以吗?

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

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

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

The interviewer asked me: can this be done?

I hesitated for a long, long time… and still didn’t know whether it could or couldn’t.

Seeing that I couldn’t answer, the interviewer then asked whether I knew about static binding and dynamic binding. (That was a hint to me.)

But I still didn’t know whether it could or couldn’t. 😥

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

他问的可以还是不可以是指这样调用能不能实现多态。也就是说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. 动态绑定:又叫晚绑定,绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行阶段。

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

举几个例子:

His question was quite vague — can it or can’t it? How should this “can” be understood? Will it report an error? Calling a member function with an object certainly won’t report an error.

What he really meant by “can it” was whether this call can achieve polymorphism. In other words, what is the output of b.print()?

At that time, all I remembered was that polymorphism depends on the object’s type; the object is of type B, so polymorphism should be achievable.

But the actual result is: polymorphism cannot be achieved

image-20220427144624074

I went back and looked up what static binding and dynamic binding are.

What I found were textbook-style definitions, but after reading enough of them, I understood a little more.

Before understanding static binding and dynamic binding, we first need to understand the two concepts of static type and dynamic type.

  1. Static type: the type at which a pointer (or reference) is declared in the program, determined at compile time.
  2. Dynamic type: the actual type of the object pointed to by a pointer (or reference), determined at runtime.

The two definitions above are my personal understanding and may not be accurate! If there are any errors, feel free to leave a comment.

I described both definitions in terms of pointers (or references), because most of the blogs I found used positive examples: they said that for A* p = new B();, p’s static type is A and its dynamic type is B.

But what about A b = B();? Does b still have the concepts of static type and dynamic type? I think b here is a stack object rather than a pointer, so it has no so-called dynamic type; it has only one type, which is type A. Therefore, the b object calls the print function in class A.

Now that static type and dynamic type have been covered, let’s talk about static binding and dynamic binding.

  1. Static binding: also called early binding. It binds to the static type; the corresponding function or attribute depends on the object’s static type, and this happens at the compile stage.
  2. Dynamic binding: also called late binding. It binds to the dynamic type; the corresponding function or attribute depends on the object’s dynamic type, and this happens at the runtime stage.

In general, virtual functions are dynamically bound, non-virtual functions are statically bound, and default function arguments are also statically bound.

Let me give a few examples:

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. 必须存在向上转型:即父类指针或引用指向子类对象。

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

Finally, let’s summarize the three necessary conditions for achieving polymorphism:

  1. There must be an inheritance relationship: the subclass inherits from the parent class.
  2. The subclass must override the parent class’s virtual function.
  3. An upcast must exist: a parent class pointer or reference points to a subclass object.

In all other cases, polymorphism cannot be achieved.