C++中的多态及虚函数大总结
多态是C++中很关键的一部分,在面向对象程序设计中的作用尤为突出,其含义是具有多种形式或形态的情形,简单来说,多态:向不同对象发送同一个消息,不同的对象在接收时会产生不同的行为。即用一个函数名可以调用不同内容的函数。
多态可分为静态多态与动态多态,静态多态的实现在于静态联编,关联出现在编译阶段而非运行期,用对象名或者类名来限定要调用的函数,称为静态关联或静态联编。常见有三种方法
(1)函数多态(函数与运算符的重载);
(2)宏多态;
(3)模板多态。
而对于动态多态的实现是运行阶段把虚函数和类对象绑定在一起的,即动态联编,动态绑定。具体的说,通过一个指向基类的指针调用虚成员函数的时候,运行时系统将能够根据指针所指向的实际对象调用恰当的成员函数实现。
当编译器使用动态绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字,这样的函数称之为虚函数(virtual functions)。根据赋值兼容,用基类类型的指针指向派生类,就可以通过这个指针来使用派生类的成员函数。如果这个函数是普通的成员函数,通过基类类型的指针访问到的只能是基类的同名成员。 而如果将它设置为虚函数,则可以使用基类类型的指针访问到指针正在指向的派生类的同名函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现运行过程的多态。可看这个例子:
#include <iostream> using namespace std; class A { public : void print( ) { cout << “A::print”<<endl ; } }; class B:public A { public : void print( ) { cout << “B::print” <<endl; } }; int main( ) { A a; B b; A *pA = &b; pA->print( ); return 0; }
此时输出A::print ,若将A类中的print( )函数声明为virtual,则此时就为动态联编 程序执行结果为: B::print。
注意点1:构造函数和静态成员函数不能是虚函数:静态成员函数不能为虚函数,是因为virtual函数由编译器提供了this指针,而静态成员函数没有this指针,是不受限制于某个对象;构造函数不能为虚函数,是因为构造的时候,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。
class A { public: virtual A( ) {}; //error }; class B { public: virtual static void func( ) {}; //error “virtual”不能和“static”一起使用 }; int main( ) {
B b; //报错
A *a=&b; return 0; }
注意点2:派生类对象的指针可以直接赋值给基类指针,如上面中的A *a=&b;*a可以看作一个类A的对象,访问它的public成员。通过强制指针类型转换,可以把a转换成B类的指针: a = &b; aa = static_cast< B * > a。此外指向基类的指针,可以指向它的公有派生的对象,但不能指向私有派生的对象,对于引用也是一样的。
class B { public: virtual void print() { cout<<"Hello B"<<endl; } }; class D : private B { public: virtual void print() { cout<<"Hello D"<<endl; } }; int main() { D d; B* pb = &d; //转换存在,无法访问 pb->print(); B& rb = d; //转换存在,无法访问 rb.print(); return 0; }
注意点3:构造函数中调用virtual函数 ,在构造函数和析构函数中调用虚函数时:他们调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。
class Transaction { public: Transaction( ){ logTransaction( ); } virtual void logTransaction( ) = 0; }; class BuyTransaction: public Transaction { public: int buyNum; virtual void logTransaction( ) { cout<< "This is a BuyTransaction"; } }; class SellTransaction: public Transaction { public: int sellNum; virtual void logTransaction( ) { cout<< "This is a SellTransaction"; } }; int main( ) { BuyTransaction b; SellTransaction s; }
以上代码应该会有报错提示,
若将基类的Transaction中虚函数logTransaction改为:
virtual void logTransaction( ) { cout<< "This is a Transaction"<<endl; };
程序执行结果为: This is a Transaction
This is a Transaction
注意点4:普通成员函数中调用虚函数,在普通成员函数中调用虚函数,则是动态联编,是多态。
#include <iostream> using namespace std; class Base { public: void func1( ) { func2( ); } void virtual func2( ) { cout << "Base::func2( )" << endl; } }; class Derived:public Base { public: virtual void func2( ) { cout << "Derived:func2( )" << endl; } }; int main( ) { Derived d; Base * pBase = & d; pBase->func1( ); return 0; }
因为,Base类的func1( )为非静态成员函数,编译器会给加一个this指针: 相当于 void func1( ) { this->func2( ); } 编译这个函数的代码的时候,由于func2( )是虚函数,this是基类指针,所以是动态联编。上面这个程序运行到func1函数中时, this指针指向的是d,所以经过动态联编,调用的是Derived::func2( )。
注意点5:虚函数的访问权限,如果基类定义的成员虚函数是私有的,我们来看看会怎么样
class Base{ private: virtual void func( ) { cout << "Base::func( )" << endl; } }; class Derived : public Base { public: virtual void func( ) { cout << "Derived:func( )" << endl; } }; int main( ) { Derived d; Base *pBase = & d; pBase->func( ); // 无法访问, private 成员(在“Base”类中声明) return 0; }
对于类的private成员 ,只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.所以即使是虚函数,也没办法访问。但是!派生类虚函数的可访问性与继承的方式和虚函数在基类的声明方式有关(public,或private)与派生类声明的方式无关(如public继承,但声明为private,但仍可访问),把上面的public与private互换位置,程序可以正常运行,并输出Derived:func( )。
注意点6:虚函数与友元,先看代码
class A; class B { private: int x; void print() { cout<<x<<endl; } public: B(int i = 0) { x = i; } friend class A; }; class A { public: void func(B b){ b.print(); } }; class C : public A { }; class D: public B { public: D(int i):B(i){} }; int main() { D d(99); A a; C c; a.func(d); c.func(d); return 0; }
程序执行结果为:99 99
由第一个99可知,A是B的友元类,A中的所有成员函数都为B的友元函数,可访问B的私有成员函数。友元类A不是基类B的一部分,更不是派生类D的一部分。从上例看,友元视乎能够被继承,基类的友元函数或友元类能够访问派生类的私有成员。但public继承是一种“is a”的关系,即一个派生类对象可看成一个基类对象。所以,上例中不是基类的友元被继承了,而是派生类被识别为基类了。而第二个99说明一个友元类的派生类,可以通过其基类接口去访问设置其基类为友元类的类的私有成员,也就是说一个类的友元类的派生类,某种意义上还是其友元类。
注意点7:析构函数通常是虚函数。虚析构函数保证了在析构时,避免只调用基类析构函数而不调用派生类析构函数的情况,保证资源正常释放,避免了内存释放。只有当一个类被用来作为基类的时候,才会把析构函数写成虚函数。
以上为个人总结,有不妥的地方欢迎指出。