第五章-多态性
多态的类型与实现
多态是指同样的消息被不同类型的对象接收时导致不同的行为
运行时多态要满足三个条件:
- 类之间满足类型兼容规则
- 声明虚函数
- 由成员函数调用,或者是通过指针、引用来访问虚函数
运算符重载
运算符重载是对已有的运算符赋予多重含义,作用于不同类型的数据时有不同的行为(实质上就是函数重载)
运算符重载的过程是在编译时完成的
重载规则:
- 除了类属关系运算符
. 、成员指针运算符.* 、域分辨符:: 、三目运算符?: 。其他运算符都可以重载 - 重载不能改变操作对象个数,同时至少有一个操作对象时自定义类型
重载规则:
1、运算符重载为类的成员函数(非静态)
//函数参数个数比原操作数少一个,后置“++”、“--”除外
返回类型 operator 运算符(形参表)
{
函数体
}
class A{
public:
A(){}
A(int m,int n):r(m),i(n){}
void show(){
cout<<r<<"+"<<i<<"i"<<endl;
}
A operator +(A &m)const
{
return A(r+m.r,i+m.i);
}
A operator -(A &m)const
{
return A(r-m.r,i-m.i);
}
private:
int r,i;
};
int main(){
A a(2,3),b(4,5),d;
d=a+b;
d.show();
d=b-a;
d.show();
return 0;
}
单目运算符++的重载
class A{
public:
A(){}
A(int m,int n):r(m),i(n){}
void show(){
cout<<r<<"+"<<i<<"i"<<endl;
}
A& operator ++(){ //前置++重载
r++;
i++;
return *this;
}
A operator ++(int){ //后置++重载
A c=*this;
++(*this);
return c;
}
private:
int r,i;
};
int main(){
A a(2,3),d;
d=a++;
d.show();
a.show();
return 0;
}
2、运算符重载为类的非成员函数
//函数参数个数与原操作数一样
返回类型 operator 运算符(形参表)
{
函数体
}
class A{
public:
A(){}
A(int m,int n):r(m),i(n){}
void show(){
cout<<r<<"+"<<i<<"i"<<endl;
}
friend A operator +(const A&,const A &);
friend A operator -(const A&,const A &);
private:
int r,i;
};
A operator +(const A &c1,const A &c2)
{
return A(c1.r+c2.r,c1.i+c2.i);
}
A operator -(const A &c1,const A &c2)
{
return A(c1.r-c2.r,c1.i-c2.i);
}
int main(){
A a(2,3),b(4,5),d;
d=a+b;
d.show();
d=b-a;
d.show();
return 0;
}
其他的重载例子
1. 对<<运算符的重载
class A{
public:
A(){}
A(int m,int n):r(m),i(n){}
friend ostream& operator <<(ostream& out,const A &n);
private:
int r,i;
};
ostream& operator <<(ostream& out,const A &n){
out<<"("<<n.r<<","<<n.i<<")";
return out;
}
int main(){
A a(2,3);
cout<<a;
return 0;
}
2. 对 [ ] 运算符的重载
class A{
public:
int x,y,z;
int & operator [](int index){
switch(index){
case 0:
return x;
case 1:
return y;
case 2:
return z;
}
}
};
int main(){
A a;
a[1]=10;
cout<<a.y<<endl; //此时a[1]即为y
return 0;
}
3. 对()运算符的重载
class A{
public:
int x,y,z;
int & operator ()(int index){
switch(index){
case 0:
return x;
case 1:
return y;
case 2:
return z;
}
}
};
int main(){
A a;
a(1)=20;
cout<<a.y<<endl; //此时a(1)即为y
return 0;
}
虚函数
虚函数必须是非静态的成员函数
虚函数是动态绑定的基础,经过派生之后,在类族中可以实现运行时多态
一般虚函数成员的语法:
virtual 函数类型 函数名(参数表);
class A{
public:
virtual void display(); //基类虚函数
};
class B:public A{
public:
void display(); //覆盖基类的虚函数
};
- 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的
- 虚函数的声明只能出现在函数原型声明中,不能出现在成员函数实现的时候
- 派生类覆盖基类的成员函数时,既可以使用virtual关键字,也可以不使用。最好是在派生类中写上virtual,这样可以清楚地提示这是一个虚函数
- 如果派生类并显式没有给出虚函数声明,系统会从名称、参数、返回值三个方面进行检查,满足条件后派生类虚函数会自动覆盖基类虚函数
class A
{
public:
virtual void display()const; //虚函数
};
class B:public A
{
public:
void display()const; //覆盖基类虚函数
};
class C:public B
{
public:
void display()const; //覆盖基类虚函数
};
void A::display()const{
cout<<"A::display()"<<endl;
}
void B::display()const{
cout<<"B::display()"<<endl;
}
void C::display()const{
cout<<"C::display()"<<endl;
}
void fun(A* ptr){ //参数为指向基类对象的指针
ptr->display();
}
int main()
{
A a;
B b;
C c;
fun(&a); //用 A对象的指针调用fun函数
fun(&b); //用 B对象的指针调用fun函数
fun(&c); //用 C对象的指针调用fun函数
return 0;
}
/*
程序运行结果为:
A::display()
B::display()
C::display()
*/
如果将fun函数的参数类型设定为A 而不是A* ,那么三次执行的结果都是A::display()。这里运用了类型兼容规则,基类指针可以指向派生类对象,基类引用可以作为派生类对象的别名,但是基类对象不能表示为派生类对象
注意事项:
- 只有虚函数是动态绑定的,如果派生类需要修改基类的行为(重写与基类同名的函数),就应该在基类中将相应的函数声明为虚函数
- 必须通过基类的指针或引用调用虚函数时才会发生动态绑定
虚析构函数
C++中不能声明虚构造函数,可以声明虚析构函数
虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生前运行的,因此虚构造函数没有意义
一个类析构函数为虚函数,其派生类的析构函数也为虚函数。析构函数设置为虚函数后,在使用指针引用时可以动态绑定,实现运行时多态,保证使用基类指针就能调用适当的析构函数针对不同对象进行清理工作。
简单来说,如果有可能通过基类指针调用对象的析构函数(通过delete),就需要让基类的析构函数成为虚函数,否则会产生不确定的结果
class A{
public:
virtual ~A(){
cout<<"A is destructed;"<<endl;
}
};
class B:public A{
public:
~B(){
cout<<"B is destructed;"<<endl;
}
};
int main(){
B *pb=new B();
A *pa=pb;
delete pa;
return 0;
}
/*
程序运行结果为:
B is destructed;
A is destructed;
*/
由于使用了许析构函数,派生类的虚构函数被调用了,动态申请的空间被正确释放,实现了多态。
抽象类
抽象类是带有纯虚函数的类,它处于类的上层,自身无法实例化,只能通过继承机制,生成非抽象派生类,然后在实例化
纯虚函数是一个在基类中声明的虚函数,在该基类中没有定义具体的操作内容,由各派生类根据实际需要给出各自的定义。
语法形式为:
virtual 函数类型 函数名(参数表)=0;
细节 :
- 在基类中对纯虚函数定义的函数体的调用,必须通过“基类 :: 函数名(参数表)”的形式。
- 如果将析构函数定义为纯虚函数,必须给出它的实现,因为派生类析构函数执行之后需要调用基类的析构函数。
-
抽象类派生出新的类之后,如果派生类给出所有纯虚函数的具体实现,派生类就不再是抽象类,可以实例化对象 -
如果派生类没有给出全部纯虚函数的具体实现,此时派生类仍然是一个抽象类 -
抽象类不能实例化,但是可以定义一个抽象类的指针和引用。通过指针或引用,可以指向并访问这个派生类的对象,进而访问派生类的成员,这种访问时多态特征的。
class A
{
public:
virtual void display()const=0; //纯虚函数
};
class B:public A
{
public:
void display()const; //覆盖基类虚函数
};
class C:public B
{
public:
void display()const; //覆盖基类虚函数
};
void A::display()const{
cout<<"A::display()"<<endl;
}
void B::display()const{
cout<<"B::display()"<<endl;
}
void C::display()const{
cout<<"C::display()"<<endl;
}
void fun(A* ptr){ //参数为指向基类对象的指针
ptr->display();
}
int main()
{
//A a;是错误的,抽象类不能实例化
B b;
C c;
fun(&b); //用 B对象的指针调用fun函数
fun(&c); //用 C对象的指针调用fun函数
return 0;
}
/*
程序运行结果为:
B::display()
C::display()
*/
Tips
- 指针类型
- 可以声明指向常量的指针,此时不能通过指针改变所指对象的值,但指针本身可以改变去指向另外的对象
int a=20,b=20;
const int* p=&a;
p=&b; //正确,p可以指向别的对象
*p=30; //错误,不能通过p改变所指对象
- 可以声明指针类型的常量,这时指针本身的值不能被改变
int a,b;
int* const p=&a;
p=&b; //错误,p为指针常量,值不能改变
- 指向函数的指针
-
函数指针就是专门用来存放函数代码首地址的变量,一旦函数指针指向了某个函数,它与函数名便具有相同的作用 -
声明一个函数指针时,也需要说明函数的返回值、形参列表,一般语法如下:
数据类型 (* 函数指针名)(形参表)
-
函数指针使用前要赋值,让指针指向一个已经存在的函数代码起始地址
函数指针名=函数名;
void fun(int d){
cout<<"hello,"<<d<<endl;
}
int main()
{
void (* Pointer)(int); //函数指针
Pointer=fun; //函数指针指向fun
Pointer(23); //函数指针调用
return 0;
}
|