ToB企服应用市场:ToB评测及商务社交产业平台
标题:
c++中的多态
[打印本页]
作者:
前进之路
时间:
2024-9-19 04:04
标题:
c++中的多态
目录
编辑
一·多态的概念:
1·1介绍:
1·2多态条件:
1·3关于明白多态中的一个例题(听说很有坑):
1·4协变:
1·5析构的重写:
1·6override和final关键字:
1·7重写/重载/隐蔽:
1·8纯虚函数和抽象类:
二·多态原理:
2·1虚表:
2·2动态和静态绑定:
2·3原理总结(仅个人明白):
三·多态收尾(最后以一道多态题结束):
一·多态的概念:
1·1介绍:
即多种形态:这里又分为静态多态和动态多态。
其中静态多态:即编译时出现的多态如:函数模版,函数参数等。
动态多态:运行时出现的多态如这里要讲的虚函数出现的多态。
1·2多态条件:
那么会有个疑问满意多态的条件是什么呢?
下面引出这两个条件:即多态出现的不得有继续关系么:
1.即基类对象指针或引用调用虚函数,(这时看传的对象如果是派生类对象就调派生类虚函数,如果是基类对象就去调基类虚函数)
2.即必要在父类的虚函数前加上virtual,而子类也可加也可不加但是一般要加,其次就是要满意虚函数的重写(覆盖):即函数名同,返回类型同,参数类型同(好比 int x=1;另一个是int x=2,这也是类似的),简记:三同。
下面举个实现简单多态的代码例子:
class A {
public: virtual void talk(int x = 1) {
cout << "我来咯" <<"->"<<x << endl;
}
};
class B : public A{
public:
virtual void talk (int x = 0) {
cout << "你来咯" << "->" << x<< endl;
}
};
//void f(A& p) {
// p.talk();
//}
int main() {
A a;
B b;
/*f(a);
f(b);*/
A& aa = a;
A& bb = b;
aa.talk();
bb.talk();
return 0;
}
复制代码
这样就形成了简单的多态。
1·3关于明白多态中的一个例题(听说很有坑):
解答:这里A是父类,B是子类,然后p指针是父类的指针,这里用p去访问子类继续过来的父类的虚函数test()然后继续抽象明白成照搬过来但是应该是存了个提醒,然后切片变成A的指针通过B的虚表(后续讲解,这里放着test函数地址,以及A的func声明+B的func定义覆盖过去)(记:覆盖父的声明加上子类的定义覆盖过去),因此是父类指针调用虚函数,满意多态条件:第一种明白:由于是父类的指针调用虚函数,传的是子类的对象,故调用子类虚表的这个虚函数故是B->1.
第二种明白:就是它是在B类内切片成A的指针去访问的,然后团体还是可以明白为在B类内操作,故这里调用的虚表还是B的。
1·4协变:
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针大概引 ⽤,派⽣类虚函数返回派⽣类对象的指针大概引⽤时,称为协变。
注:各自返回的是指针大概引用:
1·5析构的重写:
即有资源是否被完全清算完的问题。
涉及隐蔽和虚函数这两点。
这里为了可以完全清算掉资源故把析构设计成虚函数:这样的话父与子类的虚表中就会存有这两个虚函数(后期都被处理成destructor形式满意多态)这样用父类的指针或引用,当传递不同对象就调用不同的析构都清算掉资源了。
但是如果父未加virtual也就是不构成虚函数,也就是隐蔽关系。这时子隐蔽父,但是这里用的是A类型指针也就是父类型指针吸收,这时切片到访问父的析构,故子类没清算资源。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
复制代码
因此要把析构搞成虚函数并重写当用的是父指针或引用操作时,如果用父与子分别操作,那么子类析构完会多一次调用父类的析构,如果有资源的话,两次父的析构一定出问题,故析构就要虚函数这么搞。
1·6override和final关键字:
C++对函数重写的要求⽐较严酷,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重载,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果 因此C++11提供了override,可以资助⽤⼾检测是否重写。如果我们不想让派⽣类继续基类大概重写虚函数那么可以⽤final去修饰。
final无法继续:
无法重写虚函数:
1·7重写/重载/隐蔽:
这里重写比隐蔽条件更严酷,也就是如果不是重写就是隐蔽,隐蔽又称重定义,下面有张表方便我们影象:
1·8纯虚函数和抽象类:
纯虚函数就是我们在父类的虚函数后加上=0,而包含这个纯虚函数的类就是抽象类,不能实例化处对象如:
virtual void talk() = 0;
复制代码
这里大概会说为什么没内容,因为它已经是纯虚函数了,后面要想利用必须通过子类给它的定义重写了,故父类的定义没什么意义了可以不写,而子类如果增补些便去继续它,那么子类也是抽象类不能实例化出对象,因此这种操作便强制了子类必须对这个纯虚函数完成重写操作了。
class per
{
public:
virtual void talk () = 0;
};
class zhangsan:public per
{
public:
virtual void talk()
{
cout << "我是张三" << endl;
}
};
class lisi:public per
{
public:
virtual void talk()
{
cout << "我是李四" << endl;
}
};
int main()
{
//per s;
per* zs = new zhangsan;
zs->talk();
per* ls = new lisi;
ls->talk();
return 0;
}
复制代码
抽象类无法实例化对象:
被继续后完成重写:
二·多态原理:
2·1虚表:
这里我们大概会说这种多态操作是怎么样的,下面一道例题带我们进入正题:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
复制代码
思考一下这道题,一般我们肯定会认为是8,思路:这个int类型从偏移量从0开始排到5,最后放的是char,然后输出的字节应该是成员最大对齐数的整数倍也就是8,但是究竟确实12,为什么呢?这里就涉及到了虚表的概念。
虚表的全称就是虚函数的一个数组的指针(__vfptr->virtual function ptr),这内里放的是虚函数地址,方便查找:
这个指针巨细也就是地址巨细宁静台有关,32位呆板是4字节,64位呆板是8字节,这样下面这道题就不难明决了(默认的是x86,32位呆板)。
没有virtual,它确实8,因为不是虚函数无虚表。
这里还必要增补一点:
①父类的虚表和子类重写后的虚表不是同一张但是内容大概有类似的。
②当子类如果继续了多个父类,则分别在继续的子类中的父类处有个虚表,则继续几个父类,有几个虚表但是没完成重写的虚函数直接加到第一个继续的父类的虚表中。
③派生类的虚表包括基类的虚函数地址(找基类的声明),子类重写父类的虚函数地址(找子类虚函数的定义),子类自己的虚函数地址。
④虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标 记(vs)。而g++没有操作,不同编译器处理不同。
⑤虚表放在常量区。
⑥虚函数放在代码段(因为虚函数也是函数最后汇编成指令也就是放代码段)。
2·2动态和静态绑定:
静态绑定:即是不满意多态,仅是指针引用调用函数,在编译的时候确定的函数地址(如:模版,函数重载等)。
动态绑定(又叫做晚期绑定):即满意多态的条件,在运行的时候在虚表中找到对应函数的地址。
2·3原理总结(仅个人明白):
父类指针大概引用调用虚函数(访问谁的虚函数由传递给它的对象决定)子类继续父类在子类对应继续放父类的位置天生虚表,这个虚表中放的是父类虚函数的地址,然后子类如果能举行虚函数重写就给它重写定义完成覆盖操作,最后这个虚表中也就是父的声明+子的定义(存放它们对应函数地址),当利用不同对象调用不同虚表中的虚函数。
三·多态收尾(最后以一道多态题结束):
以下程序输出结果是()
class A
{
public:
A() :m_iVal(0) { test(); }
virtual void func() { std::cout << m_iVal << ‘ ’; }
void test() { func(); }
public:
int m_iVal;
};
class B : public A
{
public:
B() { test(); }
virtual void func()
{
++m_iVal;
std::cout << m_iVal << ‘ ’;
}
};
int main(int argc, char* argv[])
{
A* p = new B;
p->test();
return 0;
}
复制代码
解答:
首先这道题涉及了继续和多态的知识点(继续的构造,多态的条件,以及操作等):
第一步:A*p = new B:首先它构造了一个B类型的对象,我们就要给它初始化,但是它继续了A类,故先给父类初始化然后再给子类初始化,父类初始化就是把m_iVal=0,然后通过父类的指针调用func函数,而这里对象明显是A类的(这里构成了多态),故打印0,然后就是B类即子类再初始化,然后调用test函数,而它属于A类,故举行“切片”然后变成A类的指针去访问test,这里又是多态效应,然后对象是B类的故访问B的虚函数即m_iVal++变为1,然后打印,接着完成了初始化。
第二步:p->test():就是利用A类的指针去访问test然后又是多态即对象是B类的对象故访问B类的虚表中虚函数,m_iVal++变为2,打印。
故输出0,1,2。
最后山高路长,祝各位以后的道路一起顺风。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4