北京环保网站建设,网站建设公众号开,wordpress 预订,橱柜网站模板文章目录多态虚函数重写重定义#xff08;参数不同#xff09;协变#xff08;返回值不同#xff09;析构函数重写#xff08;函数名不同#xff09;final和override重载、重写、重定义抽象类多态的原理虚函数常见问题解析虚函数表多态
一种事物#xff0c;多种形态。换…
文章目录多态虚函数重写重定义参数不同协变返回值不同析构函数重写函数名不同final和override重载、重写、重定义抽象类多态的原理虚函数常见问题解析虚函数表多态
一种事物多种形态。换言之对于同一个行为不同的对象去完成就会产生不同的结果。
多态的构成条件
多态是继承体系中的一个行为如果要在继承体系中构成多态需要满足两个条件 必须通过基类的指针or引用调用虚函数 被调用的函数必须是虚函数并且派生类必须要对继承的基类的虚函数进行重写 虚函数
虚函数就是被 virtual 修饰的类成员函数 (这里的 virtual 和虚继承的 virtual 虽然是同一个关键字但是作用不一样)。
任何构造函数之外的非静态函数都可以是虚函数。关键字 virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数则该函数在派生类中隐式地也是虚函数。 重写
一般情况下当派生类中有一个和基类完全相同的虚函数函数名、返回值、参数完全相同则说明子类的虚函数重写了基类的虚函数。
class Human
{
public:virtual void print(){cout i am a human endl;}
};class Student : public Human
{
public:virtual void print(){cout i am a student endl;}
};void ShowIdentity(Human human) // 形参是基类引用构成多态
{human.print(); // 被调用的函数是虚函数
}int main()
{Human h;Student s;ShowIdentity(h); ShowIdentity(s);
}通常如果不满足函数名、返回值、参数完全相同则不构成重写即无法实现多态。但也有例外 重定义参数不同
参数不同则会变成重定义
class Base{
public:virtual void Show(int n 10)const{ // 提供缺省参数值std::cout Base: n std::endl;}
};class Base1 : public Base{
public:virtual void Show(int n 20)const{ // 重新定义继承而来的缺省参数值std::cout Base1: n std::endl;}
};int main(){Base* p1 new Base1; p1-Show(); return 0;
}如果子类重写了缺省值此时的子类的缺省值是无效的使用的还是父类的缺省值。
因为虚函数是动态绑定而缺省值是静态绑定。
对于 p1他的静态类型即指针的类型——Base所以这里的缺省值是 Base 的缺省值。而动态类型也就是指向的对象是 Base1所以这里调用的虚函数则是 Base1 中的虚函数。调用了 Base1 中的虚函数Base 中的缺省值因此输出 Base1:10 。
或者可以更简单的一句话描述虚函数的重写只重写函数实现不重写缺省值。 动态绑定和静态绑定 对象的静态类型对象在声明时采用的类型。是在编译期确定的。比如上面的 p1Base 是静态类型指向的对象的类型 Base1 是动态类型对象的动态类型目前所指对象的类型。是在运行期决定的。
对象的动态类型可以更改但是静态类型无法更改。
静态绑定绑定的是对象的静态类型发生在编译期。动态绑定绑定的是对象的动态类型发生在运行期。 协变返回值不同
当基类和派生类的返回值类型不同时如果基类对象返回基类的 引用or指针派生类对象返回的是派生类的 引用or指针 也能实现多态。这样实现多态的方式叫协变。
class Human
{
public:virtual Human print(){cout i am a human endl;return *this;}
};class Student : public Human
{
public:virtual Student print(){cout i am a student endl;return *this;}
};但如果返回类型不是对应类的 指针or引用则不足以构成协变 析构函数重写函数名不同
在特定条件下函数名不同也能实现多态最好的例子是析构函数编译器为了让析构函数实现多态会将它的名字处理成destructor 也就是说实际上析构函数的函数名也是“相同的”其多态实现遵循重写的规定。
class Human
{
public:~Human(){cout ~Human() endl;}
};class Student : public Human
{
public:~Student(){cout ~Student() endl;}
};可以看到如果不构成多态那么指针是什么类型的就会调用什么类型的析构函数。那么如果派生类的析构函数中有资源释放的操作而这里却没有释放掉那些资源就会导致内存泄漏的问题。
所以为了防止这种情况必须要将析构函数定义为虚函数。这也就是编译器将析构函数重命名为 destructor 的原因 final和override
final 和 override 是 C11 中提供给用户用来检测是否进行重写的两个关键字。 final 使用 final 修饰的基类虚函数不能被重写。
如果虚函数不想被派生类重写就可以用 final 来修饰这个虚函数 override override 关键字是用来检测派生类虚函数是否构成重写的关键字。C11 允许派生类显式地注明它覆盖了继承基类的虚函数。
在我们写代码的时候难免会出现些小错误如 基类虚函数没有 virtual 或者 派生类虚函数名拼错 等问题这些问题不会被编译器检查出来发生错误时也很难一下子锁定所以 C 增添了 override 这一层保险当修饰的虚函数不构成重写时就会编译错误。
具体做法是在
形参列表后面或者 const 成员函数的 const 关键字后面或者 引用成员函数的引用限定符后面
加一个关键字 override。
下例中基类虚函数没有 virtual 因此会报错 重载、重写、重定义 重载 在同一作用域函数名相同参数的类型、顺序、数量不同。重载不一定要求返回值相同参数相同、返回值不同不构成重载参数、返回值都不同则构成重载。会发现返回值不同是否构成重载还是看参数相同与否…… 重写覆盖 作用域不同一个在基类一个在派生类。函数名参数返回值必须相同协变和析构函数除外基类和派生类必须都是虚函数派生类可以不加 virtual基类的虚函数属性可以继承但是最好要加上 virtual
考虑这样一个问题下面有几个虚函数
正确答案是 3 个A 中的 fun1B 中的 fun1、fun2。原因就如第三点所说基类的虚函数属性可以继承 但是如果有 C类 继承了 B类 且也有一个 没有virtual关键字的 void fun1(); 函数 该函数并不是虚函数因为 B类 的 fun1 并没有显式声明 virtual 属性。
而形如 fun2 这样的子类是虚函数而父类没有 virtual 属性的父类的 fun2 不是虚函数虚函数不具备对称性。 重定义隐藏 作用域不同一个在基类一个在派生类函数名相同派生类和基类同名函数如果不构成重写那就是重定义
重定义无法覆盖虚函数只能覆盖普通函数但是父类被覆盖的普通函数可以通过作用域运算符调用
class A
{
public:virtual void f2(){cout A.f2() endl;}
};
class B :public A {
public:void f2(int){cout B.f2(int) endl;}
};
class C:public B{
public:// C类中的两个f2函数互相构成重载但又分别构成重定义和重写void f2() { // 重写了A类中的虚函数f2()cout C.f2() endl;}void f2(int) { // 重定义了B类中的普通函数f2(int)cout C.f2(int) endl;}
};抽象类
如果在虚函数的后面加上 0并且不进行实现这样的虚函数就叫做纯虚函数。而包含纯虚函数的类也叫做抽象基类或者接口类。抽象类不能实例化出对象因为他所具有的信息不足以描述一个对象派生类继承后也只有在重写纯虚函数后才能实例化出对象。
我们也可以对纯虚函数提供定义不过函数体必须在类的外部。
抽象类就像是一个蓝图为派生类描述好一个大概的架构派生类必须实现完这些架构至于要在这些架构上面做些什么增加什么就属于派生类自己的问题。
class Human
{
public:virtual void print() 0;
};class Student : public Human
{
public:virtual void print(){cout i am a student endl;}
};class Teacher : public Human
{
public:virtual void print(){cout i am a teacher endl;}
};void ShowIdentity(Human human)
{human.print();
}普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态所以如果不实现多态不要把函数定义成虚函数。 多态的原理
虚函数
class Human
{
public:virtual void print(){cout i am a human endl;}virtual void test1(){cout 1test1 endl;}void test2(){cout 1test1 endl;}int _age;
};class Student : public Human
{
public:virtual void print() {cout i am a student endl;}void test2(){cout 2test2 endl;}int _stuNum;
};我们创建一个 Human 类对象 h观察它的大小按理来说应该输出 4因为它只有一个 int类型 的数据成员但是结果却是 8。 可以看到奇怪的是除了 _age 之外还有个 void**void*类型的指针注意不是数组 类型的 _vfptr 这个 _vfptr 也被称为虚函数表指针其指向了一个函数指针数组这个函数指针数组也就是虚函数表其中的每一个成员指针指向的都是虚函数而不是虚函数的 test2 则没有被放入表中。 此时再创建一个 Student 类的对象 s进一步观察 我们可以看到如果派生类实现了某个虚函数的重写那么在派生类的虚函数表中重写的虚函数就会覆盖掉原有的函数如Student::print。而没有完成重写的 test1 则依旧保留着从基类继承下来的虚函数 Human::test1 。 总结 派生类会继承基类的虚函数表如果派生类完成了重写则会将重写的虚函数覆盖掉原有的函数。指针或引用指向哪一个对象就调用对象中虚函数表中对应位置的虚函数来实现多态。
继续分析构成多态的另一个条件为什么必须要指针或者引用才能构成多态
如果将派生类对象赋值给基类对象会因为对象切割导致他的内存布局整个被修改完全转换为基类对象的类型虚函数表也与基类相同所以不能实现多态。如果用基类指针或者引用指向派生类对象他们的内存布局是兼容的不会像赋值一样改变派生类对象的内存结构所以派生类对象的虚函数表得到了保留所以他可以通过访问派生类对象的虚函数表来实现多态。
总结一下派生类虚函数表的生成过程
首先派生类会将基类的虚函数表拷贝过来如果派生类完成了对虚函数的重写则用重写后的虚函数覆盖掉虚函数表中继承下来的基类虚函数如果派生类自己又新增了虚函数则添加在虚函数表的最后面
常见问题解析 内联函数可以是虚函数吗 不可以内联函数没有地址无法放进虚函数表中。 静态成员函数可以是虚函数吗 不可以静态成员函数没有 this指针无法访问虚函数表。 构造函数可以是虚函数吗 不可以虚函数表指针也是对象的成员之一是在构造函数初始化列表初始化时才生成的 析构函数可以是虚函数吗 可以上面有写最好把基类析构函数声明为虚函数防止使用基类指针或者引用指向派生类对象时派生类的析构函数没有调用可能导致内存泄漏。 对象访问虚函数快还是普通函数快 如果不构成多态虚函数和普通函数的访问是一样快的都是直接在编译时从符号表中找到函数的地址后调用。如果构成多态调用虚函数就得在运行期到虚函数表中查找就会导致速度变慢所以普通函数更快一些。 虚函数表
从上面的观察可以看出来虚函数存于虚函数表中那么虚函数表又存储在哪里呢
虚函数表在编译阶段生成存储于代码段。
详情可以看这篇博客。
注意
同一个类的不同实例对象共用同一份虚函数表。子类 特有的虚函数 会加在父类 虚函数表 中的 父类虚函数的后面 。如果子类继承多个父类、且这些父类都有虚函数表子类特有的虚函数 加在 第一个父类的虚函数表 中。如果子类继承多个父类、但只有部分父类有虚函数表子类特有的虚函数 加在 第一个有虚函数表的父类 的 虚函数表 中。如果子类继承多个父类、且这些父类都没有虚函数表子类会自己创建一个虚函数表来存储特有的虚函数。