virtual(虚函数)和多态性

虚函数(virtual关键字)和多态性
一:继承中的指针问题。
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. 如果基类指针指向派生类对象,则当对其进行增减运算时,它将指向它所认为的基类的下一个对象,而不会指向派生类的下一个对象,因此,应该认为对这种指针进行的增减操作是无效的.


二:虚函数

1. 为什么要使用虚函数:正如上面第1 和3 点所讲的,当声明一个基类指针指向派生类对象时,这个基类指针只能访问基类中的成员函数,不能访问派生类中特有的成员变量或函数。如果使用虚函数就能使这个指向派生类对象的基类指针访问派生类中的成员函数,而不是基类中的成员函数,基于这一点派生类中的这个成员函数就必须和基类中的虚函数的形式完全相同,不然基类指针就找不到派生类中的这个成员函数。使用虚函数就实现了一个接口多种方法。


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


3. 正如上面所介绍的,一般应使用基类指针来调用虚函数,如果用点运算符来调用虚函数就失去了它的意义.


4. 如果基类含有虚函数则当声明了一个基类的指针时,当基类指针指向不同的派生类时,它就会调用相应派生类中定义的虚函数版本.这种调用方法是在运行时决定的,例如在类B中声明了虚函数,C,D,E 都从B继承而来且都实现了自已的虚函数版本,那么当定义了一个B类的指针P时,当P指向子类C时就会调用子类C中定义的虚函数,当P指向子类D时就会调用子类D中定义的虚函数,当P指向子类E时就会调用子类E中定义的虚函数.


5. 虚函数须在基类中用virtual 关键字声明也可以在基类中定义虚函数,并在一个或多个子类中重新定义.重定义虚函数时不需再使用virtual关键字,当然也可以继续标明virtual关见字,以便程序更好理解。


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


7. 在子类中重定义虚函数时,虚函数必须有与基类虚函数的声明完全相同的参数类型和数量,这和重载是不同的。如果不相同,则是函数重载,就失去了虚函数的本质.


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


9. 一旦将函数声明为虚函数,则不管它通过多少层继承,它都是虚函数,例如D和B继承,而E又从D继承,那么在B中声明的虚函数,在类E中仍然是虚函数.


10.隐藏虚函数:如果基类定义了一个虚函数,但派生类中却定义了一个虚函数的重载板本,则派生类的这个版本就会把基类的虚函数隐藏掉,当使用基类指针调用该函数时只能调用基类的虚函数,而不能调用派生类的重载版本,当用派生类的对象调用基类的虚函数时就会出现错误了,因为基类的虚函数被派生类的重载版本隐藏了。


11.带默认形参的虚函数:当基类的虚函数带有默认形参时,则派生类中对基类虚函数的重定义也必须有相同数量的形参,但形参可以有默认值也可以没有,如果派生类中的形参数量和基类中的不一样多,则是对基类的虚函数的重载。对虚函数的重定义也就意味着,当用指向派生类的基类指针调用该虚函数时就会调用基类中的虚函数版本。比如基类定义virtual void f(int i=1, int j=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的重定义。


12.如果虚函数形参有默认值,那么派生类中的虚函数的形参不论有无默认值,当用指针调用派生类中的虚函数时就会被基类的默认值覆盖,即派生类的默认值不起作用。但用派生类的对象调用该函数时,就不会出现这种情况。


13.当用指向派生类的基类指针调用虚函数时是以基类中的虚函数的形参为标准的,也就是只要调用的形式符合基类中定义的虚函数的标准就行了。比如基类中定义virtual void f(int i=1,int j=2){}派生类中重定义为void f(int i, int j=3){},这时如果用派生类的对象调用这个派生类中的虚函数f 时必须至少要有一个实参,但是用指向派生类的基类指针调用该虚函数时就可以不用任何形参就能调用派生类中的这个函数f,比如语句p->f()就会调用派生类中的虚函数版本。当用指向派生类的基类指针调用虚函数时是以基类中的虚函数的形参为标准的,也就是只要调用的形式符合基类中定义的虚函数的标准就行了。


14.析构函数可以是虚函数,但构造函数不能.


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


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


17.如果基类只是声明虚函数而不定义虚函数则此虚函数是纯虚函数.任何派生类都必须实现纯虚函数的自已的版本.如果不实现纯虚函数那么该类也是抽象类。


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


19.仍然可以用抽象类声明一个指针,这个指针指向派生类对象.


20.如果派生类中未定义虚函数,则会使用基类中定义的函数.


21.虚函数虚拟特性是以层次结构的方式来继承的,例如C从B派生而且C中重定义了B中的虚函数,而D又从C派生且未重定义B中的虚函数,这时声明一个基类指针P,当P指向类D,并调用D中的虚函数时,由于D中未重定义虚函数他会调用基类中的虚函数版本,这时他会调用类C中的虚函数而不是类B中的虚函数,因为类C比类B更接近于类D.

例:虚函数的应用
class A

{public:

int a; 

virtual void f(){cout<<"继续"<<"\n";} 

virtual void h(int i=1,int j=2) {cout<<"jixu"<<"\n";}

~A(){cout<<"xi A"<<"\n";}

//virtual int b;  //错误,不能把成员变量声明为虚有的。};

class B:public A
{public: int b;
void f(int i){cout<<"paif()"<<"\n";} //重载虚函数f。
void f(){cout<<"paiõQë<<"\n";} //在派生类中重定义虚函数f
void h(){int b;b=5; cout<<"paifu"<<b<<"\n";}   //重载虚函数h的版本。注意这里不是对基类虚函数的重定义。
void h(int i,int j=3){int b; b=j,cout<<"paixu"<<b<<"\n";}//当基类中的虚函数有默认形参时,派生类中重定义基类中的虚函数的版本必须有相同数量的形参,形参可以有默认值,也可以没有。如果形参数量不一样多则是对虚函数的重载。
~B(){cout<<"xiB"<<"\n";}};
int main()
{B m; A *p=&m;
//p->b=3/错误,指向派生类的基类指针不能调用派生类中的成员,只能调用基类中的成员,除非该成员是虚函数。
p->f();//调用派生类中的函数f,输出pailxu
//p->f(4)错误,注意这里不是在调用派生类中带一个形参的f函数,因为带一个参数的f函数不是虚函数,用指向派生类的基类指针时不会调用派生类中的函数,除非这个函数是虚函数。这里基类中没有定义这种带一个形参的f函数,所以这时会出现错误。
p->A::f()//调用基类的虚函数f,输出paixu,可以用作用域运算符使用指向派生类的基类指针调用基类的虚函数
p->h()//调用派生类中的虚函数版本h输出paixuH2,用指向派生类的基类指针调用虚函数时派生类中的虚函数的默认值在这里不起作用。虽然派生类中的虚函数需要一个参数,但这里不给参数仍是调用的派生类的带两个参数的虚函数h,而不是调用派生类中的不带参数的h函数
//使用派生类对象调用成员
m.h();  //调用派生类中不带参数的h函数,如果要用对象调用派生类中带两个形参的h函数,在本例中必须使用一个实参值。
m.h(1); //调用派生类中带两个形参的h函数,输出pai˜G†V,用对象调用派生类中的虚函数时函数的默认值不受基类虚函数默认值的影响
m.A::h();} // 调用基类中的虚函数h.


13.1.8 虚析构函数

1. 为什么需要虚析构函数:当使用new运算符动态分配内存时,基类的析构函数就应该定义为虚析构函数,不然就会出问题。比如类B由类A继承而来,则有语句A *p= new A;delete p; 这时没有问题,调用类A的析构函数释放类A的资源。但如果再把类B的内存动态分配给指针p时如p= new B; delete p;如果基类的析构函数不是虚析构函数的话就会只调用基类A中的析构函数释放资源,而不会调用派生类B的析构函数,这时派生类B的资源没有被释放。


2. 解决这个问题的方法是把基类的析构函数声明为虚析构函数,即在析构函数前加virtual 关键字,定义为虚析构函数时当用delete释放派生类的资源时就会根据基类的析构函数自动调用派生类中的析构函数释放派生类的资源。


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


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

例:使用new 分配内存,但不定义为虚析构函数的情形
class A {public: int a;  ~A(){cout<<"xiA"<<"\n"; }};
class B:public A {public: int b;  ~B(){cout<<"xiB"<<"\n"; }};
class C:public B{public: int c; ~C(){cout<<"xiC"<<"\n";}};
int main()
{A *p=new A; delete p; //输出xiA
//B m; p=&m此语句没有错,但是将使指针p指向一个静态分配的内存地址,这时不能用delete语句释放指针p的资源。
//delete p;  //错误,指针p现在指向的内容不是动态分配的内存,而是静态内存,delete只能释放动态分配的内存。
p=new B;  //动态分配派生类B的内存,并把地址赋给指针p。
delete p; //输出xiA在这里没有调用派生类的析构函数释放动态分配的派生类的内存资源。
B *p1=new B; delete p1; //输出xiB xiA
p1=new C; delete p1; //输出xiB xiA注意,这里没有释放掉子类C的资源。
}
例:使用new 分配内存,且基类定义为虚析构函数的情形
class A {public: int a; virtual ~A(){cout<<"xiA"<<"\n";}  };//基类定义为虚析构函数
class B:public A {public: int b; ~B(){cout<<"xiB"<<"\n";} }; //派生类B自动继承为虚析构函数
class C:public B {public: int c;  ~C(){cout<<"xiC"<<"\n";}}; //派生类C也自动继承为虚析构函数
int main()
{A p=new  A; delete p;  //输出xiA
p=new B;
delete p; //输出xiB,xiA因为基类定义的是虚析构函数,所以在这里调用派生类的析构函数释放动态分配的派生类的内存资源,并调用基类的析构函数释放基类的资源
B *p1= new B; delete p1; //输出xiB,xiA

p1=new C;delete p1; } //输出xiC,xiB,xiA这里因为类B的析构函数被自动继承为虚析构函数,所以这里释放了子类C的释源。


作者:黄邦勇帅


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值