设计必知的设计网站 039,wordpress主题显示不,比较出名的设计网站,wordpress 排除分类目录
0.引入
1.虚函数
1. 虚函数的重写/覆盖
2. 特例1#xff1a;不加 virtual 关键字的重写
3. 特例2#xff1a;协变#xff08;了解#xff09;
2.多态的构成和细节
1. C11 的 override 和 final
1. final 不可重写
2. override 报错检查
⭕2. 重载、覆盖不加 virtual 关键字的重写
3. 特例2协变了解
2.多态的构成和细节
1. C11 的 override 和 final
1. final 不可重写
2. override 报错检查
⭕2. 重载、覆盖重写和隐藏重定义的对比
3. 多态的使用
⭕多态的两个条件
4.多态的原理
1. 虚函数表vtable
2. 动态绑定与静态绑定
5. 抽象类
1. 概念
2. 接口继承和实现继承
6. 单继承和多继承中的虚函数表
1. 单继承中的虚函数表
2. 多继承中的虚函数表
3. 菱形继承和菱形虚拟继承 0.引入
通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态。如比如买票这个行为
当普通人买票时是全价买票学生买票时是半价买票军人买票时是优先买票
1.虚函数
即被virtual修饰的类成员函数称为虚函数
class Person{public:virtual void BuyTicket() { cout 买票-全价 endl; }};
1. 虚函数的重写/覆盖
重写是指在派生类中对基类的虚函数重新实现需满足以下“三同”条件
函数名相同参数相同返回值相同
满足以上条件时子类的虚函数重写了基类的虚函数体现了接口继承与实现继承的关系。
虚函数重写是接口继承即子类提供了基类虚函数的不同实现。普通函数继承是实现继承即子类直接使用基类中的普通函数。
若子类函数不符合重写的要求则会形成隐藏关系而非重写。
2. 特例1不加 virtual 关键字的重写
在子类中即便不加 virtual 关键字虚函数依然构成重写关系。这是因为派生类继承了基类的虚函数属性但从规范性和可读性考虑建议显式加上 virtual 关键字。
析构函数的特殊处理
如果基类的析构函数是虚函数则派生类的析构函数只要定义无论是否加 virtual 关键字都会与基类析构函数构成重写。虽然析构函数的名称不同但编译器会将其统一处理为 destructor。建议在继承中将析构函数定义为虚函数以保证正确的析构行为。
示例
class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout 买票-半价 endl; }
/*注意在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议这样使用*//*void BuyTicket() { cout 买票-半价 endl; }*/
};
虽然可以不加 virtual但规范上建议加上以保持代码的可读性和一致性。
3. 特例2协变了解
协变指的是当派生类重写基类的虚函数时返回值类型可以不同但要求遵守以下规则
基类的虚函数返回基类对象的指针或引用。派生类的虚函数返回派生类对象的指针或引用。
这种情况下尽管返回值类型不同依然构成重写。
示例
class A {};class B : public A {};class Person
{
public:virtual A* f() { return new A; }
};class Student : public Person
{
public:virtual B* f() { return new B; }
};
这种形式允许返回值类型在派生类中进行“协变”即派生类返回派生类类型的指针或引用。
2.多态的构成和细节 构成条件 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写必须通过基类的指针或者引用调用虚函数 1. C11 的 override 和 final
1. final 不可重写
用途用于修饰虚函数表示该虚函数不能再被派生类重写。示例
class Car
{
public:virtual void Drive() final {}
};
使继承自 Car 的派生类都不能重写 Drive 函数。
2. override 报错检查
用途用于检查派生类的虚函数是否正确地重写了基类中的某个虚函数。如果派生类的虚函数没有重写基类的虚函数即函数签名不匹配编译器会报错。示例
class Car
{
public:virtual void Drive() {}
};class Benz : public Car
{
public:virtual void Drive() override { cout Benz endl; }
};
如果 Drive 函数的签名与 Car 中的 Drive 函数不一致编译器会报错。
⭕2. 重载、覆盖重写和隐藏重定义的对比 重载函数名相同参数列表不同通常在同一个类中或在派生类中定义多个同名但参数不同的函数。重载与继承无关可以在同一个作用域中实现。覆盖重写派生类中重写基类的虚函数要求三同函数名、参数列表、返回值类型必须与基类中的虚函数完全一致。覆盖是实现多态的关键。隐藏重定义派生类定义了一个与基类中同名但非虚函数的函数。此时基类的函数在派生类中被隐藏不能通过基类指针或引用调用。隐藏通常发生在非虚函数或静态成员函数中。 3. 多态的使用
代码示例
#include iostreamclass Person {
public:
virtual void BuyTicket() {std::cout 买票全价 std::endl;
}
};class Student : public Person {
public:
void BuyTicket() override {std::cout 买票半价 std::endl;
}
};void Func(Person people) {people.BuyTicket();
}void Test() {Person Mike;Func(Mike);Student Johnson;Func(Johnson);
}int main() {Test();return 0;
} 代码解释 定义了基类 Person其中有一个虚函数 BuyTicket()输出 买票全价。定义了派生类 Student继承自 Person 并重写了 BuyTicket() 函数输出 买票半价。定义了一个函数 Func()接收一个 Person 类型的引用作为参数并调用该参数对象的 BuyTicket() 函数。在 Test() 函数中创建了一个 Person 类的对象 Mike 和一个 Student 类的对象 Johnson分别调用 Func() 并传入对象。 观察下图的红色箭头我们看到p是指向mike对象时p-BuyTicket在mike 对象指向的类的虚表中找到虚 函数是Person::BuyTicket观察下图的蓝色箭头我们看到p是指向johnson对象时p-BuyTicket在johson 对象指向的类的虚表中 找到虚函数是Student::BuyTicket 这样就实现出了不同对象去完成同一行为时展现出不同的形态 ⭕多态的两个条件 虚函数覆盖基类的虚函数被派生类重写。对象的指针或引用调用虚函数通过基类的指针或引用来调用派生类中重写的虚函数。 两种调用
多态调用在程序运行时通过对象的虚表查找对应的虚函数地址并进行调用。虚表vtable是由虚函数指针组成的指针数组每个对象都有一个指向虚表的指针vptr。普通函数调用在编译期间确定函数的地址运行时直接调用效率高于多态调用。 4.多态的原理
1. 虚函数表vtable
虚函数表每个含有虚函数的类都有一个虚函数表vtable其中存储了该类的所有虚函数的指针。每个对象都有一个指向虚函数表的指针vptr。虚函数表的生成 派生类的虚表首先拷贝基类的虚表。如果派生类重写了基类的虚函数虚表中相应的指针会被替换为派生类的函数指针。派生类新增的虚函数会按声明顺序添加到虚表的末尾。
虚函数表存储位置虚表指针存在对象中而虚表本身存储在代码段常量区。示例
class Base
{
public:virtual void Func1(){cout Base::Func1() endl;}virtual void Func2(){cout Base::Func2() endl;}void Func3(){cout Base::Func3() endl;}
private:int _b 1;
};class Derive : public Base
{
public:virtual void Func1() override{cout Derive::Func1() endl;}
private:int _d 2;
};int main()
{Base b;Derive d;return 0;
}
在上述示例中Base 类和 Derive 类分别有自己的虚表其中 Derive 类重写了 Func1所以它的虚表中存储的是 Derive::Func1() 的地址而 Base 类的虚表中则存储 Base::Func1() 的地址。
测试看内存 发现满足多态以后的函数调用不是在编译时确定的是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。 深入理解 在C中虚函数是用于实现多态的机制。每个包含虚函数的类都有一个虚函数表虚表它是一个包含了所有虚函数地址的数组。这个表存储在类的实例中而不是函数体中。
虚函数和普通函数一样它们的代码都存在于代码段中。虚表中的每个条目都是一个指向虚函数的指针。当通过基类指针或引用调用虚函数时编译器会使用这个指针来找到实际的函数地址。对象中存储的是指向虚表的指针而不是虚表本身。这个指针通常称为“虚表指针”或“vptr”。虚表对象指针是类的一个特殊成员它指向该类的虚表。在Visual Studio 中虚表是存储在代码段中的而不是对象中。这是因为虚表的地址是静态的对于一个给定的类它不会随着对象实例的变化而变化。同一个类型的所有对象共享同一个虚表。这意味着对于同一个类的所有实例它们指向同一个虚表。在Visual Studio中子类的虚表和父类的虚表不是同一个。这是因为子类可能会添加新的虚函数或者覆盖父类的虚函数这些都会导致子类的虚表与父类的虚表不同。 总结 虚函数存在于代码段中。虚表存放在对象中每个对象有一个指向虚表的指针。虚表中存储的是虚函数的指针。再根据指针来找到地址获取空间同一个类型的所有对象共享同一个虚表。Visual Studio中子类的虚表和父类的虚表是不同的。拷贝使用对象指针--虚表 -中存的函数地址 -调用函数
2. 动态绑定与静态绑定
静态绑定早绑定在编译期间确定函数调用主要用于普通函数或函数重载效率较高。动态绑定晚绑定在运行期间根据对象的实际类型决定函数调用主要用于虚函数实现了多态灵活性高。 5. 抽象类
1. 概念
抽象类包含至少一个纯虚函数即在函数后面写 0 的虚函数的类称为抽象类不能实例化对象。派生类继承抽象类派生类必须重写抽象类中的所有纯虚函数才能实例化对象。如果没有重写纯虚函数派生类也会成为抽象类。示例
class Car
{
public:virtual void Drive() 0; // 纯虚函数
};class Benz : public Car
{
public:virtual void Drive() override{cout Benz-舒适 endl;}
};class BMW : public Car
{
public:virtual void Drive() override{cout BMW-操控 endl;}
};void Test()
{Car *pBenz new Benz;pBenz-Drive(); // 输出: Benz-舒适Car *pBMW new BMW;pBMW-Drive(); // 输出: BMW-操控
} 2. 接口继承和实现继承
接口继承虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口主要目的是为了重写虚函数并实现多态。实现继承普通函数的继承是一种实现继承派生类继承了基类的函数实现可以直接使用这些函数。建议如果不需要实现多态不要将函数定义为虚函数。 6. 单继承和多继承中的虚函数表
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;
};
可以发现 fun2()地址一样fun1()实现了重写 实现如下 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;
}多继承原理继承了两张虚函数表
观察下图可以看出多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中 在两张虚函数表中都对 fun1 进行了重写
3. 菱形继承和菱形虚拟继承
菱形继承由多重继承引发的继承关系其中基类被多个派生类继承而这些派生类又被另一个类继承形成菱形结构。菱形继承容易引发复杂性和性能问题因此不推荐使用。菱形虚拟继承通过虚拟继承解决菱形继承中的重复继承问题虽然避免了多次继承同一基类的副本但仍可能引发复杂性和性能问题。