南昌行业网站建设,jsp网站 iis,个人网站转企业,网站建设 天猫 保证金文章目录 多态概念及其触发条件重写和协变#xff08;考点1#xff09;#xff08;考点2#xff09; 虚函数表及其位置#xff08;考点3#xff09; 多继承中的虚函数表 多态概念及其触发条件 多态的概念#xff1a;通俗来说#xff0c;就是多种形态。具体点就是去完成… 文章目录 多态概念及其触发条件重写和协变考点1考点2 虚函数表及其位置考点3 多继承中的虚函数表 多态概念及其触发条件 多态的概念通俗来说就是多种形态。具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态 多态的构成条件 1.必须通过基类的指针或者引用调用虚函数即被virtual修饰的类成员函数称为虚函数2.被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 重写和协变 虚函数的重写(覆盖)派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数类型)称子类的虚函数重写了基类的虚函数 虚函数重写的两个例外 1. 协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变 2. 析构函数的重写(基类与派生类析构函数的名字不同) 如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor override和final两个关键字 考点1
这里强调一下重写重写的是实现。看以下这个场景考点
class A
{
public:A(){}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()
{A* p new B();p-test();return 0;
}打印结果为B-1说明调的是子类的func函数但是缺省值用的却是父类返回值函数名参数类型相同即构成重写重写重写的是实现壳子用的是父类的写的内容自己控制 考点2
那为什么要把析构函数构成重写呢看以下这个场景考点 class Person {
public:~Person() { cout ~Person() endl; }
};
class Student : public Person {
public:~Student() {cout ~Student() endl;delete[] ptr;}
protected:int* ptr new int[10];
};int main()
{Person* p new Person;delete p;p new Student;delete p; return 0;
}当我们用父类指针指向子类对象时期望析构的是子类对象而不是父类对象。不构成重写的话无论父类指针是指向子类对象还是父类对象析构的都是父类对象导致下面的 ptr 动态开辟的空间没有释放而内存泄漏 当我们给父类析构函数加上 virtual让其构成重写后。同时注意这里析构玩~Student后还会析构继承父类照应上面的构造先父后子析构先子后父 抽象类 在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承 动态绑定与静态绑定 虚函数表及其位置 一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数表也简称虚表通过下面这个例子来看对象模型 考点3
这时候我们再反过来思考为什么一定是父类的指针或者引用而不能是父类对象 首先我们可以看出子类对象会先拷贝父类虚函数表然后再对需要重写的虚函数进行地址修改。 假如我们把子类对象赋值给父类对象那么子类对象的虚函数表要不要拷贝给父类如果虚函数表不拷贝那么还是调用父类的函数没有构成多态。 如果拷贝了那么父类对象的虚函数表存的是子类对象修改后的虚函数如下图此时我们无法再调用父类本身被重写的函数因为无论我们传子类还是父类对象调用的都是子类对象的函数不能构成多态。 因此多态的条件一定是父类的指针或者引用这样可以避免像下面这样拷贝带来的错误。 虚表位置
class Person {
public:virtual void BuyTicket() const { cout 成人-全价 endl; }
};
class Student : public Person {
public:virtual void BuyTicket() const { cout 学生-半价 endl; }
};
int main()
{Person ps;Student st;int a 0;printf(栈%p\n\n, a);static int b 0;printf(静态区%p\n\n, b);int* p new int;printf(堆%p\n\n, p);const char* str hello world;printf(常量区%p\n\n, str);printf(虚表1%p\n, *((int*)ps));printf(虚表2%p\n, *((int*)st));return 0;
}虚表存放在哪里呢首先排除堆虚表由编译器生成不会自己去动态申请空间。其次排除栈同类型对象公用一张虚表栈都是伴随栈帧走的不能函数调用结束栈帧销毁虚表就销毁了吧。我们用打印的方式来看一下虚表是存在哪里的 看下面的代码和输出结果我们可以发现虚表是存在常量区的 多继承中的虚函数表
// 打印函数指针数组
typedef void(*FUNC_PTR) ();
void PrintVFT(FUNC_PTR* table)
{for (size_t i 0; table[i] ! nullptr; i){printf([%d]:%p-, i, table[i]);FUNC_PTR f table[i];f();//这个地址可以调用说明一定是函数}printf(\n);
}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()
{Derive d;int vft1 *((int*)d);Base2* ptr d;int vft2 *((int*)ptr);printf(第一张虚表\n);PrintVFT((FUNC_PTR*)vft1);printf(第二张虚表\n);PrintVFT((FUNC_PTR*)vft2);return 0;
}先看上面这段代码首先d对象有几张虚表呢看下面的监视窗口很明显发现d对象有两张虚表但是d对象自己的虚函数func3去哪里了其实它在第一张虚表中我们可以通过上面的代码打印观察出来f()这个地址可以调用说明它一定是函数。这里是可以认为是编译器的监视窗口故意隐藏了func3函数也可以认为是它的一个小bug 可是细心一点发现两张表中的func1地址不一样它们不是都重写了func1函数吗而且用父类指针调用会发现它们调的是同一个函数那么这里为什么地址不一样呢 看下面这个场景 注意这里你要调用的是派生类d对象的func1函数this指针应该指向d对象而这里的ptr1指针恰好指向d对象不需要改动。而ptr2指向的却是Base2对象。调用d对象的func1函数要传d对象的this指针 而不是Base2对象的this指针。所以这里第二张表的地址其实是虚地址多封装了几层是为了修正this指针 接下来我们通过汇编来看看ptr1和ptr2调用的区别更好理解Base2的虚地址 ptr1调用 ptr2调用