结论
- 虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
- 默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
1、虚函数与运行多态
虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
#include<iostream>
using namespace std;
class Employee
{
public:
virtual void raiseSalary()
{
cout<<0<<endl;
}
virtual void promote()
{ /* common promote code */ }
};
class Manager: public Employee {
virtual void raiseSalary()
{
cout<<100<<endl;
}
virtual void promote()
{ /* Manager specific promote */ }
};
class Engineer: public Employee {
virtual void raiseSalary()
{
cout<<200<<endl;
}
virtual void promote()
{ /* Manager specific promote */ }
};
// 同样,可能还有其他类型的员工.我们需要一个非常简单的函数来增加所有员工的工资.
// 请注意,emp[]是一个指针数组,实际指向的对象可以是任何类型的雇员。
// 理想情况下,这个函数应该在类之类的组织中,为了简单起见,我们将它设置为全局的
void globalRaiseSalary(Employee *emp[], int n)
{
for (int i = 0; i < n; i++)
emp[i]->raiseSalary(); // 多态调用:调用raiseSalary()
// 根据实际对象,而不是根据指针的类型
}
int main(){
Employee *emp[]={new Manager(),new Engineer};
globalRaiseSalary(emp,2);
return 0;
}
//输出
100
200
2、vptr与vtable
3、虚函数中默认参数
默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun ( int x = 10 )
{
cout << "Base::fun(), x = " << x << endl;
}
};
class Derived : public Base
{
public:
virtual void fun ( int x=20 )
{
cout << "Derived::fun(), x = " << x << endl;
}
};
int main()
{
Derived d1;
Base *bp = &d1;
bp->fun(); // 10
return 0;
}
//输出
Derived::fun(), x = 10
4、可以不可以
4.1、静态函数可以声明为虚函数吗?
静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰
原因主要有两方面:
- ①、static成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义。
- ②、虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
4.2、构造函数可以为虚函数吗?
构造函数不可以声明为虚函数。同时除了inline|explicit
之外,构造函数不允许使用其它任何关键字。
为什么构造函数不可以为虚函数?
- 尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
- 我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
代码学习:
4.3、析构函数可以为虚函数吗?
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。
#include<iostream>
using namespace std;
class base {
public:
base()
{ cout<<"Constructing base \n"; }
virtual ~base() //增减此处的virtual
{ cout<<"Destructing base \n"; }
};
class derived: public base {
public:
derived()
{ cout<<"Constructing derived \n"; }
~derived()
{ cout<<"Destructing derived \n"; }
};
int main(void)
{
derived *d = new derived();
base *b = d;
delete b;
return 0;
}
基类析构函数加入virtual
输出如下:
Constructing base
Constructing derived
Destructing derived
Destructing base
基类析构函数不加virtual
输出如下:
Constructing base
Constructing derived
Destructing base
4.4、虚函数可以为私有函数吗?
- 基类指针指向继承类对象,则调用继承类对象的函数;
int main()
必须声明为Base类的友元,否则编译失败。 编译器报错: ptr无法访问私有函数。 当然,把基类声明为public, 继承类为private,该问题就不存在了。
#include<iostream>
using namespace std;
class Derived;
class Base {
private:
virtual void fun() { cout << "Base Fun"; }
friend int main();//注释这行后,将导致编译失败。报错: ptr无法访问私有函数。
};
class Derived: public Base {
public:
void fun() { cout << "Derived Fun"; }
};
int main()
{
Base *ptr = new Derived();
ptr->fun();
return 0;
}
//输出
Derived Fun
4.5、虚函数可以被内联吗?
通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
参考例子1:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void who()
{
cout << "I am Base\n";
}
};
class Derived: public Base
{
public:
void who()
{
cout << "I am Derived\n";
}
};
int main()
{
// 这里注意虚函数who()是通过类的对象调用的(它将在编译时解析),因此可以内联。
Base b;
b.who();
// 这里虚函数是通过指针调用的,所以不能内联
Base *ptr = new Derived();
ptr->who();
return 0;
}
参考例子2:
#include <iostream>
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() // 不写inline时隐式内联
{
cout << "I am Derived\n";
}
};
int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();
// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();
// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
return 0;
}
5、RTTI与dynamic_cast
运行时类型识别,RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。
在面向对象程序设计中,有时我们需要在运行时查询一个对象是否能作为某种多态类型使用。与Java的instanceof
,以及C#的as、is
运算符类似,C++提供了dynamic_cast
函数用于动态转型。相比C风格的强制类型转换和C++ reinterpret_cast
,dynamic_cast
提供了类型安全检查,是一种基于能力查询(Capability Query
)的转换,所以在多态类型间进行转换更提倡采用dynamic_cast
。
#include<iostream>
#include<typeinfo>
using namespace std;
class B { virtual void fun() {} };
class D: public B { };
int main()
{
B *b = new D(); // 向上转型
B &obj = *b;
D *d = dynamic_cast<D*>(b); // 向下转型
if(d != NULL)
cout << "works"<<endl;
else
cout << "cannot cast B* to D*";
try {
D& dobj = dynamic_cast<D&>(obj); // 向下转型
cout << "works"<<endl;
} catch (bad_cast bc) { // ERROR
cout<<bc.what()<<endl;
}
return 0;
}
//输出
works
works
// 在使用时需要注意:被转换对象obj的类型T1必须是多态类型,即T1必须公有继承自其它类,或者T1拥有虚函数(继承或自定义)。若T1为非多态类型,使用dynamic_cast会报编译错误。
// A为非多态类型
class A{
};
//B为多态类型
class B{
public: virtual ~B(){}
};
//D为多态类型
class D: public A{
};
//E为非多态类型
class E : private A{
};
//F为多态类型
class F : private B{
}