这是关于一个普通双非本科大一学生的C++的学习记录贴
在此前,我学了一点点C语言还有简单的数据结构,如果有小伙伴想和我一起学习的,可以私信我交流分享学习资料
那么开启正题
今天分享的是关于多态的知识点
1.多态的概念及定义,实现
1.1概念
通俗来说,就是多种形态,具体来说就是不同对象去完成某种行为,会产生不同的状态
比如,买票这个行为,普通人买票是全价买票,残疾人买票是半价买票,军人买票是优先买票
1.2定义及实现
1.2.1多态的构成条件
多态是不同继承关系的类对象,去调用同一函数
多态条件:
1.必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(覆盖)
1.2.2虚函数
被virtual修饰的类成员函数称之为虚函数
class Person
{
public:virtual void Buy(){cout << "Person::买票-全价" << endl;}
};
1.2.3虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同(返回类型,函数名字,参数列表)的虚函数,称派生类的虚函数重写了基类的虚函数
class Person
{
public:virtual void Buy(){cout << "Person::买票-全价" << endl;}
};class Student : public Person
{
public:virtual void Buy(){cout << "Student::买票-半价" << endl;}
};void Func(Person& p)
{p.Buy();
}int main()
{Person p;Student s;Func(p);Func(s);return 0;
}
虚函数重写的两个例外
1.协变
基类与派生类返回类型不一样,基类虚函数返回基类对象的指针或引用,派生类对象虚函数返回派生类对象的指针或引用
2.析构函数的重写
我们来看下面的代码
class A
{
public://virtual ~A()~A(){cout << "~A()" << endl;}
};class B : public A
{
public://virtual ~B()~B(){cout << "~B()" << endl;}
};void Test1()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;
}
当我们不将析构函数写为虚函数时,就可能导致B(派生类)析构函数未调用,可能导致内存泄漏,写成虚函数后可以正常使用
但是明明他们的函数名不同,为什么能重写呢,我们来看下面
编译器其实对析构函数名称做了特殊处理,编译后统一处理成destructor,这样就符合了多态的条件
2.C++11里的override和final关键字
经过上面的学习我们可以发现,C++对函数重写的要求很严格,而在实际运用当中,我们有时候会出现一些低级小错误,比如拼写错误,而这种错误是无法检查出来的,故有了以下关键字的出现
1.override:检查派生类虚函数是否重写了某个函数,如果没有进行重写编译报错
class A
{
public:virtual void Print(){cout << "~A()" << endl;}
};class B : public A
{
public:virtual void Pirnt() override{cout << "~B()" << endl;}
};
2.final:修饰虚函数,表示虚函数不能被重写(在类后面加上表示类不能被继承)
class A
{
public:virtual void Print() final{cout << "~A()" << endl;}
};class B : public A
{
public:virtual void Print(){cout << "~B()" << endl;}
};
3.重载,重写(覆盖),重定义(隐藏)的对比
重载:两个函数在同一作用域,函数名相同,参数列表不同
重写:两个函数分别在基类和派生类,函数名 / 参数列表 / 返回值 相同(协变除外),两个函数必须是虚函数
重定义:两个函数分别在基类和派生类,函数名相同,不构成重写
4.抽象类
在虚函数后面加上 = 0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,派生类继承后也无法实例化出对象,只有重写虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,另外也体现出了接口继承
{
public:virtual void Print() = 0;
};class B : public A
{
public:void Print(){cout << " hehe" << endl;}
};void Test()
{// A a;B b;
}
上面提到了接口继承,与此对应的还有实现继承,普通函数是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生列继承的是基类虚函数的接口,目得是为了重写,达成多态,所以如果不是实现多态,不要把函数定义为虚函数
5.多态的原理
5.1虚函数表
class A
{
public:virtual void Print(){}int _a;
};void Test()
{cout << sizeof(A) << endl;
}
(32位环境下)运行以上代码可以发现输出结果不是4而是8,我们再通过调试监视窗口又可以发现除了_a成员,还有一个_vfptr放在对象的前面(vs环境下),对象的这个指针叫做虚函数表指针,每一个含有虚函数的类中都至少有一个虚函数表指针,虚函数的地址存在虚函数中
class A
{
public:virtual void Print(){cout << "A" << endl;}int _a;
};class B : public A
{
public:virtual void Print(){cout << "B" << endl;}int _b;
};
通过以上代码和测试我们可以发现
1.派生类对象也有一个虚表指针,但是和基类的不相同,它里面的虚函数如果完成了重写将会存派生类的虚函数,因此虚函数的重写也叫做覆盖
2.虚函数表本质是一个指针数组,一般情况下,这个数组最后放了一个nullptr
3.虚表存的是虚函数指针,不是虚函数,虚函数存在于代码段(常量区)
5.2多态的原理
不同对象调用函数引发多态的原因就是本质就是调用了不同的虚函数,函数调用时,通过_vfptr找到该对象的虚表,再到虚表中找到该调用的虚函数
可以看出,多态的函数调用,不是在编译时确定的,而是在运行后到对象中找到的,不满足多态的普通函数是在编译时就确定好的
这里注意,内联函数因为没有函数地址,所以是无法构成多态的
5.3动态绑定和静态绑定
1.静态绑定又叫做前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2.动态绑定又叫做后期绑定(晚绑定),在程序运行期间,具体拿到的确定程序的具体行为,调用具体函数,也称为动态多态
新手写博客,有不对的位置希望大佬们能够指出,也谢谢大家能看到这里,让我们一起学习进步吧!