形成多态的俩个条件:
1、虚函数的重写
2、父类对象的指针或者引用去调用虚函数
满足多态:跟指向对象有关,指向哪个对象就是它的虚函数
不满足多态:跟指向的对象类型有关,类型是什么调用的就是谁的虚函数
对于virtual关键字
virtual:
可以修饰原函数,完成虚函数的重写,满足多态的条件之一
可以在菱形继承中,去完成虚继承,解决数据冗余和二义性
两个地方都使用了virtual关键字,但是一点关系都没有
构成多态例子
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()
{
Student s;
Person p;
Func(s);
Func(p);
}
这是没构成多态((没有构成虚函数因为基类和派生类的同名函数不构成重写,参数不相同)
面试中可能面对的问题函数重载(不是操作符重载)、重定义(隐藏)、重写:C++ 多态与虚函数(重载、重写与重定义) - 知乎 (zhihu.com)
注意:这里子类不写virtual也构成重写(但是基类不可以),只是不规范,不推荐
协变 (了解)
构成重写virtual+三同(函数名相同,返回值相同,参数相同)
构成多态就要在重写的基础上加上基类指针或引用去调用
派生类重写基类虚函数时,与基类虚函数的返回值类型不同。即基类虚函数返回基类指针或者时基类的引用,派生类虚函数返回派生类对象的指针或引用时称之为协变
这种也构成协变(调用别的基类和派生类)
virtual析构函数
class Person
{
public:
virtual ~Person()
{
cout << "~person" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~student" << endl;
}
};
int main()
{
Person* p1 = new Person;
delete p1;
Person* p2 = new Student;//构成多态与类型无关和类型的对象指向那个就调用那个的virtual析构
delete p2;
return 0;
}
调试结果
如果不构成多态就会造成内存泄露问题
这里不构成多态构成了隐藏,因为编译器对析构函数进行了特殊处理统一的处理成destructor
那不构成多态:和类型有关,指向的对象的类型是什么就调用什么的析构函数。所以只调用了基类的析构函数但是剩下的student自己本身的类型并没有清理所以会造成内存泄漏
有一道面试题
(运行结果为多少)
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B :public A
{
public:
void func(int val = 0) { cout << "B->" << val << endl; };
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
结果为
那是如何运行的呢?
这里我已经将详细的步骤写下来了
虚函数的重写:可以理解是继承了接口,重写了实现
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()//3、这里不构成重写就是调用实际上有一个隐藏指针this,test(A*this)
{ //A*this = B* p
func();//4、this->func();this是基类指针所以构成了·多态
}
};
class B :public A
{
public:
void func(int val = 0) { cout << "B->" << val << endl; };//5、调用这里的虚函数但是这里很多人都会错以为是B->0
}; //但是不是,因为继承会把基类的函数接口给派生类就会把基类的函数接口
//virtual void func(int val = 1)继承,至于函数后边的内容才叫重写,前边的叫继承,所以才是B->1
int main(int argc, char* argv[])
{
B* p = new B;//1、创建了一个B 类型的对象
p->test();//2、p->test() == test(p)
return 0;
}
这里要注意:
第五步是因为第四步的this指针是基类指针调用的这里,虚函数构成了重写,这里this指针(就是调用的func指针)是动态绑定的(多态),即需要调用哪个作用域的函数需要看程序运行到那个时刻它指向的是什么,但是函数的参数却是静态绑定的,它不会因为A*this=&B b指向的类型的改变而改变,换句话说,我们定义this的时候指定它的类型是A*,是基类指针,所以函数的默认参数绑定的是基类的默认参数值,不会因为它指向的是派生类而改变。
C++11 final关键字(不允许继承)
c++98的做法是将基类的构造函数定义成private,派生类就不可以继承,因为派生类要先显示的调用基类构造函数
c++11的做法:final关键字的作用在继承中使用是不允许继承
override关键字(检查是否重写)
class car
{
public:
virtual void Drive() override{};//这里我们想让他构成重载但是因为函数名不相同不构成重写但是没有报错所以就要用到override
}; //检查重写有没有错误
class benc:public car
{
public:
virtual void Dirve() { cout << "benz" << endl; }
};
void func(car*a)
{
a->Drive();
}
int main()
{
car A;
benc b;
func(&b);
return 0;
}
重定义重载重写
抽象类
//纯虚函数是作用强制子类完成重写
//表示抽象类。因为现实中没有对应的实体
接口继承和实现继承
普通函数的继承是实现继承派生类继承了基类函数,可以使用函数,继承的是函数的实现,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口,所以如果不实现多态,不要把函数定义虚函数
内功修炼下篇:
多态的原理
我们先来看一下这代码
计算一下这是多少?
class A
{
public:
void func(int i)
{
cout << "zzy" << endl;
}
public:
int _a = 1;
};
int main()
{
cout << sizeof(A) << endl;
}
对我相信很多人可以算出是4,因为函数是存放在代码段的,不参与计算
可以复习一下堆区,栈区,数据段,bss段,代码区
C++ 堆区,栈区,数据段,bss段,代码区(详解)_bss区-CSDN博客
代码段(text):就是C程序编译后的机器指令,也就是我们常见的汇编代码。
数据段(data):用来存放显式初始化的全局变量或者静态(全局)变量,常量数据。
BSS段(Block Started by Symbol): 存储未初始化的全局变量或者静态(全局)变量。编译器给处理成0;
栈段(stack):存放函数调用相关的参数、局部变量的值,以及在任务切换的上下文信息。栈区是由操作系统分配和管理的区域。
堆段(heap): 动态内存分配的区域,也就是malloc申请的内存区,使用free()函数来释放内存,堆的申请释放工作由程序员控制,容易产生内存泄漏。
那个是多少呢?(面试题)
注意内存对齐
class A
{
public:
virtual void func(int i)
{
cout << "zzy" << endl;
}
public:
int _a = 1;
};
int main()
{
cout << sizeof(A) << endl;
}
答案是8或者16,因为虚函数的实现会使得里面多一个指针,指针在32位是4个字节在64位为8个字节所以当是32位,从offset(0)开始符合内存对齐原则,4+4=8,因为是class或者是struct还要满足最大成员字节的整数倍所以4的整数是8,当是64位时4+8=12,要和上面一样要满足对齐原则,所以最大是8,最接近8的整数倍为16,所以要提升到16
可以看一下这个,我也是忘记然后在看的:
【程序员老秦】谈一谈C++的内存对齐_哔哩哔哩_bilibili
进入调试模式
我们发现真的有一个指针vfptr(virtual function pointer)
其实这个函数更加精确叫虚函数表指针
来看看vfptr是什么?
vfptr是叫虚函数表指针
其实是一个指针数组(装有虚函数指针的数组)·
这里func1和func2对应的是【0】和【1】所以每创建一个虚函数就会增加一个
关系模型
多态的条件:
1、虚函数的重写
2、父类的指针或者引用去调用虚函数
为什要用父类的指针或者是引用呢?
因为父类的指针或引用可以存储子类的指针和引用,指向的是派生类的父类那一块
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
virtual void func1()
{
cout << "买票咯" << endl;
}
virtual void func2()
{
}
int _p = 1;
};
class Student :public Person
{
public:
virtual void BuyTicket() { cout << "买票——半价" << endl; }
int _s = 2;
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
}
可以看到基类p的vfptr是一个虚函数指针数组,存放了【0】BuyTicKert【1】func1【2】func2
注意:这里会将虚函数重写的函数换成子类的函数(BuyTicket()重写父类和子类都有但是子类的vfptr中的函数指针数组会存放子类的虚函数重写的地址)
所以父类的和子类的虚函数重写都是存放的各自的函数地址,根据不同的类去调用不同的虚函数
注意:虚函数的重写是为了继承接口,重写实现
多态是运行时到指向的对象的虚表中查找要调用的虚函数的地址来进行调用
形成多态时(不用看懂)
发现运行时到指向的对象的虚表中查找要调用的虚函数的地址来进行调用
未构成多态时在编译的时,就根据类型调用了函数的地址,因为确定了类型调用那个函数,函数都是通过编译形成指令存放在代码段的
但是虚函数地址会单独提取出份来存放在vfptr指针数组中,派生类会将基类的重写的虚函数覆盖
虚函数表(存放虚函数表的地址)存在数据段(rdata)上的证明
动态绑定和静态绑定
编译时:就是检查语法,生成指令,生成可执行文件.exe
运行时:父进程创建一个子进程通过exe将数据段和代码段(还有其他的区域)替换掉
查看汇编
直接就可以知道函数地址
但是多态的调用还需要去寻找地址(不需要看懂,只要知道这些东西在虚函数表中寻找虚函数的地址)