之前始终没有搞清楚C++中 “virtual” 关键字的用法。 这里刚好看到国外网站上的一系列博客,感觉讲的比较透彻,所以自己总结记录于此。
国外链接: http://www.geeksforgeeks.org/c-plus-plus/
1.虚函数与运行多态
首先看一个简单的例子:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void show() { cout<<" In Base \n"; }
};
class Derived: public Base
{
public:
void show() { cout<<"In Derived \n"; }
};
int main(void)
{
// 基类指针指向子类对象
Base *bp = new Derived;
bp->show(); // RUN-TIME POLYMORPHISM
return 0;
}
输出结果为:
In Derived
结论: 虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
虚函数控制下的运行多态有什么用?
假如我们在公司的人事管理系统中定义了一个基类 Employee(员工),里面包含了升职、加薪等虚函数。 由于Manager(管理人员)和Engineer(工程人员)的加薪和晋升流程是不一样的,因此我们需要实现一些继承类并重写这些函数。
有了上面这些以后,到了一年一度每个人都要加薪的时候,我们只需要一个简单的操作就可以完成,如下所示:
void globalRaiseSalary(Employee *emp[], int n)
{
for (int i = 0; i < n; i++)
emp[i]->raiseSalary();
}
总结: 虚函数使得我们可以创建一个统一的基类指针列表,并且调用不同子类的函数而无需知道子类对象究竟是什么。
虚函数表与虚函数指针
程序是如何知道在运行时该调用基类还是子类的函数? 这涉及到虚函数表和虚函数指针的概念。更多可以参考《C++ 虚函数表解析》。
- vtable(虚函数表): 每一个含有虚函数的类都会维护一个虚函数表,里面按照声明顺序记录了虚函数的地址。
- vptr(虚函数指针): 一个指向虚函数表的指针,每个对象都会拥有这样的一个指针。
插入一个例子:
class A
{
public:
virtual void fun();
};
class B
{
public:
void fun();
};
sizeof(A) > sizeof(B) // 因为A比B多了一个虚函数指针
这个时候我们再来看刚刚那个加薪的例子,其多态调用的形式如下图所示:
其核心要素还是我们一开始提到的结论:虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
2. 虚函数中默认参数
先看下面一段代码:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun ( int x = 0 )
{
cout << "Base::fun(), x = " << x << endl;
}
};
class Derived : public Base
{
public:
// 这里的virtual关键字可以省略,因为只要基类里面被声明为虚函数,那么在子类中默认都是虚的
// 或者定义为 virtual void fun ( int x = 10)
virtual void fun ( int x )
{
cout << "Derived::fun(), x = " << x << endl;
}
};
int main()
{
Derived d1;
Base *bp = &d1;
bp->fun();
return 0;
}
上述代码的输出始终为
Derived::fun(), x = 0
解释与总结:
默认参数不包含在函数签名里。 (函数签名定义了函数的输入与输出,包括参数及参数的类型、返回值及其类型、可能会抛出或传回的exceptions、该方法在面向对象程序中的可用性方面的信息诸如public、static或prototype等关键字等)
默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
绝不重新定义继承而来的缺省参数(Never redefine function’s inherited default parameters value.)
3. 静态函数可以声明为虚函数吗?
静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰。
比如下面的声明都是错误的:
virtual static void fun() { }
static void fun() const { }
原因主要有两方面:
- static成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义
- 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
4. 构造函数可以为虚函数吗?
构造函数不可以声明为虚函数。同时除了inline之外,构造函数不允许使用其它任何关键字。
为什么构造函数不可以为虚函数?
- 尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
尽管构造函数不可以为虚函数,但是有些场景下我们确实需要 “Virtual Copy Constructor”。 “虚复制构造函数”的说法并不严谨,其只是一个实现了对象复制的功能的类内函数。 举一个应用场景,比如剪切板功能。 复制内容作为基类,但派生类可能包含文字、图片、视频等等。 我们只有在程序运行的时候才知道我们需要复制的具体是什么类型的数据。 实现方法如下:
class Base
{
public:
Base() {};
virtual ~Base() {};
virtual Base* Clone() {return new Base(*this);}
};
class Derived
{
public:
Derived() {};
virtual ~Derived() {};
virtual Base* Clone() {return new Derived(*this);}
};
调用方法如下:
Derived d;
Base* p = d.clone(); //p实际指向的是一个继承类对象,并且该对象和d完全一样,这就实现了copy
delete p;
下面着重解释下 return new Derived(*this)这句话。
“this”是待被复制的对象的地址,“*this”相当于解地址引用。 所以“*this”的类型是 “Derived &”,是待被复制的对象的引用。 所以上面这句话的意思是: 先用new开一块空间,然后用copy构造函数 Derived(const Derived &)来初始化这块内存。 由于用户没有定义copy构造函数,因此调用编译器产生的默认copy构造函数。
5. 析构函数可以为虚函数吗?
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。
看下面的例子:
#include<iostream>
using namespace std;
class base {
public:
base()
{ cout<<"Constructing base \n"; }
// virtual ~base()
~base()
{ 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;
getchar();
return 0;
}
可能的输出结果如下(不同编译器可能有差别):
Constructing base
Constructing derived
Destructing base
可见,继承类的析构函数没有被调用,delete时只根据指针类型调用了基类的析构函数。 正确的操作是,基类和继承类的析构函数都应该被调用,解决方法是将基类的析构函数声明为虚函数。
5. 虚函数可以为私有函数吗?
虚函数可以被私有化,但有一些细节需要注意。
#include<iostream>
using namespace std;
class Derived;
class Base {
private:
virtual void fun() { cout << "Base Fun"; }
friend int main();
};
class Derived: public Base {
public:
void fun() { cout << "Derived Fun"; }
};
int main()
{
Base *ptr = new Derived;
ptr->fun();
return 0;
}
输出结果为:
Derived fun()
- 基类指针指向继承类对象,则调用继承类对象的函数;
- int main()必须声明为Base类的友元,否则编译失败。 编译器报错: ptr无法访问私有函数。 当然,把基类声明为public, 继承类为private,该问题就不存在了。
6. 虚函数可以被内联吗?
通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。 (参考《虚函数什么情况下会内联》)
#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()
{
Base b;
b.who(); // 内联调用
Base *ptr = new Derived();
ptr->who(); // 通过基类指针调用,一定不会进行内联
return 0;
}
7. 纯虚函数与抽象类
纯虚函数: 在基类中声明但不定义的虚函数,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,如virtual void funtion1()=0;
抽象类: 含有纯虚函数的类为抽象类。
下面是一个简单的抽象类的例子;
#include<iostream>
using namespace std;
class Base
{
int x;
public:
virtual void fun() = 0;
int getX() { return x; }
};
// 继承并重写基类声明的纯虚函数,如果没有重写,则该继承类也为抽象类
class Derived: public Base
{
int y;
public:
void fun() { cout << "fun() called"; }
};
int main(void)
{
Derived d;
d.fun();
return 0;
}
纯虚函数的特点以及用途总结如下
- 必须在继承类中重新声明该函数(实现可以为空),否则继承类仍为抽象类,程序无法编译通过;
- 派生类仅仅只是继承纯虚函数的接口,因此使用纯虚函数可以规范接口形式。
- 声明纯虚函数的基类无法实例化对象。 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,引入了纯虚函数,编译器要求在派生类中必须予以重写以实现多态性。
- 纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
- 可以使用指针或者引用指向抽象类性,比如下面的代码:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void show() = 0;
};
class Derived: public Base
{
public:
void show() { cout << "In Derived \n"; }
};
int main(void)
{
Base *bp = new Derived();
bp->show();
return 0;
}
- 抽象类可以拥有构造函数。示例如下:
#include<iostream>
using namespace std;
// An abstract class with constructor
class Base
{
protected:
int x;
public:
virtual void fun() = 0;
Base(int i) { x = i; }
};
class Derived: public Base
{
int y;
public:
Derived(int i, int j):Base(i) { y = j; }
void fun() { cout << "x = " << x << ", y = " << y; }
};
int main(void)
{
Derived d(4, 5);
d.fun();
return 0;
}
- 析构函数被声明为纯虚函数是一种特例,允许其有具体实现。 (有些时候,想要使一个类成为抽象类,但刚好又没有任何纯虚函数。最简单的方法就是声明一个纯虚析构函数。)
#include <iostream>
class Base
{
public:
virtual ~Base()=0; // 纯虚析构函数
};
Base::~Base()
{
std::cout << "Pure virtual destructor is called";
}
class Derived : public Base
{
public:
~Derived()
{
std::cout << "~Derived() is executed\n";
}
};
int main()
{
Base *b = new Derived();
delete b;
return 0;
}