注意:这篇文章上次更新于920天前,文章内容可能已经过时。
#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 类型,所以应该是可以实现多态的吧。
可实际上的结果是:不能实现多态
回来查了一下什么是静态绑定和动态绑定。
查出来的就是八股文式的定义,但是看的多了也多少理解一点。
了解静态绑定和动态绑定之前,要先了解静态类型和动态类型两个概念。
- 静态类型:一个指针(或引用)在程序中被声明的类型,在编译期确定。
- 动态类型:一个指针(或引用)所指对象的实际类型,在运行期确定。
以上两个定义是我的个人理解,未必准确!如有错误,欢迎留言指出。
这两个定义我都说的指针(或引用),因为我查到的大多数博客里都是用正面的例子来说明,他们说 A* p = new B();
P 的静态类型是 A, 动态类型是 B。
但是对于 A b = B();
呢?b 还有没有静态类型和动态类型这一说?我觉得这里面 b 是一个栈区对象,而不是指针,没有所谓的动态类型,它只有一个类型,那就是 A 类型,所以 b 对象调用的是 A 类中的 print 函数。
说完了静态类型和动态类型,接下来说静态绑定和动态绑定。
- 静态绑定:又叫早绑定,绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译阶段。
- 动态绑定:又叫晚绑定,绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行阶段。
一般情况下,虚函数是动态绑定的,非虚函数是静态绑定的,函数的默认参数也是静态绑定的。
举几个例子:
#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;
}
最后总结一下实现多态的三个必要条件:
- 需要有继承关系,子类继承父类。
- 子类需要重写父类的虚函数。
- 必须存在向上转型:即父类指针或引用指向子类对象。
其余情况均不能实现多态。