秒懂C++之多态
玛丽亚后 2024-09-10 10:35:01 阅读 100
目录
一. 多态的概念
二. 多态的定义及实现
多态的构成条件
虚函数重写的例外
协变(基类与派生类虚函数返回值类型不同)
析构函数的重写(基类与派生类析构函数的名字不同)
练习例题
final
override
重载、覆盖(重写)、隐藏(重定义)的对比
三. 抽象类
四. 多态的原理
虚函数表
五. 单继承和多继承关系中的虚函数表
一. 多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
就比如拿我们的支付宝红包举例子,老用户往往会扫出很小的红包,而新用户却往往能扫到大额红包~
二. 多态的定义及实现
多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
必须通过父类的指针或者引用调用虚函数 被调用的函数必须是虚函数(加上virtual),且派生类必须对基类的虚函数进行重写(函数名相同,参数相同,返回值相同)
<code>class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
正常情况下调用函数是通过其指针或引用类型然后去调该类型的类成员函数~而在多态中是看父类指针或引用的对象,通过对象去调用该对象的类成员函数~
虚函数重写的例外
协变(基类与派生类虚函数返回值类型不同)
class A {};
class B : public A {};
class Person {
public:
virtual A* f() { return new A; }
};
class Student : public Person {
public:
virtual B* f() { return new B; }
};
重写是必须要求要相同返回值的,但也可以有例外,就是返回值可以不同,但是两个返回值必须得是指针或引用,同时得保证这两个返回值构成继承关系。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时称为协变~
ps:不常出现,了解即可~
析构函数的重写(基类与派生类析构函数的名字不同)
class Person {
public:
~Person()
{
cout << "~Person()" << endl;
}
};
// 重写实现
class Student : public Person {
public:
~Student()
{
// delete _ptr;
cout << "~Student()" << endl;
}
};
int main()
{
Person* p1 = new Person;
delete p1;
Person* p2 = new Student;
delete p2;
return 0;
}
如果我们没有用虚函数,那么在代码执行中p1去调用父类的析构函数,p2也会去调用父类的析构函数~
可是这样忽略了一个问题:如果我们new出来的子类当中有额外的资源呢?本来子类的析构函数可以去清理这个额外资源,但由于正常调用而去调用父类的析构导致内存泄漏~
所以为了能让析构函数也形成多态的效果,我们选择使用虚函数进行重写~
而且在同名函数这块也进行了特殊处理,一律把析构的函数名当作destructor来看~使重写的条件名字,最后在父类指针的调用下根据指向对象来调用对象的成员函数~
练习例题
ps小知识点:
如果父类的virtual去掉,那就是与子类构成隐藏关系了,就不再是虚函数无法构成重写,也就没有多态只能是普通的调用(看类型)。如果子类的virtual去掉,那仍是虚函数,仍符合多态并且为多态调用(看对象)
<code>// 以下程序输出结果是什么()A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
本题答案选B~ 接下来我来为大家分析解答~
首先我们发现子类func没有virtual而父类有,那么这也是能构成虚函数重写的~
然后我们可以得出在父类中的隐藏this指针类型是父类的,而父类指针去调用func函数是满足多态条件的,那么就要去找到所调用的对象而非类型,而test函数是被指向B对象的指针所调用的~那么说明多态作用下func调用的是B类的成员函数func~
又因为B类中的func函数中virtual是没有的,那么它只能去用父类的函数声明然后再结合自己的定义作组合,所以在这个过程中子类的func函数是用了父类func中的val再加上实现的B->最终形成答案B~
如果是普通对象调用或者非父类指针去调用那也就没那么多事了~
final
final:修饰虚函数,表示该虚函数不能再被重写
如果我们想要让子类无法继承父类有两种方法:
法一:让父类构造私有
<code>class A
{
public:
protected:
int _a;
private:
A()
{}
};
class B : public A
{};
int main()
{
B bb;
return 0;
}
这样子类就无法实例化出对象了,因为子类构造必须调用父类构造~
法二:使用final
class A final
{
public:
protected:
int _a;
private:
A()
{}
};
class B : public A
{};
int main()
{
B bb;
return 0;
}
override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
重载、覆盖(重写)、隐藏(重定义)的对比
三. 抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。
<code>class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
Car c;//无法实例化出对象
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
return 0;
}
子类只有进行虚函数重写避开去调用父类的纯虚函数就可以实例化了~从另一种角度上看抽象类也有强迫子类进行多态调用的提醒作用~
四. 多态的原理
虚函数表
//sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char _ch;
};
int main()
{
cout << sizeof(Base) << endl;
Base bb;
return 0;
}
很多人会说是8,但实际上却为12。因为在有了虚函数后对象里面还会多出一个指针~
该指针内放置虚函数Func1的地址~
我们再来修改一下代码~
<code>class Base
{
public:
void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
char _ch = 'a';
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
cout << sizeof(Base) << endl;
Base bb;
cout << sizeof(Derive) << endl;
Derive dd;
return 0;
}
我们在父类多添加了一个虚函数和普通函数,在子类添加了一个虚函数
不出我们意外,都多出了一个指针~
其实虚函数建立的时候在内存中会有虚函数表这种对象~而虚函数表也就是函数指针的数组~
当有多个虚函数的时候就会出现一个指针(_vfptr)去指向虚函数数组(虚函数表),然后访问数组里面各个虚函数的地址~
子类中是没有func2函数的,那么就会去拿父类中func2的地址,而子类是有自己的func1函数的,那么在虚函数表中就会让子类的func1地址去覆盖父类的func1地址~
ps:虚函数表中先声明的放在前面~
五. 单继承和多继承关系中的虚函数表
我们再来对虚函数表做一些补充~
首先如果是普通调用,那么函数地址是在编译时就确定的。在编译链接的时候从符号表中找到函数地址,然后去调用它。而在多态调用中是通过指针进入对应对象虚函数表,在里面找到虚函数地址并成功调用它~
下面我们来了解一些多继承关系中的虚函数表~
<code>class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VF_PTR)();
void PrintVFT(VF_PTR* vft)
{
for (size_t i = 0; vft[i] != nullptr; i++)
{
printf("[%d]:%p->", i, vft[i]);
VF_PTR f = vft[i];
f();
//(*f)();
}
cout << endl << endl;
}
int main()
{
Derive d;
cout << sizeof(d) << endl;
Base1* ptr1 = &d;
Base2* ptr2 = &d;
PrintVFT((VF_PTR*)(*(int*)ptr1));
PrintVFT((VF_PTR*)(*(int*)ptr2));
return 0;
}
由于vs的监视窗口无法查看2个以上的虚函数,所以我们这里选择人工打印出所属对象的虚函数表里面的内容~
首先我们需要取得类对象中的前4个字节,因为那里代表指向虚函数表的指针~所以我们对切片过的指针ptr1进行强转为int*类型再解引用就可以拿到其指针地址了~然后我们再通过对指针强转为(VF_PTR*类型)因为我们的打印函数中打印的是函数指针,所以需要实参与形参类型相同以便接收~最后我们再把得到的函数指针去回调func函数形成多态的效果~
在多继承中有两张虚函数表,一张代表Base1,一张代表Base2~
而在Base1表中原有的虚函数会被子类独有的虚函数覆盖,例如子类只复写了func1与func3虚函数,那么在Base1中就会被子类的func1与func3虚函数覆盖~
而在Base2表中同样如此,不过需要注意的是func3最后只放在了先声明的那个父类中,Base2是不放func3的~
总结:对于func3这种在父类没有的虚函数,一般会放在最先声明的类中~至于其他的该拷贝拷贝,该替换替换~
最后是其他知识点的总结:
inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。
静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针
对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
数表中去查找。
虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况
下存在代码段(常量区)的。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。