C++进阶—多态

news/2024/11/9 0:34:53/

目录

0. 前言

1. 多态的概念

1.1 概念

2. 多态的定义及实现

2.1多态的构成条件

2.2 虚函数

2.3虚函数的重写

2.4 C++11 override 和 final

 3. 抽象类

3.1 概念

3.2 接口继承和实现继承

4.多态的原理

4.1虚函数表

4.2 变态选择题分析多态调用

4.3 多态的原理

4.4 动态绑定与静态绑定

5.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

5.2 多继承中的虚函数表

5.3. 菱形继承、菱形虚拟继承

 6. 继承和多态常见的面试问题

6.1 概念查考

6.2 问答题


0. 前言

需要声明的,以下代码及解释都是在vs2022下的x86程序中,涉及的指针都是4bytes。 如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题 等等

1. 多态的概念

1.1 概念

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

举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。

再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的 活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5 毛....。其实这背后也是一个多态行为。

        支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;

总结一下:同样是扫码动作,不同的用户扫 得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

2. 多态的定义及实现

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:

  1.         必须通过基类的指针或者引用调用虚函数
  2.         被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

2.3虚函数的重写

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

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*//*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同) 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

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. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{Person* p1 = new Person;Person* p2 = new Student; delete p1;delete p2;return 0;
}

2.4 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

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

class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};

上边代码会报错,无法对Drive进行重写!

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

class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

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

 3. 抽象类

3.1 概念

虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
void Test()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}

3.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

4.多态的原理

4.1虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在 虚表,虚表存在对象中。注意上面的回答的错的。但是很多都是这样深以为然的。注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的?

由上述可知:

只要存在virtual修饰的函数即虚函数,非纯虚类,在实例化对象时,便会额外包含一个指针,即指向VirtualFunctionsTable虚函数表的指针__vfptr。虚函数表指针指向的是一个数组,数组存放的元素为函数指针,因此虚表指针,本质是一个函数指针数组的首元素地址

4.2 变态选择题分析多态调用

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

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

 多态调用函数时,是在编译器运行时决议,通过虚表找到被调函数的地址!!!

4.3 多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket

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 Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}
  1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚 函数是Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调 用虚函数。反思一下为什么?
  5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

void Func(Person* p)
{p->BuyTicket();
}
int main()
{Person mike;Func(&mike);mike.BuyTicket();return 0;
}
// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
...p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE  mov         eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1  mov         edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE  mov         eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA  call        eax  
00头1940EC  cmp         esi,esp  
}
int main()
{
... 
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调
用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址mike.BuyTicket();
00195182  lea         ecx,[mike]
00195185  call        Person::BuyTicket (01914F6h)  
... 
}

4.4 动态绑定与静态绑定

  1.  静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载,普通函数调用
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
  3. 上边的多态汇编代码分析,即如果满足多态的调用原则,就会在运行时决议(动态多态),如果不满足多态的调用原则,编译器就会在编译时决议(静态多态),直接call地址
  4. 如果在使用原对象指针或者引用直接调用情况下,虽然不满足多态调用原则,但是如果其函数为虚函数,会生成虚函数表,并按照虚函数表指针调用,即运行时决议!!!

总结:在不使用多态时,尽量避免使用virtual,减轻在调用函数时编译器的负担!只要使用了virtual,无论是否是多态调用还是普通调用,都会生成一个虚表指针!

5.单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面已经看过了,没什么需要特别研究的

5.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;
};

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

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]);//vTable[i]();VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数//指针的指针数组,这个数组最后面放了一个nullptr// 1.先取b的地址,强转成一个int*的指针// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再//编译就好了。VFPTR * vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}

总结:

        若基类新增了虚函数,那么,编译器会将虚函数依次写入到其所继承的基类虚表中,如果对基类虚函数进行了重写,将会进行覆盖重写虚表中的同名同参同返回值(或协同)的函数的函数指针

5.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;
}

分析多继承下,其对象存储模型:

  1.  多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中!!!
  2.  多继承会产生两个虚表指针,一个属于Base1,一个属于Base2(和菱形虚拟继承相     似,产生两个虚基表指针记录偏移量),记录自身类域虚函数,以及重写覆盖!!!

5.3. 菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们一般我们也不需要研究清楚,因为实际中很少用。

如果好奇心比较强,可以去看下面的两篇链接文章。

C++ 对象的内存布局 | 酷 壳 - CoolShell

C++ 虚函数表解析 | 酷 壳 - CoolShell

 6. 继承和多态常见的面试问题

6.1 概念查考

1. 下面哪种面向对象的方法可以让你变得富有( )

        A: 继承         B: 封装         C: 多态         D: 抽象

2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关, 而对方法的调用则可以关联于具体的对象。

        A: 继承         B: 模板         C: 对象的自身引用         D: 动态绑定

3. 面向对象设计中的继承和组合,下面说法错误的是?()

        A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复 用,也称为白盒复用

        B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动 态复用,也称为黑盒复用

        C:优先使用继承,而不是组合,是面向对象设计的第二原则

        D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现

4. 以下关于纯虚函数的说法,正确的是( )

        A:声明纯虚函数的类不能实例化对象

        B:声明纯虚函数的类是虚基类

        C:子类必须实现基类的纯虚函数

        D:纯虚函数必须是空函数

5. 关于虚函数的描述正确的是( )

        A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型

        B:内联函数不能是虚函数

        C:派生类必须重新定义基类的虚函数

        D:虚函数可以是一个static型的函数

6. 关于虚表说法正确的是( )

        A:一个类只能有一张虚表        

        B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表

        C:虚表是在运行期间动态生成的

        D:一个类的不同对象共享该类的虚表

7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )         A:A类对象的前4个字节存储虚表地址,

        B类对象前4个字节不是虚表地址 B:A类对象和B类对象前4个字节存储的都是虚基表的地址

        C:A类对象和B类对象前4个字节存储的虚表地址相同

        D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

8. 下面程序输出结果是什么? ()

class A {
public:A(char* s) { cout << s << endl; }~A() {}
};
class B :virtual public A
{
public:B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1){cout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

A:class A class B class C class D         B:class D class B class C class A

C:class D class C class B class A         D:class A class C class B class D

9. 多继承中指针偏移问题?下面说法正确的是( )

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

A:p1 == p2 == p3         B:p1 < p2 < p3         C:p1 == p3 != p2         D:p1 != p2 != p3

10. 以下程序输出结果是什么()

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

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

6.2 问答题

  1. 什么是多态?答:多态就是指多种形态,不同对象做同一件事情,结果不同,使用父类对象的指针或者引用去调用!可以传父类对象,也可以传子类对象
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考本文内容
  3. 多态的实现原理?答:参考本文内容,去虚表找!
  4. 默认函数可以是虚函数吗???
    1. inline函数可以是虚函数吗?答:可以,不过inline只是一个建议,如果是多态调用,编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去找函数地址,call地址;非多态调用,便又可能构成inline。
    2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表,而static成员函数都是编译时决议,多态调用都是运行时决议,它是virtual没有价值。
    3. 构造函数可以是虚函数吗?答:不能,因为,virtual函数是为了实现多态,运行时去虚表找对应的虚函数进行调用,对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,因此无法在未初始化的虚表中找到构造函数。
    4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考本文内容
    5. 拷贝构造和赋值operator=可以是虚函数吗?答:1.拷贝构造不可以,但是拷贝构造是类名,继承关系下,类名不可能相同,是虚函数没用,最主要,拷贝构造也是构造,未初始化虚表中找不到拷贝构造!!!2.赋值语法上可以是虚函数,但是,赋值写成多态并没有什么意义,因为在继承关系中,类名不可能相同,即使赋值,参数不同,无法构成重写
  5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象调用,是一样快的。如果是指针对象或者是引用对象,则访问普通函数更快,因为构成多态,指针对象或者是引用对象需要在运行时调用虚函数,到虚函数表中去查找虚函数地址。
  6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针,而虚函数表一般情况下存在代码段(常量区)的。 
  7. C++菱形继承的问题?虚继承的原理?答:参考继承文内容。注意这里不要把虚函数表和虚基表搞混了。
  8. 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。


http://www.ppmy.cn/news/547568.html

相关文章

C++ 第五弹模板

目录 1. 泛型编程 2. 模板&#xff1a;参数类型化 3. 模板分类 4. 在模板参数类表中&#xff1a;class 和 typename 5.模板参数列表&#xff1a;template 6. 模板的特化&#xff1a;对模板进行特殊化的处理 7. 分离编译 1. 泛型编程 泛型编程是一种编程风格&#xff0c;其…

超赞的EPUB阅读软件推荐

超赞的EPUB阅读软件推荐 阅读是一个有修养的人所必备的习惯&#xff0c;纸制的书本的弊端在电子阅读的时代显现出来&#xff0c;一款好用的EPUB阅读器显然是必不可少的。还在为网上五花八门的EPUB阅读器选择而烦恼么&#xff1f;来看看这篇整合了最好用的EPUB阅读器推荐吧&…

最好用的几款epub阅读器

最好用的几款epub阅读器 放假在家的你是不是天天睡觉&#xff0c;打游戏&#xff1f;转眼间寒假已经过去大半&#xff0c;是时候该收收心开始学习了。当然学习不仅仅是上课学习到的知识&#xff0c;生活中有非常多的知识。什么方法是获取知识最快的方法&#xff0c;当然是读书…

mac上最好用的5款epub阅读器

mac上最好用的5款epub阅读器 现在是互联网快速发展的时代&#xff0c;就连阅读我们都可以借助电子设配来完成。喜欢看电子书的朋友一定对txt&#xff0c; pdf和epub等格式非常的熟悉&#xff0c;这些都是电子书的常用格式。 在众多的格式中&#xff0c;我常用的是epub格式&am…

Epub 阅读器 - iOS

因项目需求接触的 EPub 阅读器,前前后后尝试了很多库,最后找到了个相对兼容不错的展开了调试; 其中对解压缩和数据加载方面进行了改造优化,使其更加的完美; 其大概原理是首先将 epub 文件解压后得到其资源文件包,其中会包含相应的文件夹,通过 OEBPS 文件夹中的资源文件提取所需…

iosepub阅读器_epub格式电子书阅读器 iOS版

更新记录 0.1.2(2020-01-16) 1.更新一下注意点&#xff0c;提高一下容错率 0.1.1(2020-01-14) 1.修复本地缓存导致的位置错乱问题 查看更多 原生插件通用使用流程&#xff1a; 购买插件&#xff0c;选择该插件绑定的项目。 在HBuilderX里找到项目&#xff0c;在manifest的app原…

EPUB阅读APP推荐

EPUB阅读APP推荐 计算机和网络技术与传统产业相结合&#xff0c;产生了很多新鲜事物&#xff0c;诸如电子商务、网上社区。计算机和互联网技术与图书出版相结合&#xff0c;出现了电子书。电子书的出现对传统出版业产生了巨大的冲击。 电子书的载体、传播媒介和发行方式与传统…

实用epub阅读器分享

实用epub阅读器分享 相对于阅读纸质书&#xff0c;现在大部分年轻人还是更喜欢电子阅读的方式。基本上大家都会使用电脑和手机来看电子书。电子书格式很多&#xff0c;txt&#xff0c;pdf。mobi,exe,chm等等。epub格式是现在比较受欢迎的电子书格式&#xff0c;可以支持插图&a…