什么是多态
所谓的多态就是就是有多种形态。对应的就是同一个行文,会出现不同的形态。具体到一个生活中的实例就是:在火锅店消费的时候。同样是结账,但是却有不同的结果
普通用户:全价
vip会员:88折
学生:69折
类似这样的例子还有很多。而在面向对象程序设计中也需要多态,所以c++语言中也引进了多态!
重写的概念
首先,在谈多态之前不得不提一下一个概念—>重写,而在谈到重写这个概念之前,就要提到隐藏。来看下面这段代码:
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
class B :public A
{
public:
void func()
{
cout << "B::func()" << endl;
}
};
int main()
{
B b;
b.func();
return 0;
}
先看运行结果
很显然,这里的func和父类中的同名函数func构成的是隐藏的关系 再来看下面这一段代码
class A
{
public:
//virtual关键字声明函数,此时的函数就变成了虚函数
virtual void func()
{
cout << "A::func()" << endl;
}
};
class B :public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
};
此时的B类的func和A类的func不再是隐藏关系了,而是更加特殊的重写关系 两个函数构成重写有如下的要求:
1.两个函数要分别在两个类的作用域,并且这两个类必须要有继承关系
2.父类的同名函数必须是虚函数
3.构成重写关系的两个函数返回值,函数。参数名相同
4.父子函数构成协变也构成重写
满足以上四个条件,子类和父类的同名函数构成的关系就是重写,而重写关系和隐藏关系是互斥的,也就是说如果两个函数构成重写,那么这两个函数就不是隐藏关系!
那么前面三个条件已经演示过了,那么我们重点来看最后一个重写的特例:两个函数构成协变也是重写的关系 首先,我们通过一段代码来看看什么是协变
class A
{
public:
virtual A* func()
{
cout << "A::func()" << endl;
return nullptr;
}
};
class B :public A
{
public:
virtual B* func()
{
cout << "B::func()" << endl;
return nullptr;
}
};
在上面的代码里面,这里的返回值一个是A类的指针,一个是B类的指针,不符合我们重写的第二条的规则:虚函数的返回值的类型不同。但是这里同样构成重写关系 但是前提是,这两个函数仅仅只能是返回值不一样,并且返回值分别是必须是指针或者引用,并且返回的指针或者引用必须构成父子关系!
假设把返回类型改成下面这样:
class A
{
public:
virtual A func()
{
cout << "A::func()" << endl;
return A();
}
};
class B :public A
{
public:
virtual B func()
{
cout << "B::func()" << endl;
return B();
}
};
那么,在实际的应用中,协变应用相对较少。这也可以认为是c++语法设计的一个"坑",所以我们只要了解有协变这种特殊的情况就可以了。尽量在实际的设计中严格按照前面的3个条件设计重写。
值得一提的是,如果父类函数里面的虚函数加了virtual,即使子类的函数前面不加vitual也是虚函数。这也是c++设计的一个不足的地方,虽然可以不加,但是良好的习惯还是在子函数前面也加上virtual。
C++11新增的两个关键字
虽然继承可以实现代码的复用,但是在有的实际的场景下,有一些类不适合设计继承,所以为了解决这个问题。c++11引进了final关键字,接下来我们就通过代码来看看final关键字。
//final关键字
//final关键字,修饰类的时候,这个类不能被继承
class A final
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
class B :public A
{
public:
};
int main()
{
return 0;
}
final不仅可以修饰类,还可以修饰父类的虚函数,表示这个虚函数不能被继承。
class A
{
public:
//final修饰虚函数,这个函数无法被子类继承
virtual void func() final
{
cout << "A::func()" << endl;
}
};
class B:public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
};
这是c++11新增的final关键字。但是又有一种情况,有的时候父类的虚函数必须被子类重写,但是由于各种原因没有进行重写,造成了严重的错误。为了避免这种情况发生带来的损失。 c++11引入了一个新的关键字—>override
//override的作用:强制检查子类是否重写父类的虚函数
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
};
class B :public A
{
public:
//父类同名函数不是虚函数,所以这里并没有重写父类的func
virtual void func() override
{
cout << "B::func()" << endl;
}
};
对比final和override我们不难可以得出下面的结论
1.final是在父类中使用,限制子类不能继承父类
2.override是在子类中使用,强制子类要重写父类中的虚函数
重载,隐藏,重写三者的区别
结合前面的对于重写的分析,接下来我们来对比一下三个概念:
1.重载:两个函数在同一个作用域,两个函数根据不同的参数的类型和顺序修饰形成不同的函数地址.
2.隐藏:两个函数分别在在基类和派生类的作用域,当前作用域的同名函数隐藏外部作用域同名的函数,隐藏也叫做重定义.
3.重写:两个同名虚函数在基类和派生类的作用域,函数的返回值,参数完全相同(协变例外).这时候两个函数构成重写关系.
多态如何触发
接下来,我们就来谈一谈在c++里面怎么触发多态.要触发多态有如下两个条件:
1.基类的指针或者引用指向派生类对象
2.派生类的虚函数完成了对基类虚函数的重写.
这两个条件都很重要,缺少任何一个都不能构成多态的调用
接下来,我们用代码来验证多态的触发条件.
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
};
class B:public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
};
int main()
{
A a;
B b;
A* pa = &a;
pa->func();
cout << endl;
B* pb = &b;
pb->func();
cout << endl;
pa = pb;
pa->func();
return 0;
}
先来看对应的调用结果:
首先,第1和第2个易于理解,A类的指针指向A类对象的地址调用的自然就是A类的func,对于pb也是如此.而当pa指向B类对象的地址时,首先满足多态的第一个条件:基类的指针或引用指向派生类对象,接着派生类完成了对基类虚函数的重写,满足多态的两个条件!所以最后调用的是B类的重写的虚函数.
抽象类的概念
有的时候,一些东西是没办法具体化出实际对象的。而在c++语言里面也提供了抽象类的这一个概念.
所谓的抽象类,就是没办法实例化出对象的类!接下来我们来看怎么从语法上定义一个抽象类.
//}
//抽象类:只有纯虚函数的类是抽象类
//纯虚函数:虚函数声明后面=0
class A
{
public:
virtual void func() = 0;
};
//抽象类不能创建对象
int main()
{
A a;
return 0;
}
假如这个时候有一个类B来继承这个抽象类A,但是没有重写里面的纯虚函数,那么这个B也是抽象类.
/抽象类:只有纯虚函数的类是抽象类
//纯虚函数:虚函数声明后面=0
class A
{
public:
virtual void func() = 0;
};
class B:public A
{
public:
};
int main()
{
//A a;
B b;
return 0;
}
而当子类重写纯虚函数以后,子类才能创建对象!
/抽象类:只有纯虚函数的类是抽象类
//纯虚函数:虚函数声明后面=0
class A
{
public:
virtual void func() = 0;
};
//B类必须重写才能创建对象!
class B:public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
};
int main()
{
//A a;
B b;
return 0;
}
值得一提的是:纯虚函数是可以有函数体的,但是实际意义并不大,因为纯虚函数是一定要被重写的!而g++下会直接报错
对比override我们可以得到如下的结论;
1.override只是检查是否重写,而抽象类是强迫子类必须重写
2.override修饰函数,子类依旧可以创建对象
虚拟构函数
前面我们说过,父子析构函数会构成隐藏关系.但是有的时候,子类直接管理堆上的动态资源.单纯只用父类的析构函数没办法解决问题!所以这时候就需要使用子类特定的析构函数,所以我们把父类的析构定义成virtual
class A
{
public:
A(int a)
:_a(a)
{}
~A()
{
cout << "~A" << endl;
}
protected:
int _a;
};
class B :public A
{
public:
B()
:A(0)
,pb(new int(4))
{}
~B()
{
cout << "~B" << endl;
delete pb;
pb = nullptr;
}
private:
int* pb;
};
int main()
{ //赋值兼容的转换
A* pa = new B;
//delete会调用析构函数
delete pa;
return 0;
}
运行结果:
可以看到这里只调用了A的析构函数所以这里存在严重的内存泄露问题! 所以我们希望调用到B的析构函数,所以我们就要把父类的析构函数声明成虚函数
class B :public A
{
public:
B()
:A(0)
,pb(new int(4))
{}
virtual ~B()
{
cout << "~B" << endl;
delete pb;
pb = nullptr;
}
private:
int* pb;
};
int main()
{
A* pa = new B;
delete pa;
return 0;
}
可以看到,这里我们调用到了子类的析构函数处理了资源,所以我们的建议是基类的析构函数尽量声明成虚函数,基类的成员的访问控制符尽量用保护!
以上就是本文的主要内容,希望大家可以一起进步.