硬核两万字带你理解C++之多态
Posted 小赵小赵福星高照~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核两万字带你理解C++之多态相关的知识,希望对你有一定的参考价值。
多态
文章目录
多态: 多态就是函数调用的多种形态,调用函数更加灵活,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
话不多说,我们首先来看这样的一段程序,让大家感知一下多态是什么意思,这是一段不构成多态的程序:
class Person
public:
void BuyTicket()
cout<<"正常排队 - 全价买票"<<endl;
protected:
int _age;
string _name;
class Student : public Person
public:
void BuyTicket()
cout<<"正常排队 - 半价买票"<<endl;
protected:
//...
class Soldier : public Person
public:
void BuyTicket()
cout<<"优先排队 - 全价买票"<<endl;
protected:
//...
void Func(Person* ptr)
ptr->BuyTicket();
int main()
Person ps;
Student st;
Soldier sd;
Func(&ps);
Func(&st);
Func(&sd);
return 0;
可以看到此时并没有构成多态,都打印的是父类的函数,因为形成多态具有两个条件
1、子类重写父类的虚函数
2、必须是父类的指针或者引用去调用虚函数
这两个条件什么意思,我们下面讲解,我们再来看看构成多态的程序:
class Person
public:
virtual void BuyTicket()
cout<<"正常排队 - 全价买票"<<endl;
protected:
int _age;
string _name;
class Student : public Person
public:
virtual void BuyTicket()
cout<<"正常排队 - 半价买票"<<endl;
protected:
//...
class Soldier : public Person
public:
virtual void BuyTicket()
cout<<"优先排队 - 全价买票"<<endl;
protected:
//...
void Func(Person* ptr)
//ptr指向父类对象调用父类的虚函数,指向子类对象调用子类虚函数
ptr->BuyTicket();
int main()
Person ps;
Student st;
Soldier sd;
Func(&ps);
Func(&st);
Func(&sd);
return 0;
此时满足了多态条件,看到了指针指向的对象是什么类就调用哪个类的虚函数:
那么什么是虚函数呢?多态的原理是什么呢?上面为什么就满足了多态的条件呢?等等问题我们下面开始深入理解多态:
有些书籍会把多态进行更细划分:
-
静态的多态:函数重载,调用同一个函数,传不同的参数,就有不同的行为/形态
-
动态的多态:父类指针或引用调用重写虚函数,不同的对象去调用,就有不同的行为/行为,父类指针或者引用指向父类,调用的就是父类的虚函数,父类指针或者引用指向哪个子类,调用的就是子类的虚函数
int main()
int i;
char ch;
cin >> i;
cin >> ch;
cout << i << endl;
cout << ch << endl;
return 0;
这里其实就是静态的多态,看起来我们用的是一个函数,但是实际不是的,这个底层就是多态实现的:operator>>(int i); operator>>(char i);
int main()
int i = 0,j = 1;
double d = 1.1, e = 2.2;
swap(i,j);
swap(d,e);
return 0;
多态就是函数调用的多种形态,以上都是静态的多态,这里的静态是指编译时确定的,在编译阶段通过函数修饰规则去找函数。动态的多态:不同的类型对象去完成同一件事情,产生的动作是不一样的
多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。
Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
-
必须通过基类的指针或者引用调用虚函数
-
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person
public:
virtual void BuyTicket()
cout<<"买票-全价"<<endl;
;
注意:
- 只有类的非静态成员函数才可以加virtual
- 虚函数这里virtual和虚继承中用的virtual是同一个关键字,但是他们之间没有关系,这里的虚函数是为了实现多态,虚继承是为了解决菱形继承的数据冗余和二义性
构成多态的条件之一是虚函数重写:
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
class Person
public:
virtual void BuyTicket()
cout<<"买票-全价"<<endl;
;
class Student : public Person
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
cout<<"买票-半价"<<endl;
;
class Soldier : public Person
public:
//子类的虚函数重写了父类的虚函数
virtual void BuyTicket()
cout<<"买票-优先"<<endl;
;
void f(Person& p)
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p.BuyTicket();
void f(Person* p)
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p->BuyTicket();
int main()
Person p;//普通人
Student st;//学生
Soldier so;//军人
f(p);
f(st);
f(so);
f(&p);
f(&st);
f(&so);
return 0;
注意多态需要满足的条件:
-
必须通过基类的指针或者引用调用虚函数
-
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
正常的虚函数重写,要求虚函数的函数名、参数、返回值都要相同,但是协变例外
虚函数重写的两个例外:
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A;
class B : public A ;
class Person
public:
virtual A* f()
cout<<"A* Person::f()"<<endl;
return new A;
;
class Student : public Person
public:
virtual B* f()
cout<<"B* Student::f()"<<endl;
return new B;
;
int main()
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
- 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person
public:
~Person() cout << "~Person()" << endl;
;
class Student : public Person
public:
~Student() cout << "~Student()" << endl;
;
int main()
Person p
Student s;
return 0;
不构成重写:
先析构子类的对象,调用子类的析构函数,在子类的析构函数结束时自动的调用了父类的析构函数,最后调用父类的析构函数析构父类
构成重写:
在普通场景下,父子类的析构函数是否构成重写不重要,没什么影响,那么看下面的场景(new对象特殊场景):
class Person
public:
//建议把父类的析构函数定义成虚函数
//这样子类的虚函数方便重写父类的虚函数
~Person() cout << "~Person()" << endl;
;
class Student : public Person
public:
//Student和Person析构函数名看起来不相同,但是他们构成虚函数重写
~Student() cout << "~Student()" << endl;
;
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
不构成重写:
为什么会这样呢?
delete p1;//p1->destuctor()+operator delete(p1)
delete p2;//p2->destuctor()+operator delete(p2)
因为delete时底层会去调用该对象类的析构函数和operator delete,不是虚函数时,他们构成隐藏,因为p1和p2都是父类指针,所以他们都是去调用父类的析构函数
构成重写时:
只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
注意:
当子类的虚函数重写了父类的虚函数,子类虚函数不写virtual关键字也认为它是虚函数,完成了重写:
class Person
public:
virtual void BuyTicket()
cout<<"买票-全价"<<endl;
;
class Student : public Person
public:
//子类的虚函数重写了父类的虚函数
void BuyTicket()
cout<<"买票-半价"<<endl;
;
void f(Person& p)
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p.BuyTicket();
int main()
Person p;//普通人
Student st;//学生
f(p);
f(st);
return 0;
但是父类重写的函数不加virtual就不行,因为子类是先继承父类的虚函数,继承下来以后有virtual属性了,子类只是重写这个virtual函数,严格来说这个也算是C++语言设计的坑
针对虚函数重写,给一个建议:尽量不要写协变,尽量严格按重写要求来,这样代码更容易进行维护
C++11 override和final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序
写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
如果不想虚函数被重写,那么就在虚函数后面加关键字final:
class Person
public:
virtual void BuyTicket() final
cout<<"买票-全价"<<endl;
;
class Student : public Person
public:
//子类的虚函数重写了父类的虚函数
void BuyTicket()
cout<<"买票-半价"<<endl;
;
void f(Person& p)
//传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
p.BuyTicket();
int main()
Person p;//普通人
Student st;//学生
f(p);
f(st);
return 0;
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Person
public:
virtual void BuyTicket()
cout << "买票-全价" << endl;
;
class Student : public Person
public:
void BuyTicket(int i) override
cout << "买票-半价" << endl;
;
void f(Person& p)
p.BuyTicket();
int main()
Person p;//普通人
Student st;//学生
f(p);
f(st);
return 0;
如何设计出一个不能被继承的类?将该类的成员的访问限定符设置为私有:
class A
private:
A()
class B:public A
public:
B()
int main()
B b;
return 0;
这个是可以被继承,但是父类的成员子类不可见,子类定义对象不能使用父类的成员。
在C++11中支持了final这个关键字:
class A final
;
class B:public A
;
int main()
return 0;
final修饰一个类,这个类不能被继承,不管定不定义对象直接报错。
重载、覆盖(重写)、隐藏的对比
总结:
多态:调用一个函数时,展现出多种形态(通过调用不同的函数,完成不同的行为)。多态分为静态的多态和动态的多态:比如函数重载就是静态的多态,在编译时确定地址。动态的多态:条件:1、子类继承父类,完成虚函数的重写 2、父类的指针或者引用去调用这个重写的虚函数。
父类的指针或者引用指向父类对象,调用的是父类的虚函数
父类的指针或者引用指向子类对象,调用的是子类的虚函数
虚函数的重写条件:1、要是虚函数 2、函数名、参数、返回值都相同
例外:
1、协变(返回值不一样,父类的虚函数返回的是基类对象指针和引用,子类的虚函数返回的是子类对象指针和引用)
2、析构函数
3、子类中的重写的虚函数可以不加virtual关键字(建议加上)
抽象类
概念
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。
//抽象类
class Car
public:
virtual void Drive() = 0;
;
int main()
Car c;//不能实例化出对象
return 0;
抽象类不能实例化出对象,可以更好的去表示现实世界中没有实例对象对应的抽象类型,比如:植物、人、动物,它体现了接口继承,强制子类去重写虚函数,如果不重写,继承下来还是纯虚函数,照样无法实例化出对象。
//抽象类
class Car
public:
virtual void Drive() = 0;
;
class Benz : public Car
public:
virtual void Drive()
cout<<"Benz-舒适"<<endl;
;
int main()
//Car c;//不能实例化出对象
Car* pBenz = new Benz;
pBenz->Drive();
return 0;
要注意和override区分,override检查子类虚函数是否完成重写。纯虚函数是强制子类去重写虚函数,如果不重写,继承下来还是纯虚函数,照样无法实例化出对象。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
虚函数表
这里常考一道笔试题:sizeof(Base)是多少?
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
public:
virtual void Func1()
cout << "Func1()" << endl;
private:
int _b = 1;
char _ch = 'a';
;
int main()
cout << sizeof(Base) << endl;
return 0;
为什么是12呢?根据内存对齐应该是8呀,这里为什么会是12。
是因为只要包含虚函数的类,该类的对象就包含一个虚函数表指针(简称虚表指针),这个虚函数表指针就是用来实现多态的:
这个虚表指针指向一个数组,这个数组的元素是函数指针,这里面的函数指针指向该类中的虚函数
虚函数被编译成指令后,还是和普通函数一样,存在代码段,只是它的地址放在虚表中
需要注意的是:
里跟虚继承那里是不一样的,他们虽然都用了virtual关键字,但是他们的使用场景完全不一样,解决的也是不一样的问题,他们之间没有关联,虚继承产生的是虚基表,虚基表里面存的是距离虚基类的偏移量
class Person
public:
virtual void BuyTicket() cout << "买票-全价" << endl;
virtual void Func1()
cout<<"Person::Func1()"<<endl;
;
class Student : public Person
public:
virtual void 狂肝两万字带你用pytorch搞深度学习!!!