当前位置: 首页 > news >正文

b2b网站开发Java/太仓网站制作

b2b网站开发Java,太仓网站制作,亚马逊美国官网,网站界面大小文章目录概念定义及实现虚函数与重写(覆盖)多态构成条件虚函数重写的例外协变接口继承析构函数的重写C11 的 final 和 override重载、隐藏(重定义)、重写(覆盖)的对比抽象类多态的原理虚函数表动态绑定和静态绑定补充:…

文章目录

  • 概念
  • 定义及实现
    • 虚函数与重写(覆盖)
    • 多态构成条件
    • 虚函数重写的例外
      • 协变
      • 接口继承
      • 析构函数的重写
    • C++11 的 final 和 override
    • 重载、隐藏(重定义)、重写(覆盖)的对比
  • 抽象类
  • 多态的原理
    • 虚函数表
    • 动态绑定和静态绑定
    • 补充:VS监视窗口下的虚表
    • 补充:小题
  • 多继承的虚函数
  • 小问题

概念

多态的概念多态就是多种形态,具体来说就是去完成某个行为,当不同类型的对象去完成时会产生出不同的状态

例子:比如买票这件事,不同的人去买是不一样的,成人买是全价,学生买是半价,军人享有优先权。

定义及实现

虚函数与重写(覆盖)

  • virtual 修饰的函数被称为虚函数。

  • 派生类中有与基类中的在返回类型、函数名、参数列表都完全相同的虚函数,则称派生类中的该虚函数重写(覆盖)了基类的虚函数。

  • 注意:参数列表相同是指参数的个数、类型以及类型的顺序相同。与参数名称,缺省值无关。

以买票为例,有以下继承关系,其中各写一个买票 BuyTicket 虚函数,派生类虚函数重写基类虚函数:

class Person
{
public://虚函数virtual void BuyTicket(){cout << "Person:买票-全价" << endl;}
};class Student : public Person
{
public://虚函数virtual void BuyTicket(){cout << "Student:买票-半价" << endl;}
};class Soldier : public Person
{
public://虚函数virtual void BuyTicket(){cout << "Soldier:优先买票-全价" << endl;}
};

👆:派生类的虚函数的 virtual 也可以省略不写,因为先继承了基类函数接口声明,它依然是虚函数。但是我们为了规范,应该加上 virtual

注意

  • virtual 关键字只在声明时加上,在类外实现时不能加
  • staticvirtual 是不能同时使用的

多态构成条件

多态的实现有两个要求

  1. 派生类虚函数重写基类虚函数(重写:三同+虚函数)。
  2. 基类指针或者引用去调用虚函数。

第一点已经满足了,要满足第二点我们还要再实现一个函数:

void Pay(Person* ptr)
{ptr->BuyTicket();
}

也可以传引用:

void Pay(Person& ref)
{ref.BuyTicket();
}

注意:不可以直接传对象,否则就不满足第二点


最后是一个简单的菜单(传指针):

void test1()
{int option;Person p;Student st;Soldier so;do{cout << "请选择身份:";cout << "1.成人 2.学生 3.军人" << endl;cin >> option;switch (option){case 1:Pay(&p);break;case 2:Pay(&st);break;case 3:Pay(&so);break;default:cout << "输入错误" << endl;break;}} while (option != -1);
}
//结果:
//请选择身份:1.成人 2.学生 3.军人
//1
//Person:买票-全价
//请选择身份:1.成人 2.学生 3.军人
//2
//Student:买票-半价
//请选择身份:1.成人 2.学生 3.军人
//3
//Soldier:优先买票-全价
//请选择身份:1.成人 2.学生 3.军人

虚函数重写的例外

协变

对于虚函数的返回值,如果是父子关系的类型的指针或引用,依然构成虚函数重写。

例子:

AB 是父子关系,Person 类和 Student 类中的虚函数返回类型虽然不同,但是依然构成虚函数重载,满足多态的条件。

class A
{};class B : public A
{};class Person
{
public:virtual A* f(){cout << "virtual A* Person::f()" << endl;return nullptr;}
};class Student : public Person
{
public:virtual B* f(){cout << "virtual B* Student::f()" << endl;return nullptr;}
};void test2()
{Person p;Student s;Person* ptr = &p;ptr->f();ptr = &s;ptr->f();
}
//结果:
//virtual A* Person::f()
//virtual B* Student::f()

接口继承

上文提到:派生类的虚函数的 virtual 也可以省略不写,因为先继承了基类函数接口声明,它依然是虚函数。但是我们为了规范,应该加上 virtual

这其实是个坑,如下题:


以下程序输出的结果是什么?( )

class A
{
public:virtual void func(int val = 1){cout << "A->" << val << endl;}virtual void test(){func();}
};class B : public A
{
public:void func(int val = 0){cout << "B->" << val << endl;}
};int main()
{B* p = new B;p->test();return 0;
}

A. A->0 B. B->1 C. A->1 D. B->0 E. 编译出错 F. 以上都不正确


答案:

B

解析:

B 继承了 A 类的 test 函数,所以可以调用,但是 test 函数内部的 this 指针依然是 A* 类型。pB* 类型,传给 this 会发生切割。然后再用 this 指针去调用 func 函数,也就是 this->func()this 是基类指针,func 构成虚函数重写,所以满足多态的两个条件。this 指向的是 B 类型对象,则调用 B 类里的虚函数,该虚函数继承了基类虚函数的接口,也就是说,B 类里的 func 的参数列表显式写出来的缺省值是没有意义的,是用来迷惑你的,真正的缺省值还是 1,最后打印 B->1,选 B

总结

  1. 派生类的虚函数继承是接口继承,virtual 和参数列表缺省值都会原样照搬基类的虚函数。
  2. 虚函数重写指的是重写函数实现。

扩展:

p->test(); 改成 p->func(); 结果是什么?( )

答案:

D

解析:

这里是直接调用,没有构成多态,也就没有接口继承,所以结果就是 B->0,选 D

析构函数的重写

上篇文章提到:基类和派生类的析构函数构成隐藏关系,由于多态的需要,析构函数名会被统一处理成 destructor

只要加上 virtual 满足三同。即可构成重写(覆盖)关系。

如下场景:

class Person
{
public:~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:~Student(){cout << "~Student()" << endl;}
};void test4()
{Person* p = new Student;delete p;
}
//结果:
//~Person()

p 虽然指向的是 Student 类型的对象,但是由于本身是 Person* 所以只会调用 Person 的析构函数,而调不到 Student 的析构函数,如果 Student 类内有动态分配空间,那么就会造成内存泄漏。

这里就需要多态调用析构函数:

给析构函数加上 virtual,构成函数重写,p 作为基类指针去调用

class Person
{
public:virtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:virtual ~Student(){cout << "~Student()" << endl;}
};void test4()
{Person* p = new Student;delete p;
}
//结果:
//~Student()
//~Person()

建议:如果设计的一个类,可能会作为基类,其析构函数最好定义为虚函数。

C++11 的 final 和 override

final

  1. 修饰虚函数,表示该虚函数不能被重写

该关键字写在虚函数后面

class Car
{
public:virtual void Drive() final //final函数{}
};class Benz : public Car
{
public:virtual void Drive() //此处报错:无法重写“final”函数 "Car::Drive"{cout << "Benz" << endl;}
};
  1. 修饰类,表示不能被继承
class Car final	//final类
{
public:virtual void Drive(){}
};class Benz : public Car	//此处报错:不能将“final”类类型用作基类
{
public:virtual void Drive(){cout << "Benz" << endl;}
};

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

该关键字写在派生类虚函数的后面:

class Car
{
public:void Drive() //未被virtual修饰{}
};class Benz : public Car
{
public:virtual void Drive() override //此处报错:使用“override”声明的成员函数不能重写基类成员{cout << "Benz" << endl;}
};

重载、隐藏(重定义)、重写(覆盖)的对比

img

抽象类

  • 在虚函数后面写上 =0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类)。
  • 抽象类不能实例化出对象派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化出对象
  • 纯虚函数也可以实现函数体,但是没有意义,在基类中只给出声明,它的实现留给派生类去做
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

如下 Drive 函数是一个纯虚函数,Car 是一个抽象类:

class Car	//抽象类
{
public:virtual void Drive() = 0;	//纯虚函数
};

此时基类无法定义出对象:

void test5()
{Car c;	//此处报错:不允许使用抽象类类型 "Car" 的对象
}

但是 Car* 指针还是可以创建的。

例子:

class Car
{
public:virtual void Drive() = 0;
};class BMW : public Car
{
public:virtual void Drive(){cout << "BMW" << endl;}
};class Benz : public Car
{
public:virtual void Drive(){cout << "Benz" << endl;}
};void test5()
{Car* pBMW = new BMW;Car* pBenz = new Benz;pBMW->Drive();pBenz->Drive();
}
//结果:
//BMW
//Benz

建议:对于不需要实例化对象的基类,其内部虚函数最好写成纯虚函数,规范其派生类虚函数重写。

多态的原理

虚函数表

class Base
{
public:virtual void Fun(){cout << "Fun()" << endl;}
private:int _b = 1;
};

sizeof(Base) 是多少?


答案:32位平台下是8,64位平台下是16。

其实除了 _b 成员,还多了一个虚函数表指针 __vfptr,虚函数表简称虚表。


下面对 Base 类增加一个虚函数 Fun2 和一个普通函数 Fun3Derive 类继承 Base,重写 Fun1。然后创建对象,观察监视窗口。

class Base
{
public:virtual void Fun1(){cout << "Base::Fun1()" << endl;}virtual void Fun2(){cout << "Base::Fun2()" << endl;}void Fun3(){cout << "Base::Fun3()" << endl;}
private:int _b = 1;
};class Derive : public Base
{
public:virtual void Fun1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};void test6()
{Base b;Derive d;
}

img

虚表指针 __vfptr 其实就是函数指针数组

可以发现,普通函数 Fun3 没有进入虚表。Derive 类里的 Fun1 完成了重写,d 的虚表中存的是重写后的 Derive::Func1,未被重写的 Fun2 存放的还是原来的。

由此得出:

  • 重写是语法层的概念,派生类对基类虚函数实现进行了重写。

  • 覆盖是原理层的概念,派生类的虚表拷贝基类虚表进行了修改,覆盖要重写的虚函数指针。

所以多态调用的实现,是依靠运行时,去指向的对象的虚表中查找调用函数的地址

对比多态调用和普通调用:

  • 多态调用:运行时决议——运行时确定调用函数的地址(查虚函数表)
  • 普通调用:编译时决议——编译时确定调用函数的地址

下面写一个多态调用和一个普通调用,对比二者的汇编代码

void test6()
{Base b;Derive d;Base* p = &d;p->Fun1();	//多态调用b.Fun3();	//普通调用
}
//结果:
//Derive::Func1()
//Base::Fun3()
	p->Fun1();	//多态调用
00B22C45  mov         eax,dword ptr [p]  //p存的是d对象的地址,这里就是将p移动到eax中
00B22C48  mov         edx,dword ptr [eax]  //[eax]就是取eax指向的内容,这里就是将d对象的虚表指针移动到edx
00B22C4A  mov         esi,esp  
00B22C4C  mov         ecx,dword ptr [p]  
00B22C4F  mov         eax,dword ptr [edx]  //[edx]就是取edx指向的内容,这里就是将虚表里的虚函数指针移动到eax
00B22C51  call        eax  //通过eax里的虚函数指针调用函数
00B22C53  cmp         esi,esp  
00B22C55  call        __RTC_CheckEsp (0B2131Bh)  b.Fun3();	//普通调用
00B22C5A  lea         ecx,[b]  
00B22C5D  call        Base::Fun3 (0B2151Eh)  //这里就是直接call,因为函数地址已经在编译时确认了

为什么派生类赋值给基类对象无法构成多态

对象切片的时候,派生类只会拷贝成员给基类对象,不会拷贝虚表指针。况且从逻辑上讲,基类对象本就应该调用基类里的函数,如果拷贝了虚表指针,基类对象调用的却是派生类函数,就发生混乱了。而指针和引用可以明确表示,自己指向的是哪一类的对象就调用哪一类的函数。

动态绑定和静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行区间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

补充:VS监视窗口下的虚表

A 类有一个虚函数 Fun1B 类重写 Fun1,然后单独写了一个虚函数 Fun2。然后创建对象,观察监视窗口:

class A
{virtual void Fun1(){cout << "A::Fun1()" << endl;}
};
class B : public A
{virtual void Fun1(){cout << "B::Fun1()" << endl;}virtual void Fun2(){cout << "B::Fun2()" << endl;}
};
void test7()
{A a;B b;
}

img

按理说,虚函数指针应该进入虚表,B 类有两个虚函数,所以 B 的虚表应该有两个虚函数指针。但是监视窗口只显示了一个。还有一个虚函数指针去哪了?

打开内存窗口看看:

img

我们发现,它的下一个位置还存着一个地址 0x0059146f,它是虚函数 Fun2 的地址吗?

我们可以通过以下代码打印虚表并调用虚函数:

typedef void(*V_FUN)();void PrintVFTable(V_FUN* a)
{for (size_t i = 0; i < 2; ++i){printf("[%d]:%p->", i, a[i]);	//打印函数指针V_FUN f = a[i];f();	//调用函数}
}void test7()
{A a;B b;PrintVFTable((V_FUN*)*(int*)&b); //这里传参要取b的前4个字节
}
//结果:
//[0]:00591479->B::Fun1()
//[1]:0059146F->B::Fun2()

👆原理

要打印 b 的虚表,首先就要获得 b__vfptr ,但是我们不能直接 b.__vfptr 去取;注意到它其实就是 b 的前4个字节,只能通过 *(int*)&b 取出,说明:b 不能直接转换成 int,而 &b 可以转换成 int*,然后解引用就可以获得 b 的前4个字节;最后将这 4 个字节转换成 V_FUN* 也就是函数指针数组类型进行传参。

V_FUN 是我们自己 typedef 出来的函数指针类型,方便接下来的使用;PrintVFTable 的形参 V_FUN* a 就是函数指针数组,内部使用for循环打印 a 数组的前两个函数指针并调用函数。

总结

由结果看出,它成功调用了 Fun2,确实是 Fun2 的地址,这说明,vs监视窗口看到的虚函数表不一定是真实的,可能被处理过。

补充:小题

虚表存在哪个区域?

  • 一个类对应一个虚表,同类型的不同对象存的虚表指针都指向这个虚表。

  • 所以虚表首先不可能在栈和堆,栈里是存函数栈帧的,虚表不会随函数的开始结束而开辟销毁,也不可能在堆区,因为它不是动态开辟的。

那么到底存在哪呢?我们下面来实验一下:

void test8()
{int a = 0;int* p = new int;static int b = 1;const char* str = "hello world";A aa;printf("栈区:%p\n", &a);printf("堆区:%p\n", p);printf("静态区(数据段):%p\n", &b);printf("常量区(代码段):%p\n", str);printf("虚表:%p\n", *(int*)&aa);
}
//结果:
//栈区:004FF650
//堆区:006B4160
//静态区(数据段):00D3D008
//常量区(代码段):00D3AF24
//虚表:00D3ADD0

虚表的地址和常量区更接近,说明虚表存在常量区(代码段)

多继承的虚函数

以下是一个多继承体系,基类 Base1Base2 分别有两个虚函数 fun1 fun2,派生类继承它俩,并重写 fun1 ,增加一个独有的虚函数 fun3。最后创建派生类对象然后通过监视窗口进行观察。

class Base1
{
public:virtual void fun1(){cout << "Base1::fun1()" << endl;}virtual void fun2(){cout << "Base1::fun2()" << endl;}
private:int b1;
};class Base2
{
public:virtual void fun1(){cout << "Base2::fun1()" << endl;}virtual void fun2(){cout << "Base2::fun2()" << endl;}
private:int b2;
};class Derive : public Base1, public Base2
{
public:virtual void fun1(){cout << "Derive::fun1()" << endl;}virtual void fun3(){cout << "Derive::fun3()" << endl;}
private:int d1;
};
void test8()
{Derive d;
}

img

d 里面存在两个虚表指针,分别指向从 Base1Base2 继承的虚表,并且它们俩的 fun1 都被重写了。

那么问题来了:

fun3 去哪里了?它的函数指针也应该被存进虚表啊。

上面补充过了,监视窗口不一定准,我们还是要自己打印虚表:

typedef void(*V_FUN)();
void PrintVFTable(V_FUN* a)
{cout << "虚表地址:" << a << endl;for (size_t i = 0; a[i] != nullptr; ++i){printf("[%d]:%p->", i, a[i]);V_FUN f = a[i];f();}cout << endl;
}

👆:对打印虚表函数稍加改进:增加打印虚表地址,循环条件改成 a[i] != nullptr 因为在vs下,虚表末尾的下一位置一定为空。

通过监视窗口可以看出,d 的前4个字节一定是 Base1 类的虚表指针,Base2 的虚表指针距离 Base1 虚表指针 sizeof(Base1) 的长度,需要对 &d 转换成 char* 后进行偏移。

void test8()
{Derive d;PrintVFTable((V_FUN*)*(int*)&d);PrintVFTable((V_FUN*)*(int*)((char*)&d + sizeof(Base1)));
}
//结果:
//虚表地址:00139B94
//[0]:00131028->Derive::fun1()
//[1]:001312DA->Base1::fun2()
//[2]:001310A0->Derive::fun3()
//
//虚表地址:00139BA8
//[0]:0013111D->Derive::fun1()
//[1]:001310EB->Base2::fun2()

通过结果可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

菱形继承

B Cfun 函数重写,虚拟继承防止数据冗余和二义性。

class A
{
public:virtual void fun(){}int _a;
};class B : virtual public A
{
public:virtual void fun(){}int _b;
};class C :virtual public A
{
public:virtual void fun(){}int _c;
};class D : public B, public C//此处报错:“D” : “void A::fun(void)”的不明确继承
{
public:};

但是报错了,这是怎么回事?

因为虚拟继承会将 D 的两个虚表指针变为一个虚表指针,但是这一个虚表指针应该指向 B 类型的虚表还是 C 类型的虚表是不明确的,所以会报错。

解决方式:在 D 内也重写 fun 函数:

//略。。。
class D : public B, public C
{
public:virtual void fun(){}
};

小问题

  1. inlinevirtual 可以同时修饰函数吗?

    答:可以,但是如果是多态调用,编译器会忽略这个 inline ,虚函数会放到虚表中。

  2. 静态成员函数可以是虚函数吗?

    答:不可以,因为静态成员函数没有 this 指针,它的调用可以不借助于对象,而直接使用 :: 调用。违背了多态的概念,无法用于实现多态。

  3. 构造函数可以是虚函数吗?

    答:不可以,因为虚函数存在虚表中,要调用就需要虚表指针。但是虚表指针是在构造函数初始化列表才初始化的。也就是说,要调用构造函数就需要虚表指针,但是不调构造函数没有虚表指针,这就矛盾了,最终导致对象无法被创建。

  4. 析构函数可以是虚函数吗?

    答:可以,并且最好把基类的析构函数定义成虚函数, 具体参考本章:定义及实现/虚函数重写的例外/析构函数的重写。

  5. 对象访问普通函数快还是虚函数快?

    答:对于普通对象的调用是一样快的,如果是指针或引用的调用,则调用普通函数快,因为其与虚函数构成多态,运行时调用虚函数需要去虚函数表中去找。

  6. 虚函数表是在什么阶段生成的,存在哪?

    答:在编译阶段生成,存在常量区(代码段)。

http://www.lbrq.cn/news/1322443.html

相关文章:

  • 电子商务网站的建设/友情链接交换形式有哪些
  • 怎么做自己的设计网站/b2b免费推广平台
  • dw做网站怎么换图片/今天国内最新消息
  • 网站根目录验证文件在哪里/搜索引擎名词解释
  • 快速开发手机网站/国内搜索引擎排行榜
  • 网站建设注意什么/百度搜索引擎推广步骤
  • 中山小榄网站建设/整站优化排名
  • 沈阳做网站的科技公司/怎么快速刷排名
  • php网站 怎么取得后台管理权限/百度在线扫题入口
  • 慧聪网怎样做网站友情链接/开发一个app软件多少钱
  • 太原网站seo服务/seo服务公司怎么收费
  • 淘宝做网站的都是模板/seopeix
  • 怎么做下载类的网站吗/今天最新新闻10条
  • 规范网站建设/免费建网站的步骤
  • 网站顶级栏目403/公司推广策划
  • 领手工在家做的网站2019/长沙网站推广服务公司
  • php英文商城网站建设/网站公司
  • 公司网站建设费用多少/百度seo在线优化
  • 安顺网站建设公司/搜索引擎营销与seo优化
  • 东莞网站建设提供商/软件开发培训学校
  • wordpress多主题插件/seo推广服务
  • 西乡做网站的公司/西安专业网络推广平台
  • 做音乐网站的目的和意义/互动营销案例100
  • 盐田高端网站建设/seo如何优化网站推广
  • 合肥知名网站建设公司/aso优化app推广
  • 做文案应该关注的网站推荐/企业网站建设论文
  • 网站建设招代理/俄罗斯网络攻击数量增长了80%
  • 做一家开发网站的公司/搜索引擎营销广告
  • 前端和后端/整站优化案例
  • 涿州网站建设/凡科建站官网登录
  • 【文章素材】3dBackgroundBoxes(3D背景盒子组件)项目及文章思路
  • Gitee
  • 用 TensorFlow 1.x 快速找出两幅图的差异 —— 完整实战与逐行解析 -Python程序图片找不同
  • 因为想开发新项目了~~要给老Python项目整个虚拟环境
  • 脚手架搭建React项目
  • Python爬虫07_Requests爬取图片