这次面试,面试官问了我多态的问题,由于当时多静态多态和动态多态不是很清楚,出了丑,所以在这里我总结了一下和大家分享一下多态问题。
6.多态
6.1 静态多态性机制
函数重载、运算符重载。
6.2 动态多态性机制:
虚函数、抽象类。
6.3动态多态性
基类和派生类中可以定义相同原型的函数,但函数体不同,运行时函数处理不同类的函数时,能够绑定对象所属类定义的函数体。动态多态性需要通过动态绑定(晚绑定)来实现。
6.4 多态实现
需要通过绑定机制,绑定是将一个标识符名和一个存储地址联系在一起的过程。
(1)编译时的多态通过静态绑定实现
绑定工作在编译连接阶段完成:函数重载
(2)运行时的多态通过动态绑定实现
绑定工作在程序运行阶段完成:虚函数
6.5 运算符重载
(1)=, [ ], (), ->这四个运算符只能被重载为类的非静态成员函数,其他的可以被友元重载。
(2). ?: sizeof :: .* 这五个运算符不能重载,其他运算符均可重载。
6.5.1 重载为类成员的运算符函数
(1)定义形式:
函数类型 operator 运算符(形参)
{
.......
}
(2)重载双目运算符为成员函数
A.如果要重载双目运算符B为类成员函数,使之能够实现表达式oprd1 B oprd2, 其中oprd1必须为一个自定义类(假设为A类)的对象。
B.如果oprd1不是自定义类的对象,则运算符B不能被重载为类的成员函数。
C.如果oprd1是自定义类A的对象,则B应被重载为A 类的成员函数,形参类型应该是oprd2所属的类型。
例:
Complex Comlex::operate + (const Complex &c2) const{//这里传对象的引用&c2现
//对于传对象,开销比较小,效率比较高吧。为了保证实参的安全性,这里用的
//是常引用,是只读的引用,只读取数据参与运算,不可以改变原来的操作数。
//创建一个临时无名对象作为返回值
return Complex(real+c2.real, imag+c2.img);
}
(3)单目运算符“++”重载为成员函数
对于前置单目运算符,重载函数没有形参,对于后置单目运算符,重载函数有一个int型形参。
例:
Class Clock{
Public: //外部接口
Clock(int hour=0,int minute=0,int second=0);
Void showTime() const;
Clock & operator++(); //前置单目运算符重载
Clock & operator++(int); //后置单目运算符重载
Private:
Int hour,minute,second;
}
Clock & operator++(){ //前置单目运算符重载函数
second++;
if(second>=60){
minute-=60;
minute++;
if(minute>=60){
minute-=60;
hour=(hour+1)%24;
}
}
return *this;
}
Clock Clock operator++(int){ //后置单目运算符重载函数
//注意形参表中的整形参数
Clock old=*this;
++(*this); //调用前置“++”运算符
return old;
}
(4)存在问题,前置++和后置++定义有很大区别,不只是形参int问题,还有&,Clock不同。为什么呢?
6.5.2 运算符重载为非成员函数
(1)函数的形参代表依自左至右次序排列的各操作数
A. 参数个数 = 原操作数个数(后置++、--除外)
B. 至少应该有一个自定义类型的参数
(2)后置单目运算符++和--的重载函数,形参列表中要增加一个int,但不必写形参名。
(3)如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元。
(4)双目运算符B重载为非成员函数后,
表达式: oprd1 B oprd2
等同于 operator B(oprd1, oprd2)
(5)前置单目运算符B重载后,
表达式 B oprd
等同于 operator B(oprd)
(6)后置单目运算符++和--重载后,
表达式 oprd B
等同于 operator B(oprd, 0)
例:以非成员函数形式重载Complex的加减法运算和”<<”运算符
A. 将+、-(双目)重载为非成员函数,并将其成名为复数类的友元,两个操作都是复数类的常引用。
B. 将<<(双目)重载为非成员函数operator<<(ostream &out,const Complex &c),并将其声明为复数类的友元,以支持按下面形式输出复数类对象a、b:
Cout<<a<<b;相当于:operator<<(operator<<(cout,a),b);
C. 它的左操作数是std::ostream引用,右操作数为复数类的常引用
D. 返回std::ostream引用。
Class Complex{
Public:
complex(double r=0.0,double i=0.0):real(r),imag(i){}
friend Complex operator + (const Complex &c1 , const Complex &c2);
friend Complex operator - (const Complex &c1, const Complex &c2);
friend ostream & operator<<(ostream &out, const Complex &c);
//c++规定:流对象不允许复制,因此只能用引用&
Private:
Double real;
Double imag;
}
Complex operator +(const Complex &c1, const Complex &c2){
Return Complex(c1.real+c2.real, c1.imag + c2.imag);
}
Complex operator - (const Complex &c1, const Complex &c2){
Return Complex(c1.real - c2.real, c1.imag - c2.imag);
}
ostream & operator<<(ostream &out, const Complex &c){
out<< “(”<<c.real<<”,”<<c.imag<<”)”;
return out;
}
6.6一般虚函数成员
6.6.1 为什么要有虚函数
问题:如何能在运行时绑定函数调用表达式与函数代码?
解决:用虚函数(用virtual关键字说明的函数)
6.6.2
C++中在默认情况下,是采用静态绑定,也就是编译时的绑定。
只有用virtual关键字特别说明的函数才进行动态绑定。
虚函数必须是非静态的成员函数,虚函数经过派生后仍为虚函数。
可以实现运行过程中的多态。
6.6.3 虚函数的声明
Virtual 函数类型 函数名(参数表);
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
在派生类中可以对基类中的成员函数进行覆盖:
派生类继承基类的接口
通过覆盖虚函数改造从基类继承而来的功能。
虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。但将虚函数声明为内联函数也不会引起错误。
例:
Class Base1{
Public:
Virtual void display() const; //虚函数,其中的const表示display是常成员函
//数,只能调用不修改成员变量的函数,和调用常成员变量,因为加上const
//之后display不允许修改数据,一旦调用了可修改的编译,报错。
};
void Base1::disply() const{
cout<< “Base1::display()”<<endll;
}
Class Base2::public Base1{
Public:
virtual void display() const;
};
void Base2::display() const{
cout<<”Base2::display()”<<endl;
}
Class Derived::public Base2{
Public:
Virtual void display() const;
};
Void Derived::display() const{
cout<<”Derived::display()”<<endl;
}
注意: 只要基类中定义了虚函数,派生类中在生命相同原型的函数,也将自动成为虚函数,可以不写virtual关键字。但是最好还是在虚函数前加virtual,使得程序具有更好的可读性。
6.6.4 什么叫做动态绑定和静态绑定
所谓静态绑定是指在程序编译过程中,把函数(方法或者过程)调用与响应调用所需的代码结合的过程称之为静态绑定。 动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法 除了限制访问,访问方式也决定哪个方法将被子类调用或哪个属性将被子类访问.
6.7 虚析构函数
在c++中,不能声明虚构造函数,但是可以声明虚析构函数。
6.7.1 虚析构函数的声明语法:
Virtual ~类名();
6.7.2 为什么要用虚析构函数
(1)如果一个类的析构函数是虚函数,那么由它派生而来的所有子类的析构函数也是虚函数。析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作。
(2)简单的来说:如果有可能通过基类指针调用对象的析构函数,就需要让基类的析构函数成为虚函数,否则会产生不确定的后果。
(3)自己理解:这里的虚析构函数是用来,通过基类指针删除派生类对象时调用派生类的析构函数的。如果不把基类的析构函数设置成虚析构函数,派生类的析构函数是不被执行的,会造成内存泄露!
6.7.3 虚表
(1)每个多态类有一个虚表(virtual table)
(2)虚表中由当前类的各个虚函数的入口地址
(3)每个对象有一个指向当前类的虚表的指针(虚指针vptr)
6.7.4 动态绑定的实现:
(1)构造函数中为对象的虚指针赋值
(2)通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址。
(3)通过该入口地址调用虚函数
6.8抽象类
6.8.1什么是抽象类
(1)带有纯虚函数的类是抽象类,抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。
(2)抽象类不能实例化,不能定义一个抽象类的对象,但是可以定义一个抽象类的指针和引用。通过指针或引用,就可以指向并访问派生类的对象,进而访问派生类的成员。
(3)如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类。
6.8.2纯虚函数
(1)纯虚函数的声明格式:
Virtual 函数类型 函数名 (参数表) = 0;
实际上,纯虚函数和一般虚函数成员的原型在书写格式上的不同就在于后面加了“=0”。声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。纯虚函数的函数体由派生类给出。
6.8.3 多态类型和非多态类型
C++的类类型分为两类——多态类型和非多态类型。多态类型是指有虚函数的类型,非多态类型是指所有的其他类型。
对非多态类的公有继承,应当慎重,而且一般没有太大的必要。
6.8.4 dynamic_cast执行基类向派生类的转换
dynamic_cast是与static_cast,const_cast,reinterpret_cast并列的4种类型转换操作符之一。它可以将基类的指针显示的转换为派生类的指针,或将基类的引用显示转换为派生类的引用。
转换前类型必须是指向多态类型的指针,或多态类型的引用,而不能是指向非多态类型的指针或非多态类型的引用。
例子:
Void fun(Base *b){
b->fun1();
//尝试将b转换为Derived1指针
Derived1 *d = dynamic_cast<Derived1*>(b);
if(d!=0) d->fun2(); //判断转换是否成功
}