hello,各位读者大大们你们好呀
系列专栏:【C++的学习】
本篇内容:多态的定义及实现;多态的构成条件;虚函数;虚函数的重写(覆盖);协变;destructor;C++11 override和final;重载、覆盖、隐藏的对比;抽象类;多态原理;虚函数表;动态绑定和静态绑定
⬆⬆⬆⬆上一篇: 基础IO(三)
作者简介:轩情吖,请多多指教(> •̀֊•́ ) ̖́-
多态是在不同继承关系的类对象,去调用同一函数,产生不同行为
构成多态的两个条件:
①必须通过基类的指针或引用调用虚函数
②被调用的函数必须是虚函数,且派生类必须要对基类的虚函数进行重写
#include
using namespace std;
class Person
{
public:
virtual void Print(void)//加上virtual就是虚函数
{
cout << "Person Print()" << endl;
}
};
class Student:public Person
{
public:
virtual void Print(void)
//函数名相同,按理来说构成隐藏,但这边由于是虚函数,所以说构成重写
{
cout << "Student Print()" << endl;
}
};
int main()
{
Student s;
Person& p = s;//p对s中属于Person数据的部分做了引用,因此它是指向Student类型的
p.Print();
Person p1;
Person& p2 = p1;//p2是对Perosn数据做了引用,因此它是指向Perosn类型的
p2.Print();
return 0;
}
即被virtual修饰的类成员函数
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派生类的虚函数不加virtual关键字时,也可以构成重写(因为继承后基类的虚函数被继承了下来了,在派生类依旧保持虚函数属性),况且要明白的一点是重写的是函数的实现,而不是函数的接口
可以发现,没有了virtual后,依旧可以正常运行
此时我们可以尝试一下,如果去掉基类的virtual会怎么样呢?
#include
using namespace std;
class Person
{
public:
void Print(void)
{
cout << "Person Print()" << endl;
}
};
class Student:public Person
{
public:
virtual void Print(void)
{
cout << "Student Print()" << endl;
}
};
int main()
{
Student s;
Person& p = s;
p.Print();
Person p1;
Person& p2 = p1;
p2.Print();
return 0;
}
可以看到结果都掉用了,Person中的函数,这是由于没有构成多态的条件,因此它不是多态,当p调用Print时,发现自己是Person类型,就直接调用了Person的函数,p2也是同理。
总结来说就是当不满足多态时,看调用者的类型,调用这个类型的成员函数,没有多态,就没有接口继承
#include
using namespace std;
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: 以上都不正确
这道题实际上选的是B
解析:
首先,主函数中new了一个B对象出来,此时的p是B类型的指针,此时p调用test函数,而test函数是继承自A类型的,因此,test的this指针是A类型的,所以说把p传过去后,变成了A类型的指针,指向B类型中A的那部分内容。在test函数内部,又调用了func函数,那么此时调用的它的是A类型的this指针,再加上func函数构成了重写,所以说构成了多态。this指针指向的是B类型中A的那部分内容,所以说会调用A的func,而前面也说过,重写的是函数的实现而不是接口,因此val的值是1
这个也同样验证了没有多态,就没有接口继承,简单来说就是没有构成多态,就和原来调用函数一样,没有任何区别。
①协变
基类和派生类虚函数返回值类型不同,必须是父子关系指针或引用
②析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,编译器对析构函数的名称做了特殊处理,编译器编译后,析构函数的名称统一处理成destructor
#include
using namespace std;
class Person
{
public:
virtual void Print(void)//加上virtual就是虚函数
{
cout << "Person Print()" << endl;
}
virtual ~Person()
{
cout << "~Perosn" << endl;
}
};
class Student:public Person
{
public:
virtual void Print(void)
//函数名相同,按理来说构成隐藏,但这边由于是虚函数,所以说构成重写
{
cout << "Student Print()" << endl;
}
virtual ~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
//编译器之所以要那么处理是因为下面这种情况
Person* p = new Student;
delete p;
//如若没有构成多态,p就会调用它对应类型的析构函数,
//可是这样做就会导致并不是由原来的s的析构函数析构的
//,就可能导致有内存泄露
return 0;
}
①final:修饰虚函数,表示该虚函数不能再被重写
②override:检查派生类虚函数是否重写了基类某个虚函数,如果没有,编译报错
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫作接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
#include
using namespace std;
class Person
{
public:
virtual void Print(void)=0
{
cout << "Person Print()" << endl;
}
virtual ~Person()
{
cout << "~Perosn" << endl;
}
};
class Student:public Person
{
public:
virtual void print(void)
{
cout << "Student Print()" << endl;
}
virtual ~Student()
{
cout << "~Student" << endl;
}
};
int main()
{
Person p;
Student s;
return 0;
}
普通函数的继承是一种实现继承,派生类继承了基类函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口(函数头),目的是为了重写,达成多态
大家先看这段代码中,Person的大小占几个字节?
#include
using namespace std;
class Person
{
public:
virtual void Print(void)
{
cout << "Person Print()" << endl;
}
};
class Student:public Person
{
public:
virtual void Print(void)override
{
cout << "Student Print()" << endl;
}
};
int main()
{
cout << sizeof(Person) << endl;
return 0;
}
结果为什么会是8而不是1(空类,但有1字节做占位符)呢
可以通过调试看到p1中有一个数组指针,它其实是虚函数表指针,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也被称为虚表
我们可以看到在子类类型的s中也同样含有一个虚表指针,它是继承于父类的。
派生类的虚表生成:①先将基类中的虚表内容拷贝一份到派生类虚表中②如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
其实对象中的虚表指针是在构造函数的初始化列表初始化的
当在基类当中再加入一个虚函数时,虚表当中也会出现其身影
可是当在子类中加入虚函数,并没有其身影
但是其实子类中的虚函数也会存在于虚表之中,但由于编译器的故意操作或可能是bug,导致隐藏了,可以使用打印的方法将其打印出来查看
#include
using namespace std;
class Person
{
public:
Person()
:_a(1)
{}
virtual void Print(void)
{
cout << "Person Print()" << endl;
}
virtual void Func(void)
{
cout << "Func" << endl;
}
private:
int _a;
};
class Student:public Person
{
public:
Student()
{
}
virtual void Print(void)override
{
cout << "Student Print()" << endl;
}
virtual void Func1(void)
{
cout << "Func1" << endl;
}
};
typedef void(*p)();//函数指针重命名,防止指针太过复杂,难以理解
void vPrint(p table[])
{
//虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了个nullptr
for (int i = 0; table[i]; i++)
{
printf("[%d]->%p\n", i,table[i]);
(table[i])();
cout << endl;
}
}
int main()
{
Student s;
Person p1;
//s的前8个字节就是_vfptr,因此通过long long*强转,可以得到前八个字节的地址,
//解引用后是long long类型,因此又必须转换成对应形参的类型
//可以发现虚表指针的地址也是首元素地址,因此当解引用后,可以拿到首元素
vPrint((p*)*(long long*)&s);
return 0;
}
额外的概念:
①虚表存的是虚函数指针,虚函数存在代码段,指针又存到了虚表,对象中存的是虚表指针,虚表存在代码段(VS)
②虚表是共享的
③虚表是在编译阶段生成的
多继承派生类的未重写的虚函数放在继承的第一个基类的虚函数表中
#include
using namespace std;
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; }
private:
virtual void func3() { cout << "Derive::func3" << endl; }
int d1;
};
typedef void(*p)();
void vPrint(p table[])
{
int i = 0;
for (; table[i]; i++)
{
printf("[%d]:%p->", i, table[i]);
table[i]();
}
cout << endl;
}
int main()
{
Derive d;
vPrint((p*)(*(long long*)&d));
Base2& b = d;
vPrint((p*)(*(long long*)&b));
return 0;
}
仔细的同学可以发现,在刚刚的结果中,为什么子类的func1函数覆盖了两个父类的func1函数后,地址会是不一样的,但是调用的结果却是没有问题?
我们要利用汇编来理解一下
两个变量都构成多态,此时调用func1
运行至call,并按f11,逐语句调试
可以发现,最终调用了到了func1函数,接下来我们看b2
步骤跟调试b1差不多,但是能够发现,我们的b2比b1多走了一层,才到最后的函数调用
问题就出在这里,其中它多执行了一条语句,它的意思是减去16个字节,rcx中存的是this指针,也就是this指针减去16个字节。为什么需要这样做呢?思考一下当&b1作为this指针传过去时完全没有问题,因为&b1正好指向d的第一个地址处,但是b2并不是,因此要减去b1所占的字节大小,才能匹配
在Base1中有一个int类型以及一个虚表指针,因此为12字节,再加上内存对齐,所以说是16字节。这样做的原因其实是为了修正this指针的位置。
①inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
② 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
③构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
④对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
C++多态的知识大概就讲到这里啦,博主后续会继续更新更多C++的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!