网页制作与网站建设从入门到精通 下载,网站开发 慕课,网站域名密码找回,最新企业网站文章目录 静态的多态动态的多态虚函数虚函数的重写#xff08;覆盖#xff09;利用虚函数重写实现多态重写的两个例外1.协变2.析构函数的函数名不同 C11的override和final 重载#xff0c;重写#xff08;覆盖#xff09;#xff0c;重定义#xff08;隐藏#xff09;抽… 文章目录 静态的多态动态的多态虚函数虚函数的重写覆盖利用虚函数重写实现多态重写的两个例外1.协变2.析构函数的函数名不同 C11的override和final 重载重写覆盖重定义隐藏抽象类接口继承和实现继承多态实现的原理虚函数表虚函数表的存储位置 一些关于多态的问题 多态是不同继承关系的类对象去调用同一个函数产生了不同效果的行为。 静态的多态
调用同一个函数产生不同效果的行为这不就是函数重载吗函数重载其实是一种静态的多态相同的函数名传不同的参数调用的函数也就不同但是调用哪个函数是在编译阶段就已经被确定好了。函数重载是一种编译时绑定也就是静态绑定。常用的流插入和流体取也是一种函数重载
动态的多态
动态的多态才是本篇文章中要讲的主要内容它在调用函数时与与类型无关而是与它所存放的对象有关普通调用是按类型。具体调用哪个函数运行时才知道又叫运行时绑定也就是动态绑定。
多态的构成必须要满足两个条件
1.必须要通过父类的引用或者指针作为形参来调用 为什么一定要是父类的引用或者指针对于这个问题《深度探索C模型》中这样说“一个pointer或一个reference之所以支持多态是因为它们并不引发内存任何“与类型有关的内存委托操作 会受到改变的。只有它们所指向内存的大小和解释方式 而已”。 对于上述话可以这样理解 指针和引用类型只是要求了基地址和这种指针所指对象的内存大小与对象的类型无关相当于把指向的内存解释成指针或引用的类型。如果直接把子类对象赋值给父类对象就牵扯到内存模型编译器就会回避虚机制无法达到多态的效果。 2.被调用的函数必须是虚函数
虚函数
所谓的虚函数就是被virtual关键字所修饰的函数
class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl; }//这就是虚函数
};虽然虚函数和虚继承都使用了virtual关键字但是两者之间没有任何联系 虚函数的重写覆盖
如果子类中有和父类一样的虚函数返回值类型函数名称参数列表相同那么就称该子类的虚函数重写了父类的虚函数。
这里有一点需要注意如果父类在声明的时候加了virtual即使子类在声明同名函数时不加virtual也会完成重写可以理解为子类在继承父类时将虚属性也继承下来了但这样写是不规范的建议不要这样写。
虚函数的重写也可以被称为虚函数的覆盖因为带有虚函数的类都有一个虚函数表在继承的时候子类会继承父类的虚函数表如果子类对某一个虚函数进行重写了那么该虚函数在子类的虚函数表中就会被重写的虚函数覆盖。
利用虚函数重写实现多态
class Person
{
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout 买票-半价 endl; }};
void Func(Person p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}可以看到虽然我都是调用Func函数但最后Func函数帮我调用到了不同的函数这就是多态。 有了多态以后在调用函数的时候首先要看该函数是否构成多态如果构成多态那么就不用考虑类型只需要看该变量中存放的是何种对象按照对象去调用函数如果不构成多态那么就只看类型无论该变量中存的是何对象都不影响只看类型调用这里如果不是多态就都调用Person的函数。
重写的两个例外
1.协变
子类对于父类函数的重写返回类型可以不同但必须要是返回父子类关系的指针或引用即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用称为协变
class Person
{
public:virtual A* f() {return new A;}
};
class Student : public Person
{
public:virtual B* f() {return new B;}
};2.析构函数的函数名不同
析构函数的函数名要求必须要与类名相同也就是说各个类的析构函数名都不同但是其实编译器会将析构函数的函数名统一处理成destructor。
析构函数不但能重写并且析构函数建议定义成虚函数。 如果我定义了一个子类的对象并将该子类对象赋值给一个父类的指针当我释放父类的时候只会调用父类的析构函数也就是说只释放了子类中父类的那一部分资源而没有释放子类的资源这就可能会导致内存泄漏。 如果我将析构函数定义为虚函数并重写那么我在释放父类指针的时候调用的是子类的析构函数子类析构函数对于父类那一部分资源通过父类的析构函数清理同时也会清理自己的资源。 class Person
{
public://virtual ~Person~Person(){cout ~Person endl;}public:int _a;
};class Student :public Person
{
public://virtual ~Student~Student(){cout ~Student endl;}public:int _b;
};int main()
{Person *p1new Person;Person* p2 new Student;delete p1;delete p2;return 0;
} C11的override和final
1.被final关键字修饰的函数不能被重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout Benz-舒适 endl;}
}; 2.使用override关键字检查该函数是否被重写如果没有重写就报错
class Car
{
public:virtual void Drive() {}
};
class Benz :public Car
{
public:virtual void Drive() override { cout Benz-舒适 endl; }
};重载重写覆盖重定义隐藏
重载 1.要在同一个作用域中 2.函数名相同参数列表相同返回值可以不同 重写覆盖 1.两个函数分别在父类和子类的作用域中 2.返回值相同协变除外函数名相同参数列表相同 3.只有虚函数才构成重写 重定义隐藏 1.两个函数分别在父类和子类的作用域中 2.函数名相同只要不构成重写就是重定义 抽象类
与虚函数对应的还有一个纯虚函数只要在虚函数声明的最后加上0那么这个虚函数就变成了纯虚函数。如果一个类包含纯虚函数那么这个类就是抽象类。抽象类不能实例化对象并且如果继承抽象类的子类不对纯虚函数进行重写的话子类也是一个抽象类无法实例化对象。纯虚函数规范了子类必须重写此外纯虚函数更体现了接口继承。
class A
{
public:virtual void test() 0;int _a;
};class B :public A
{
public:virtual void test(){cout 重写了纯虚函数 endl;}int _b;
};
int main()
{A a;B b;return 0;
}接口继承和实现继承
普通函数的继承是一种实现继承子类继承了父类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承子类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数 。
多态实现的原理
首先我们来计算一下下面这个类的大小
class A
{
public:virtual void test();
}按照我们类和对象阶段所说一个没有成员变量的类就是空类空类的大小为1字节用来占位。 但这个类的大小为4字节 这是因为如果一个类中有虚函数那么该类中会有一个隐藏的指针该指针指向一个虚函数表。
虚函数表 可以看到虚函数表中存放的是虚函数的地址所谓虚函数的重写其实就是将重写过的虚函数的地址覆盖到原虚函数地址上。 因为每个类都有自己独立的虚函数表所以不同的类对象就可以通过不同的虚函数表访问到不同的虚函数。 在vs下虚函数表都是以空结尾但是Linux下就不是 一个变量中如果存放的是子类的对象那么该变量中的前四个字节就是子类所对应的虚函数表该表中存放的是子类所对应的虚函数。如果放的是父类的对象那么该变量的前四个字节就是父类的虚函数表其中存放的也就是父类所对应虚函数的地址。多态的产生也就是根据对象的前四个字节找到虚函数表调用其中的虚函数实现的。
虚函数表的存储位置
此外虚函数表存储在常量区中代码段通过下面的代码可以看到各个区域的地址虚表的地址和哪个最为接近就可以说明虚表在哪个区域经观察确实是在代码段上。
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;char _ch;
};class Derive : public Base
{
public:virtual void Func1(){cout Derive::Func1() endl;}void Func3(){cout Derive::Func3() endl;}
private:int _d 2;
};
int main()
{int a 0;cout 栈: a endl;int* p1 new int;cout 堆: p1 endl;const char* str hello world;cout 代码段/常量区: (void*)str endl;static int b 0;cout 静态区/数据段: b endl;Base be;cout 虚表: (void*)*((int*)be) endl;Base* ptr1 be;int* ptr2 (int*)ptr1;printf(虚表:%p\n, *ptr2);Derive de;cout 虚表: (void*)*((int*)de) endl;Base b1;Base b2;return 0;
}
补充 子类不但有从父类继承下来的成员还有子类自己特有的成员也就是说子类除了有重写父类的虚函数还有自己特有的虚函数。 对于单继承的类来说子类特有的虚函数和重写的虚函数放在同一张虚函数表中 对于多继承的子类来说它继承了多少个有虚函数的类就有几张虚函数表但子类所特有的虚函数会放在第一张虚函数表中 一些关于多态的问题
什么是多态
答多态就是多种形态不同的对象来做同一件事产生不同效果的行为就可以称为多态。
inline函数可以是虚函数吗
答可以不过编译器就忽略inline属性这个函数就不再是inline因为虚函数要放到虚表中去。
静态成员可以是虚函数吗
答不能因为静态成员函数没有this指针使用类型::成员函数的调用方式无法访问虚函数表所以静态成员函数无法放进虚函数表。
构造函数可以是虚函数吗
答不能因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
对象访问普通函数快还是虚函数更快
答首先如果是普通对象是一样快的。如果是指针对象或者是引用对象则调用的普通函数快因为构成多态运行时调用虚函数需要到虚函数表中去查找。
虚函数表是在什么阶段生成的存在哪的
答虚函数表是在编译阶段就生成的一般情况下存在代码段(常量区)的。