佛山建站网站模板,南宁建筑规划设计集团有限公司,django 和 wordpress,中国建设工程交易网多态简单介绍 多态就是多种形态#xff0c;是不同的对象去完成同一个动作所产生的结果可能有多种。这种多种的形态我们称之为多态。
比如#xff1a;我们在买票的时候的时候#xff0c;可能有成人全价#xff0c;儿童半价#xff0c;军人免票等等。对于成人#xff0c;儿…多态简单介绍 多态就是多种形态是不同的对象去完成同一个动作所产生的结果可能有多种。这种多种的形态我们称之为多态。
比如我们在买票的时候的时候可能有成人全价儿童半价军人免票等等。对于成人儿童军人这三个不同的对象在买票同一动作当中就产生了不同的结果。
多态的定义 和 实现 多态出现在同一继承关系当中的不同类对象比如上述说的 Person对象买票全价Student对象买票半价。
在多态的组成方面有两大必须的条件
必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
虚函数
在知道多态是如何构成之前我们先来认识一种特殊的成员函数---虚函数。
注意
其中的 virtual 虽然可以用来修饰虚函数和虚继承但是此时的虚函数和虚继承没有任何关系可以理解为 virtual 修饰函数就是虚函数修饰继承关系及时虚继承。关于虚函数 virtual 的修饰只要在 函数的返回值之前加上 vitual 修饰的函数就是虚函数了。只要类当中的成员函数可以加 virtual 修饰 变成虚函数普通的全局函数是不能加 virtual 变成虚函数的。 虚函数定义如
class Person {
public:virtual void buy() { cout 全价 endl;}
};
全局函数不能加 virtual 修饰变成虚函数 虚函数的重写 虚函数和其他成员函数一样但是虚函数有一个特征虚函数支持重写覆盖。 如果在派生类当中有一个和基类当中相同的虚函数两者之间返回值函数名参数列表完全相同我们认为此时派生类重写了基类当中的虚函数。 class Person
{
public:virtual void buy() {cout Perosn:全价 endl;}
};class Student : public Person
{
public:virtual void buy() {cout Student:半价 endl;}
};
上述子类student就重写了 父类Perosn当中的 buy 这个虚函数。 对于上述 这种虚函数的使用场景通过指针或者引用来调用虚函数
void func(Person people)
{people.buy();
}int main()
{func(Person());func(Student());return 0;
}
输出
Perosn:全价
Student:半价
这样的话我们就可以做到类似于自动识别对象然后去购买不同的票了。 请注意我们在调用虚函数的时候一定是使用 引用或者指针的方式来调用虚函数而且子类父类当中的函数都应该是 virtual 修饰的子类重写过的虚函数否则无法实现多态如下我们把func函数当中的 Person 参数类型 改为 Person
void func(Person people)
{people.buy();
} 输出
Perosn:全价
Perosn:全价
我们发现结果都是 “全价”。没有多态现象出现。 同样如果父类的函数没有加 virtual 修饰输出结果和上述一样但是如果父类虚函数加了 virtual 修饰子类函数没有加 virtual 修饰是可以实现多态的。----但是就算能够实现多态建议还是把子类和父类的虚函数都加上 virtual 修饰。 编译器在这里支持派生类不用加 virtual 是因为编译器对于派生类的检查只是检查派生类符不符合 “三同”的 多态条件。不同可能看该函数和父类当中的虚函数函数名相同就是别成隐藏了相同才会去认为该函数是虚函数的重写。 因为 派生类 继承了 父类的 virtual 修饰的虚函数而子类当中的 重写只是对 父类当中虚函数的实现部分进行 重写。
class Person
{
public:virtual void buy() {cout Perosn:全价 endl;}
};class Student : public Person
{
public:void buy() {cout Student:半价 endl;}
};void func(Person people)
{people.buy();
}int main()
{Person perosn;func(perosn);Student student;func(student);return 0;
} 输出
Perosn:全价
Student:半价 像上述的实现多态例子中的 Student 类型 对象传参到 Person 类型参数接收这里发生了 子类 到 父类的 切割。 有了切割当传入参数就是父类的时候不需要切割这类直接就是调用父类对象的引用来调用buy这个函数如果传入的是子类的话就会发生切割指向子类此时就是子类的引用所以调用的是子类的buy函数。 具体切割是如何切割法可以看以下博客C - 继承_chihiro1122的博客-CSDN博客 但是这里就有一个问题我们知道对象当中只存储成员变量不存储成员函数而且就算是子类的引用只是访问的是父类当中子类的那一部分成员编译器在此处究竟是如何做到区分两个虚函数的呢
虚函数重写的两个特殊情况 协变 这种情况是 -- 基类和派生类虚函数的返回值类型不同。
但是这里虚函数的返回值类型是有规定的如果是只是普通类型的返回值类型不同是会报错的 如果不是协变引起的虚函数返回值类型不同编译器是会报编译错误的。 只允许 基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用的情况而我们把这种称为 协变。而且父类虚函数 和 子类虚函数 的返回值类型 必须同时是 指针 或者 引用如果是像 指针 和 引用 岔着用是不行的编译器会报错 如下代码所示
class Person
{
public:virtual Person* buy() {cout Perosn:全价 endl;return 0;}
};class Student : public Person
{
public:Student* buy() {cout Student:半价 endl;return 0;}
}; 虽然协变指定是父类虚函数返回值是父类的指针或引用紫烈虚函数返回值是子类的之怎或引用但是只要是满足继承关系的类按照上述的方式去使用协变也是可以的就是说上述返回值不一定是 Person 和 Student也可以是其他父子关系。 如下代码所示在 Person 和 Student 的虚函数返回值类型使用 A 和 B 其他继承关系 class A
{
public:};class B : public A
{
};class Person
{
public:virtual A* buy() {cout Perosn:全价 endl;return 0;}
};class Student : public Person
{
public:B* buy() {cout Student:半价 endl;return 0;}
}; 但是协变是一个 坑由上面说的种种细节可以看出来细节很多不好记。而且协变在日常当中的使用频率也很少。不如不支持这个语法。但是在学校考试 和 面试当中经常考。 析构函数的重写
class Person {
public:virtual ~Person() {cout ~Person() endl;}
};
class Student : public Person {
public:virtual ~Student() { cout ~Student() endl; }
};
虽然上述的 Person 和 Student 两个类的析构函数名字看上去不同但是实际上继承当中的 父类 和子类的 析构函数是可以 构成虚函数的。
如上述例子 ~Perosn和 ~Student两个函数子类可以重写。 之所以支持是因为类的虚构函数都被处理为了destructor 这个统一的名字。这样处理的目的也是为了让 子类和父类的析构函数构成重写。 如果不这样处理会出现一些问题子类重写的话会出现一些问题
class Person {
public:~Person() {cout ~Person() endl;}
};
class Student : public Person {
public:~Student() { cout ~Student() endl; }
};int main()
{Person* p new Person();delete p;p new Student();delete p;return 0;
}
如上所示我们希望输出的结果是
~Person()
~Student()
~Person()
但实际输出却是
~Person()
~Person()
出现这个问题的原因是也 p 指针的类型。我们知道普通对象 看当前调用的类型来决定调用 哪一个对象的析构函数当前调用者 p 的类型是 Person*所以自然只会调用 Person 对象的析构函数对于 delete p 释放顺序是 p-destructor operator delete(p) 这里调用的是 Person的析构函数但是这里我们不希望调用 Perosn的析构函数。
这里我们希望 p 指向那个对象就调用哪一个对象的析构函数而不是看 p 指针的类型来决定调用哪一个对象的 析构函数。如果看类型的话一直调用的就是 p 的类型的析构函数。但是 p 这个指针有可能指向父类也有可能指向子类。
我们希望 p-destructor调用的析构函数是一个多态调用而不是一个普通调用。 所以这里我们要使用多态来实现在 detele 底层实现当中就是使用 指针来调用 析构函数的指针已经实现了现在还差重写所以才有了上述的 析构函数重写。 final 和 override 上述我们也介绍了如果实现函数重写我们也发现C当中对于重写函数的规定还不少缺一样都会导致重写失败。有些错误甚至在编译器时期是不会报错的只有在程序运行之后才能发现问题此时在发现问题就只能去debug在代码量很多的场景当中特别麻烦。
所以在C11 当中新增了 两个关键词 final 和 override 来帮助我们检查是否重写。
final
final 关键字是用来阻止某一虚函数被子类重写 final 关键词修饰位置 和 之前 const 修饰 this 指针一样是在 参数列表括号的右边。而且只能放在父类的虚函数上 当父类的 虚函数被 final 修饰之后子类就不能再重写父类的这个虚函数了。 override
override用于帮助派生类检查是否完成重写如果没有会报错 这样就方式我们因为派生类没有重写完成而导致后序debug的麻烦了。 虚函数的指针 与 虚函数表 多态的一些底层原理 下面这个例子应该输出什么
class Bass
{
public:virtual void func(){}private:char _b;
};int main()
{cout sizeof(Bass) endl;Bass b;return 0;
} 上述输出不是1而是8。我们知道类的大小只计算成员大小不计算函数。
我们打开调试发现在 b 这个对象当中多了一个 _vfptr 指针virtual function。 这指针是 虚函数表 指针 这就是为什么没有实现多态不要把虚函数搞到类当中去因为虚函数会被放进虚函数表当中其实严格来说虚函数还是存储在代码段当中的而虚函数表当中存储的是各个虚函数的地址。 这个虚函数表在重写之后会发生变化我们来看下面这个例子
class Person {
public:virtual void BuyTicket() {cout 买票-全价 endl;}int _a 1;
};
class Student : public Person {
public:virtual void BuyTicket() {cout 买票-半价 endl;}int _b 2;
};void Func(Person p)
{p.BuyTicket();
}int main()
{Person Mike;Student jason;Func(Mike);Func(jason);return 0;
} 在上述这个代码当中Mike 对象父类对象当中有下面这两个部分 _vfptr 是虚函数表的指针此时的虚函数表当中存储的是 Person父类当中虚函数的地址此时只有一个地址因为只写了一个虚函数如果有多个虚函数的话有几个虚函数虚函数表当中就有几个地址。 Jason对象子类对象当中有下面两个部分 我们发现在子类对象 jason 当中有一个父类对象父类对象当中也有一个虚函数表指针此时虚函数表当中也只有一个地址这个地址已经发生了改变指向了子类重写的虚函数。 总结重写也可以叫覆盖重写是我们写代码层面所看到的覆盖是底层逻辑当中子类重写的虚函数地址覆盖了父类虚函数的地址。 此时我们就明白下面这个函数是如果实现传入父类就调用父类的函数传入子类就调用子类的函数了 传入父类看到就是父类直接调用父类的函数传入子类切片之后看到的还是父类如果是普通调用在编译的时候就确定了地址编译器判断是不是普通的调用很简单看符不符合多态不符合就是普通调用。如果是普通调用就直接看p的类型p的类型是Person那么就直接在Person当中找到这个函数的地址所以就不能实现多态。符合多态就和上述说的一样运行时到指向的对象的虚函数表当中找调用。 重载重写覆盖重定义隐藏的对比 虚函数和多态的例题 很多人看到满足多态的条件以为输出的是 B-0 但是实际输出却是 B-1。 我们发现上述的func函数满足 虚函数重写子类父类的虚函数函数名返回值参数类型和个数都是相同的注意不要看val 的缺省参数不同就认为这里不满足多态参数列表相同只要求 参数个数 和 参数类型相同即可 而且在 test函数当中调用的 func 函数使用指针调用的 因为 func函数是本类当中的成员函数本类当中的成员是需要用 this-func() 这样的形式来访问的而这里的this指针是父类还是子类的 指针呢 答案是父类的。因为子类继承父类当中的成员不是直接进行拷贝赋值而是调用父类的构造函数在子类当中构造出一个父类的子对象这个子对象我们可以理解为子类当中父类对象成员。然而test函数是存在于代码段的他也不是在子类和父类当中都有存在也就是说test函数只在代码段当中存储了一份而不是在子类和父类当中都存储了一份。 因为父类对象是直接在子类当中存储的子类不会单独的看test函数而是把父类对象看做是一个整体test就在这个整体当中所以test对象当中的 调用 func函数使用的this指针是 A*父类指针。 而在主函数当中的指针p指向的是 B 子类对象又满足多态所以此时肯定是调用子类当中的 func函数所以输出 B- 是正确的。 但是要注意的是重写只是重写函数当中 实现部分对于函数名返回值参数列表还是使用的是父类的。所以此处的 val 的缺省参数才是 父类当中的1而不是子类当中的0。 可以理解为重写是父类 的 函数名返回值参数列表 子类函数实现。 现在我们把上述例题修改一下把 test函数挪到 B 函数当中其他不变 此时输出结果就是 B-0 了 。因为此时的 test函数不满足多态的条件此时的test函数当中调用的 func函数的 this 指针不是A*父类指针了而是 B* 子类指针。 所以此时 test当中的 调用 func函数就只是一个简单的 在本类当中调用本类的其他函数的情况。