wordpress子文件夹建站,wordpress pc客户端,wordpress显示当前时间,深圳制作网站制作公司哪家好一.多态的概念
1.多态
多态(polymorphism)的概念#xff1a;通俗的来说#xff0c;就是多种形态。多态分为静态多态(编译时多态)和动态多态(运行时多态)#xff0c;而我们讲的多态大部分都是动态多态。
静态多态主要就是我们前面了解过的函数模板和函数重载#xff0c;它…一.多态的概念
1.多态
多态(polymorphism)的概念通俗的来说就是多种形态。多态分为静态多态(编译时多态)和动态多态(运行时多态)而我们讲的多态大部分都是动态多态。
静态多态主要就是我们前面了解过的函数模板和函数重载它们传不同的参数就可以调用不同的函数通过参数不同达到多种形态这所以叫编译时多态是因为实参传递给实参的参数匹配过程是在编译时完成的我们一般把编译时归为静态运行时归为动态。
运行时多态简单来说就是对于同一个函数不同的对象去调用的会完成不同的行为以此来达到多种形态。比如买火车票这个行为普通人买票就是全价学生买票就买学生票(有优惠)军人买票可以优先买票。
这里先演示一下多态的运行结果
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 p;student s;func(p);func(s);return 0;
}
对这段程序来说person和student都有买票这个行为但是不同的对象行为是不同的我们用一个基类的指针去调用buyticket这个函数其运行结果会根据对象的不同而不同当基类指针接收的是一个基类对象的地址时调用的就是基类的buyticket当基类指针接受的是一个派生类的对象的地址时调用的就是派生类的buyticket。 2.虚函数
类的成员函数前面加上virtual来修饰那么这个成员就被称为虚函数。注意非成员函数不能被virtual修饰。
class A
{virtual bool max(int a, int b){return a b;}protected:int _a;int _b;
};
当该类作为基类被派生类继承后在派生类中该虚函数依旧还是虚函数。
3.多态实现的前提
多态是在继承体系中基类对象和派生类对象去调用同一函数产生了不同的行为。
要实现多态的效果必须满足下面的两个条件
1、必须是基类的指针或者引用调用该成员函数因为基类的指针或者引用既可以表示基类对象也可以表示派生类对象。
2、被调用的成员函数必须是虚函数且在派生类中已经完成了重写/覆盖。
3.1虚函数的重写/覆盖
派生类中有一个与基类三同(虚函数返回值、函数名、形参列表完全相同ps形参列表相同只要求形参的类型相同不要求形参名相同)的虚函数即派生类的虚函数重写了基类的虚函数。
注意在重写基类的虚函数时派生类的虚函数加不加virtual都可以(因为继承后基类的虚函数被派生类继承下来了该函数在派生类中依旧保持虚函数属性)但是这样写并不规范建议在重写时也加上virtual。 我们现在会看前面给出的那段程序首先虚函数在派生类中完成了重写且在调用该函数时是利用基类的指针调用的(引用也可以)不同的是基类的指针指向的对象不同第一次指向基类对象第二次指向派生类对象。 多态面试题1
以下程序输出的结果是什么
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
首先创建了一个B类对象的指针然后调用了test函数B类对象的指针之所以可以调用test函数是因为B从A继承了它然后在test函数中又调用了func函数要注意的是这里调用func函数时其实满足了多态的条件因为在成员函数中调用其他成员函数时默认有一个this指针。这个this指针就是基类对象的指针然后func完成了重写且这里的this指向的是一个B类对象所以这里应该调用B类的func函数所以应该打印B-。
那按道理来说应该是B-0为什么是B-1呢
注意重写的本质是重写虚函数的函数体所以在我们调用重写后的虚函数时其返回值、函数名、形参表都与基类的相同不同的只有函数体。所以这里本质上val还是1.
而当我们直接调用B类中的func时此时与多态无关val用的就是0.
注意不要修改重写后的函数缺省值。 4.虚函数重写——协变
先前说虚函数重写时要求三同。
但是其返回值可以不同但要求返回值是具有父子关系的类型的指针/引用这个规则称为协变。
class A{};
class B:public A{};class person
{
public://virtual person* buyticket()virtual A* buyticket(){cout 成人票 endl;return nullptr;}
};class student : public person
{
public://virtual student* buyticket()virtual B* buyticket(){cout 学生票 endl;return nullptr;}
};
我们既可以直接用person/student类的指针作为返回值也可以用另一组具有父子关系的类作为返回值。这也是一种协变。协变也满足多态的条件也可以实现多态。
5.析构函数的重写
基类的析构函数为虚函数只要派生类定义了析构函数无论是否加virtual都与基类的析构函数构成重写。虽然这两个析构函数的名字不同但其实在编译阶段所有的析构函数的函数名都被处理成了destructor所以也满足了三同。
所以只要基类的析构函数是虚函数派生类只要显式定义析构函数就构成了重写。 面试题2
为什么基类中的析构函数建议设计成虚函数
为了避免基类指针指向派生类对象时调用析构函数不能完全清理派生类对象的全部数据造成的内存泄露问题。
我们可以借助下面这段程序来理解
class A
{
public:virtual ~A(){cout ~A() endl;}
};class B :public A
{
public:~B(){cout ~B() endl;}protected:int* _ptr;
};int main()
{A* pa new A;A* pb new B;delete pa;delete pb;return 0;
}
当我们分别用基类的指针指向了基类对象和派生类对象时析构pa时就是很普通的析构但是当析构pb时此时满足了多态会先调用B的析构函数然后再调用A的析构函数(派生类析构函数调用结束时会自动调用基类的析构函数)就可以将pb指向的内容全部销毁。
但是如果没有将A类的析构函数定义为虚函数的话此时析构pb就不会产生多态效果直接调用了A的析构但是pb指向的是B类对象等于它只析构了B类对象中的A类部分还有B类自己的部分没有析构这就导致了内存泄漏。 6.override、final override是C11中新增的一个关键字用来检测派生类中的指定函数是否完成了重写。也就是说用override修饰的函数必须是基类虚函数的重写否则就会报错。 我们用override修饰了派生类的这个函数编译阶段编译器就会向上搜索判断这个函数是否为基类的虚函数的重写如果是不报错不是就报错。 我们看到这里的函数名写错了所以不构成重写。 所以我们可以利用这个关键字来替我们检查是否完成了重写。 如果我们不想这个虚函数被派生类重写那么我们可以用final来修饰它。 7.重载/重写/隐藏的对比
这是三个研究的都是同名函数之间的关系。 8.纯虚函数和抽象类
当一个虚函数以0为结尾时这个虚函数就是纯虚函数
class A
{
public:virtual void func() 0{}
};
纯虚函数可以有函数体也可以没有。
包含纯虚函数的类叫做抽象类。抽象类不能用来定义对象但是可以作为基类
如果继承该抽象类的派生类没有重写该纯虚函数的话该派生类也是一个抽象类。 二.多态的原理
1.虚函数表指针
下面这段程序在32位下运行结果是什么
A.编译报错 B.运行报错 C.8 D.12
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;
}答案是D
答案不是8而是12的原因是在Base类对象的头部还有一个虚函数表指针__vfptr(注意有些平台可能会放到对象的最后面这个跟平台有关)这个指针指向一个函数指针数组该函数指针数组里面存储的都是Base类的虚函数的指针。而在32位下指针的大小是4字节加上int和char在进行内存对齐的话刚好是12字节。
一个含有虚函数的类中至少都有一个虚函数表指针因为一个类所有的虚函数的地址都要被放到这个类对象的虚函数表中虚函数表也简称为虚表。 同一个类的不同对象公用同一个虚表
当派生类没有重写该虚函数时此时派生类和基类的虚表指针的内容是一样的但是虚函数表指针不同。
重写后派生类就会将之前的那个地址给覆盖掉
2.多态的原理
针对下面的程序在func中当p指向的是person类的对象时就调用的是person的buyticketp指向的是student类的对象时就调用student的buyticket这是为什么呢
我们在前面说了一个有虚函数的类的对象都会有一个虚函数表指针里面存放着该类所有虚函数的地址。在满足多态的前提下在运行时当p指向的是person对象时p就会到person类的虚表里面去找对应虚函数的地址然后去调用当p指向的是student对象时p就会到student类的虚表里面去找对应虚函数的地址然后调用。
class person
{
public:virtual void buyticket(){cout person endl;}protected:string _name;int _age;
};class student : public person
{
public:virtual void buyticket() override{cout student endl;}protected:int _id;
};void func(person* p)
{p-buyticket();
}int main()
{person p;student s;func(p);func(s);return 0;
} 3.动态绑定和静态绑定
对不满足多态条件(指针/引用调用虚函数)的函数调用是在编译时绑定的也就是在编译时确定调用函数的地址叫做静态绑定
满足多态条件的函数调用是在运行时绑定的也就是在运行时到指定对象的虚表中找到调用函数的地址叫做动态绑定。 4.虚函数表 1、基类对象的虚函数表中存放基类所有的虚函数的地址。非虚函数的地址是不会存在里面的。 2、派生类由两部分构成继承下来的基类和自己的成员一般情况下继承下来的基类中有虚函数表指针自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个它们只是里面存储的地址相同。这就像基类对象和派生类对象中的基类部分。 这里调式窗口看不到B类自己的func4虚函数可以借助内存窗口观察 3、派⽣类中重写的基类的虚函数派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函 数地址 4、派⽣类的虚函数表中包含基类的虚函数地址派⽣类重写的虚函数地址派⽣类⾃⼰的虚函数地 址三个部分。 虚函数表本质是⼀个存虚函数指针的指针数组⼀般情况这个数组最后⾯放了⼀个0x00000000标 记。(这个C并没有进⾏规定各个编译器⾃⾏定义的vs系列编译器会再后⾯放个0x00000000 标记g系列编译不会放) 5、虚函数存在哪的 虚函数和普通函数⼀样的编译好后是⼀段指令都是存在代码段的只是虚函数的地址⼜存到了虚表中 6、虚函数表存在哪里? 为了确认我们可以写一段程序用来判断其在那块空间上存储 我们可以将其和每个空间上的变量的地址进行比较如果存储在同一个空间的话地址应该比较接近为了获取虚表指针存储的地址因为该指针在对象的头部其大小是四个字节所以我们可以先将其强转成int*然后在对其进行解引用只拿到前四个字节的内容就可以拿到里面的地址。 class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }void func5() { cout Base::func5 endl; }
protected:int a 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func1 endl; }void func4() { cout Derive::func4 endl; }protected:int b 2;
};int main()
{int i 0;static int j 1;int* p1 new int;const char* p2 xxxxxxxx;printf(栈:%p\n, i);printf(静态区:%p\n, j);printf(堆:%p\n, p1);printf(常量区:%p\n, p2);Base b;Derive d;Base* p3 b;Derive* p4 d;printf(Person虚表地址:%p\n, *(int*)p3);printf(Student虚表地址:%p\n, *(int*)p4);printf(虚函数地址:%p\n, Base::func1);printf(普通函数地址:%p\n, Base::func5);return 0;
} 我们对比可以发现虚表地址和常量区的地址接近所以可以认为虚表就存储在常量区中。 多态部分的细节
1、被virtual修饰的成员函数称为虚函数
2、virtual关键字只在声明时加上在类外实现时不能加
3、static和virtual是不能同时使用的
4、实现多态是要付出代价的如虚表虚表指针等所以不实现多态就不要有虚函数了
5、抽象类可以定义指针而且经常这样做其目的就是用父类指针指向子类从而实现多态
6、基类有几张虚表派生类就有几张派生类自己的虚函数不会开一个新的虚表存储自己虚函数的地址其地址会存放到第一张虚表的末尾。 下面程序的运行结果
答案 0 1 2
new B时会调用B类的构造函数但是调用之前会先调用基类的构造函数然后执行test()此时多态还没有形成所以调用的就是A类的func()打印0
然后到B类的构造函数执行test()由于基类已经创建成功虚表已经存在了所以此时构成了多态调用B类的func打印1
最后执行p-test()满足多态后打印2
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;
} 完~