您的位置:首页 > 科技 > IT业 > 武侯区旅游网站建设_装潢设计公司排行_最近新闻热点_服务营销7p理论

武侯区旅游网站建设_装潢设计公司排行_最近新闻热点_服务营销7p理论

2025/3/31 2:13:18 来源:https://blog.csdn.net/2302_81120572/article/details/146206560  浏览:    关键词:武侯区旅游网站建设_装潢设计公司排行_最近新闻热点_服务营销7p理论
武侯区旅游网站建设_装潢设计公司排行_最近新闻热点_服务营销7p理论

目录

一、非类型模版参数

1.1、非类型模版参数的介绍

1.2、array

二、模版的特化

三、为什么模版不支持分离编译

四、继承

4.1、继承的概念

4.2、继承的定义和访问

4.3、基类和派生类之间的转换

4.4、继承中的作用域

4.5、派生类的默认成员函数

4.6、继承与友元

4.7、继承与静态成员

4.8、多继承及菱形继承

4.9、虚继承

4.10、继承和组合

五、多态

5.1、多态的概念

5.2、多态的定义和实现

5.2.1、多态的构成条件

5.2.2、虚函数

5.2.3、虚函数重写的两个例外

5.2.4、override 和 final 关键字

5.3、重载,重写,隐藏的对比

5.4、抽象类

5.4.1、概念

5.4.2、纯虚函数和抽象类

5.4.3、练习

5.4.4、虚表

5.5、多态的原理


一、非类型模版参数

1.1、非类型模版参数的介绍

假如我们要实现一个静态的栈,如图:

对于静态的栈而言,这里的 N 的大小是固定好的,上面的宏定义为10就是10,但是如果我们想要100块空间呢?那就需要重新再写一个栈,将这个栈空间大的大小定义为100,如果我们对于空间的大小还有更多需求又必须是静态的栈呢?这时我们手动来写太过麻烦,所以就可以使用非类型模版参数来解决这个问题。如图:

这里通过非类型模版参数我们就可以在实例化对象时指定当前静态数组的大小,这样只需要传一个非类型模版参数就可以控制栈的大小,非常方便,但是,其实在底层还是创建出了很多空间大小不一的栈,只不过这个过程不需要我们来写了,是由编译器根据模版自动生成的。其次,这个传入的非类型模版参数的值是固定的,也就是说它一旦传入就是个常量,在类内部是不允许修改的,如图:

结果:

这里如果我们只是创建这个对象而不去调用这个方法,这个问题有可能会暴露不出来,因为编译器会按需实例化,也就是说如果我们不调用这个类的方法,它就不会实例化出来它。

注意:在C++20以前只允许整型的非类型模版参数,在C++20以后允许 double 等内置类型,但是对于 string 这种自定义类型还是不可以。

1.2、array

C++中将静态数组封装成了一个类,在这个类中就使用了非类型模版参数,如图:

该类实例化时要指定存储的数据类型和数组的大小,且一旦实例化数组的大小就固定了。如图:

C语言中已经有数组了,且C++有兼容C语言,那为什么还要封装静态数组呢?实际上是为了检查越界问题,C语言中的数组检查越界很困难,这就导致了它检查越界并不严格,只有临近数组末尾的那几个位置可以较为精准的检测,所以C++将数组封装为一个类,这样重载它的操作符和一些方法实现的时候就可以进行严格的检查了。但是C++封装的静态数组也存在缺陷,它并不会主动初始化开辟每一块空间。实际场景中要使用数组时更多使用 vector。

二、模版的特化

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些 错误的结果,需要特殊处理,这时就可以使用模版特化。如图:

第一个模版函数是一个比较大小的函数,但是如果我们要比较一些特殊的类型可能会出错,例如比较我们之前实现的日期类的指针,我们希望传入指针可以比较出日期的大小,但实际上可能会出现错误的结果,因为这段逻辑传入指针时会比较指针的大小,所以我们特化出第二个版本,只要是Date* 都会走第二个函数,这样就可以得到正确的结果。

特化这里还有一些需要注意的特殊点(函数形参表必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误)。如图:

这里在第一个函数参数上加 const 和 & 修饰,这时特化版本的 const 和 & 要像图中特化版本这样放,否则就会报错,这是因为第一个函数中,const 修饰的是 x,当 x 是指针时,x 不可以改变,但是它指向的数据是可以修改的,所以特化版本的 const 也要修饰 x,但是如果放到 Date* 前面就是修饰指针指向的数据,x 本身是可以改变的,但 x 指向的数据就不可以修改了,这和第一个函数 const 修饰的含义不同,从而引发问题。

实际上,函数模版的特化容易引发一些意想不到的错误,所以在日常中如果要特殊化函数模版的某种类型,我们可以直接将这个类型参数的函数直接实现出来,构成重载。(只是针对函数模版特化,类模版特化使用起来还可以)如图:

类特化:

类特化也是针对某些特殊类型,进行特殊处理。如图:

偏特化/半特化:

上面写的都是将所有模版参数全部特化成具体的类型,这种叫做全特化,还可以特化一部分,叫做偏特化或者半特化。如图:

上述代码是偏特化的其中一种表现形式—部分特化,将模板参数类表中的一部分参数特化。偏特化一共有两种表现形式,第二种为:参数更进一步的限制,偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一 个特化版本。如图:

上图特化版本限定传入指针,也就是说如果我们传入的两个类型都是指针就会走这个特化版本,T1*,T2* 代表需要传入指针,但是推导出来的 T1 和 T2 不是指针,如图中所说,假如我们传入的是 int *,T1 和 T2 就是 int,T1* 和 T2* 才是指针,这样设计使我们使用也更加方便,需要使用指针就使用 T1* 和 T2*,需要使用正常的类型可以直接使用 T1 和 T2。除了限定传入指针类型,限定传入引用也是可以的,只需要将 T1* 和 T2* 更改为 T1& 和 T2& 即可。

三、为什么模版不支持分离编译

声明:(在Func.h文件中)

定义:(在Func.cpp文件中)

首先我们在分离编译的情况下调用一下正常的函数,如图:(在test.cpp文件中)

从图中可以看出,正常的函数在分离编译的情况下是可以正常调用的,这是为什么呢?首先函数调用的语句在变成汇编代码的时候会变成 call 语句,call 后面加函数的起始地址,而这个起始地址就是函数被编译后的第一条指令(语句)的地址,包含了函数定义的 .cpp 文件会被编译从而生成函数的地址,再将这个地址放入符号表中,而头文件不会被编译,头文件通过 include 被包含到会使用这些函数的文件中,在预编译时头文件就会在这些文件展开,这样使用函数的地方虽然没有定义但是有声明,这样编译器就知道有这个函数,等到使用的时候就会上符号表中去找这个函数的地址,进而完成调用。

而模版函数则不可以,如图:

这是因为模版跟普通函数相比,模版还需要实例化,但是分离编译过程中,在函数调用的地方虽然知道模版实例化成什么,但只有声明,没有定义,无法实例化,在函数定义的地方,又不知道实例化成什么(因为不同 .cpp 文件在编译时是不会进行交互的),不实例化就不会被编译,不会生成指令,也就没有函数地址,这样符号表中没有地址,链接的时候也就找不到这个函数,所以就会报错。这个问题可以通过显式实例化解决,但是这个方法并不常用,因为不方便,所以最好的解决这个问题的办法是使用模版时不要进行分离编译。显示实例化:(在 Func.cpp 文件中)

四、继承

4.1、继承的概念

继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称为派生类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。

下面我们看一个例子,如图:

上图中学生和教师类继承了人这个类,这样人这个类中所有的东西在学生和教师类里虽然没有写,但是已经都具有了,因为继承过来了,需要注意的是成员变量虽然继承过来了,但各自的对象都独立有这样一份成员变量,使用起来互不影响,而对于继承过来的成员方法都使用同一份(构造函数除外)。如图:

先在 person 类中增加这个方法,并将所有成员公有(student也公有),方便后面使用。

这里可以看到即使是继承的成员变量,也是各自对象都独立有一份自己的,使用起来不会互相影响

通过转到汇编代码可以看出,继承的函数使用的都是同一份,因为它们的函数地址都是相同的。

4.2、继承的定义和访问

被继承的类叫做基类,也叫父类,如 person,而继承了别人的类叫做派生类,也叫做子类,如 student。继承语法如图:

访问限定符有三种,继承方式也有三种,如图:

父类成员的访问限定符和子类的继承方式会作用于子类继承过来的成员的被访问权限(也就是说,子类继承过来的成员被哪种访问限定符修饰取决于父类的访问限定符和子类继承时的继承方式),如图:

我们先看基类是 private 成员的,这种情况下,无论以何种方式继承,对于基类私有的部分在子类中都是不可见的,这里的不可见指的是无法访问,但仍然继承下来了,只是无法使用而已。如图:

但这里的无法使用是相对的,并不是绝对的,虽然在子类中确实不可以使用,但是可以通过父类提供的方法进行间接的访问和使用。如图:(Print 方法在上图父类中有写)

当基类成员不是私有时,子类继承过来的成员被哪种访问限定符修饰取决于基类访问限定符和继承方式中权限小的那一个,哪个权限小,继承过来的成员就被哪种访问限定符修饰。

总结:对于父类私有成员,子类无论哪种继承都无法访问,对于父类其他成员,继承后被哪种访问限定符修饰取决于父类成员访问限定符和继承方式中权限小的那一个。

从继承这里我们也可以看出 protect 的意义:正是因为有了继承,protect 才有意义,对于父类不想让外界访问也不想让子类访问的成员,父类可以用 private 修饰,对于父类不想让外界访问,但想让子类访问的成员可以用 protect 修饰。

使⽤关键字 class 时默认的继承⽅式是private,使⽤ struct 时默认的继承⽅式是public,不过最好显示的写出继承⽅式。在实际运⽤中⼀般使⽤都是 public 继承,几乎很少使⽤ protetced/private 继承,也不提倡使⽤ protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。

4.3、基类和派生类之间的转换

public 继承的派⽣类对象可以赋值给基类的对象/基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。如图:(还是使用上面的 Person 和 Student 类)

注意:基类对象不能赋值给派⽣类对象。

4.4、继承中的作用域

(1)在继承体系中基类和派⽣类都有独⽴的作⽤域。

解释:这是因为基类和派生类属于不同的类,而在C++中是有类域的概念的,不同的类域相互之间属于独立的作用域。所以基类和派生类都有独立的作用域。

(2)派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显示访问)

从上图代码可以看出,父类和子类中定义同名成员不会报错且程序可以正常运行,这是因为同一作用域中不可以定义同名变量,但是父类和子类是不同的类域,属于不同作用域,可以定义同名变量。且从图中可以看到,打印 _num 变量时打印的是子类里的,这是因为在子类中使用成员变量会优先在子类中搜索,如果找到就使用,如果没找到才会去父类中查找,父类中没有就会在全局域中进行查找。如果还没有就会报错了。不过隐藏的父类成员并不是不可以使用了,如果需要使用可以通过指定类域的方式进行指定调用父类中的某个成员。如图:

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。如图:

注意在实际中继承体系⾥⾯最好不要定义同名的成员。

4.5、派生类的默认成员函数

父类:

子类:

构造函数:

在子类中,我们将其继承过来的父类成员当成一个整体,子类生成的默认构造中,对于自己的成员变量还是遵循内置类型是否初始化看编译器,自定义类型调用该自定义类型的构造,而继承来的父类成员会调用父类的构造函数进行初始化。且编译器自动生成的子类的默认构造只能调用父类的默认构造(不需要传参的)。

如果想自己写子类的构造,对于父类的成员我们不可以自己直接初始化,必须调用父类的构造进行初始化,且必须先初始化父类成员(因为子类成员可能会用到父类成员进行初始化,不先初始化父类成员,父类成员是随机值,导致用到父类成员的子类成员也是随机值)。如图:

拷贝构造:

如图:在子类生成的默认拷贝构造中,内置类型还是执行浅拷贝(值拷贝),自定义类型调用该自定义类型的拷贝构造,对于父类成员调用父类的拷贝构造。在没有资源需要申请的情况下,使用默认生成的拷贝构造就足够了,但是如果需要进行资源申请,就需要自己写。如图:(这里只演示如何写拷贝构造,并没有进行资源申请)

这里切割就派上用场了,直接将传进来的子类对象传入父类构造中,会进行切割,切割出来的正是子类对象中父类的那一部分,根据形参匹配规则,传入父类对象会调用父类的拷贝构造,进而完成子类中父类部分的拷贝。

赋值重载:

赋值重载和拷贝构造类似。如图:

析构函数:

默认生成的析构函数对于内置类型不做处理,自定义类型去调用该自定义类型的析构,继承来的父类成员会调用父类的析构进行处理,那如果我们想在自己写的析构函数中调用父类的析构呢?如图:

正常来说,子类继承父类后,父类里的方法子类是可以直接调用的(私有除外),例如上面的子类构造函数中就直接调用了父类构造函数,那为什么这里需要指定类域呢?这是因为由于多态,析构函数的名字被统一处理为了 destructor(),所以父类和子类的析构函数会构成隐藏,所以需要指定类域但是实际上如果我们需要自己写子类的析构函数时,我们不可以在子类的析构中调用父类的析构,因为编译器会自动的在调用完子类的析构后去调用父类的析构,这样做是为了保证先析构子类,在析构父类,之所以要保证这个顺序是因为子类析构中可能用到父类资源,如果在子类中显示调用父类析构,父类一定在子类所有成员都析构之前析构了,这时如果要用到父类资源,就可能会出问题。

结论:子类析构中直接释放子类资源就行,父类资源会在子类析构后编译器自动调用父类析构进行释放。

如图:

从上面图中,我们就可以看到子类析构后编译器自动调用了父类析构,且构造函数是先调用父类再调用子类,析构函数是先调用子类,再调用父类。

4.6、继承与友元

友元关系不能继承,也就是说父类的友元不能访问子类私有和保护成员。如图:

这里 Student 类需要前置声明,否则会报错,这是因为函数的友元声明在 Student 类上面,函数的形参需要这个类的对象,而查找这个类时会向上查找,但是这个类在它的友元声明下面,就会找不到这个类,所以需要对 Student 这个类进行前置声明。

从上图中我们还可以看出父类的友元不会被子类继承,所以在函数中访问子类保护的成员时会报错。

4.7、继承与静态成员

基类定义了 static 静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个 static 成员实例。如图:

4.8、多继承及菱形继承

单继承:一个派生类只有一个直接基类时称这个继承关系为单继承。

多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是先继承的基类在前面,后继承的基类在后面,派生类成员放到最后面。

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

菱形继承代码:

二义性问题:

这里 Assistant 继承了 Student 和 Teacher 类,并且 Student 和 Teacher 又都继承了 Person 类,这导致 Assistant 类中有两个 Person类,a 对象直接调用 Person 类中的 _name 属性时由于不知道是哪个 Person 中的,导致报错(调用不明确)。

数据冗余问题:

我们可以通过指定类域访问来解决二义性问题,但是这又会导致数据冗余问题。上面代码将从Student 和 Teacher 类继承来的 _name 分别进行了赋值,但是实际上我们只需要一份就可以。

4.9、虚继承

因为多继承会导致菱形继承,而菱形继承会导致二义性和数据冗余,所以C++引入了虚继承来解决这个问题,如图:

从上图可以看出虚继承解决了菱形继承带来的问题。

上图这种也是菱形继承,如果想要通过虚继承解决菱形继承的问题,需要将 virtual 关键字加在 B和 D上,因为 A 的冗余最开始就是由它们两个造成的。

4.10、继承和组合

(1)public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。

(2)组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。

组合如图:

在实际编码中,更推荐使用组合。原因:

(1)继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤ (white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可⻅。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度⾼。

(2)对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse), 因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于保持每个类被封装。

(3)优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系更适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。

五、多态

5.1、多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这里我们主要了解运⾏时多态。编译时多态(静态多态)主要就是我们前⾯的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。

5.2、多态的定义和实现

5.2.1、多态的构成条件

多态是⼀个继承关系下的不同类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如 Student 继承了 Person。Person 对象买票全价,Student 对象优惠买票。

多态实现的两个必要条件:

(1)必须是基类的指针或者引⽤调⽤虚函数。

(2)被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。

说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派⽣类之间才能有不同的函数,多态的不同形态效果才能达到。

5.2.2、虚函数

类成员函数前⾯加 virtual 修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加 virtual修饰。如图:

虚函数的重写/覆盖:

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。如图:

这里我们封装一个通过父类的引用来调用买票方法的函数,我们通过父类和子类的对象对这个函数的调用来看多态的效果。如图:

要想构成多态,必须满足构成多态的两个必要条件,少一个都不可以。调用函数时使用父类的指针或者引用都可以。如图:

另外,实际上子类在重写虚函数时 virtual 关键字可以不写。但是父类必须写,因为如果父类不写这个函数就不是虚函数了。如图:

重写和隐藏的关系:

重写其实是一种特殊的隐藏,因为重写需要函数名,函数形参列表,函数返回值类型都相同,但是只要函数名相同就构成了隐藏,所以重写可以理解成一种规则更加苛刻的隐藏,重写在使用起来,如果满足多态规则,就会展现出多态的效果,如果不满足多态规则,就会展现出隐藏的效果。如图:(这里 func 是上面图中通过父类引用调用购票方法的函数)

5.2.3、虚函数重写的两个例外

协变:派⽣类重写基类虚函数时,与基类虚函数返回值类型可以不同。但基类和派生类返回的对象必须是具有继承关系的类的指针或者引用,且基类必须返回继承关系中基类的指针或者引用,派生类必须返回继承关系中派生类的指针或者引用。如图:

析构函数的重写:基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀为 destructor,所以基类的析构函数加了 vialtual 修饰,派⽣类的析构函数就构成重写。

析构函数重写的意义:我们可以看下面一段代码

上面代码中,子类 Student 中存在资源申请,但是如果我们通过 new 来给子类对象申请空间并交给父类指针管理时,析构该指针时调用的是父类的析构函数,这样就造成了子类中申请的资源没有被释放,进而导致内存泄漏。那么将父类析构函数写成虚函数,让父类和子类析构函数构成重写呢?如图:

在继承关系中,父类析构函数写成虚函数并与子类析构函数构成重写还是比较有意义的,但是正常类的析构函数没有必要写成虚函数。

5.2.4、override 和 final 关键字

有些情况下由于疏忽,⽐如函数名写错,参数写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。使用:(override关键字要放到子类里面

这里构成重写,则不会有任何问题。

这里没有构成重写,会报错。

如果我们不想让派⽣类重写这个虚函数,那么可以⽤ final 去修饰。final 关键字放到父类中。如图:

5.3、重载,重写,隐藏的对比

5.4、抽象类

5.4.1、概念

在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。

5.4.2、纯虚函数和抽象类

下图中 A 类中的 func 为纯虚函数,所以 A 为抽象类。抽象类不允许实例化对象。

虽然抽象类不可以实例化出对象,但是我们可以写一个类来继承它,并重写里面的纯虚函数,这时这个子类是可以实例化出对象并可以调用重写后的方法的。如图:

5.4.3、练习

我们看下面一段代码,思考它的运行结果是什么?

结果:

解释:

首先 B 类继承了 A 类,且 A 类中有 test 方法,B 类中没有,所以 p 指针调 test 方法的时候会去 A 类中调用,A 类的 test 方法中又调用了 func 方法,类的成员函数是有一个隐含的 this 指针的,这个 func 方法就是通过这个 this 指针调用的,又因为是在 A 类的方法中调用的 func 方法,所以这个 this 指针类型是 A*,这样就满足了多态的条件了,首先 func 方法是虚函数,且完成了重写,其次调用 func 函数的指针是父类指针,这样根据多态的规则,这里会调用 B 类的 func 方法,但是多态调用的本质其实是使用了父类虚函数的函数头加上子类的函数体进行调用的,所以这里 val 是父类函数头中的值,即为1,函数体是子类的,所以是 B->。

5.4.4、虚表

我们先看一段代码和它的运行结果,如图:

根据内存对齐规则,这里算出来的 Base 类的大小应该是8,为什么实际打印出来是12呢?这是因为这个类中有虚函数,而虚函数的地址都存放在一个叫做虚表的东西里,而为了能够找到这个虚表,含有虚函数的类中会额外多出一个指针,这个指针指向虚表。这段代码是在x86环境下跑的,所以指向虚表的指针大小为4个字节。我们可以通过监视窗口来看这个指向虚表的指针。如图:

b 是为了观察指针创建的 Base 类的对象。

5.5、多态的原理

我们可以结合上图进行理解多态的原理,实际上父类和子类中各有一个虚函数的指针,它们分别指向各自的虚表,父类虚表中存储着父类的所有虚函数的地址,子类虚表中对于没有进行重写的虚函数存放的地址和父类虚表中该函数的地址是相同的,即对于没有进行重写的虚函数不会生成多份,在子类和父类虚表中会指向同一个函数,而对于重写了的虚函数,子类虚表中会存放重写后的虚函数的地址。这样如果是父类对象通过父类指针调用虚函数,就会通过该父类对象的虚表指针找到父类的虚表进而找到父类的虚函数,而如果是子类对象通过父类指针调用虚函数,会发生切割,而切割出来传给父类指针的那一部分中就包含了子类自己的虚表的指针,这样根据虚表指针找到的就是子类自己的虚表,进而调用子类重写的虚函数,从而展现出多态的效果。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com