多态专题

【1】关于继承中的指针应该注意哪些?

(1)指向基类的指针可以指向派生类对象。

当基类指针指向派生类对象时,这种指针只能访问派生对象从基类继承而来的那些成员,

而不能访问子类特有的元素,除非应用强类型转换。

例如有基类B和从B派生的子类D,则

B  *p;

D  dd; 

p = ⅆ是可以的,指针p 只能访问从基类派生而来的成员,不能访问派生类D特有的成员.因为基类不知道派生类中的这些成员。

(2)不能使派生类指针指向基类对象.

(3)如果派生类中覆盖了基类中的成员变量或函数,则当声明一个基类指针指向派生类对象时,这个基类指针只能访问 基类中的成员变量或函数。

例如:基类B 和派生类D 都定义了函数f,则

B *p;

D m;

p=&m;

m.f()将调用基类中的函 数f()而不会调用派生类中的函数f()。

(4)如果基类指针指向派生类对象,则当对其进行增减运算时,它将指向它所认为的基类的下一个对象。

而不会指向派生类的下一个对象,因此,应该认为对这种指针进行的增减操作是无效的。

【2】为什么需要虚函数?

正如上面第(1)和(3)点所讲的,当声明一个基类指针指向派生类对象时,

这个基类指针只能访问基类中的成员函数,不能访问派生类中特有的成员变量或函数。

如果使用虚函数就能使这个指向派生类对象的基类指针访问派生类中的成员函数,而不是基类中的成员函数。

基于这一点派生类中的这个成员函数就必须和基类中的虚函数的形式完全相同,不然基类指针就找不到派生类中的这个成员函数。

使用虚函数就实现了一个接口多种方法。

【3】应用虚函数应该注意哪些事项?

(1)注意不能把成员变量声明为虚有的,也就是说virtual 关键字字不能用在成员变量前面。

(2)一般应使用基类指针来调用虚函数,如果用点运算符来调用虚函数就失去了它的意义。

(3)如果基类含有虚函数,则当声明了一个基类的指针,基类指针指向不同的派生类时,它就会调用相应派生类中定义的虚函数版本。

这种调用方法是在运行时决定的,例如在类B中声明了虚函数,C,D,E 都从B继承而来且都实现了自已的虚函数版本,

那么当定义了一个B类的指针P:

当P指向子类C时就会调用子类C中定义的虚函数,

当P指向子类D时就会调用子类D中定义的虚函数,

当P指向子类E时就会调用子类E中定义的虚函数。

(4)虚函数须在基类中用virtual 关键字声明也可以在基类中定义虚函数,并在一个或多个子类中重新定义。

重定义虚函数时不需再使用virtual 关键字,当然也可以继续标明virtual 关键字,以便程序更好理解。

(5)包括虚函数的类被称为多态类.C++使用虚函数支持多态性。

(6)在子类中重定义虚函数时,虚函数必须有与基类虚函数的声明完全相同的参数类型和数量,这和重载是不同的。

如果不相同,则是函数重载,就失去了虚函数的本质意义。

(7)虚函数不能是声明它的类的友元函数,必须是声明它的类的成员函数,不过虚函数可以是另一个类的友元。

(8)一旦将函数声明为虚函数,则不管它通过多少层继承,它都是虚函数。

如D和B继承,而E又从D继承,那么在B中声明的虚函数,在类E中仍然是虚函数。

(9)隐藏虚函数:如果基类定义了一个虚函数,但派生类中却定义了一个虚函数的重载版本,则派生类的这个版本就会把基类的虚函数隐藏掉,

当使用基类指针调用该函数时只能调用基类的虚函数,而不能调用派生类的重载版本。

当用派生类的对象调用基类的虚函数时就会出现错误了,因为基类的虚函数被派生类的重载版本隐藏了。

(10)带默认形参的虚函数:当基类的虚函数带有默认形参时,则派生类中对基类虚函数的重定义也必须有相同数量的形参,但形参可以有默认值也可以没有。

如果派生类中的形参数量和基类中的不一样多,则是对基类的虚函数的重载。

对虚函数的重定义也就意味着,当用指向派生类的基类指针调用该虚函数时就会调用基类中的虚函数版本。

比如基类定义virtual void f(int i=1, intj=2){};

则派生类中必须定义带有两个形参的函数f 才是对基类虚函数f 的重定义,不然就是函数f 的重载版本。

比如派生类中定义的void f(),void f(int i),void f(int i=2)都是对函数f 的重载,不是对f 的重定义。

而void f(int i, int j) ,void f( int  i, int j=3) ,void f(int i=4, int j=5)都是对虚函数f 的重定义。

(11)如果虚函数形参有默认值,那么派生类中的虚函数的形参不论有无默认值,当用指针调用派生类中的虚函数时就会被基类的默认值覆盖,

即派生类的默认值不起作用。但用派生类的对象调用该函数时,就不会出现这种情况。

(12)当用指向派生类的基类指针调用虚函数时是以基类中的虚函数的形参为标准的,也就是只要调用的形式符合基类中定义的虚函数的标准就行了。

比如基类中定义virtual void f(int i=1,int j=2){};

派生类中重定义为void f(int i, int j=3){};

这时如果用派生类的对象调用这个派生类中的虚函数f 时必须至少要有一个实参,

但是用指向派生类的基类指针调用该虚函数时就可以不用任何形参就能调用派生类中的这个函数f,比如语句p->f()就会调用派生类中的虚函数版本。

(13)析构函数可以是虚函数,但构造函数不能。

(14)纯虚函数声明形式为virtual 类型 函数名(参数列表)=0;注意后面的等于0。

(15)如果类至少有一个纯虚函数,则这个类就是抽象的。

(16)如果基类只是声明虚函数而不定义虚函数则此虚函数是纯虚函数.任何派生类都必须实现纯虚函数的自已的版本。

如果不实现纯虚函数那么该类也是抽象类。

(17)抽象类不能有对象,抽象类只能用作其它类的基类,因为抽象类中的一个或多个函数没有定义,所以不能用抽象类声明对象。

(18)抽象类可以用来声明一个指针,这个指针指向其派生类对象。

(19)如果派生类中未定义虚函数,则会使用基类中定义的函数。

(20)虚函数虚拟特性是以层次结构的方式来继承的。

例如C从B派生而且C中重定义了B中的虚函数,而C又从C派生且未重定义B中的虚函数,

这时声明一个基类指针P,当P指向类C,并调用C中的虚函数时,

由于C中未重定义虚函数它会调用基类C中的虚函数版本不是类B中的虚函数,

因为类C比类B更接近于类D。

(21)静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。

(22)内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。

即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。

虚函数应用示例代码如下:

 1 #include<iostream>
 2 using  namespace std;
 3 
 4 class A
 5 {
 6 public:
 7     int a;
 8 //  virtual int b;   //error!不可以这样声明成员变量
 9     virtual void f()
10     {
11         cout<<"基类虚函数f"<<endl;
12     }
13     virtual void h(int i=1,int j=2)
14     {
15         cout<<"基类虚函数h"<<endl;
16     }
17     ~A()
18     {
19         cout<<"基类析构函数"<<endl;
20     }
21 };
22 
23 class B:public A
24 {
25 public:
26     int b;
27     void f(int i)    //重载虚函数f
28     {
29         cout<<"子类f()"<<endl;
30     }
31     void f()  //重定义也就是覆盖
32     {
33         cout<<"子类虚函数f"<<endl;
34     }
35     void h()  //重载虚函数h的版本
36     {
37         int b = 5;
38         cout<<"子类h()"<<b<<endl;
39     }
40     void h(int i,int j=3) //注意这里可以有默认值,也可以没有。只要保证参数列一致
41     {
42         int b;
43         b=j;
44         cout<<"子类虚函数h"<<b<<endl;
45     }
46     ~B()
47     {
48         cout<<"子类析构"<<endl;
49     }
50 };
51 void main()
52 {
53     B m;
54     A *p = &m;
55 //  p->b=3;  //error!指向派生类的基类指针不能调用派生类的数据成员,仅仅只能调用虚成员函数
56     p->f();
57 //  p->f(4)   //error!不是派生类的虚函数
58     p->A::f();//调用基类中的虚函数f.可以使用作用域运算符实现
59     p->h();//调用派生类中的虚函数版本h输出,用指向派生类的基类指针调用虚函数时派生类中的
60     //虚函数的默认值在这里不起作用。 
61     //虽然派生类中的虚函数需要一个参数,但这里不给参数仍是调用的派生类的带两个参数
62     //的虚函数,而不是调用派生类中的不带参数的函数使用派生类对象调用成员 
63 
64     m.h(); //调用派生类中不带参数的函数,如果要用对象调用派生类中带两个形参的函数,在本例中必须使用一个实参值。
65     m.h(1);//调用派生类中带两个形参的函数,输出,用对象调用派生类中的虚函数时函数的默认值不受基类虚函数默认值的影响
66     m.A::h();//调用基类中的虚函数h
67 }
68 
69 /*
70 子类虚函数f
71 基类虚函数f
72 子类虚函数h2
73 子类h()5
74 子类虚函数h3
75 基类虚函数h
76 子类析构
77 基类析构函数
78  */

【4】虚析构函数注意哪些事项?

(1)为什么需要虚析构函数?

当使用new 运算符动态分配内存时,基类的析构函数就应该定义为虚析构函数,不然就会出问题。

比如类B 由类A 继承而来,则有语句

A *p= new A;

delete p; 这时没有问题,调用类A 的析构函数释放类 A 的资源。

但如果再把类B 的内存动态分配给指针p 时如

p= new B;

delete p;

如果基类的析构函数不是虚析构函数的话,就会只调用基类A 中的析构函数释放资源,而不会调用派生类B 的析构函数。

这时就导致派生类B 的资源没有被释放。

(2)解决问题(1)的方法是把基类的析构函数声明为虚析构函数。即在析构函数前加virtual 关键字。

当定义为虚析构函数时,在用delete 释放派生类的资源时就会根据基类的析构函数自动调用派生类中的析构函数释放派生类的资源。

(3)只要基类中的析构函数是虚析构函数,则该基类的派生类中的析构函数自动为虚析构函数。

虽然派生类中的析构函数前没有virtual 关键字,析构函数名字也不一样,但派生类中的析构函数被自动继承为虚析构函数。

(4)如果要使用new 运算符分配内存,最好将析构函数定义为虚析构函数。

(5)析构函数为虚函数与不为虚函数的示例代码如下:

<1>析构函数不为虚函数

 1 #include<iostream>
 2 using  namespace std;
 3 class A
 4 {
 5 public:
 6     int a;
 7     ~A()   //注意析构函数不为虚函数
 8     {
 9         cout<<"Destroy A"<<endl;
10     }
11 };
12 class B:public A
13 {
14 public:
15     int b;
16     ~B()
17     {
18         cout<<"Destroy B"<<endl;
19     }
20 };
21 class C:public B 
22 {
23 public:
24     int c;
25     ~C()
26     {
27         cout<<"Destroy C"<<endl;
28     }
29 };
30 void  main()
31 {
32     A *p = new  A;
33     delete p;  
34 
35     B m;
36 //  p = &m; //此语句没有错,但是指向的是栈内存,
37 //  delete p; //非动态内存,不可如此操作
38 
39     p = new B;
40     delete p;//这里没有调用派生类的析构函数释放动态分配的内存资源
41 
42     B *p1 = new B; 
43     delete p1; //输出析构B  析构A
44     p1 = new C;
45     delete p1; //输出析构B  析构A。这里没有释放掉子类C的资源
46 }
47 
48 /*
49 Destroy A   
50 Destroy A
51 Destroy B
52 Destroy A
53 Destroy B
54 Destroy A
55 Destroy B
56 Destroy A
57  */

<2>析构函数为虚函数

 1 #include<iostream>
 2 using  namespace std;
 3 class A
 4 {
 5 public:
 6     int a;
 7     virtual ~A()   //注意析构函数为虚函数
 8     {
 9         cout<<"Destroy A"<<endl;
10     }
11 };
12 class B:public A
13 {
14 public:
15     int b;
16     ~B()
17     {
18         cout<<"Destroy B"<<endl;
19     }
20 };
21 class C:public B 
22 {
23 public:
24     int c;
25     ~C()
26     {
27         cout<<"Destroy C"<<endl;
28     }
29 };
30 void  main()
31 {
32     A *p = new  A;
33     delete p;  
34 
35     B m;
36 //  p = &m; //此语句没有错,但是指向的是栈内存,
37 //  delete p; //非动态内存,不可如此操作
38 
39     p = new B;
40     delete p;//这里没有调用派生类的析构函数释放动态分配的内存资源
41 
42     B *p1 = new B; 
43     delete p1; //输出析构B  析构A
44     p1 = new C;
45     delete p1; //输出析构B  析构A。这里没有释放掉子类C的资源
46 }
47 
48 /*
49 Destroy A
50 Destroy B
51 Destroy A
52 Destroy B
53 Destroy A
54 Destroy C
55 Destroy B
56 Destroy A
57 Destroy B
58 Destroy A
59  */

希望有所收获,仅此而已。

 

 

作者:kaizen
声明:本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此声明,且在文章明显位置给出本文链接,否则保留追究法律责任的权利。
签名:顺序 选择 循环
原文地址:https://www.cnblogs.com/Braveliu/p/2843098.html