大尺寸图网站,中国糕点网页设计网站,企业官网首页设计,wordpress 关闭本地化#x1f497;个人主页#x1f497; ⭐个人专栏——C学习⭐ #x1f4ab;点击关注#x1f929;一起学习C语言#x1f4af;#x1f4ab; 目录
一、多态概念
二、多态的定义及实现
1. 多态的构成条件
2. 虚函数 2.1 什么是虚函数 2.2 虚函数的重写 2.3 虚函数重写的两个… 个人主页 ⭐个人专栏——C学习⭐ 点击关注一起学习C语言 目录
一、多态概念
二、多态的定义及实现
1. 多态的构成条件
2. 虚函数 2.1 什么是虚函数 2.2 虚函数的重写 2.3 虚函数重写的两个例外
2.3.1 协变
2.3.2 析构函数的重写
3. override 和 final
4. 重载、覆盖(重写)、隐藏(重定义)的对比
三、抽象类
1. 概念
2. 接口继承和实现继承
四、多态的原理
1. 虚函数表
2. 多态的原理
3. 动态绑定与静态绑定
五、单继承和多继承关系的虚函数表
1. 单继承中的虚函数表
2. 多继承中的虚函数表 一、多态概念
多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会 产生出不同的状态。
举个栗子比如买票这个行为当普通人买票时是全价买票学生买票时是半价买票军人 买票时是优先买票。
多态的优势在于它增加了代码的灵活性和可扩展性。通过多态我们可以编写通用的代码处理多种类型的对象使得程序更易于理解、扩展和维护。
二、多态的定义及实现
1. 多态的构成条件
那么在继承中要构成多态还有两个条件
必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 多态是在不同继承关系的类对象去调用同一函数产生了不同的行为。比如Student继承了 Person。Person对象买票全价Student对象买票半价。 class Person
{
public:virtual void BuyTicket() {cout 买票全价 endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout 买票半价 endl; }
};
void Func(Person people)
{people.BuyTicket();
}
int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
} 2. 虚函数 2.1 什么是虚函数
虚函数virtual function是面向对象编程中的一个概念它是用来实现多态的机制之一。
虚函数即被virtual修饰的类成员函数称为虚函数。 虚函数是在基类中声明的一种特殊的成员函数用关键字virtual进行声明。当在派生类中重写override这个函数时可以使用关键字override来显式标识。 通过将函数声明为虚函数可以实现动态绑定dynamic binding即在运行时根据对象的实际类型来调用相应的函数。这意味着当通过基类指针或引用调用虚函数时实际调用的是对应的派生类中的函数。 class Person
{
public:virtual void BuyTicket() { cout 买票全价 endl; }
}; 2.2 虚函数的重写
虚函数的重写是指在派生类中重新定义override基类中已经声明的虚函数。通过重写虚函数可以在派生类中提供自己的实现以替代基类中的默认实现。
重写虚函数的规则如下
基类中的虚函数必须使用关键字virtual进行声明。派生类中的重写函数必须具有相同的名称、参数列表和返回类型。派生类中的重写函数必须使用关键字override进行标识以确保编译器进行正确的检查。重写函数可以被声明为虚函数但这不是必需的。 class Animal
{
public:virtual void makeSound() {cout Animal is making a sound endl;}
};class Dog : public Animal
{
public:void makeSound() override { // 重写虚函数cout Dog is barking endl;}
};class Cat : public Animal
{
public:void makeSound() override { // 重写虚函数cout Cat is meowing endl;}
};int main()
{Animal* animal new Animal();Animal* dog new Dog();Animal* cat new Cat();animal-makeSound(); // 输出 Animal is making a sounddog-makeSound(); // 输出 Dog is barkingcat-makeSound(); // 输出 Cat is meowingdelete animal;delete dog;delete cat;return 0;
} 在上述代码中Animal类中的makeSound()函数被声明为虚函数。派生类Dog和Cat分别重写了这个函数并提供了自己的实现。 在main()函数中通过基类指针调用makeSound()函数时实际上会根据指针指向的对象类型来调用相应的派生类中的函数。这就是虚函数的重写实现多态性的一种方式。 2.3 虚函数重写的两个例外
2.3.1 协变
虚函数的协变covariant是指派生类可以返回比基类更具体的类型。
换句话说派生类可以重写基类中的虚函数并返回一个派生类类型的指针或引用而不仅仅是基类类型的指针或引用。
协变发生在满足以下条件时
基类中的虚函数必须返回指针或引用类型。派生类中重写的函数可以返回指向派生类的指针或引用类型该派生类是基类中返回类型的派生类。 class Animal
{
public:virtual Animal* create() {return new Animal();}
};class Dog : public Animal
{
public:Dog* create() override { // 协变的重写函数return new Dog();}
};class Cat : public Animal
{
public:Cat* create() override { // 协变的重写函数return new Cat();}
};int main()
{Animal* animal new Animal();Animal* dog new Dog();Animal* cat new Cat();Animal* newAnimal animal-create();Animal* newDog dog-create();Animal* newCat cat-create();delete animal;delete dog;delete cat;delete newAnimal;delete newDog;delete newCat;return 0;
} 在上述代码中Animal类中的create()函数返回一个指向Animal类型的指针。派生类Dog和Cat分别重写了这个函数并返回指向Dog和Cat类型的指针。 在main()函数中通过调用基类指针的create()函数实际上会调用对应派生类中重写的create()函数。由于协变的存在可以将返回的指针赋值给指向基类的指针这样便保留了派生类的类型信息。 注意为了实现协变返回类型必须是指针或引用而不能是值类型。否则无法满足派生类返回类型更具体的要求。
2.3.2 析构函数的重写
如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字 都与基类的析构函数构成重写虽然基类与派生类析构函数名字不同。虽然函数名不相同 看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor。
以下是一个示例展示了虚函数中的析构函数重写是没有意义的 class Animal
{
public:Animal() {cout Animal constructor called. endl;}virtual ~Animal() {cout Animal destructor called. endl;}virtual void speak() {cout Animal speaks. endl;}
};class Dog : public Animal
{
public:Dog() {cout Dog constructor called. endl;}~Dog() {cout Dog destructor called. endl;}void speak() override {cout Dog barks. endl;}
};int main()
{Animal* animal new Dog();animal-speak();delete animal;return 0;
} Animal类定义了一个虚函数speak()而派生类Dog中重写了该函数。在main()函数中通过基类指针创建了一个Dog对象然后调用speak()函数。 从结果可以看出析构函数按照预期顺序调用无需在派生类中显式重写。同时虚函数speak()也可以正确地根据对象的实际类型调用对应的版本。 3. override 和 final 从上面可以看出C对函数重写的要求比较严格但是有些情况下由于疏忽可能会导致函数名字母次序写反而无法构成重载而这种错误在编译期间是不会报出的只有在程序运行时没有 得到预期结果才来debug会得不偿失因此C11提供了override和final两个关键字可以帮助用户检测是否重写。 1. final修饰虚函数表示该虚函数不能再被重写 class Base
{
public:virtual void foo() final {// 函数实现}
};class Derived : public Base
{
public:void foo() override { // 编译错误不能重写被声明为final的虚函数// 函数实现}
};2. override: 检查派生类虚函数是否重写了基类某个虚函数如果没有重写编译报错。 class Base
{
public:virtual void foo() {// 函数实现}
};class Derived : public Base
{
public:void foo() override { // 使用override关键字重写基类的虚函数// 函数实现}
}; 4. 重载、覆盖(重写)、隐藏(重定义)的对比
三、抽象类
1. 概念
在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口 类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生 类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。 抽象类可以用于实现接口的一致性和多态性。通过基类指针或引用可以在运行时动态地选择调用派生类的实现。 class Animal
{
public:virtual void makeSound() 0; // 纯虚函数void sleep() {cout Zzz... endl;}
};class Dog : public Animal
{
public:void makeSound() override {cout Woof! endl;}
};class Cat : public Animal {
public:void makeSound() override {cout Meow! endl;}
};int main()
{Animal* animalPtr new Dog();animalPtr-makeSound(); // 输出 Woof!animalPtr-sleep(); // 输出 Zzz...delete animalPtr;animalPtr new Cat();animalPtr-makeSound(); // 输出 Meow!animalPtr-sleep(); // 输出 Zzz...delete animalPtr;return 0;
} Animal是一个抽象类包含一个纯虚函数makeSound()和一个普通的成员函数sleep()。Dog和Cat是Animal的派生类必须实现makeSound()函数。通过基类指针可以调用派生类的实现。 需要注意的是无法直接实例化抽象类因为它包含纯虚函数没有具体的实现。派生类必须实现所有纯虚函数才能被实例化。抽象类可以作为基类来定义其他类从而实现代码的复用和扩展。
2. 接口继承和实现继承
接口继承和实现继承是面向对象编程中的两种继承方式。 接口继承Interface Inheritance是指一个接口interface可以继承自另一个接口继承接口的子接口会继承父接口的所有方法定义。接口继承主要用于定义一组共享的方法规范子接口可以继续扩展这些方法规范同时可以添加自己的方法规范。接口继承可以实现多继承的效果一个类可以同时实现多个接口。 实现继承Implementation Inheritance是指一个类class可以继承自另一个类继承类会继承父类的属性和方法。实现继承主要用于类之间的层级关系子类可以继承父类的行为和状态同时还可以添加自己特有的行为和状态。实现继承是一种单继承的方式一个类只能直接继承一个父类。 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。 四、多态的原理
1. 虚函数表
我们来看下面的代码想一下sizeof(a)是多少。 class A
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};
int main()
{A a;cout sizeof(a) endl;return 0;
} 第一眼看去这不就是4吗我们来看一下运行结果 为什么呢我们打开监视窗口来看一下 我们发现a里面不仅仅存在一个_b还有一个_vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。 一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数 的地址要被放到虚函数表中虚函数表也简称虚表。 那么派生类中这个表放了些什么呢我们接着往下分析针对上面的代码我们做出以下改造
我们增加一个派生类Derive去继承Base Derive中重写Func1 Base再增加一个虚函数Func2和一个普通函数Func3 class A
{
public:virtual void Func1(){cout A::Func1() endl;}virtual void Func2(){cout A::Func2() endl;}void Func3(){cout A::Func3() endl;}
private:int _a 1;
};
class B : public A
{
public:virtual void Func1(){cout Derive::Func1() endl;}
private:int _b 2;
};
int main()
{A a;B b;cout sizeof(a) endl;cout sizeof(b) endl;return 0;
} 我们再来看下这个代码结果是多少呢 这又是为什么呢我们继续来看一下调试窗口 通过观察和测试我们发现了以下几点问题
派生类对象b中也有一个虚表指针b对象由两部分构成一部分是父类继承下来的成员虚表指针也就是存在部分的另一部分是自己的成员。基类a对象和派生类b对象虚表是不一样的这里我们发现Func1完成了重写所以b的虚表中存的是重写的B::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但是不是虚函 数所以不会放进虚表。虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。
总结一下派生类的虚表生成 先将基类中的虚表内容拷贝一份到派生类虚表中如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 这里还有一个童鞋们很容易混淆的问题虚函数存在哪的虚表存在哪的 答虚函数存在虚表虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。 注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。那么虚表存在哪的呢实际我们去验证一下会发现vs下是存在代码段的。 2. 多态的原理
上面分析了这个半天了那么多态的原理到底是什么我们来继续探索。 class A
{
public:virtual void Func1(){cout A::Func1() endl;}
private:int _a 1;
};
class B : public A
{
public:virtual void Func1(){cout Derive::Func1() endl;}
private:int _b 2;
};
int main()
{A a;B b;return 0;
} 我们会疑惑为什么多态可以实现指向父类调用父类函数 指向子类调用子类函数
我们来看看虚表里的指针内容 在监视窗口里我们可以看到a对象和b对象里的内容我们继续打开内存窗口取地址a 我们看到第一个内容就是_vfptr的地址继续通过_vfptr的地址我们就能看到虚表里的内容这时我们又看到了虚函数的地址。 我们来取地址b发现和上诉同理。 但是这时我们发现怎么在B类里的A的虚函数地址和它原本在A类里的地址不一样这就是虚函数的重写覆盖。 当一个类声明了虚函数时编译器会为该类生成一个虚函数表。虚函数表是一个存储了虚函数指针的表格每个包含虚函数的类都会有自己的虚函数表。虚函数表的每个表项存储了相应虚函数的地址。对于派生类虚函数表中的表项可能会被重写以指向派生类中的虚函数。 当使用基类指针或引用调用虚函数时编译器会根据对象的实际类型来查找虚函数表并调用相应的虚函数。这种通过虚函数表来实现动态绑定的机制使得C中的对象能够在运行时根据实际类型来确定调用的函数而不是在编译时就确定下来。 3. 动态绑定与静态绑定
动态绑定和静态绑定是两种不同的函数调用机制它们在编程语言中起到的作用也不同。
静态绑定静态分派 静态绑定是在编译时期完成的也称为早期绑定。在静态绑定中函数调用的目标函数是在编译时根据对象的静态类型确定的。当调用一个函数时编译器会根据对象的声明类型来确定要调用的函数而不管对象的实际类型。
静态绑定适用于非虚函数和静态函数这些函数的调用目标不会发生改变。在静态绑定中对象的方法调用是基于声明类型进行的也就是编译期间根据对象的静态类型来确定调用哪个函数无法体现多态性。
动态绑定动态分派 动态绑定是在运行时期完成的也称为晚期绑定。在动态绑定中函数调用的目标函数是根据对象的实际类型来确定的。当调用一个虚函数时编译器会根据对象的实际类型来确定要调用的函数。
动态绑定适用于虚函数虚函数表vtable是实现动态绑定的一种机制。每个包含虚函数的类都会有一个虚函数表该表存储了类中虚函数的地址。运行时系统通过指向基类的指针或引用查找对象实际的类型并根据其虚函数表来确定要调用的函数。
动态绑定允许程序在运行时根据对象的实际类型来选择调用的函数实现了多态性。这样可以实现基于对象的具体行为并提高代码的灵活性。
五、单继承和多继承关系的虚函数表
1. 单继承中的虚函数表 class Base
{
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a;
};
class Derive :public Base
{
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() {cout Derive::func4 endl; }
private:int b;
};
int main()
{Base b;Derive d;return 0;
} 观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这 两个函数也可以认为是他的一个小bug。那么我们如何查看d的虚表呢下面我们使用代码打印 出虚表中的函数。
class Base
{
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int _a;
};
class Derive :public Base
{
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() {cout Derive::func4 endl; }
private:int _b;
};
typedef void(*VFTPTR)();void PrintVFPtr(VFTPTR a[])
{for (size_t i 0; a[i] ! 0; i){printf(a[%d]:%p\n, i, a[i]);}cout endl;
}int main()
{Base b;Derive d;PrintVFPtr((VFTPTR*)*(int*)b);PrintVFPtr((VFTPTR*)*(int*)d);return 0;
}
2. 多继承中的虚函数表 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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout 虚表地址 vTable endl;for (int i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
}
int main()
{Derive d;VFPTR* vTableb1 (VFPTR*)(*(int*)d);PrintVTable(vTableb1);VFPTR* vTableb2 (VFPTR*)(*(int*)((char*)d sizeof(Base1)));PrintVTable(vTableb2);return 0;
} 观察下图可以看出多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中