文章目录:
🎭什么是多态?
多态通常来说就是多种形态。当类之间存在层次结构,且类之间通过继承关联时,就会用到多态。 c++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。具体点来说就是去完成某个行为,不同的对象去完成时会产生不同的状态 |
🪁多态在生活中也有很多应用:比如我们出行买票的时候,普通人买票时买全价票,学生买学生折扣票,军人则优先买票。或者是我们打游戏的时候匹配机制会根据我们的游戏等级来匹配相应的对手。
即不同的对象去完成同样的一件事会得到不同的结果,这也是一种多态的行为。
🎭多态定义及其实现
构成多态的条件
🧿多态是在不同继承关系的类对象去调用同一个函数,产生了不同的结果。如Student类和Solider类去继承了Person类。Person对象全价买票,Student对象折扣买票,Solider对象优先买票。
静态的多态 — 函数重载
动态的多态 — 基类的指针或者引用调用虚函数
1.基类的指针或者引用指向基类,调用的就是基类的虚函数。
2.基类的指针或者引用指向哪一个派生类,就调用的是那个被指向派生类的虚函数。
以下是构成多态所需要的条件:
- 类之间必须是继承关系。
- 被调用的函数必须是虚函数,且派生类需要对基类的虚函数进行重写。
- 通过基类的指针或者引用调用虚函数。
虚函数的重写
虚函数:被virtual修饰的类成员函数叫做虚函数。
🎮虚函数的重写(覆盖):派生类中有一个与基类完全相同( 派生类虚函数与基类的虚函数的返回值类型、函数名、参数列表完全相同 )的虚函数,称为派生类的虚函数重写了基类的虚函数。
class Person
{
public:
virtual void BuyTicket() { cout << "普通票 - 全价" << endl; }
};
class Student :public Person
{
public:
virtual void BuyTicket() { cout << "学生票 - 七折" << endl; }
};
class Soldier:public Person
{
public:
virtual void BuyTicket() { cout << "军人优先买票" << endl; }
};
// 传不同类型的对象,调用的是不同的函数,实现了调用的多态性
void function(Person* p)
{
p->BuyTicket();
}
void function(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student st;
Soldier so;
// 通过基类的引用调用虚函数
function(p);
function(st);
function(so);
// 通过基类的指针调用虚函数
function(&p);
function(&st);
function(&so);
return 0;
}
上诉代码测试:
🎃注意:
1、只有类的非静态成员函数可以是虚函数。
2、虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何联系。
虚函数的重写也有两个例外:
1.协变(基类与派生类的虚函数返回值类型不相同)
🎊派生类重写基类虚函数时,与基类虚函数返回值类型不同。在c++中,只要原来的返回类型是指向基类的指针或者引用,新的返回类型是指向派生类的指针或者引用,覆盖的方法就可以改变返回值类型。这样的类型称为协变返回类型(Covariant returns type)。
class A
{};
class B:public A
{};
class Person
{
public:
virtual A* function()
{
cout << "vitual A* Person::function()" << endl;
return new A;
}
};
class Soldier:public Person
{
public:
virtual B* function()
{
cout << "virtual B* Student::function()" << endl;
return new B;
}
};
int main()
{
Person p;
Soldier so;
Person* ptr;
ptr = &p;
ptr->function();
ptr = &so;
ptr->function();
return 0;
}
正常的虚函数重写要求虚函数:函数名、参数、返回类型都要相同,但是协变是一个例外,这里我们只需要了解以下即可。
2.析构函数的重写(基类与派生类析构函数的名字不相同)
🎊若基类的析构函数为虚函数,派生类析构函数只要定义了,无论是否加上virtual关键字,都与基类的虚构函数构成重写,即使派生类与基类的析构函数名并不相同。因为这里编译器对析构函数的名称进行了特殊的处理,编译后析构函数的名称被统一处理为destructor。所以基类的虚构函数与派生类的析构函数构成重写。
class Person
{
public:
// 这里建议把基类的虚函数定义为虚函数,方便派生类去重写基类的虚函数
virtual ~Person()
{
cout << "virtual ~Person()" << endl;
}
};
//Student和Person的析构函数名虽然不同,但是编译器编译时把析构函数名统一处理destructor,所以它们构成虚函数重写
class Student :public Person
{
public:
virtual ~Student()
{
cout << "virtual ~Student()" << endl;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1; // p1 -> 析构函数() + operator delete(p1)
delete p2; // p2 -> 析构函数() + operator delete(p2)
return 0;
}
❓在上述代码中如果析构函数不加virtual会怎么样?
✔️加上virtual后的析构函数
注意: 派生类重写基类的虚函数可以不加virtual,也构成重写。但是基类重写的函数就必须加virtual,因为派生类继承基类的虚函数下来之后有了virtual属性,派生类只是重写virtual这个函数。
建议我们自己写的时候,派生类和基类重写的函数都加上virtual关键字。
c++11新玩法 override 和 final
🕹️以上可看出,c++对虚函数重写的要求很严格,在一些情况下,我们可能忘记或者写错函数名称,导致其不符合重写的规则,在程序编译时我们无法发现其中的错误,而程序运行时发生错误。因此:c++11新增了 override 和 final 两个关键字,可以帮助我们检查程序是否完成重写。
override关键字 |
c++多态(polymorphism)是通过虚函数来实现的,虚函数允许派生类重新定义虚函数,而派生类重新定义基类就叫做覆盖(override),也可以叫做重新。override的作用是检查派生类虚函数是否重写了基类的某个虚函数,如没有重写则编译报错。
🪀override告诉编译器Student::void character()
是要重写基类的某个函数,但是编译器在它所继承的基类中没有找到void character()
函数,所以就会编译报错。override直接告诉编译器用于重写基类的某个虚函数。
final关键字 |
1️⃣ 禁止继承,c++11中,将类用final修饰表示无法继承。
2️⃣ final修饰虚函数时,表示该虚函数无法被重写。
重载、重写(覆盖)、隐藏(重定义)对比
🎭抽象类
🧩包含纯虚函数的类称为抽象类(也叫接口类),一个抽象类只是有一个纯虚函数。抽象类只能作为基类派生出新的子类,而抽象类不能实例化出对象,但是可以使用指向抽象类的指针。派生类继承后也不能实例化出对象,只有重写虚函数,派生类才能够实例化出对象。在开发中可以通过纯虚函数建立接口,让程序员自己填写接口实现。纯虚函数还规范了派生类必须重写。纯虚函数体现了接口继承。
纯虚函数:在虚函数的后面加上 =0,则这个函数就是纯虚函数。纯虚函数是被标明为不具体实现的虚函数成员,不具有函数功能。纯虚函数不能被直接调用,只能提供一个与派生类相一致的接口。
纯虚函数的声明:virtual 返回类型 函数名(参数列表)=0
class Person
{
public:
// 纯虚函数
virtual void Dity() = 0;
};
class Student:public Person
{
public:
virtual void Dity()
{
cout << "学习" << endl;
}
};
class Soldier :public Person
{
public:
virtual void Dity()
{
cout << "保卫祖国" << endl;
}
};
int main()
{
Person* st = new Student;
st->Dity();
Person* so = new Soldier;
so->Dity();
return 0;
}
注意:抽象类只能作为基类使用,不能实例化出抽象类的对象。
接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类的函数,可以使用函数,继承的主要是函数的实现。虚函数的继承是一种接口继承,派生类继承的是虚函数的接口,目的是为了派生类重写基类虚函数,达到实现多态的目的,所以继承的是基类的接口。若我们不需要实现多态,那么我们没有必要将函数定义为虚函数。
在类的设计中,应遵循以下规则:
- 纯虚函数:派生类必须实现的函数,在基类中实现是没有意义的,因为基类拥有纯虚函数的基类不能实例化出对象。
- 虚函数:继承类必须有的接口,可以自己实现,也可以不实现用基类的。
- 普通函数:继承类函数有的接口,需使用基类实现。
🎭多态的原理
虚函数表
❓首先我们来看这样一道笔试题:计算sizeof(Base)是多少?
❓原本应该是4个字节,为什么是8个字节呢?下面我们在看一个代码:
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 = 0;
};
class Derive : public Base
{
public:
virtual void Func1() { cout << "Derive::Func1()" << endl; }
private:
int _d = 1;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过监视窗口来看一下:
通过观察测试,发现以下问题:
- 派生类对象d中也存了一个虚表指针,d对象由两部分构成,一部分是基类继承下来的函数和成员,另一部分是自己原有的成员及函数。
- 基类对象b和派生类对象d中的虚表是不一样的,我们对基类中的虚函数Func1完成了重写,所以d中虚表存的是重写的Derive::Func1(),所以虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法层面的说法,覆盖则是原理层面的说法。Func2()在基类中是虚函数,被派生类继承下来也是虚函数,所以放进了虚表。Func3()在基类中不是虚函数,所以被派生类继承了也不是虚函数。
- 派生类中虚表的生成:将基类的虚表内容拷贝一份到派生类的虚表中,若派生类对基类的某个虚函数进行了重写,则用派生类重写的虚函数覆盖虚表中基类的那个虚函数,派生类自己独有的虚函数按其在派生类中的声明次序添加到派生类虚表的后面。
- 虚函数表本质是一个存放虚函数指针的指针数组,在vs下这个数组最后面放了一个空指针(nullptr),但是在其它平台不一定是这样做的。
❓那么多态到底是怎么实现的呢?
class Person
{
public:
virtual void BuyTicket() { cout << "普通票 - 全价" << endl; }
};
class Student :public Person
{
public:
virtual void BuyTicket() { cout << "学生票 - 七折" << endl; }
};
void function(Person* p)
{
p->BuyTicket();
}
int main()
{
Person p;
Student st;
function(&p);
function(&st);
return 0;
}
🎾所以多态的实现是依靠不同对象的虚函数指针指向的虚函数表实现的,因为里面存储了对应的虚函数。如下图所示:
🔮要构成多态,我们需要两个条件:1.基类虚函数重写 2.对象的指针或者引用调用虚函数。满足多态条件后,构成多态。对象的指针或者引用调用虚函数时,不是编译时决议,而是运行时决议,即程序开始运行时到指定的对象虚函数表中去调用对应的虚函数,所以指向基类对象,调用是基类虚函数,指向派生类对象,调用是派生类虚函数。
这里思考一个问题,为什么多态的条件之一是基类的指针或者引用调用,而基类对象确不可以呢?
void function(Person* p)
{
p->BuyTicket();
}
void function(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student st;
function(&p);
function(&st);
return 0;
}
🎃使用基类的指针或引用时,实际上是一种切片行为,切片时会让基类指针或者引用指向指针或者引用基类和子类的那一部分。所以,我们用p和st调用虚函数时,它们通过虚表指针找到的虚表和虚函数是不一样的,因此实现了多态。
Person p1;
Person p2;
🎃使用基类对象时,派生类切片只会拷贝成员变量给基类,不会将vfptr拷贝过去,拷贝过去就混乱了。基类对象中到底是基类的虚函数表指针还是派生类的虚函数表指针都是有可能的,那么我们调用基类的虚函数还是派生类的虚函数就不确定了。无法构成多态。
总结: 构成多态,指向那个对象就调用哪个对象的函数,与对象有关。不构成多态,对象是什么类型就调用哪个函数,跟类型有关。
问:虚表是存储在哪里的呢?(栈、堆、数据段(静态区)、代码段(常量区))
为了确定虚函数表存储在哪里,我们写一个代码验证以下:
思路:取虚函数表地址依次与其它存储区变量的位置比对确定。
class Base
{
public:
virtual void Func1()
{
cout << "virtual Base::Func1()" << endl;
}
};
int j;
int main()
{
//取虚表的地址打印一下 - 对象的前4/8个字节就是虚表的地址
Base b;
Base* p = &b;
printf("vfptr:%p\n", *((int*)p));
int i;
printf("栈上地址:%p\n", &i);
printf("数据段上地址:%p\n", &j);
int* k = new int;
printf("堆上地址:%p\n", k);
const char* ch = "ab";
printf("代码段上地址:%p\n", ch);
return 0;
}
运行结果:
🎯结论:由此可以看出,虚函数表存储在:代码段(常量区)。
静态绑定与动态绑定
静态类型:对象在声明时所用的类型,在编译时确定。
动态类型:通常指一个指针或者引用目前指向对象的类型,在程序运行时决定。
静态绑定:静态绑定又称为早期绑定,在程序编译期间确定了程序的行为,也叫做静态多态。例如:函数重载。
动态绑定:动态绑定又称为后期绑定,在程序运行时,根据拿到的类型确定程序的具体行为,调用具体函数,也叫做动态多态。
🧩下面我们通过买票例子的汇编代码来具体理解一下动态绑定与静态绑定:
class Person
{
public:
virtual void BuyTicket() { cout << "普通票 - 全价" << endl; }
};
class Student :public Person
{
public:
virtual void BuyTicket() { cout << "学生票 - 七折" << endl; }
};
🎯按照以下方式调用基类和派生类中的BuyTicket函数,不构成多态,因此是在编译时确定函数地址:
int main()
{
Person p;
Student st;
p.BuyTicket();
st.BuyTicket();
return 0;
}
如下图所示,不构成多态,编译器直接调用函数的地址。即编译时确定了函数的地址。
🎯按照下列构成多态的方式调用BuyTicket函数,函数地址是运行时确定的。
void function(Person* p)
{
p->BuyTicket();
}
int main()
{
Person p;
Student st;
function(&p);
function(&st);
return 0;
}
如下图所示,构成多态,编译器运行时确定调用。相比于不构成多态时的调用,多态的调用翻译成汇编指令之后就多了几条指令,因为在运行时,先到指定对象的虚函数表中找到要调用函数的地址,然后进行函数调用。
这样我们就可以很好的理解动态绑定和静态绑定了,动态绑定即运行时决议,静态绑定即编译时决议。
动态绑定是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联与具体的对象。
🎭单继承与多继承关系的虚函数表
单继承中的虚函数表
以下列单继承为例,来看一看派生类和基类的虚函数表模型。
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int _a;
};
class Drive
{
public:
virtual void func1() { cout << "Drive::func1" << endl; }
virtual void func3() { cout << "Drive::func3" << endl; }
virtual void func4() { cout << "Drive::func4" << endl; }
private:
int _d;
};
int main()
{
Base b;
Drive d;
return 0;
}
通过监视窗口我们看见了基类和派生类的虚函数表,但是却不能看见派生类中的func3()和func4()这两个虚函数,因为监视窗口对其进行了处理,隐藏了真实的情况。那我们应该怎样查看d完整的虚表呢?
1️⃣ 通过内存窗口查看
内存窗口呈现了派生类虚表指针的真实情况,让我们看到了完整的虚表。但是这里我们保持怀疑的态度,最后两个地址到底是不是存储的func3()和func4()两个虚函数的调用方法呢?所以呢我们接下来要通过另外一种方式来验证。
2️⃣ 打印虚表的内容
使用以下代码打印基类和派生类对象虚表中存储的内容,用虚函数地址调用对应的虚函数打印虚函数的函数名,明确最后两个地址到底是不是存储的func3()和func4()两个虚函数的调用方法。
//对虚函数指针类型进行重命名
typedef void(*VFPTR)();
void PrintVFTable(VFPTR* ptr)// 函数指针的数组指针
{
printf("虚表地址:%p\n", ptr);
// 依次取虚表中虚函数指针打印并调用,观察调用的哪一个函数
// 在vs平台下,虚函数表最后一个值存的空指针
for (size_t i = 0;ptr[i] != nullptr;++i)
{
printf("[%d]:%p -> ", i, ptr[i]);
ptr[i]();
}
cout << endl;
}
int main()
{
//怎样取到虚表指针的地址?
//
// 在32位平台下,取出对象的头4byte,就是虚表的指针,虚表是一个存虚函数指针的指针数组,
// 这个数组的最后放了一个空指针(nullptr)。因此我们先取到对象的地址,强制转换为int*的指针,解
// 引用就可以取到头4byte的值,这个值指向虚表指针,然后转换为函数指针类型VFPTR*,虚表指针
// 传递给函数即可。
Base b;
PrintVFTable((VFPTR*)(*(int*)&b));
Drive d;
PrintVFTable((VFPTR*)(*(int*)&d));
return 0;
}
运行测试:
❗注意:在vs编译器下,我们打印虚表的时候是以空指针为结尾的标志的,但是vs编译器有时候对虚表的处理不干净,最后不是以空指针结尾,导致程序运行崩溃。这里我们的解决方法是:点击目录栏 - 生成 - 清理解决方案,然后再一次编译运行就可以了。这是vs编译器存在的一个问题。
✅单继承中派生类虚函数表是怎样生成的?
- 派生类继承基类的虚函数表。
- 对派生类中重写的虚函数地址进行覆盖。
- 将派生类中非继承而来的虚函数地址新增到虚表中。
多继承中的虚函数表
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 Drive:public Base1,public Base2
{
public:
virtual void func1() { cout << "Drive::func1()" << endl; }
virtual void func3() { cout << "Drive::func3()" << endl; }
private:
int _d;
};
通过监视窗口查看虚表,我们没有看到虚表中存储Drive::func3()。派生类Drive继承了两个基类的虚表,对于派生类重写的虚函数func1()进行了覆盖。
监视窗口显示被处理,显示不全,所以我们通过以下两种方法查看基类和派生类中虚表存储的真实情况:
1️⃣ 通过内存窗口查看
2️⃣ 打印虚表的内容
typedef void(*VFPTR)();
void PrintVFTable(VFPTR* ptr)
{
cout << "虚表地址 :" << ptr << endl;
for (size_t i = 0;ptr[i] != nullptr;++i)
{
printf("[%d]:%p -> ", i, ptr[i]);
ptr[i]();
}
cout << endl;
}
int main()
{
Base1 b1;
PrintVFTable((VFPTR*)(*(int*)&b1));
Base2 b2;
PrintVFTable((VFPTR*)(*(int*)&b2));
Drive d;
PrintVFTable((VFPTR*)(*(int*)&d)); // 打印d的第一个虚表地址和内容
// 指向第一个虚表的地址向后移Base1大小个字节就指向Base2了
PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1)))); //打印d的第一个虚表地址和内容
return 0;
}
运行结果:
✅多继承中派生类虚函数表是怎样生成的?
- 派生类按继承声明顺序将基类各虚表内容继承到派生类的虚表中去。
- 对派生类中重写的虚函数地址进行覆盖。
- 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
注意:菱形虚拟继承也是多继承中的一种继承方式,但在实际工程中不建议设计出菱形继承和菱形虚拟继承,不仅太复杂程序容易出问题,而且这种模型访问基类的成员或者函数有一定的性能消耗。若对菱形继承内存布局感兴趣,这里推荐一篇大佬的文章。
🎭多态经典题目讲解
1、下面程序输出结果是什么 ?(A)
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
class A
{
public:
A(const char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A
{
public:
B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(const char* s1, const char* s2, const char* s3, const 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;
}
题目分析:
每个字符串的首地址分别传到D类拷贝构造函数的形参中,先初始化基类,且初始化列表执行顺序与继承声明顺序有关,先继承B再继承C,所以先初始化B,后初始化C。
2、多继承中指针偏移问题?下面说法正确的是( )。
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
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;
}
题目分析:
将Dirve派生类的对象d的地址赋值给基类的指针,实际上是一种切片行为。
3、以下程序输出结果是什么()
A: A->0
B: B->1
C: A->1
D: B->0
E: 编译出错
F: 以上都不正确
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;
}
题目分析:
子类继承重写父类虚函数:
- 接口继承。(B中func不写virtual也是虚函数,符合多态的条件,缺省值也是用基类的)。
- 重写的函数实现。
🎭多态夺命11连问
1.什么是多态?
c++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。具体来说就是去完成某个行为,不同的对象去完成会产生不同的状态。
2.什么是重载、重写(覆盖)、隐藏(重定义)?
重载:函数在同一作用域,函数名相同,参数不同。
重写(覆盖):函数分别在基类和派生类的作用域,且函数名、参数、返回值都必须相同(除协变外)。
重定义(隐藏):函数分别在基类和派生类的作用域,且函数名相同,两个基类和派生类的同名函数不构成重写就是重定义。
3.多态的实现原理?
构成多态的基类对象和派生类对象中都包含一个虚函数表指针,该指针指向一个虚函数表,虚函数表中存储的是该类对应的虚函数调用方法。当基类的指针或引用指向基类对象时,可通过基类指针或引用找到虚表指针,虚表中找到基类中对应的虚函数调用。当基类的指针或引用指向子类对象时,可通过基类指针或引用找到虚表指针,虚表中找到子类中对应虚函数的调用方法。
4.inline函数可以是虚函数吗?
可以,但对于编译器只是一种建议,编译器会忽略掉inline属性,就不再是inline。inline函数会在调用的地方展开,没有地址。因为虚函数要放到虚表中去,所以不能定义为inline函数。
5.静态成员函数可以是虚函数吗?
不能。静态成员函数没有this指针,使用
类型::成员函数
的调用方式无法访问虚函数表,因此静态成员函数不能放进虚函数表中。
6.构造函数可以是虚函数吗?
不能。对象中的虚函数表指针是在构造函数初始化列表阶段初始化的。
7.析构函数可以是虚函数吗?
可以。最好把基类的虚函数定义为虚函数。例如:用基类的指针分别new一个基类对象和子类对象,程序结束时分别调用它们各自的析构函数清理资源时,若我们不将基类析构函数定义为虚函数,因为编译器统一把虚函数名处理为destructor后,基类的析构函数对子类的析构函数构成隐藏,则基类的资源无法清理,可能造成内存泄漏等问题。
8.对象访问普通函数还是虚函数更快?
若访问的是普通对象,那么一样快的。如访问的是指针对象或者引用对象,则访问普通函数更快,因为构成多态,运行时调用虚函数需要到虚表里面去找,会有一定时间消耗。
9.虚函数表是在什么阶段生成的?存在哪里?
虚函数表是在编译阶段生成的,一般情况下存储在代码段(常量区)。
10.c++菱形继承的问题?虚继承的原理?
c++菱形继承因子类对象有两份基类成员,有数据冗余和二义性的问题。
虚继承只会存储一份基类的成员,通过虚机表得到偏移量来访问基类的成员,解决了数据冗余和二义性的问题。
11.什么是抽象类?抽象类的作用?
抽象类即不能实例化出对象的类,抽象类体现的是接口继承,强制子类重写抽象类的纯虚函数,否则也不能实例化出对象。规范了派生类必须重写纯虚函数。