虚函数,是为了实现C++运行时的多态,而产生的。还有编译时的动态,那是重载实现的。
特点
1. 通过虚函数机制,子类可以重写基类的虚函数,同名的函数,在不同的子类中有多重实现。
2. 基类可以通过指针,引用的方式,动态绑定子类,然后调用子类重写的虚函数(不是重写的函数不可以调用)。
3. 基类中含有一个vtable 指针,指向一张表 —— 虚函数表,简称 “虚表” 。每个含有虚函数的类中都保存着一个指向虚表的指针, 而虚表中保存了该类各个虚函数的地址。程序会找到子类的虚函数表,然后调用子类的函数。
4. 核心理念就是通过基类访问派生类定义的函数。
虚函数实现的多态必须要借助于指针
因为一个基类的指针可以指向子类的内存空间,一个基类的实例却不可以指向子类的内存空间,因为一个对象实例的内存空间也是确定的,只能调用这块内存空间的函数 ,指针就不同了,指针可以指向其他内存空间。就是说, 虚函数只能通过 -> 来实现动态,不可以通过 . 来实现动态。
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () {
cout << "Retangle" << endl ;
}
} ;
int main () {
Retangle One ;
Gragh ptr = One ; // 基类对象的实例 = 子类对象的实例
ptr.draw () ;
return 0 ;
}
运行结果
可见,直接用基类对象实例,访问不到子类对应的虚函数,是不能实现多态的。
指针的实现
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () {
cout << "Retangle" << endl ;
}
} ;
int main () {
Retangle One ;
Gragh *ptr = &One ; // ptr 取One 的地址, 指向 One 的内存空间
ptr->draw () ;
return 0 ;
}
运行结果
虚函数实现的多态可以借助引用实现
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () {
cout << "Retangle" << endl ;
}
} ;
int main () {
Retangle One ;
Gragh &ptr = One ;
ptr.draw () ;
return 0 ;
}
运行结果
构造函数不可以是虚函数
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual Gragh () {} // 无参数构造函数 编译错误
virtual Gragh ( int _data ) {} // 带参数构造函数 编译错误
virtual void draw () {
cout << "基类的虚函数 draw " << endl << endl ;
}
} ;
class Point : public Gragh {
public:
void draw () {
cout << "子类的虚函数 draw" << endl << endl ;
}
} ;
int main () {
Point Four ;
Gragh *C = &Four ;
C->draw () ;
return 0 ;
}
以上编译不通过的。
静态成员函数不可以是虚函数
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual static void draw () { // 编译报错
cout << "基类的虚函数 draw " << endl << endl ;
}
} ;
class Point : public Gragh {
public:
void draw () {
cout << "子类的虚函数 draw" << endl << endl ;
}
} ;
int main () {
Point Four ;
Gragh *C = &Four ;
C->draw () ;
return 0 ;
}
以上编译不通过
内联函数 ( inline ) 不可以是虚函数
1. inline 函数发生在编译期间,而 virtual 函数行为发生在运行期间(要根据对象类型来调用虚函数),这两者按理是不兼容的。
2. virtual 函数需要函数地址,inline 函数没有地址,这两者冲突的。
虚函数的访问权限可以是子类(派生类)的 public , protected , private 中,对应虚函数的函数
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << endl << "基类的虚函数 draw " << endl << endl ;
}
} ;
class Point : public Gragh {
public:
virtual void draw ( int ans ) {
cout << endl << "子类的虚函数 draw" << endl << endl ;
}
protected:
virtual void draw () {
cout << endl << "子类的 protected 函数被基类访问了" << endl << endl ;
}
} ;
int main () {
Point Four ;
Gragh *C = &Four ;
C->draw () ;
return 0 ;
}
运行结果
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << endl << "基类的虚函数 draw " << endl << endl ;
}
} ;
class Point : public Gragh {
public:
virtual void draw ( int ans ) {
cout << endl << "子类的虚函数 draw" << endl << endl ;
}
private:
virtual void draw () {
cout << endl << "子类的 private 函数被基类访问了" << endl << endl ;
}
} ;
int main () {
Point Four ;
Gragh *C = &Four ;
C->draw () ;
return 0 ;
}
运行结果
很奇怪,基类指针通过虚函数,竟然可以访问子类的 private 函数(重写的虚函数),可能是因为虚函数表只根据对象类型来选择虚函数,而不看是不是 private,绕过了C++的 private 访问权限。
子类重写的虚函数,参数·返回类型·函数类型 要和基类相同
虚函数只有函数返回类型,形参个数,形参类型都相同,才能有统一的接口。
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << "基类的虚函数 draw " << endl << endl ;
}
} ;
class Point : public Gragh {
public:
virtual void draw ( int ans ) { // 函数名相同,但参数不同
cout << "子类的虚函数 draw" << endl << endl ;
}
} ;
int main () {
Point Four ;
Gragh *C = &Four ;
C->draw () ;
return 0 ;
}
运行结果
而且还要注意 const 和引用限定符的干扰,const 声明的虚函数,尽管参数相同,也不会被非 const 基类调用。
虚函数:override
1. 只能放在派生类的虚函数声明中,只能存在于派生类,而且不可以写在类定义外。
2. 如果派生类(子类)确定是重写了基类中的某个虚函数,最好显式地加上 override, 让编译器去检查子类重写的虚函数和基类虚函数的一致性,包括参数,函数名,const , 引用限定等。
3. 目的是检查虚函数是否被重写了 | 基类虚函数是否被修改了
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () override { // virtual 可加可不加
cout << endl << "Retangle" << endl << endl ;
}
} ;
int main () {
Retangle One ;
Gragh *ptr = &One ;
ptr->draw () ;
return 0 ;
}
class Gragh {
public:
virtual void draw () {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw ( int arg = 1 ) override { // override 检查, 参数不一致
cout << endl << "Retangle" << endl << endl ;
}
} ;
或者是
class Gragh {
public:
virtual void draw ( int arg = 1 ) { // 基类的虚函数的参数被修改
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () override { // override 检查到,基类不存在要重写的虚函数
cout << endl << "Retangle" << endl << endl ;
}
} ;
所以,虚函数之间的覆盖,继承必须要达到高度的一致,参数,函数名,函数类型等等。
override 只是提供了一种检查。
虚函数:final
1. 防止派生类重写基类的虚函数,( 重写点到为止 )
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw ( ) final { // 明确了子类不可以重写基类的虚函数
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () override { // override 有没有,都会报错
cout << endl << "Retangle" << endl << endl ;
}
} ;
int main () {
Retangle One ;
Gragh *ptr = &One ;
ptr->draw () ;
return 0 ;
}
编译通不过。
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () final {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw ( int arg = 1 ) { // final 对这个不起作用
cout << endl << "Retangle" << endl << endl ;
}
} ;
int main () {
Retangle One ;
Gragh *ptr = &One ;
ptr->draw () ;
return 0 ;
}
这个是可以运行的,子类中函数参数和基类虚函数不一致,不受 final 的制约。
基类指针不可以访问子类自定义的成员变量和自定义的成员函数
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << "ans = 1 " << endl ;
}
} ;
class Point : public Gragh {
public:
int child_member ;
Point () {
child_member = 100 ;
}
// 子类中对应的函数可以不写 virtual, 最好写上, 增强可读性
// 如果希望子类的子类不再重写这个函数, 可以写成 virtual void draw () override final // C++11 支持
virtual void draw () {
cout << "ans = 2" << endl ;
}
} ;
int main () {
Point Four ;
Gragh *C = &Four ;
C->draw () ; // 正确
cout << "child_member = " << C->child_member << endl ;
// C->child_member 错误, 基类指针, 指向子类对象, 可以通过虚函数访问子类成员函数
// 但是不可以访问子类自己定义的成员变量
// 也不可以访问子类的函数 ( 和基类(虚) 函数不同名 | 同名却参数不同 )
return 0 ;
}
以上编译错误。
纯虚函数
特点
1. 带有纯虚函数的类是抽象类,只能被继承,不可以声明实例,因为纯虚函数只有声明,没有定义,实例不知道纯虚函数的具体实现(具体实现在子类)。
2. 纯虚函数只是提供子类(派生类)的接口,要在子类中重写该函数的实现。
3. 纯虚函数可以写函数体,但是必须写在类定义外,不能写在类定义内。不过纯虚函数本来就是只提供一个接口,不提供实现,所以,纯虚函数一般不写函数实现,即使写了,子类也没有继承的必要,因为这样一来,纯虚函数也没有 “纯虚” 的意义。如果偏要写,子类调用这个要显式调用,例如 Gragh::draw
4. 如果子类对象要实例化,纯虚函数在子类中一定要有实现。因为如果纯虚函数在子类中没有实现,子类的虚函数表中记录的那个虚函数地址是基类(父类)的纯虚函数的地址,这样一来,子类也带有一个纯虚函数,子类也成了抽象类,这时候,子类不可以有实例。但是抽象类的子类也可以是抽象类,只是其对象不能实例化。
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () = 0 ; // 纯虚函数
} ;
void Gragh::draw () { // 偏要写, 要写在类之外; 根本没必要写函数体
cout << "Gragh" << endl ; // 虽然编译器允许, 但不要这样写
}
class Retangle : public Gragh {
public:
virtual void draw () {
cout << "Retangle" << endl ;
}
} ;
int main () {
Retangle One ; // Gragh 不能声明实例
Gragh *ptr = &One ;
ptr->draw () ;
return 0 ;
}
例子2:解释 4
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () = 0 ; // 会报错
} ;
class Retangle : public Gragh {
public:
// virtual void draw () {
// cout << "Retangle" << endl ;
// }
} ;
int main () {
Retangle One ; // 会报错, 因为 Retangle 继承了 Gragh 的纯虚函数, 没有实现
Gragh *ptr = &One ; // Retangle 也成了一个抽象类, 不可以有实例
ptr->draw () ;
return 0 ;
}
纯虚函数和虚函数的区别 :
2. 虚函数不仅定义了一个统一的接口,供子类重写,覆盖掉原来的虚函数,以达到多种不同的实现;还为一些没重写该虚函数的子类提供了统一的继承,也就是默认的函数行为,如果子类自己不声明这个函数,就默认用父类的虚函数。
3. 纯虚函数的意义,不仅仅是提供了一个统一的接口,更重要的是规范子类的行为,强制要求所有继承它的派生类,如果其对象要实例化,就必须具体实现这个纯虚函数 ,所以说纯虚函数通过强制子类重写虚函数,比普通的虚函数更容易实现多态 。
如果现在,有一个这样的基类,内部有一个虚函数,这个虚函数不是纯虚函数,有函数定义,也有函数体,但是函数体是空的,也就是基类虚函数没有任何操作,如下:
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {}
} ;
int main () {
Gragh One ;
One.draw () ;
return 0 ;
}
编译是通过的。
上面这个和带有纯虚函数的类有共同点:
基类虚函数都没有具体实现,都提供了一个统一的接口,供子类重写虚函数,以达到多态。
class Gragh {
public:
virtual void draw () {}
} ;
class Gragh {
public:
virtual void draw () = 0 ;
} ;
也有很多不同点:
1. 上面的虽然虚函数没有具体实现,但是虚函数既有声明,又有函数体,其对象可以被实例化
下面的是纯虚函数,只有声明,没有函数体,必须要靠子类重写,其对象不可以被实例化
2. 下面的纯虚函数更容易做到多态!
因为如果子类要实例化对象,纯虚函数会强制继承它的子类重写它,以此达到多态,比较规范。
但是,上面的不会强制其子类重写它。如果子类中忘记重写函数,子类就会直接继承基类的虚函数,子类的功能容易缺失,而且编译不报错,不好找错。
虚函数:动态绑定
静态绑定:编译时,根据声明的类型调用函数
动态绑定:运行时,根据对象真实类型调用函数,需要 virtual
可以通过虚函数 virtual 来实现动态绑定。
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () {
cout << endl << "基类 Gragh " << endl << endl ;
}
void normal () {
cout << endl << "基类 Gragh 的 normal " << endl << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () {
cout << endl << "子类 Retangle " << endl << endl ;
}
void normal () {
cout << endl << "子类 Retangle 的 normal " << endl << endl ;
}
} ;
int main () {
Retangle One ;
// ptr 是基类 Gragh 指针
Gragh *ptr = &One ; // ptr 的静态类型是 Gragh , 动态类型是 Retangle
ptr->draw () ; // draw 函数在 Gragh 是虚函数, 动态绑定, 按照实际指向的对象( 内存空间 ) 来调用函数
ptr->normal () ; // normal 函数在 Gragh 不是虚函数, 静态绑定, 按照声明的对象类型来调用函数
ptr = NULL ;
return 0 ;
}
运行结果
动态绑定,通俗地理解,就是 virtual 使得基类指针,可以调用子类中重写的虚函数。实质就是,如果子类重写了基类的虚函数,子类重写的虚函数覆盖了基类的虚函数,子类的虚函数表上原来存储的是基类虚函数的地址,重写之后,子类的虚函数表存储的就是子类自己写的这个虚函数的地址了。但,如果子类没有重写基类的虚函数,子类的虚函数表上存储的还是基类虚函数的地址。
如果在上个例子中继续加入一个子类, 改变 ptr 指向的子类内存,ptr 就可以根据实际指向的对象类型,调用对应类的函数。这也就解释了,为什么虚函数只能通过指针来实现,因为指针可以动态指向一块内存。
如果 ptr 原来就是 Retangle * 类型,就直接调用 Retangle 的函数了(Retangle 有子类另外)。
虚函数和缺省参数
缺省参数,可以理解为,如果函数调用处不传入参数,函数会传入默认的一个参数。缺省参数是静态绑定的,即使子类继承自父类,利用虚函数的动态绑定机制,缺省参数依旧是静态的。
举个例子:
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw ( int arg = 1 ) {
cout << endl << "参数 : " << arg << endl << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw ( int arg = 2 ) {
cout << endl << "参数 : " << arg << endl << endl ;
cout << "Retangle" << endl << endl ;
}
} ;
int main () {
Retangle One ;
Gragh *ptr = &One ; // ptr 静态类型是 Gragh , 动态类型是 Retangle
Retangle *qtr = &One ; // qtr 静态类型是 Retabgle , 动态类型也是 Retangle
ptr->draw () ; // 缺省参数 = 1, 运行时调用的是动态类型 Retangle 的 draw
qtr->draw () ; // 缺省参数 = 2, 运行时调用的是动态类型 Retangle 的 draw
ptr = qtr = NULL ;
return 0 ;
}
运行结果
两个都调用的是 Retangle类 的 draw 函数, 但是缺省参数却不同。
虚函数和 const
在成员函数后加上 const , 意为该函数不可修改其数据成员( mutable 除外 ) 。虚函数上加上 const , 也就是加上了 const 的限制,虚函数其他用法依旧。
不过,要注意几点
1. 基类虚函数是 const , 子类可以直接继承,但是如果要重写该常量函数,也必须是 const ;
基类指针如果要调用 const 函数, 也必须是 const
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () const {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () const { // 必须加上 const
cout << "Retangle" << endl ;
}
} ;
int main () {
const Gragh *ptr = new Retangle ; // 基类指针也必须是 const
ptr->draw () ;
ptr = NULL ;
return 0 ;
}
运行结果
如果基类指针不调用 const 函数,就不能加 const ,即使基类中存在 const 类的虚函数,如下:
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () const {
cout << "Gragh" << endl ;
}
void init () {
cout << endl << "基类的 not const 普通成员函数" << endl ;
}
virtual void other () {
cout << endl << "基类的 not const 虚函数" << endl << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () const {
cout << endl << "Retangle" << endl << endl ;
}
virtual void other () {
cout << endl << "子类的 not const 虚函数" << endl << endl ;
}
} ;
int main () {
Gragh *ptr = new Retangle ;
ptr->init () ; // 访问基类非 const 成员
ptr->other () ; // 基类指针访问子类的非 const 虚函数
ptr = NULL ;
return 0 ;
}
运行结果:
2. 如果基类虚函数是 const , 那么基类指针通过虚函数实现多态时,会调用子类中的 const 函数,不会调用非 const 函数
// 2. 如果基类虚函数是 const , 那么基类指针通过虚函数实现多态时,会调用子类中的 const 函数,不会调用非 const 函数
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () const {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
virtual void draw () const { // 根据 const 特性, 找覆盖原虚函数的 const 函数
cout << endl << "子类 const 的 draw 函数" << endl << endl ;
}
virtual void draw () { // 而不会找非 const 的函数
cout << endl << "子类 Not const 的 draw 函数" << endl << endl ;
}
// void draw () { // 无论是不是虚函数, 只要非 const, 就不会被 const 的基类调用
// cout << "子类 Not const 的 draw 函数" ;
// }
} ;
int main () {
const Gragh *ptr = new Retangle ; // 基类指针也必须是 const
ptr->draw () ;
ptr = NULL ;
return 0 ;
}
运行结果
但是,如果把 Gragh *ptr 声明为非 const 基类指针,调用的就会是非 const 的子类虚函数了。
也说明了一点, const 可以实现函数的重载 ! 同名,同参数的函数,因为 const 产生了差异,实现多态。
纯虚函数和 const
如果不改变虚函数, 经常表示成 const = 0 ,其实 const 和 = 0 没关系,const 表示不可修改, = 0 表示是纯虚函数。
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
virtual void draw () const = 0 ;
} ;
class Retangle : public Gragh {
public:
virtual void draw () const { // 子类重写 const 函数
cout << endl << "Retangle" << endl << endl ;
}
} ;
int main () {
Gragh *ptr = new Retangle ;
ptr->draw () ;
ptr = NULL ;
return 0 ;
}
运行结果
虚函数:基类作为统一的函数参数
用统一的基类指针,代表不同的子类作为函数参数,简单明了。
#include <bits/stdc++.h>
using namespace std ;
class Gragh {
public:
int inches ;
virtual void draw () {
cout << "Gragh" << endl ;
}
} ;
class Retangle : public Gragh {
public:
Retangle () {
inches = 200 ;
}
virtual void draw () {
cout << "Retangle" ;
}
} ;
class Square : public Gragh {
public:
Square () {
inches = 100 ;
}
virtual void draw () {
cout << "Square" ;
}
} ;
void Combine ( Gragh *a , Gragh *b ) {
a->draw () ;
cout << " + " ;
b->draw () ;
cout << " = " << a->inches + b->inches << endl << endl ;
}
int main () {
Gragh *a = new Retangle ;
Gragh *b = new Square ;
Combine ( a , b ) ;
return 0 ;
}
运行结果