类模板和虚函数,都是实现多态的重要方法。
类模板,锁定算法、步骤,偏重类型相同。
虚函数,偏重步骤不相同。
7.5.2虚基类
当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。
在继承中产生歧义的原因有可能是继承类继承了基类多次,从而产生了多个拷贝,即不止一次的通过多个路径继承类在内存中创建了基类成员的多份拷贝。虚基类的基本原则是在内存中只有基类成员的一份拷贝。这样,通过把基类继承声明为虚拟的,就只能继承基类的一份拷贝,从而消除歧义。用virtual限定符把基类继承说明为虚拟的。
如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。
如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象。
要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类称为虚基类。
虚继承声明使用关键字virtual
1 没有声明虚基类
2 声明了虚基类
1 没有声明虚基类
//可以看到,创建一个ABobj的类,创建了两个obj基类
//1>main.cpp(60) : error C2385 : 对“num”的访问不明确
//1> main.cpp(60) : note : 可能是“num”(位于基“obj”中)
//1> main.cpp(60) : note : 也可能是“num”(位于基“obj”中)
1 #include <iostream> 2 3 class obj 4 { 5 public: 6 int num; 7 obj(int data) :num(data)//构造函数 8 { 9 std::cout << "obj create" << std::endl; 10 } 11 ~obj()//析构函数 12 { 13 std::cout << "obj delete" << std::endl; 14 } 15 }; 16 17 class Aobj :public obj 18 { 19 public: 20 Aobj(int data) :obj(data)//构造函数 21 { 22 std::cout << "Aobj create" << std::endl; 23 } 24 ~Aobj()//析构函数 25 { 26 std::cout << "Aobj delete" << std::endl; 27 } 28 }; 29 30 class Bobj :public obj 31 { 32 public: 33 Bobj(int data) :obj(data)//构造函数 34 { 35 std::cout << "Bobj create" << std::endl; 36 } 37 ~Bobj()//析构函数 38 { 39 std::cout << "Bobj delete" << std::endl; 40 } 41 }; 42 43 class ABobj :public Aobj, public Bobj 44 { 45 public: 46 ABobj(int x, int y) :Aobj(x), Bobj(y)//构造函数 47 { 48 std::cout << "ABobj create" << std::endl; 49 } 50 ~ABobj()//析构函数 51 { 52 std::cout << "ABobj delete" << std::endl; 53 } 54 }; 55 56 void main() 57 { 58 ABobj *p = new ABobj(10, 20); 59 60 //std::cout << p->num; 61 62 //1>main.cpp(60) : error C2385 : 对“num”的访问不明确 63 // 1> main.cpp(60) : note : 可能是“num”(位于基“obj”中) 64 // 1> main.cpp(60) : note : 也可能是“num”(位于基“obj”中) 65 66 std::cout << p->Aobj::num << std::endl;//10 67 std::cout << p->Bobj::num << std::endl;//20 68 69 delete p; 70 71 system("pause"); 72 } 73 74 //没有声明虚基类 75 //可以看到,创建一个ABobj的类,创建了两个obj基类 76 77 //obj create 78 //Aobj create 79 //obj create 80 //Bobj create 81 //ABobj create 82 //10 83 //20 84 //ABobj delete 85 //Bobj delete 86 //obj delete 87 //Aobj delete 88 //obj delete 89 //请按任意键继续. . .
2 声明了虚基类
//可以看到,创建一个ABobj的类,只创建了一个obj基类
1 #include <iostream> 2 3 class obj 4 { 5 public: 6 int num; 7 obj(int data) :num(data)//构造函数 8 { 9 std::cout << "obj create" << std::endl; 10 } 11 ~obj()//析构函数 12 { 13 std::cout << "obj delete" << std::endl; 14 } 15 }; 16 17 class Aobj :virtual public obj//声明obj是虚基类 18 { 19 public: 20 Aobj(int data) :obj(data)//构造函数 21 { 22 std::cout << "Aobj create" << std::endl; 23 } 24 ~Aobj()//析构函数 25 { 26 std::cout << "Aobj delete" << std::endl; 27 } 28 }; 29 30 class Bobj :virtual public obj//声明obj是虚基类 31 { 32 public: 33 Bobj(int data) :obj(data)//构造函数 34 { 35 std::cout << "Bobj create" << std::endl; 36 } 37 ~Bobj()//析构函数 38 { 39 std::cout << "Bobj delete" << std::endl; 40 } 41 }; 42 43 class ABobj :public Aobj, public Bobj 44 { 45 public: 46 ABobj(int x) :obj(x), Aobj(x), Bobj(x)//构造函数,直接初始化虚基类obj 47 { 48 std::cout << "ABobj create" << std::endl; 49 } 50 ~ABobj()//析构函数 51 { 52 std::cout << "ABobj delete" << std::endl; 53 } 54 }; 55 56 void main() 57 { 58 ABobj *p = new ABobj(10); 59 60 std::cout << p->num << std::endl;//10 61 62 std::cout << p->Aobj::num << std::endl;//10 63 std::cout << p->Bobj::num << std::endl;//10 64 65 delete p; 66 67 system("pause"); 68 } 69 70 //声明了虚基类 71 //可以看到,创建一个ABobj的类,只创建了一个obj基类 72 73 //obj create 74 //Aobj create 75 //Bobj create 76 //ABobj create 77 //10 78 //10 79 //10 80 //ABobj delete 81 //Bobj delete 82 //Aobj delete 83 //obj delete 84 //请按任意键继续. . .
virtual
virtual
第8章 虚函数与多态性
多态性是指一个名字,多种语义;或界面相同,多种实现。
重载函数是多态性的一种简单形式。
虚函数允许函数调用与函数体的联系在运行时才进行,称为动态联编。
8.3 虚函数与动态联编
8.4 纯虚函数与抽象类
8.5 虚函数和多态性的应用
小结
8.3 虚函数与动态联编
虚函数一般起到接口作用。
冠以关键字virtual的成员函数称为虚函数。
实现运行时多态的关键首先是要说明虚函数,另外,必须用基类指针调用派生类的不同实现版本。
8.3.1虚函数和基类指针
基类指针虽然获取派生类对象地址,却只能访问派生类从基类继承的成员。
4 用派生类指针引用一个基类对象;
//static_cast用于一般的数据类型转换
zi *pzifu = static_cast<zi *>(new fu);
1 #include <iostream> 2 3 class fu 4 { 5 public: 6 void name() 7 { 8 std::cout << "父类" << std::endl; 9 } 10 }; 11 12 class zi :public fu 13 { 14 public: 15 void name() 16 { 17 std::cout << "子类" << std::endl; 18 } 19 }; 20 21 class sun :public zi 22 { 23 public: 24 void name() 25 { 26 std::cout << "孙类" << std::endl; 27 } 28 }; 29 30 void main() 31 { 32 fu *pfu = new fu; 33 pfu->name();//父类 34 35 zi *pzi = new zi; 36 pzi->name();//子类 37 38 sun *psun = new sun; 39 psun->name();//孙类 40 41 zi *pzifu = static_cast<zi *>(new fu);//4 用派生类指针引用一个基类对象 42 pzifu->name();//子类 43 44 sun *psunfu = static_cast<sun *>(new fu);//4 用派生类指针引用一个基类对象 45 psunfu->name();//孙类 46 47 system("pause"); 48 }
3 用基类指针引用一个派生类对象;
((zi *)pfu)->name();//强制类型转换
1 #include <iostream> 2 3 class fu 4 { 5 public: 6 void name() 7 { 8 std::cout << "父类" << "x=" << x << std::endl; 9 } 10 int x; 11 fu(int a) :x(a) 12 { 13 14 } 15 }; 16 17 class zi :public fu 18 { 19 public: 20 void name() 21 { 22 std::cout << "子类" << "x=" << x << ",y=" << y << std::endl; 23 } 24 int y; 25 zi(int a, int b) :fu(a), y(b) 26 { 27 28 } 29 }; 30 31 class sun :public zi 32 { 33 public: 34 void name() 35 { 36 std::cout << "孙类" << "x=" << x << ",y=" << y << ",z=" << z << std::endl; 37 } 38 int z; 39 sun(int a, int b, int c) :zi(a, b), z(c) 40 { 41 42 } 43 }; 44 45 void main() 46 { 47 fu fu1(1); 48 zi zi1(2, 3); 49 sun sun1(4, 5, 6); 50 51 fu *pfu = &fu1; 52 pfu->name();//父类 53 54 pfu = &zi1;//3 用基类指针引用一个派生类对象 55 pfu->name();//父类 56 57 pfu = &sun1; 58 pfu->name();//父类 59 60 ((zi *)pfu)->name();//强制类型转换,子类 61 ((sun *)pfu)->name();//强制类型转换,孙类 62 63 system("pause"); 64 }
面试
虚函数实质上是函数指针,占4个字节(32位),8个字节(64位)
1 #include <iostream> 2 3 class fu 4 { 5 public: 6 virtual void name()//虚函数实质上是函数指针,占4个字节(32位),8个字节(64位) 7 { 8 std::cout << "父类" << "x=" << x << std::endl; 9 } 10 int x; 11 fu(int a) :x(a) 12 { 13 14 } 15 }; 16 17 void main() 18 { 19 std::cout << sizeof(fu) << std::endl;//8 20 21 system("pause"); 22 }
面试
单一继承
调用虚函数表
顺序:从上到下,如果是有继承,则调到基类,继续从上到下
1 #include <iostream> 2 3 class A 4 { 5 virtual void g()//虚函数。跳过 6 { 7 std::cout << "A::g" << std::endl; 8 } 9 private: 10 virtual void f()//虚函数。第2 11 { 12 std::cout << "A::f" << std::endl; 13 } 14 }; 15 class B : public A 16 { 17 void g()//实现了基类的虚函数,覆盖了基类的虚函数。第1 18 { 19 std::cout << "B::g" << std::endl; 20 } 21 virtual void h()//新的虚函数。第3 22 { 23 std::cout << "B::h" << std::endl; 24 } 25 }; 26 27 typedef void(*Fun)(void); 28 29 void main() 30 { 31 32 std::cout << sizeof(A) << std::endl;//4,虚函数实质上是函数指针 33 std::cout << sizeof(B) << std::endl;//4,虚函数实质上是函数指针 34 35 B b; 36 Fun pFun; 37 38 for (int i = 0; i < 3; i++)//由于覆盖了基类的虚函数,因此有3个虚函数 39 { 40 pFun = (Fun)*((int*)* (int*)(&b) + i); 41 //&b取b地址 42 //32位的函数指针是4个字节,(int*)(&b)读取4个字节 43 //(int*)* (int*)(&b)转换成指针 44 45 pFun(); 46 } 47 48 system("pause"); 49 }
4
4
B::g
A::f
B::h
请按任意键继续. . .
调试->局部变量
面试
多重继承
调用虚函数表
1 #include <iostream> 2 3 class A 4 { 5 virtual void g()//虚函数。跳过 6 { 7 std::cout << "A::g" << std::endl; 8 } 9 private: 10 virtual void f()//虚函数。第2 11 { 12 std::cout << "A::f" << std::endl; 13 } 14 virtual void j()//虚函数。第3 15 { 16 std::cout << "A::j" << std::endl; 17 } 18 }; 19 20 class H 21 { 22 virtual void M()//虚函数。 23 { 24 std::cout << "H::M" << std::endl; 25 } 26 }; 27 28 class B : public A, public H 29 { 30 void g()//实现了基类的虚函数,覆盖了基类的虚函数。第1 31 { 32 std::cout << "B::g" << std::endl; 33 } 34 virtual void o()//虚函数。第4 35 { 36 std::cout << "B::o" << std::endl; 37 } 38 virtual void h()//虚函数。第5 39 { 40 std::cout << "B::h" << std::endl; 41 } 42 }; 43 44 typedef void(*Fun)(void); 45 46 void main() 47 { 48 std::cout << sizeof(A) << std::endl;//4 49 std::cout << sizeof(H) << std::endl;//4 50 std::cout << sizeof(B) << std::endl;//8,多重继承有2个虚函数表 51 52 B b; 53 Fun pFun; 54 55 for (int i = 0; i < 5; i++) 56 { 57 pFun = (Fun)*((int*)* (int*)(&b) + i); 58 59 pFun(); 60 } 61 std::cout << " "; 62 63 Fun pFun1 = (Fun)*((int *)*((int*)(&b) + 1));//基类H 64 65 pFun1(); 66 }
4
4
8
B::g
A::f
A::j
B::o
B::h
H::M
请按任意键继续. . .
//没有virtual,会一直调用基类的函数
//如果有virtual,会根据对象的类型调用不同的函数
1 #include <iostream> 2 3 //没有virtual,会一直调用基类的函数 4 //如果有virtual,会根据对象的类型调用不同的函数 5 6 class fu 7 { 8 public: 9 virtual void name()//虚函数实质上是指针,占4个字节(32位),8个字节(64位) 10 { 11 std::cout << "父类" << "x=" << x << std::endl; 12 } 13 int x; 14 fu(int a) :x(a) 15 { 16 17 } 18 }; 19 20 class zi :public fu 21 { 22 public: 23 void name() 24 { 25 std::cout << "子类" << "x=" << x << ",y=" << y << std::endl; 26 } 27 int y; 28 zi(int a, int b) :fu(a), y(b) 29 { 30 31 } 32 }; 33 34 class sun :public zi 35 { 36 public: 37 void name() 38 { 39 std::cout << "孙类" << "x=" << x << ",y=" << y << ",z=" << z << std::endl; 40 } 41 int z; 42 sun(int a, int b, int c) :zi(a, b), z(c) 43 { 44 45 } 46 }; 47 48 void main() 49 { 50 fu fu1(1); 51 zi zi1(2, 3); 52 sun sun1(4, 5, 6); 53 54 fu *pfu = &fu1; 55 pfu->name();//父类 56 57 pfu = &zi1;//3 用基类指针引用一个派生类对象 58 pfu->name();//子类 59 60 pfu = &sun1; 61 pfu->name();//孙类 62 63 system("pause"); 64 }
注意:
一个虚函数,在派生类层界面相同的重载函数都保持虚特性。
虚函数必须是类的成员函数。
不能将友元说明为虚函数,但虚函数可以是另一个类的友元。
析构函数可以是虚函数,但构造函数不能是虚函数。
3 用基类指针引用一个派生类对象;
构造的时候将会调用基类的构造函数、派生类的构造函数,析构的时候,会调用基类的析构函数,但不会调用派生类的构造函数。
这样将会内存泄漏。
解决办法:使用虚析构函数。
1 #include <iostream> 2 3 using namespace std; 4 5 class A 6 { 7 public: 8 A()//构造函数不可以是虚函数,如果是虚函数,那么将不会构造 9 { 10 std::cout<<"a create"<<std::endl; 11 } 12 ~A() 13 { 14 std::cout<<"a delete"<<std::endl; 15 } 16 }; 17 18 class B:public A 19 { 20 public: 21 B() 22 { 23 std::cout<<"b create"<<std::endl; 24 } 25 ~B() 26 { 27 std::cout<<"b delete"<<std::endl; 28 } 29 }; 30 31 int main(int argc, char *argv[]) 32 { 33 cout << "Hello World!" << endl; 34 35 A *p=new B; 36 delete p; 37 38 return 0; 39 }
输出结果:
a create
b create
a delete
8.3.2虚函数的重载特性
在派生类中重载基类的虚函数要求函数名、返回类型、参数个数、参数类型和顺序完全相同。
如果仅仅返回类型不同,C++认为是错误重载。
如果函数原型不同,仅函数名相同,丢失虚特性。
8.4 纯虚函数与抽象类
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本。
纯虚函数为各派生类提供一个公共界面。
纯虚函数说明形式:
virtual 类型 函数名(参数表)=0;
一个具有纯虚函数的基类称为抽象类。
//有一个纯虚函数,都是抽象类,无法实例化
//抽象类不可以用于函数的参数以及返回值类型,但是抽象类指针可以用于函数的参数以及返回值类型
1 #include <iostream> 2 3 class base 4 { 5 public: 6 virtual void run() = 0;//有一个纯虚函数,都是抽象类,无法实例化 7 virtual ~base() 8 { 9 10 } 11 }; 12 13 base *test(base *p)//抽象类不可以用于函数的参数以及返回值类型,但是抽象类指针可以用于函数的参数以及返回值类型 14 { 15 base *pbase(nullptr); 16 return pbase; 17 }
//抽象类无法实例化对象,但是可以实例化指针
//纯虚函数,抽象类与虚函数起到接口的作用
//用同一接口完成不同的功能
//纯虚函数完全就是为了接口的存在,有了纯虚函数的类无法实例化
//虚函数占4个字节,就是函数指针实现的
1 #include <iostream> 2 3 class base 4 { 5 public: 6 virtual void run() = 0;//有一个纯虚函数,都是抽象类,无法实例化 7 virtual ~base() 8 { 9 10 } 11 }; 12 13 class boy :public base 14 { 15 public: 16 void run() 17 { 18 std::cout << "男孩奔跑" << std::endl; 19 } 20 }; 21 22 class girl :public base 23 { 24 public: 25 void run() 26 { 27 std::cout << "女孩奔跑" << std::endl; 28 } 29 }; 30 31 void main() 32 { 33 //抽象类无法实例化对象,但是可以实例化指针 34 //纯虚函数,抽象类与虚函数起到接口的作用 35 //用同一接口完成不同的功能 36 //纯虚函数完全就是为了接口的存在,有了纯虚函数的类无法实例化 37 //虚函数占4个字节,就是函数指针实现的 38 boy boy1; 39 girl girl1; 40 base *p(nullptr);//空指针 41 42 p = &boy1; 43 p->run(); 44 45 p = &girl1; 46 p->run(); 47 48 system("pause"); 49 }
使用QT,创建一个抽象类,并派生出多个类以分别实现Dialog,QLabel,QPushButton
1 #include "dialog.h" 2 #include <QApplication> 3 4 class basebase 5 { 6 public: 7 virtual void show()=0; 8 virtual void hide()=0; 9 }; 10 11 class base:public basebase 12 { 13 public: 14 virtual void resize(int x,int y)=0; 15 virtual void move(int cx,int cy)=0; 16 }; 17 18 class mydialog:public base//Dialog 19 { 20 public: 21 Dialog w; 22 void show() 23 { 24 w.show(); 25 } 26 void hide() 27 { 28 w.hide(); 29 } 30 void resize(int x,int y) 31 { 32 w.resize(x,y); 33 } 34 void move(int cx,int cy) 35 { 36 w.move(cx,cy); 37 } 38 }; 39 40 class mybutton:public base//QPushButton 41 { 42 public: 43 QPushButton w; 44 void show() 45 { 46 w.show(); 47 } 48 void hide() 49 { 50 w.hide(); 51 } 52 void resize(int x,int y) 53 { 54 w.resize(x,y); 55 } 56 void move(int cx,int cy) 57 { 58 w.move(cx,cy); 59 } 60 }; 61 62 class mylabel:public base//QLabel 63 { 64 public: 65 QLabel w; 66 void show() 67 { 68 w.show(); 69 } 70 void hide() 71 { 72 w.hide(); 73 } 74 void resize(int x,int y) 75 { 76 w.resize(x,y); 77 } 78 void move(int cx,int cy) 79 { 80 w.move(cx,cy); 81 } 82 }; 83 84 int main(int argc, char *argv[]) 85 { 86 QApplication a(argc, argv); 87 //Dialog w; 88 //w.show(); 89 90 mydialog dialog1; 91 mybutton button1; 92 mylabel label1; 93 base *pbase(nullptr); 94 95 pbase=&dialog1; 96 pbase->show(); 97 pbase->resize(100,200); 98 99 pbase=&button1; 100 pbase->show(); 101 pbase->resize(200,200); 102 103 pbase=&label1; 104 pbase->show(); 105 pbase->resize(300,200); 106 107 return a.exec(); 108 }
当编译系统编译含有虚函数的类时,将为它建立一个虚函数表,表中的每一个元素都指向一个虚函数的地址。
一个或者多个虚函数,或者有多个纯虚函数都占4个字节
一个指针存储了虚函数表的地址
8.5 虚函数和多态性的应用
虚函数和多态性使成员函数根据调用对象的类型产生不同的动作。
多态性特别适合于实现分层结构的软件系统,便于对问题抽象时定义共性,实现时定义区别。
小结
虚函数和多态性使软件设计易于扩充。
派生类重载基类接口相同的虚函数其虚特性不变。
如果代码关联在编译时确定,称为静态联编。代码在运行时关联称为动态联编。
基类指针可以指向派生类对象、基类拥有虚函数,是支持多态性的前提。
虚析构函数可以正确释放动态派生类对象的资源。
纯虚函数由派生类定义实现版本。
具有纯虚函数的类称为抽象类。抽象类只能作为基类,不能建立对象。抽象类指针使得派生的具体类对象具有多态操作能力。