多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子,比如买火车票,成人全价票,学生半价票,军人优先票。
再比如支付宝,为了争夺在线支付市场,支付宝经常做二维码领取红包活动,有的人可以扫出来8块、有的人可以扫出来10块,而你只能扫出来几毛钱或者几分钱,其实这也是一种多态行为,支付宝首先会分析你的支付宝数据,再看你是否经常使用支付宝,或者再看看你是不是新用户,再通过数据分析给你相对应金额,比如你是新用户那么获取金额就是rand()%99,如果你经常使用支付宝或者没钱就是rand()%1。
多态的定义以及实现
class Person
{
public:
virtual void BuyTicket()
{
cout << "购票价格->全价" << endl;
}
};
class Student:public Person
{
public:
virtual void BuyTicket()
{
cout << "购票价格->半价" << endl;
}
};
void func(Person& s)
{
s.BuyTicket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
return 0;
}
函数是怎么去调用符合不同条件的函数的呢?
构成多态的条件
条件1:虚函数的重写 ——父子类中两个虚函数,三同(函数名、参数、返回)
条件2:父类指针或引用去调用虚函数
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
如果父类加了virtual 子类不写virtual也同样构成虚函数的重写,因为子类继承父类后,虚函数属性也被继承了下来,子类的函数不加virtual同样保持虚函数属性。但是不推荐这样写——不规范,如果要写虚函数的重写,一定要父类和子类都写上virtual。
虚函数重写的两个例外
1.协变
协变:父类和子类的虚函数返回值可以不同,但必须是父子类的指针或者引用。
class A
{
};
class B : public A
{
};
class Person
{
public:
virtual A* f()
{
cout << "A::f()" << endl;
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
cout << "B::f()" << endl;
return new B;
}
};
int main()
{
Person *p = new Student;
p->f();
Person* p1 = new Person;
p1->f();
return 0;
}
2.析构函数的重写(基类与派生类析构函数的名字不同)
无论是析构还是什么函数,只要在父类加上virtual了,都构成多态。
class Person
{
public:
virtual ~Person()
{
cout << "~Person() " << endl;
}
virtual void BuyTicket()
{
cout << "购票价——全价" << endl;
}
};
class Student:public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
virtual void BuyTicket()
{
cout << "购票价——半价" << endl;
}
};
int main()
{
Person p;
Student st;
return 0;
}
正常先析构子再析构父。
如果父类不加virtual
class Person
{
public:
~Person()
{
cout << "~Person() " << endl;
}
virtual void BuyTicket()
{
cout << "购票价——全价" << endl;
}
};
class Student:public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
virtual void BuyTicket()
{
cout << "购票价——半价" << endl;
}
};
int main()
{
Person* p1 = new Person;
delete p1;
return 0;
}
在前面继承中学过,父类和子类的析构后面会被统一认为是destruction(),这里形成隐藏,父类析构被隐藏,为了解决这个问题,前面加个virtual形成虚函数。
class Person
{
public:
~Person()
{
cout << "~Person() " << endl;
}
virtual void BuyTicket()
{
cout << "购票价——全价" << endl;
}
};
class Student:public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
virtual void BuyTicket()
{
cout << "购票价——半价" << endl;
}
};
int main()
{
Person* p1 = new Person;
delete p1;//p1->destruction() operator delete(p1)
Person* p2 = new Student;
delete p2;
return 0;
}
这里父类析构了2次,而子类没有析构,如果子类析构存在指针的释放,没有调用子类析构,就会形成内存的泄漏问题。(用父类指针指向子类对象就存在这个问题,用子类指针指向子类对象则不存在这个问题)
普通调用 看指针或者引用的类型or对象的类型
多态调用 看指针或者引用指向的对象
只有在父类析构函数加上virtual才能保证形成多态,只有子类student的析构函数重写了父类Person的析构函数,下面delete对象调用析构函数时,才能形成多态,才能保证p1和p2指向的对象正确的调用析构函数。
class Person
{
public:
virtual ~Person()
{
cout << "~Person() " << endl;
}
virtual void BuyTicket()
{
cout << "购票价——全价" << endl;
}
};
class Student:public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
virtual void BuyTicket()
{
cout << "购票价——半价" << endl;
}
};
int main()
{
Person* p1 = new Person;
delete p1;//p1->destruction() operator delete(p1)
Person* p2 = new Student;
delete p2;
Student*p3 = new Student;
delete p3;
return 0;
}
class Person
{
public:
virtual ~Person()
{
cout << "~Person() " << endl;
}
virtual void BuyTicket()
{
cout << "购票价——全价" << endl;
}
};
class Student:public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
virtual void BuyTicket()
{
cout << "购票价——半价" << endl;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
p1->BuyTicket();
p2->BuyTicket();
delete p1;
delete p2;
return 0;
}
普通调用
其实他们只要不构成多态 也有隐藏关系 想调用父类作用域函数就用类名+域作用限定符
构成多态 就有重写关系 看调用的对象的类型 去调用该对象类型的函数
重载、覆盖(重写)、隐藏(重定义)的对比
重载:
1.两个函数在同一作用域
2.函数名相同和参数不同
重写(覆盖):
1.两个函数分别在不同作用域(基类和派生类)
2.函数名 返回值 参数必须相同(协变除外)
3.两个函数必须是虚函数
隐藏(重定义)
1.两个函数分别在不同作用域(基类和派生类)
2.函数名相同
3.在基类和派生类的同名函数不构成重写就是重定义(隐藏)
多态面试题
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: 以上都不正确
这道题你觉得应该选什么呢?
首先我们来分析一下,子类指针new了一个B类对象,p再去找test(),首先去子类寻找test(),找不到就去到父类找到了test(),而func()是虚函数重写,this->func(),this肯定是A*,又是多态调用,去子类func()函数,所以答案是B->0,实则不然,多态调用,虚函数重写 重写就是实现重写,因为子类和父类的func()函数构成虚函数,子类是继承父类的,这里面的虚函数调用,相当于把父类的虚函数声明(父类虚函数声明参数是1)和子类的虚函数实现一起合并使用了,所以是B->1。
如何实现一个类,不能被继承呢?
1.实现父类构造私有化
class A
{
public:
protected:
int _a;
private:
A()
{
}
};
class B :public A
{
};
int main()
{
B b;
return 0;
}
final和override
2.加个final
C++ finla,被final修饰的类为最终类,不能被继承。
class A final
{
public:
protected:
int _a;
};
class B :public A
{
public:
};
int main()
{
B b;
return 0;
}
final还有一个作用,就是虚函数不能再被重写
class Car
{
public:
virtual void Drive() final
{
}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
return 0;
}
override检查虚函数是否构成重写
class Car
{
public:
virtual void Drive()
{
}
};
class Benz :public Car
{
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
return 0;
}
如果没有构成重写就报编译错误
class Car
{
public:
void Drive()
{
}
};
class Benz :public Car
{
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
return 0;
}
抽象类
概念:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class car
{
public:
纯虚函数
virtual void Drive() = 0;
};
class Benz :public car
{
public:
virtual void Drive()
{
cout << "Benz->舒适" << endl;
}
};
class BMZ :public car
{
public:
virtual void Drive()
{
cout << "BMZ->操控" << endl;
}
};
int main()
{
car c;
return 0;
}
car c是抽象类(车是一个抽象概念 不需要实例化出对象,只有品牌才会去实例化出对象,所以car实例不化对象是合理的),不能实例化出对象,其实抽象类间接强制了子类必须重写虚函数,否则子类也实例化不出对象。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
虚函数可以是接口继承也可以是一种实现继承(看调用方式,如果是多态调用就是接口继承,普通调用使用域作用限定符也可以去访问,但主流还是一种接口继承)。
多态原理
class Base
{
public:
virtual void f()
{
cout << "virtual f() " << endl;
}
private:
int _b;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
这里大小为什么是8呢?
通过调试窗口我们可以看到b里面除了私有成员变量_b还有个指针——_vfptr
这个指针放在了对象前面(有些平台会放在对象后面,跟平台有关)
这个指针叫虚函数表指针 简称虚表指针_vfptr v-virtual f-function
一个含有虚函数的类中都至少都有一个虚函数表指针
因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
我们来添加个子类继承函数 来深入研究下原理
class Base
{
public:
virtual void f1()
{
cout << " Base:: f1() " << endl;
}
virtual void f2()
{
cout << " Base:: f2() " << endl;
}
void f3()
{
cout << "Base:: f3() " << endl;
}
private:
int _b = 12;
};
class fk :public Base
{
public:
virtual void f1()
{
cout << "fk:: f1() " << endl;
}
private:
int _fk = 3;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
fk k;
cout << sizeof(k) << endl;
return 0;
}
父类有一个私有成员变量+一个虚表指针 = 8
子类继承了父类的一个私有成员+一个独有成员+一个虚表指针 = 12。
如果有多个虚函数 会把虚函数放到表里 其实是函数指针的数组->虚函数表
我们通过调试窗口可以看见,子类的虚函数重写已经把父类的虚函数重写覆盖了。
虚函数的重写也叫覆盖。 重写是语法层的概念 覆盖是原理层的概念
派生类对象k中也有一个虚表指针,k对象有两部分组成,一个是从父类继承下来的成员,另一个部分是自己的成员。
派生类的虚表f1函数完成了虚函数重写,f2也放进了虚表中,因为是继承下来的,f3不会放进虚表中,因为f3不是虚函数。
class Base
{
public:
virtual void f1()
{
cout << " Base:: f1() " << endl;
}
virtual void f2()
{
cout << " Base:: f2() " << endl;
}
void f3()
{
cout << "Base:: f3() " << endl;
}
private:
int _b = 12;
};
class fk :public Base
{
public:
virtual void f1()
{
cout << "fk:: f1() " << endl;
}
private:
int _fk = 3;
};
void ff(Base* bb)
{
bb->f1();
}
int main()
{
Base b;
cout << sizeof(b) << endl;
fk k;
cout << sizeof(k) << endl;
ff(&b);
ff(&k);
return 0;
}
多态调用,在运行时去虚函数表找函数地址(因为在编译时 已经把父类虚函数表和子类虚函数表各自列出来了),在进行调用,所以指向父类调用的是父类的虚函数,指向子类调用的是子类的虚函数。
普通调用:编译时通过调用者类型,来确定函数的地址。
virtual调用的函数只有一份 只是调用方式不同。
在类和对象存储方式一般是存储成员变量,然后把成员函数放在公共代码段区域(普通调用)。
而且函数定义和编译好以后是一串指令 函数的第一句指令一般是jmp指令
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* p = new Benz;
p->Drive();
p = new BMW;
p->Drive();
return 0;
}
jmp以后才会进入函数 开始建立栈帧以及内部实现
vs里面函数的地址一般是jmp指令的地址
那么虚函数和普通函数都存储在什么地方?都在代码段。
因为函数在编译后就会变成一堆指令 都会存在代码段。
只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
虚表中存在的虚函数的地址
满足多态调用的时候,运行时指向的对象中找到对应的虚函数
p指向benz,虚表中找到的benz重写虚函数
调用p指向bmw,虚表中找到的bmw重写虚函数,进行调用
普通调用把成员变量存储在代码段,成员函数在公共代码区域段
多态调用把成员变量和虚表指针存储在代码段(类函数表地址),再通过虚表指针去虚表里面找虚函数的地址调用不同虚函数。
静态绑定和动态绑定
1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态
比如:函数重载
表面是自动识别类型,实质上是函数重载
io流重载了各种内置类型数据的函数重载
本质是在编译期间通过c++的函数名修饰规则再去调用
2.动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具行为,调用具体的函数,也称为动态多态。
单继承和多继承关系的虚函数表
需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类 的虚表模型前面我们已经看过了,没什么需要特别研究的
单继承中的虚函数表
class Base
{
public:
virtual void f1()
{
cout << " Base:: f1() " << endl;
}
virtual void f2()
{
cout << " Base::f2() " << endl;
}
void f3()
{
cout << "Base::f3() " << endl;
}
private:
int _b = 12;
char _ch = 'a';
};
class fk :public Base
{
public:
virtual void f1()
{
cout << "fk::f1() " << endl;
}
private:
int _fk = 3;
};
int main()
{
Base b;
fk f;
return 0;
}
在fk子类中添加一个虚函数,会不会提现在虚函数表中呢?
class Base
{
public:
virtual void f1()
{
cout << " Base:: f1() " << endl;
}
virtual void f2()
{
cout << " Base::f2() " << endl;
}
void f3()
{
cout << "Base::f3() " << endl;
}
private:
int _b = 12;
char _ch = 'a';
};
class fk :public Base
{
public:
virtual void f1()
{
cout << "fk::f1() " << endl;
}
virtual void f3()
{
cout << "fk::f3() " << endl;
}
private:
int _fk = 3;
};
int main()
{
Base b;
fk f;
return 0;
}
在监视窗口看不见 但在内存地址中可以看到第第二个虚函数地址后面不再是00 00 00 00 而是有了一串地址 0x00e9146a百分之90概率就是f3虚函数的地址
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
但在linux g++情况下后面并没有放空指针。
虚函数表是存在哪个区域的?
堆?栈?静态区?常量区?
class Base
{
public:
virtual void f1()
{
cout << " Base:: f1() " << endl;
}
virtual void f2()
{
cout << " Base::f2() " << endl;
}
void f3()
{
cout << "Base::f3() " << endl;
}
private:
int _b = 12;
char _ch = 'a';
};
class fk :public Base
{
public:
virtual void f1()
{
cout << "fk::f1() " << endl;
}
virtual void f3()
{
cout << "fk::f3() " << endl;
}
private:
int _fk = 3;
};
int main()
{
Base bb;
fk k;
int a = 1;
static int b = 4;
int* p = new int;
const char* str = "pppp";
printf("栈区:%p\n", &a);
printf("堆区:%p\n", p);
printf("静态区:%p\n", &b);
printf("常量区:%p\n", str);
Base* p1 = &bb;
fk* p2 = &k;
printf("Base:%p\n", *(int*)p1);
printf("fk:%p\n", *(int*)p2);
return 0;
}
因为虚函数表地址存在对象中前四个字节,而指针之间可以互相强转 因此先强转然后再解引用最后类型转换即可获得对象中前四个字节的地址,通过结果可以观察到虚函数表的地址更接近于常量区,因此虚函数表存储在常量区。
如果一个类有多个实例化对象,都是指向同一个虚函数表。
虚函数表是在编译时就已经形成了,而虚表指针是在类构造函数才形成
在构造函数初始化列表后再形成虚表指针
class Base
{
public:
Base()
:_b(2)
{
cout << "Base()" << endl;
}
virtual void f1()
{
cout << " Base:: f1() " << endl;
}
virtual void f2()
{
cout << " Base::f2() " << endl;
}
void f3()
{
cout << "Base::f3() " << endl;
}
private:
int _b = 12;
};
class fk :public Base
{
public:
virtual void f1()
{
cout << "fk::f1() " << endl;
}
virtual void f3()
{
cout << "fk::f3() " << endl;
}
private:
int _fk = 3;
};
typedef void(*VF_PTR)();
打印虚表 本质是打印虚函数指针数组
void Prifvtf(VF_PTR* vtf)
{
for (size_t i = 0; vtf[i] != nullptr; i++)
{
printf("[%d]:%p\n", i, vtf[i]);
}
}
int main()
{
fk k;
Prifvtf((VF_PTR*)(*((int*)&k)));
return 0;
}
可以看见是有3个虚函数的(包括f3)。
多继承中的虚函数表
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;
};
int main()
{
//20
cout << sizeof(Derive) << endl;
return 0;
}
*p1和*p2地址相等吗?虽然都是b的切割赋值
不相同 因为存在偏移量
里面到底是fun1 func2都存 还是只存一个func1 还是只存func2
可以从上图结果看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
虚表指针存的是虚函数地址 虚基表存储的偏移量