C++【多态】
文章目录
- 1、多态的概念
- 2、多态的定义及实现
- 2-1、多态的构成条件
- 2-2、虚函数
- 2-3、虚函数的重写
- 2-4 多态样例
- 2-5、协变
- 2-6、 析构函数与virtual
- 2-7、函数重载、函数隐藏(重定义)与虚函数重写(覆盖)的对比
- 2-8、override 和 final(C++11提供)
- 3、抽象类
- 3-1、概念
- 3-2、接口继承和实现继承
- 4、多态的原理
- 4-1、虚函数表(虚表)
- 4-2、多态的原理
- 4-3、动态绑定与静态绑定
- 5、单继承和多继承关系的虚函数表
- 5-1、单继承中的虚函数表
- 5-2、多继承中的虚函数表
- 5-3、菱形继承、菱形虚拟继承
- 6、总结(重点)
1、多态的概念
多态的概念:
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举例:
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
再举个例子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为
注:以上支付宝红包问题等例子纯属瞎编,大家仅供娱
2、多态的定义及实现
2-1、多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(下面都会讲到,先记住这个结论)
2-2、虚函数
虚函数
:即被virtual修饰的类成员函数称为虚函数。 这里的虚函数和前面的虚拟继承没有任何关系,只不过是同时用了virtual关键字。虚拟继承是为了解决数据冗余和二义性的!
class Person
{
public:virtual void BuyTicket() //这里的函数就是虚函数{ cout << "买票-全价" << endl;}
};
2-3、虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
虚函数的重写又叫做虚函数的覆盖;
函数的隐藏又叫做函数的重定义:基类与派生类的成员函数名称相同,那么基类的成员函数在派生类中被隐藏
简单理解为:重写(覆盖)比隐藏(重定义)更加复杂——关键字virtual和三个相同
三个相同:
1、函数名相同
2、函数参数的类型和数量相同
2、函数返回值类型相同
2-4 多态样例
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;}
};//多态条件
//1、虚函数重写
//2、由父类/基类指针或者引用调用虚函数void Func(Person& p){//父类的引用调用虚函数p.BuyTicket();
}int main()
{Person ps;Student st;Soldier sd;Func(ps);Func(st);Func(sd);return 0;
}//void Func(Person* p) {
// p->BuyTicket();
//}
//
//int main()
//{
// Person* ps = new Person;
// Student* st = new Student;
// Soldier* sd = new Soldier;
//
// Func(ps);
// Func(st);
// Func(sd);
// return 0;
//}
上面就是多态的调用。
传入参数的类型是派生类类型,而函数参数是基类类型的引用。即便函数参数类型是父类的引用/指针,但是本质上参数引用/指针,引用的对象/指向的对象是派生类中,包含父类成员的那一部分(切片/切割)
基类指针/引用调用虚函数:指针/引用的内容是子类中父类的那一部分!
得出结论:
1、普通调用:跟对象调用类型有关(也就是将子类中包含父类的成员,切割拷贝给父类)
2、多态调用:跟基类指针/引用,指向的对象/引用的对象,的类型有关(子类包含的父类成员,不用给父类,直接由父类的指针/引用指向)
不满足多态条件的情况:
1、不是由父类/基类的指针/引用对虚函数的调用:
这里就是把子类中包含父类的成员切割/切片给给父类了
2、没有形成虚函数重写
情况一:父类和子类都不是虚函数
情况二:父类不是虚函数,子类是虚函数
情况三:父类是虚函数,子类不是虚函数
这里的情况三居然能够调用正确,这是为什么呢?
我们可以理解为:派生类继承了父类的虚函数,所以,派生类中继承下来的函数就是具有虚函数属性的。然后又对函数体重写,这样就构成了虚函数重写了
虚函数重写的本质就是:对函数体进行重写,函数体也就是函数的实现!!!
因此,子类的虚函数可以不加virtual关键字,但是父类必须加上virtual关键字(这里推荐都加上virtual)
那么,出了以上的情况之外,还有什么特殊情况也是构成多态的呢?
特例——协变
2-5、协变
上面的三个相同条件中,返回值可以不同,但是返回值必须是一个父子类关系的指针/引用
甚至下面情况也可以:
那么,到现在为止,除了上面两个特殊情况以外,再不满足多态的条件就构成不了多态了!!!
2-6、 析构函数与virtual
对于普普通调用析构函数而言:
class Person
{
public://virtual ~Person()~Person(){cout << "Person delete:" << _p << endl;delete[] _p;}
protected:int* _p = new int[10];
};
class Student : public Person
{
public:~Student()//一般情况下,子类析构结束,会自动调用父类的析构{cout << "Student delete:" << _s << endl;delete[] _s;}
protected:int* _s = new int[20];
};
int main()
{Person p;Student s;return 0;
}
确实,上面调用的确析构函数是不是虚函数无所谓,都是调用正确的
但是,如果调用变成下面情况了呢?
int main()
{//Person p;//Student s;Person* ptr1 = new Person;Person* ptr2 = new Student;delete ptr1;delete ptr2;return 0;
}
delete的具体行为:
1、使用指针调用析构函数
2、operator delete(指针)
调用方式:
1、普通调用:跟对象调用类型有关
2、多态调用:跟基类指针/引用,指向的对象/引用的对象,的类型有关
所以,原因就是:
那么,如果是多态调用是不是就避免内存泄漏了呢?
我们在析构函数前面加上virtual关键字,变成虚函数;
析构函数没有参数和返回值,最后只剩下一个函数名了,这里的析构函数名我们看着好像不相同,其实是相同的:
析构函数的函数名会被自动处理成为
destructor
最终,通过多态调用,我们避免了内存泄漏;也了解了为什么析构函数的函数名都被转换成了destructor
,不这样做就发生了内存泄漏了
所以,析构函数无脑加上virtual关键字就行了
注意:
构造函数不能无脑加virtual,因为虚函数的运行是建立在对象之上的,我们把构造函数变成虚函数,在执行构造函数时,对象都没有生成。所以构造函数不能加上virtual关键字
2-7、函数重载、函数隐藏(重定义)与虚函数重写(覆盖)的对比
2-8、override 和 final(C++11提供)
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
1、 final:修饰虚函数,表示该虚函数不能再被重写
class A final//c11之后的方法——final关键字
{
//private://c11之前的方法——构造函数私有
// A()
// {}
public:A(){}
};class B : public A
{
};int main()
{B bb;B* ptr = new B;return 0;
}
两种方法使基类不能被继承:
1、构造函数私有
2、定义类后面加上final关键字
2.、override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{public:virtual void Drive(){}
};class Benz :public Car {public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
3、抽象类
3-1、概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
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;}
};
void Test()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}
int main()
{Test();return 0;
}
异常现象:
当不需要基类生成对象的时候,可以把基类写成抽象类!
3-2、接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数
例题1:
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;
}
//选什么?
//A: A->0 B : B->1 C : A->1 D : B->0 E : 编译出错 F : 以上都不正确
p的类型是B*,当我们执行p->test()时,会使用继承下来的test()函数,但是test函数来自于A,而对于指针/引用来说都不会发生强转,所以B原封不动的继承了A的test()函数,但是B继承的test()函数中的this指针仍然是A*。也就是说func()函数的调用this->func()中的this是A*。这个时候,A是父类的指针,func()函数构成了虚函数重写,所以,A->func()就是一个多态调用,先打印B->,这个时候重点又来了,虚函数重写,重写的只是函数体,函数接口没有被重写!所以,A*->func()用着A中的函数接口,调用B中的函数体,也就是函数实现!
例题2:
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
//选什么? A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
这里的p3和p1虽然相同但是意义不一样。p3是整体,而p1是局部
4、多态的原理
4-1、虚函数表(虚表)
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{cout << sizeof(Base) << endl;return 0;
}
我们前面学过,如果知道类的成员变量,采用内存对齐就能够算出类的大小,但是这里真的和我们想的一样吗?
为什么是8呢?
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析:
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};class Derive : public Base
{public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}
可以看到,子类继承父类的虚函数之后,发生了虚函数重写,那么子类的虚表继承下来的虚函数地址就不是原来继承下来的地址,而是由重写的虚函数进行覆盖,变成了一个新的虚函数
通过观察和测试,我们发现了以下几点问题:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在 虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,Linux g++下大家自己去验证?
所以,__vfptr是一个指针,全名为虚函数表指针(虚表指针),指向虚函数表(虚表),虚函数表里面存放的就是我们的虚函数地址!
我们来看看虚表是不是存放在代码段的:
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}
private:int _b = 1;char _ch;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}void Func2(){cout << "Derive::Func2()" << endl;}
private:int _d = 2;
};
int main()
{int a = 0;cout << "栈:" << &a << endl;int* p1 = new int;cout << "堆:" << p1 << endl;const char* str = "hello world";cout << "代码段/常量区:" << (void*)str << endl;static int b = 0;cout << "静态区/数据段:" << &b << endl;Base be;cout << "虚表:" << (void*)*((int*)&be) << endl;//拿到前面4个字节地址Base* ptr1 = &be;int* ptr2 = (int*)ptr1;printf("虚表:%p\n", *ptr2);Derive de;cout << "虚表:" << (void*)*((int*)&de) << endl;Base b1;Base b2;return 0;
}
对上面代码进行改进:
class Base
{
public:virtual 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;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}void Func3(){cout << "Derive::Func3()" << endl;}
private:int _d = 2;
};
int main()
{cout << sizeof(Base) << endl;Base b;Derive d;// 普通调用 -- 编译时/静态 绑定Base* ptr = &b;ptr->Func3();ptr = &d;ptr->Func3();// 多态调用 -- 运行时/动态 绑定ptr = &b;ptr->Func1();ptr = &d;ptr->Func1();return 0;
}
普通调用在编译的时候,通过类型就能够锁定函数是谁,直接call该函数地址,进行调用
多态调用确定不了,因为多态调用不确定函数是调用父类的还是子类的,虽然看到的都是一个父类的对象,但是存在两种情况:
1、父类对象;2、子类中父类的那一部分 无论是什么情况,多态调用都是通过指向的对象的内部取虚表指针,再到虚表里面找到对应的函数进行调用(指向谁,调用谁)
简单来说就是:编译器将子类虚表中重写的虚函数地址覆盖完成之后,再一次性把所有虚表放到代码段/常量区当中!
同一个类的对象共享同一个虚表!
4-2、多态的原理
上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket
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 Mike;Func(Mike); Student Johnson;Func(Johnson);return 0;
}
- 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚 函数是Person::BuyTicket。
- 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中 找到虚函数是Student::BuyTicket。
- 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
- 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调 用虚函数。反思一下为什么?
- 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的
void Func(Person* p)
{p->BuyTicket();
}int main()
{Person mike;Func(&mike);mike.BuyTicket(); return 0;
}// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
...p->BuyTicket();// p中存的是mike对象的指针,将p移动到eax中001940DE mov eax,dword ptr [p]// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx001940E1 mov edx,dword ptr [eax]// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax00B823EE mov eax,dword ptr [edx]// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。001940EA call eax 00头1940EC cmp esi,esp
}int main()
{
... // 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址mike.BuyTicket();00195182 lea ecx,[mike]00195185 call Person::BuyTicket (01914F6h)
...
}
4-3、动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。
5、单继承和多继承关系的虚函数表
需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的
5-1、单继承中的虚函数表
class Base {public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }private:int a;
};class Derive :public Base {public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }void func4() { cout << "Derive::func4" << endl; }private:int b;
};int main()
{Base b;Derive d;return 0;
}
我们可以看到子类的func1函数进行了重写,func2函数没有进行重写,但是子类的func3函数也是虚函数啊,怎么子类虚表里面没有func3函数的地址呢?
这是因为vs的监视窗口对原代码进行了处理,我们监视窗口看到的并不是原生的内容
我们通过内存窗口来看看:
linux下虚表不是以空结束!
我们直接来打印出这两个虚表指针:
typedef void(*func_t)();
void Print(func_t f[])
{for (int i = 0; f[i] != nullptr; ++i){printf("[%d]:%p\n", i, f[i]);f[i]();}cout << endl;
}
int main()
{Base b;//Print((func_t*)(*(int*)&b));//int*在x86下面才是4个字节,x64下面是8个字节//Print((func_t*)(*(long long*)&b));Print((func_t*)(*(void**)&b));//指针都是4个字节大小,void*不能解引用,void**可以,*(void**)一样是看4个字节Print((func_t*)(*(long long**)&b));//*(long long**)一样是看4个字节Derive d;//Print((func_t*)(*(int*)&d));//Print((func_t*)(*(long long*)&d));Print((func_t*)(*(void**)&b));Print((func_t*)(*(long long**)&b));return 0;
}
所有的虚函数都是会进虚表的!
单继承中,派生类自己的虚函数在虚表中继承下来的基类虚函数的后面
5-2、多继承中的虚函数表
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(*func_t)();
void Print(func_t f[])
{for (int i = 0; f[i] != nullptr; ++i){printf("[%d]:%p\n", i, f[i]);f[i]();}cout << endl;
}
int main()
{Base1 b1;Base2 b2;Print((func_t*)(*(void**)&b1));Print((func_t*)(*(void**)&b2));Derive d;return 0;
}
也就是放到第一个继承的类里面
接下来打印虚表地址:
int main()
{Base1 b1;Base2 b2;//Print((func_t*)(*(void**)&b1));//Print((func_t*)(*(void**)&b2));Derive d;Print((func_t*)(*(void**)&d));//第一张虚表Print((func_t*)(*(void**)((char*)&d + sizeof(Base1))));//char*不能少,不然一次加Derive大小Base2* p = &d;Print((func_t*)(*(void**)p));//自动偏移,找到子类中,Base2类的一部分return 0;
}
所以,多继承中,如果子类有未重写的虚函数,会放在第一个继承的父类的虚表中!
5-3、菱形继承、菱形虚拟继承
这个就不多讲了吧,实际中菱形继承本来用的就是,更何况是菱形虚拟继承
C++虚函数表解析
C++对象的内存布局
想深入了解可以观看这两篇文章
6、总结(重点)
- 什么是多态?
多态分为静态多态和动态多态。
静态多态是在编译时绑定(比如说:函数重载,根据函数名修饰规则等等可以直接确定调用函数);
动态多态是运行时绑定,通过虚函数重写之后,父类的指针/引用来调用重写的虚函数,指向父类调用父类,指向子类调用子类(与指针/引用的类型无关,与指向的对象类型有关)。通过虚表指针找到代码段/常量区对应的虚表中的函数地址,拿到虚函数之后调用!
- 什么是重载、重写(覆盖)、重定义(隐藏)?
函数重载:两个函数在同一个作用域内,并且函数名,参数(参数个数,参数类型,参数类型的顺序)相同
虚函数重写(覆盖):两个函数分别在基类(父类)和派生类(子类)的作用域中,都是虚函数(有virtual关键字修饰),并且两个函数的函数名、参数、返回值都相同(协变除外)
函数的重定义(隐藏):两个函数分别在基类(父类)和派生类(子类)的作用域中,函数名相同就构成函数的重定义。并且在基类和子类的两个同名函数不构成重写就构成重定义
- 多态的实现原理?
一个接口,多种方法
- 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
- 存在虚函数的类都有一个一维的虚函数表,简称为虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
- 类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
- 虚函数表是一个存储类成员函数指针的数据结构——函数指针数组结构。
- 虚函数表是由编译器自动生成与维护的(这也说明了虚函数的重写/覆盖是编译器帮助我们完成的)。
- virtual成员函数会被编译器放入虚函数表中。
- 当存在虚函数时,每个对象中都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当进行test(parent *base)函数的时候,C++编译器不需要区分子类或者父类对象,只需要再base指针中,找到vptr指针即可)。
- vptr一般作为类对象的第一个成员。
注:vs的监视窗口vptr指针指向的虚表一般只存放两个虚函数地址,分别是是vptr[0]和vptr[1],这是编译器自主处理的结果,我们要通过内存窗口观察
- inline函数可以是虚函数吗?
对于多态调用而言:
1、语法层面(理论上):理论上来说是不可以的,inline是函数在类中展开,将代码保存在了类里面,但是这就与虚函数相违背了;一个是编译时就将函数展开;一个是在运行时通过虚表指针,找到虚表,拿到里面虚函数的地址,最后再调用虚函数;两种情况不可能同时存在。
2、实际操作:实际操作我们会发生,编译器(vs系列)只是发生警告,并不会报错,并且还能够正确编译。因为编译器在遇到inline和virtual两个关键字的时候,自动的忽略了inline属性,使得inline失效
但对于普通调用而言:
是可以的,普通调用会继续保存inline属性,因为普通调用没有虚表指针,虚表这些东西
- 静态成员可以是虚函数吗?
不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数的地址无法放进虚函数表。
静态成员函数不属于类中的任何一个对象和实例,属于类共有的一个函数。也就是说,它不能用this指针来访问,因为this指针指向的是每一个对象和实例
对于virtual虚函数,它的调用恰恰使用this指针。在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr(4字节)->vtable ->virtual虚函数。
所以说,static静态函数没有this指针,也就无法找到虚函数了。所以静态成员函数不能是虚函数。他们的关键区别就是this指针。
- 构造函数可以是虚函数吗?
不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。我们把构造函数变成虚函数放到虚表之后,虚表指针无法得到初始化,这样虚表指针和虚表就断开连接了
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
可以,并且最好把基类的析构函数定义成虚函数。
场景:Person* ptr2 = new Student;
这种父类指针指向子类的时候,就需要将析构函数定义为虚函数
- 对象访问普通函数快还是虚函数更快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?
虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。(虚表指针的初始化是在构造函数初始化列表阶段完成初始化的)
- C++菱形继承的问题?虚继承的原理?
1、数据冗余:虚基类的成员在派生类中会保存两份,这样多保存就产生了数据冗余
2、二义性:虚基类的成员在派生类中会保存两份,在调用的时候如果不指明基类就会产生二义性,从而报错
注意这里不要把虚函数表和虚基表搞混了:
虚函数表:里面存放的是虚函数的地址,用来构建多态
虚基表:存放着类的偏移量,为了防止数据冗余和二义性
- 什么是抽象类?抽象类的作用?
抽象类:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)
抽象类作用:抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系
相关文章:
C++【多态】
文章目录1、多态的概念2、多态的定义及实现2-1、多态的构成条件2-2、虚函数2-3、虚函数的重写2-4 多态样例2-5、协变2-6、 析构函数与virtual2-7、函数重载、函数隐藏(重定义)与虚函数重写(覆盖)的对比2-8、override 和 final&…...
缓存预热、缓存雪崩、缓存击穿、缓存穿透,你真的了解吗?
缓存穿透、缓存击穿、缓存雪崩有什么区别,该如何解决? 1.缓存预热 1.1 问题描述 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存…...
【Java基础】018 -- 面向对象阶段项目上(拼图小游戏)
目录 拼图小游戏(GUI) 一、主界面分析 1、练习一:创建主界面1 2、练习二:创建主界面2(JFrame) 3、练习三:在游戏界面中添加菜单(JMenuBar) ①、菜单的制作 4、添加图片&a…...
【网络~】
网络一级目录二、socket套接字三、UDP数据报套接字四、TCP流套接字一级目录 1.局域网、广域网 2.IP地址是什么? IP地址是标识主机在网络上的地址 IP地址是如何组成的? 点分十进制,将32位分为四个部分,每个部分一个字节ÿ…...
手写JavaScript中的call、bind、apply方法
手写JavaScript中的call、bind、apply方法 call方法 call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。 function Product(name, price) {this.name name;this.price price; }function Food(name, price) {Product.call(this, name, price);t…...
JAVA练习46-将有序数组转换为二叉搜索树
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 前言 提示:这里可以添加本文要记录的大概内容: 2月10日练习内容 提示:以下是本篇文章正文内容,下面案例可供参考 一、题目-…...
linux(centos7.6)docker
官方文档:https://docs.docker.com/engine/install/centos/1安装之前删除旧版本的docker2安装yum install-y yum-utils3配置yum源 不用官网的外国下载太慢 推荐阿里云yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.r…...
微信小程序滚动穿透问题
文章目录1、catchtouchmove"true"2、page-meta3、wx.setPageStyle做小程序的开发业务中,经常会使用弹窗,当弹窗里的内容过多时,要滚动查看,然后经常会遇到滚动弹窗,弹窗底下页面也跟着滚。解决思路ÿ…...
安全—06day
负载均衡反向代理下的webshell上传负载均衡负载均衡下webshell上传的四大难点难点一:需要在每一台节点的相同位置上传相同内容的webshell难点二:无法预测下一次请求是哪一台机器去执行难点三:当我们需要上传一些工具时,麻烦来了&a…...
PostgreSQL入门
PostgreSQL入门 简介 PostgreSQL是以加州大学伯克利分校计算机系开发的POSTGRES, 版本 4.2为基础的对象关系型数据库管理系统(ORDBMS) 支持大部分SQL标准并且提供了许多现代特性 复杂查询外键触发器可更新视图事务完整性多版本并发控制 …...
自媒体人都在用的免费音效素材网站
视频剪辑、自媒体人必备的剪辑音效素材网站,免费下载,建议收藏! 1、菜鸟图库 音效素材下载_mp3音效大全 - 菜鸟图库 菜鸟图库是一个综合性素材网站,站内涵盖设计、图片、办公、视频、音效等素材。其中音效素材就有上千首…...
Java数据结构中二叉树的深度解析及常见OJ题
本篇文章讲述Java数据结构中关于二叉树相关知识及常见的二叉树OJ题做法讲解(包含非递归遍历二叉树) 目录 一、二叉树 1.1二叉树概念 1.2特殊的二叉树 1.3二叉树性质 1.4二叉树基本性质定理题 1.5二叉树遍历基本操作 1.6二叉树遍历的前中后非递归写法 1.7…...
算法顶级比赛汇总
可参赛的算法比赛 阿里云天池大数据竞赛 时间:每年各个季度很多类型都会出题(比赛总时间大概为两个月) 内容:各个类型的算法题都会出、奖金上万不等 形式:在线提交(提交后在线检查结果)、离线…...
Android MVI框架搭建与使用
MVI框架搭建与使用前言正文一、创建项目① 配置AndroidManifest.xml② 配置app的build.gradle二、网络请求① 生成数据类② 接口类③ 网络请求工具类三、意图与状态① 创建意图② 创建状态四、ViewModel① 创建存储库② 创建ViewModel③ 创建ViewModel工厂五、UI① 列表适配器②…...
第九节 使用设备树实现RGB 灯驱动
通过上一小节的学习,我们已经能够编写简单的设备树节点,并且使用常用的of 函数从设备树中获取我们想要的节点资源。这一小节我们带领大家使用设备树编写一个简单的RGB 灯驱动程序,加深对设备树的理解。 实验说明 本节实验使用到STM32MP1 开…...
Ubuntu 系统下Docker安装与使用
Ubuntu 系统下Docker安装与使用Docker安装与使用Docker安装安装环境准备工作系统要求卸载旧版本Ubuntu 14.04 可选内核模块Ubuntu 16.04 使用 APT 安装安装 Docker CE使用脚本自动安装启动 Docker CE建立 docker 用户组测试 Docker 是否安装正确镜像加速Docker使用拉取镜像创建…...
DHCP安全及防范
DHCP安全及防范DHCP面临的威胁DHCP饿死攻击仿冒DHCP Server攻击DHCP中间人攻击DHCP Snooping技术的出现DHCP Snooping防饿死攻击DHCP Snooping防止仿冒DHCP Server攻击DHCP Snooping防止中间人攻击DHCP Snooping防止仿冒DHCP报文攻击DHCP面临的威胁 网络攻击无处不在ÿ…...
【流畅的python】第一章 Python数据模型
文章目录第一章 Python 数据模型1.1 python风格的纸牌1.2 如何使用特殊方法-通过创建一个向量类的例子1.3 特殊方法汇总第一章 Python 数据模型 python最好的品质是一致性 python解释器碰到特殊句法时,会使用特殊方法去激活一些基本的对象操作 这些特殊的方法以两个…...
from文件突然全部变为类cs右击无法显示设计界面
右击也不显示查看设计器 工程文件 .csproj中将 <Compile Include"OperatorWindows\Connection.cs" /> <Compile Include"OperatorWindows\Connection.Designer.cs"> <DependentUpon>Connection.cs</DependentUpon> &…...
使用arthas中vmtool命令查看spring容器中对象的某个属性
场景: 线上环境我想查看spring中容器某个对象的属性值 vmtool命令 方式一: vmtool --action getInstances -c [类加载器的hash] --className [目标类全路径] --limit 10 -x 2 实例:查询该类的全部属性情况(该类是一个spri…...
四种幂等性解决方案
什么是幂等性? 幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。 在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。 幂等…...
【Nacos】Nacos配置中心客户端配置更新源码分析
上文我们说了服务启动的时候从远程Nacos服务端拉取配置,这节我们来说下Nacos服务端配置的变动怎么实时通知到客户端,首先需要注册监听器。 注册监听器 NacosContextRefresher类会监听应用启动发布的ApplicationReadyEvent事件,然后进行配置…...
按钮防抖与节流-vue2
防抖与节流,应用场景有很多,例如:禁止重复提交数据的场景、搜索框输入搜索条件,待输入停止后再开始搜索。 防抖 点击button按钮,设置定时器,在规定的时间内再次点击会重置定时器重新计时,在规定…...
PyTorch学习笔记:nn.SmoothL1Loss——平滑L1损失
PyTorch学习笔记:nn.SmoothL1Loss——平滑L1损失 torch.nn.SmoothL1Loss(size_averageNone, reduceNone, reductionmean, beta1.0)功能:创建一个平滑后的L1L_1L1损失函数,即Smooth L1: l(x,y)L{l1,…,lN}Tl(x,y)L\{l_1,\dots,l…...
2年时间,涨薪20k,想拿高薪还真不能老老实实的工作...
2016年开始了我的测试生活。 2016年刚到公司的时候,我做的是测试工程师。做测试工程师是我对自己的职业规划。说实话,我能得到这份工作真的很高兴。 来公司的第一个星期,因为有一个项目缺人,所以部门经理提前结束了我的考核期&a…...
Spark - Spark SQL中RBO, CBO与AQE简单介绍
Spark SQL核心是Catalyst, Catalyst执行流程主要分4个阶段, 语句解析, 逻辑计划与优化, 物理计划与优化, 代码生成 前三个阶段都由Catalyst负责, 其中, 逻辑计划的优化采用RBO思路, 物理计划的优化采用CBO思路 RBO (Rule Based Optimization) 基于规则优化, 通过一系列预定好…...
NeurIPS/ICLR/ICML AI三大会国内高校和企业近年中稿量完整统计
点击文末公众号卡片,找对地方,轻松参会。 近日,有群友转发了一张网图,统计了近年来中国所有单位在NeurIPS、ICLR、ICML论文情况。原图如下: 中稿数100: 清华(1) 北大(2) 占比:22.6%。 累计数…...
Android IO 框架 Okio 的实现原理,到底哪里 OK?
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 前言 大家好,我是小彭。 今天,我们来讨论一个 Square 开源的 I/O 框架 Okio,我们最开始接触到 Okio 框架还是源于 Square 家的 OkHttp 网络…...
一文讲解Linux 设备模型 kobject,kset
设备驱动模型 面试的时候,有面试官会问,什么是Linux 设备驱动模型?你要怎么回答? 这个问题,突然这么一问,可能你会愣住不知道怎么回答,因为Linux 设备驱动模型是一个比较整体的概念࿰…...
linux配置密码过期的安全策略(/etc/login.defs的解读)
长期不更换密码很容易导致密码被破解,而linux的密码过期安全策略主要在/etc/login.defs中配置。一、/etc/login.defs文件的参数解读1、/etc/login.defs文件的内容示例[rootlocalhost ~]# cat /etc/login.defs # # Please note that the parameters in this configur…...
郑州网站建设 推广/石家庄网站建设
函数声明和函数表达式 1.函数声明的格式不再赘述; 2.函数表达式的定义:是其他表达式的一部分的函数(作为赋值表达式的右值,或者作为其他函数的参数)叫作函数表达式。函数表达式非常重要,在于它能准确地在我…...
dreamweaver绿色版下载/seo搜索引擎优化推荐
本文刚刚发在51CTO.com网站,文章链接:[url]http://soft.51cto.com/art/200611/34788.htm[/url]11月中旬,CA EXPO 2006分别在上海和北京召开。作为一直与CA保持联系的记者,51CTO老杨受邀参与北京站的会议。我是2000年开始接触和了解…...
吴忠网站设计公司/四川网站seo
备份文/爱掏蜂窝的熊(简书作者)原文链接:http://www.jianshu.com/p/0b6f5148dab8著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。序 在日常开发中,app难免会发生崩溃。简单的崩溃…...
创建网站/网页推广平台
嵌入式软件设计第7次实验报告 学号:140201234 姓名:王凯 组别:第四组 实验地点:D19 一、实验目的: 1.熟悉网线的制作(T568B标准直连线) 2.学会使用HTML语言…...
网站开发所需基础知识/深圳网站优化公司
故障现象:逻辑DG数据库日志能够应用,但是确不会将应用后的日志删除,查看日志发现已经自动设置了TURNING OFF LOG AUTO DELETECompleted: alter database register logfile /archivelog/archive_2_25797_614088933.arcTue Apr 24 11:06:10 201…...
58网站怎么样做效果会更好/千锋教育出来好找工作吗
电脑加网络硬盘步骤有哪些电脑加网络硬盘步骤有哪些?今天应届毕业生小编要给大家介绍的是电脑加网络硬盘的方法!下面是具体步骤请大家仔细观看!一、申请开通请在“用户注册”页面按要求注册。二、使用方式(网络硬盘新IP地址是:10.100.48.29)网络硬盘有以下几种访问…...