说到多态,就要先搞清楚对象的类型。
对象类型分类
- 静态类型:对象声明时的类型,是在编译时确定的。
- 动态类型:目前所指对象的类型,是在运行时确定的。
具体看下面例子:
class B
{};
class D :public B
{};
int main()
{
D *d = new D;
B *b = d;
return 0;
}
多态
顾名思义,意思是具有多种形式或形态的情形,最初来源于希腊语,
在C++语言中多态有着更广泛的含义
多态分类
说到这,我们先来弄清楚继承体系中函数名相同的成员函数的几种不同的关系:
重载
重写(覆盖)
*访问修饰符可以不同:假如基类中的虚函数是protected,派生类中的虚函数可以不是protected型的。
- 重定义(隐藏)
静态多态
编译器在编译期间完成的,又叫静态联编,早绑定。编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
看下面例子:
class B
{
public :
void Show()
{
cout << _b << endl;
}
public:
int _b;
};
class D :public B
{
public:
void Show()
{
cout << _b << endl;
cout << _d << endl;
}
public:
int _d;
};
int main()
{
D *d = new D;
d->_b = 1;
d->_d = 2;
B *b;
b = d;
b->Show();
return 0;
}
此时基类的对象b指向派生类的对象d,这时调用Show(),结果并不如我们所想。打印出的结果是1,这个Show函数是基类的,这是因为对象b是静态的,在编译时已经确定它的类型是B类,在运行时无法修改。如果想要调用派生类的Show(),就要引入动态多态了。
动态多态
在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。又叫动态/晚绑定。
想要解决上面的问题,其实只要在基类Show函数的返回值前面加上关键字virtual把它定义成虚函数就ok啦。如:
virtual void Show()
其他不变(派生类中Show函数前该关键字可加可不加,但为了显式构成虚函数,一般会在派生类中添加该关键字)。
动态绑定的条件
1、必须是虚函数
2、通过基类类型的引用或者指针调用虚函数
- 来看看下面这个例子,想一想这段代码会打印出什么呢?
class B
{
public :
virtual void FunTest1(int _iTest)
{
cout << "B::FunTest1()" << endl;
}
void FunTest2(int _iTest)
{
cout << "B::FunTest2()" << endl;
}
virtual void FunTest3(int _iTest1)
{
cout << "B::FunTest3()" << endl;
}
virtual void FunTest4(int _iTest)
{
cout << "B::FunTest4()" << endl;
}
};
class D :public B
{
public :
virtual void FunTest1(int _iTest)
{
cout << "D::FunTest1()" << endl;
}
virtual void FunTest2(int _iTest)
{
cout << "D::FunTest2()" << endl;
}
void FunTest3(int _iTest1)
{
cout << "D::FunTest3()" << endl;
}
virtual void FunTest4(int _iTest1, int _iTest2)
{
cout << "D::FunTest4()" << endl;
}
};
int main()
{
B* pB = new D;
pB->FunTest1(0);
pB->FunTest2(0);
pB->FunTest3(0);
pB->FunTest4(0);
//pB->FunTest4(0, 0);
return 0;
}
话不多说直接来看结果吧!
这个结果如果让你惊讶,说明你和我一样,初看时不细心不细心不细心,重要的事情说三遍。
仔细观察,会发现在派生类中实现重写的只有FunTest1()和FunTest3()。所以派生类中只有这两个函数被访问到。基类中的FunTest2()并没有定义为虚函数,所以即便派生类中声明它为虚函数,基类对象也是访问不到的。而基类中的FunTest4(),虽然在两个类中都定义为虚函数,但是接收的参数个数不同。这两个函数没有构成重写,但是函数名相同,所以算是重定义啦。
加注释的那行代码是不能通过编译的哦!因为FunTest4()没有构成重写,所以基类指针访问时访问的是基类的FunTest4(),而基类的FunTest4()只接收一个参数,所以给它传两个参数时编译器肯定会报错啦!
虚函数
virtual关键字修饰的类成员函数,并且在派生类中需要重新实现的返回类型必须相同以及函数原型(函数名、参数列表、参数先后顺序)也相同的函数。(协变除外)
协变
class B
{
public :
virtual B* Fun1()
{
return this;
}
};
class D :public B
{
public:
virtual D * Fun1()
{
return this;
}
};
上面例子的两个函数构成虚函数,称为协变。即基类中的虚函数返回值类型为基类类型的指针或引用,并且派生类中重写的虚函数返回值类型为派生类类型的指针或引用,就叫做协变。
纯虚函数&抽象类
在成员函数的形参列表后面写上=0,则成员函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。纯虚函数在派生类中重新定义以后,派生类才能实例化出对象。
比如virtual void FunTest1()
是虚函数,则virtual void FunTest1() = 0;
就是纯虚函数。而包含纯虚函数的类就是抽象类。
- 基础知识看完了,我们来思考个问题吧!
构造函数、静态成员函数、友元函数、重载赋值操作符函数以及析构函数能不能定义为虚函数?为什么?
我在VS2013编译环境中对以上问题做了测试,并经过求证,得到以下结果:
- 构造函数不能定义为虚函数。构造函数在创建对象时用于对其成员变量进行初始化,而虚函数用于实现多态,需要通过已经创建好的对象来调用,二者冲突。
- 静态成员函数不能定义为虚函数。虚函数要通过对象来调用,即隐含参数this指针,而静态成员函数不存在this指针。
- 友元函数不能定义为虚函数。类的友元函数不属于类的成员函数,但是虚函数的前提是类的成员函数。
- 重载赋值操作符函数可以定义为虚函数。但是无意义且降低效率,浪费空间。因为在每一个类中,如果没有显式定义,系统都会给一个默认的赋值操作符重载函数,使用赋值操作符时,系统会自动调用。定义为虚函数时没有多大意义,且虚表占用内存空间,降低访问效率。
- 析构函数一般建议声明为虚函数。防止内存泄漏。比如:
class B
{
public :
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
};
class D:public B
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
};
int main()
{
B* b = new D;
delete b;
return 0;
}
运行结果为:
B()
D()
~B()
可以看到上面例子中没有调用D的析构函数,但是却调用了D的构造函数创建了一个无名对象。程序结束时为无名对象申请的内存并没有释放。但是如果将基类B的析构函数定义为虚函数,就不会存在这种问题。
**不建议在构造函数或析构函数内调用虚函数。
小结
1、派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)
2、基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性,即派生类中该函数加不加关键字virtual都可以。
3、只有类的非静态成员函数才能定义为虚函数,静态成员函数不能定义为虚函数。
4、如果在类外定义虚函数,只能在声明函数时加virtual关键字,定义时不用加。
5、构造函数不能定义为虚函数,虽然可以将operator=定义为虚函数,但最好不要这么做,使用时容易混淆。
6、不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
7、最好将基类的析构函数声明为虚函数。(析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里编译器做了特殊处理)
8、虚表是所有类对象实例共用的
以上内容如有错误,欢迎指出!