c++—多态【万字】【多态的原理】【重写的深入学习】【各种继承关系下的虚表查看】

目录

  • C++—多态
    • 1.多态的概念
    • 2.多态的定义及实现
      • 2.1多态的构成条件
      • 2.2虚函数的重写
        • 2.2.1虚函数重写的两个例外:
          • 2.2.1.1协变
          • 2.2.1.2析构函数的重写
      • 2.3 c++11的override和final
        • 2.3.1final
        • 2.3.2override
      • 2.4 重载、重写、重定义的对比
    • 3.抽象类
      • 3.1抽象类的概念
      • 3.2接口继承和实现继承
    • 4.多态的原理
      • 4.1虚函数表
        • 两个易错题:
          • 4.1.1子类当中的虚函数表是怎么生成的?
          • 4.1.2虚函数存在哪里?虚函数表存在哪里?
      • 4.2多态的原理
      • 4.3动态绑定和静态绑定
    • 5.单继承和多继承关系的虚表
      • 5.1单继承下的虚表
      • 5.2多继承下的虚表
      • 5.3菱形继承以及菱形虚拟继承的虚表
        • 5.3.1菱形继承的虚表
        • 5.3.2菱形虚拟继承的虚表
    • 6.继承和多态常见面试题
      • 6.1概念选择题
      • 6.2程序选择题
      • 6.3问答题

C++—多态

1.多态的概念

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

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

2.多态的定义及实现

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

2.1多态的构成条件

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

  1. 必须通过父类的引用或者是指针去调用虚函数
  2. 子类需对父类的虚函数进行重写

这里虚函数的virtual和之前讲的虚继承的virtual不是一回事,是没有关联的,要分开来看,不能弄到一起去。虽然都用到了virtual。

来看一段简单的多态的代码:

// 重写
// 要构成重写,1.必须都是虚函数 2.返回值 函数名,参数列表必须完全一致
class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:// 父类的虚函数构成重写—virtual void BuyTicket() {cout << "买票-半价" << endl;}
};// 构成多态
// 1. 必须通过父类的引用或者是指针去调用虚函数
// 2. 子类需对父类的虚函数进行重写
void fun(Person& p) // 这里是指针也行
{p.BuyTicket(); 
}int main()
{Person p;Student s;s.BuyTicket();//买票 - 半价p.BuyTicket();// 买票 - 全价fun(s);//买票 - 半价fun(p);// 买票 - 全价return 0;
}

下图是有关上述代码的分析:

image-20240910093109174

2.2虚函数的重写

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

要注意:

重写是对父类的继承下来的虚函数进行实现部分的重写,虚函数的函数名和参数列表和返回值都是继承下来的,重写只是对父类虚函数的实现部分进行重写,因此调用多态时调用子类的虚函数的时候,不会使用子类的缺省值,这里不懂可以看第6部分程序选择题的第10题

上面使用的代码中就有虚函数的重写

class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:// 父类的虚函数构成重写—virtual void BuyTicket()  // 这里这个virtual不写也能构成重写,但是不规范{cout << "买票-半价" << endl;}
};

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

2.2.1虚函数重写的两个例外:
2.2.1.1协变

协变——父类和子类的虚函数的返回值可以不同

前面我们说虚函数重写需要函数名,返回值,参数列表相同才能构成重写。但是协变是一个例外,如果父类虚函数的返回值是父类对象的指针或者引用,子类虚函数的返回值是子类对象的指针或者引用。就构成协变,此时仍然构成重写

// 重写
// 要构成重写,1.必须都是虚函数 2.返回值 函数名,参数列表必须完全一致
class Person 
{
public:// 协变virtual Person& BuyTicket(){cout << "买票-全价" << endl;return *this;}
};class Student : public Person
{
public:// 要注意,父类的返回值是指针,子类的必须也是指针,父类返回值是引用,子类的也必须是引用,保持一致,才能构成协变,构成重写。virtual Student& BuyTicket(){cout << "买票-半价" << endl;return *this;}
};

如果父类和子类的返回类型不一致,就会报错,既不相同,也不协变。

image-20240910102100725

2.2.1.2析构函数的重写

面试题:析构函数是否需要定义为虚函数?

我们先来看看如果不定义为虚函数是什么样子的。

// 虚函数重写的另一个例外:析构函数的重写// 面试题:析构函数是否需要定义成虚函数呢?class Person 
{
public:// 假设先不定义成虚函数~Person() {cout << "~Person()" << endl;}
};
class Student : public Person 
{
public:~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;delete p1;Person* p2 = new Student;delete p2;return 0;
}

image-20240910104245153

我们发现如果不定义为虚函数,它只会根据类型来调用析构函数,比如Person类的对象或者指针和引用,就调用Person类的析构函数。但是实际上并不希望这样,因为在person* p2 = new Student中,创建了一个子类对象切片赋值给了父类的指针,如果只调用Perosn类的析构函数,就有可能导致,子类有部分资源没有被释放和清理。这不是我们期望的、

我们期望的应该是多态,也就是指针和引用指向谁,就调用谁的析构函数。因此就要让 Peron和Student类的析构函数构成重写,让父类的指针或者引用去调用重写的析构函数。从而构成多态。将两个类的析构函数定义为虚函数,这样就能构成重写了。

但是有一个疑问,明明父类和子类的析构函数名字不同,一个是~Person,一个是~Student,这怎么能构成重写?但是实际上我们在继承那边也提到过,在编译器处理之后,析构函数的名字是一样的,都是destructor并且析构函数没有返回值,这就满足了 函数名、返回值、参数列表相同了,自然构成了重写

实际上只要父类的析构函数是虚函数,子类是否是虚函数都构成重写,前面提及过原因,但是为了规范性还是加上比较好。

让析构函数构成重写关系之后,我们通过指针去调用析构函数的情况就能构成多态,就能对相应的类型调用相应的构造函数,就解决了上面出现的问题

修改后代码如下:

// 虚函数重写的另一个例外:析构函数的重写// 面试题:析构函数是否需要定义成虚函数呢?class Person 
{
public:virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person 
{
public:// 对于编译器来说,析构函数的函数名都是一样的,都是destructor。因此这里构成重写virtual ~Student() // 只要父类的析构函数是虚函数,这里继承下来子类是可以不加virtual的,但是不规范,最好还是加{ cout << "~Student()" << endl; }
};
int main()
{Person* p1 = new Person;delete p1;Person* p2 = new Student;delete p2;return 0;
}

image-20240910110348022

结果就是正确的。调用的是子类的析构函数,调用完子类的析构函数之后,会自动调用父类的析构函数,保证子类对象先清理子类成员再清理父类成员的顺序

2.3 c++11的override和final

2.3.1final

**final:**final可以修饰成员函数和类,被修饰的成员函数不能再被重写。被修饰的类不能再被继承

class Car
{
public:// 该函数被final修饰,不能再被重写virtual void Drive() final {}
};class Benz :public Car
{public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};
2.3.2override

检查子类虚函数是否重写了父类的某个虚函数,如果没有重写编译报错

实例:

//override关键字
class Car
{
public:virtual void Drive() {}
};class Benz :public Car
{public:// override 如果该子类虚函数没有对对应的父类虚函数进行重写就会报错,算是一个检查virtual void Drive() override{ cout << "Benz-舒适" << endl; }
};

2.4 重载、重写、重定义的对比

如图所示:

image-20240911000504463

3.抽象类

3.1抽象类的概念

纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数

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

实例:

// 抽象类//由于类中存在纯虚函数,因此Car类是抽象类
class Car
{
public:virtual void Drive() = 0; // 纯虚函数可以不实现,实现了,如果不指定调用,无法调用到
};// Benz类继承了抽象类Car,如果不把纯虚函数进行重写的话,Benz就还是抽象类,那么就无法实例化出对象、
class Benz :public Car
{
public:virtual void Drive() // 重写纯虚函数{cout << "Benz-舒适" << endl;}
};class BMW :public Car
{
public:// 纯虚函数强制子类去进行重写virtual void Drive() override{cout << "BMW-操控" << endl;}
};void Test()
{Car* pBenz = new Benz;Benz b;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}// 纯虚函数的作用:
// 1.强制子类去完成重写
// 2.表示抽象类型
// 【即现实中没有对应实体的 比如车和植物,单说一个车是没有意义的,必须告诉我是什么品牌的车】int main()
{Test();return 0;
}

3.2接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

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

4.多态的原理

4.1虚函数表

先来看一段代码:

// 常考的一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl; // 16// 如果Base内的Func1函数不是虚函数,那么输出的是16Base b;return 0;
}

输出结果是16。为什么呢?

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

因此,在Base类当中有一个虚函数,有虚函数就会存在虚函数表指针。这个虚函数表就是一个指针数组。因此Base类的对象中会存在一个虚函数表指针,来指向虚函数表,而在x64的环境下,指针的大小是8字节,还有一个int类型的_b成员,根据内存对齐规则,就是16个字节了

image-20240911183408728

这里这个虚函数表指针是随机值的原因是它在构造函数的时候才会被初始化

image-20240911193517543

那如果有一个子类继承了Base类,子类当中又会发生什么事情呢?

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()
{cout << sizeof(Base) << endl;Base b;Derive d;return 0;
}

调试观察d和b对象可以看到

image-20240911200711436

  1. 子类对象d当中也有一个虚函数表指针,这是继承自父类Base的。
  2. 子类的虚表指针的地址和父类的虚表指针地址不一样,说明不是指向同一个虚表,他们各自拥有自己的虚表
  3. 父类的虚表当中存的是自己的两个虚函数,由于子类对Func1完成了重写,子类d的虚表存着的一个是重写之后的虚函数Func1,还有一个继承下来的虚函数Func2。两个虚表的第二个指针都是一样的,说明指向的是同一个函数,都是父类的虚函数Func2
  4. 虚函数的重写也叫覆盖、覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法
  5. 虚函数表本质是一个存虚函数指针的函数指针数组,一般情况这个数组最后面放了一个nullptr
两个易错题:
4.1.1子类当中的虚函数表是怎么生成的?
  1. 首先将父类的虚函数表的内容拷贝一份放到自己的虚函数表中
  2. 如果子类当中对父类的虚函数进行了重写/覆盖,就把对应的重写后的虚函数地址覆盖到虚表中对应的虚函数地址上。让对应的虚函数地址指向子类重写过后的虚函数。
  3. 如果子类自己新增加了虚函数,虚表当中要根据虚函数的声明次序,相应的添加新的虚函数的地址。
4.1.2虚函数存在哪里?虚函数表存在哪里?
  1. 虚函数和普通函数一样存在代码段里,不是存在虚函数表里的,是虚函数的地址/指针存在了虚函数表里面。
  2. 虚函数表不是存在对象里面的!虚函数表在VS里面是存在代码段里的,是虚函数表指针存在对象里。对象可以根据这个指针找到虚函数表,在根据虚函数表里存着的虚函数指针,去找到虚函数。

虚函数表为什么要存在代码段,而不放在栈区,下面这个图就是原因:

image-20240911233510713

虚函数表是否真的存在代码段,我们也可以进行验证:

代码如下:

void test()
{Base b;// 这里*(int*)&b拿到的就是b变量的前四个字节,在x86环境下,前四个字节就是虚函数表指针printf("vftptr:%p\n", *(int*)&b); // vftptr:00D89B34int i = 0;int* pi = &i;int* p2 = new int;const char* p3 = "hello";printf("栈变量地址:%p\n", pi); //栈变量地址:00FDF710printf("堆变量地址:%p\n", p2); // 堆变量地址:0148E438printf("代码段常量地址:%p\n", p3); // 代码段常量地址:00D89B80printf("代码段类中函数地址:%p\n", &Base::Func3);// 代码段类中函数地址:00D811E0printf("代码段函数地址:%p\n", test); // 代码段函数地址:00D81280
}

我们发现位于代码段的地址都距离的很近,因此虚函数表指针就是存在代码段的

4.2多态的原理

先来看一段实现多态的代码:

// 多态的原理
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{// p根据传进来的类型决定指向的是什么类型p.BuyTicket(); // p指向什么类型这里调用的就是对应类型的虚表指针,多态就是这样实现的// 如果p指向子类,这里会切片赋值,p指向的是子类对象的父类部分,调用的就是子类的虚表指针// 子类对父类的虚函数BuyTicket完成了重写,因此重写后的虚函数地址就会对虚表内的地址进行覆盖。// 多态就这样实现了、
}int main()
{// 多态的原理Person p;Func(p);Student s;Func(s);return 0;
}

多态的实现原理如下图所示:

image-20240911205455195

简单来说,上面之所以能实现多态就是因为

  1. 如果p指向的是父类的对象,那么拿到的就是父类的虚表指针,找到的是父类的虚表,调用的是父类的虚函数。
  2. 如果p指向的是子类的对象,那么拿到的就是子类的虚表指针,找到的是子类的虚表,调用的是子类的虚函数、
  3. 能调用子类的虚函数的前提是要对父类的虚函数进行重写,重写之后的虚函数地址会对原来的虚函数地址进行覆盖。这样调用的就是子类的虚函数。

多态的原理:多态是在运行的时候根据指向的对象去查找对应的虚函数表,找到对应的虚函数地址去进行调用。

这就是为什么多态要满足两个条件:

  1. 子类要对父类虚函数实现重写
  2. 要由父类的指针或者引用调用虚函数

对于多态的原理,我们可以看它满足多态和不满足多态时的汇编代码来分析

image-20240911222135985

4.3动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载

  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

image-20240912123200518

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

这里研究的虚表都是子类的虚表,父类的虚表和之前讲的是一样的。

5.1单继承下的虚表

监视窗口给我的所看到的虚表不一定是真实的

这段代码是父类和子类的代码。

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() override{cout << "Derive::Func1()" << endl;}virtual void Func3(){cout << "Derive::Func3()" << endl;}private:int _d = 2;
};

如下图所示:
image-20240912173149452

我们发现监视窗口给出的单继承下的子类的虚表是看不到子类自己多出来的虚函数的。Func3这个虚函数的地址就看不到

因此我们可以自己来编写一个代码,来查看真正的虚函数表

// 监视窗口的虚函数表是不够真实的,如果子类多添加了自己的虚函数,其虚表不会添加对应虚函数的的地址。
// 现在编写一个代码去查看真正的虚表内的虚函数地址// 重命名一个函数指针类型的变量 VF_PTR
typedef void(*VF_PTR)();//void PrintVFTable(VF_PTR pTable[]) // 这里写成数组也是没问题的,但是实际上这里传的也是指针,形参不会有真正的数组
void PrintVFTable(VF_PTR* pTable)
{// 拿到虚表指针后,打印虚表内的虚函数地址。虚表本质是个顺序表for (size_t i = 0; pTable[i] != nullptr; i++) // 虚表最后一个元素是nullptr,也可以认为是0{printf("vfTable[%d]: %p->", i, pTable[i]);VF_PTR f = pTable[i]; // 拿到虚表中对应第i个的虚函数地址f(); // 调用f指向的虚函数}
}int main()
{//test();Base b;Derive d;// 打印父类虚表cout << "打印父类虚表:\n";PrintVFTable((VF_PTR*)(*(int*)&b));//(*(int*)&b)这一步拿出的是父类的虚表指针, // 但是这里这个地址还得强转成VF_PTR*的类型才能传参// 打印子类虚表cout << "打印子类虚表:\n";PrintVFTable((VF_PTR*)(*(int*)&d));return 0;
}

如下图所示:

image-20240912212059612

image-20240913085320745

如果子类虚表出现了许多地址,就说明编译器出bug了,清理一下解决方案就行

image-20240912173434159

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

此时子类占多少个字节?

	Base1 b1;Base2 b2;Derive d;cout << sizeof(b1) << endl; // 8/16cout << sizeof(b2) << endl;// 8/16cout << sizeof(d) << endl;// 20/40

两个父类都是一个虚表指针 + 一个int类成员,都是8/16个字节

因此子类是8 + 8 + 4 = 20 个字节,刚好符合最大对齐数4的倍数。这是x86的环境下的大小是20个字节。

现在我们先通过调试窗口来观察一下子类的虚表

image-20240912235647498

我们发现这个监视窗口给的虚表仍然有点问题,因此我们还是要自己打印虚表

// 声明VF_PTR为函数指针类型
typedef void(*VF_PTR)();void PrintVFTable(VF_PTR pTable[])
{for (size_t i = 0; pTable[i] != nullptr; i++){printf("vfTable[%d]: %p->", i, pTable[i]);VF_PTR f = pTable[i];f(); // 调用f所指向的虚函数}cout << endl;
}

这个函数和上面打印单继承的虚表函数是一样的。

但是调用上传参要有所不一样,两个虚表指针的位置位于两个父类部分的头四个字节。

	// 打印子类d对象中第一个虚表——在Base1父类部分cout << "打印子类中Base1父类部分的虚表\n";PrintVFTable((VF_PTR*)(*(int*)&d));// 打印子类d对象中第二个虚表——在Base2父类部分cout << "打印子类中Base2父类部分的虚表\n";PrintVFTable((VF_PTR*)(*((int*)((char*)&d + sizeof(Base1)))));// 让首地址先转成char*类型的指针,这样+上一个sizeof(Base1)才是第二个父类Base2的位置// 此时在转换成int*类型,再解引用就能取出Base2的头四个字节,就是虚表指针,再强转VF_PTR*

打印的虚表如下图所示:

image-20240913001229843

我们会发现:

  1. 子类的fun3虚函数被放到了第一个虚表中
  2. 重写后的虚函数func1对两个虚表都进行了覆盖。但是两个虚表记载func1的地址不一样
  3. 无论是单继承还是多继承,子类都没有虚表指针,子类的虚表都是继承自父类的,多继承就是多个虚表指针,但是都是包含在对应父类部分的。

image-20240913002431084

5.3菱形继承以及菱形虚拟继承的虚表

更为复杂菱形继承和菱形虚拟继承的虚表可以看下面两个博客

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

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

5.3.1菱形继承的虚表

看一个菱形继承的代码:

class Person
{
public:virtual void func1(){cout << "Person::func1()" << endl;}virtual void func2(){cout << "Person::func2()" << endl;}string _name; // 姓名
};
class Student : public Person
{
public:virtual void func1(){cout << "Student::func1()" << endl;}
protected:int _num; //学号
};
class Teacher : public Person
{
public:virtual void func1(){cout << "Teacher::func1()" << endl;}
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher // 多继承
{
public:virtual void func1(){cout << "Assistant::func1()" << endl;}virtual void func3(){cout << "Assistant::func3()" << endl;}
protected:string _majorCourse; // 主修课程
};int main()
{cout << sizeof(Assistant) << endl; // 100/152  主要受string类型的影响,所以比较大Person p;Student s;Teacher t;Assistant a;return 0;
}

菱形继承的虚表简单来说如下图所示:

image-20240913093209662

监视窗口给的虚表不完全对,看不到fun3的地址。

我们自己用代码看一下:

这里这个查看虚表的函数和前面用的是一样的

image-20240913220228483

  1. 还是和多继承的一样,子类有多少父类,就会有多少个虚表,虚表是继承父类下来的
  2. 每个子类的虚表都继承自父类的虚表,但是都是独立的,相当于我开了个新空间,将父类的虚表内容拷贝过来了。
  3. 子类的自己的虚函数还是会放在第一个父类的虚表中。
5.3.2菱形虚拟继承的虚表

为了方便调试,我们把string类型的成员都改成了int

// 菱形虚拟继承的虚表
class Person
{
public:virtual void func1(){cout << "Person::func1()" << endl;}virtual void func2(){cout << "Person::func2()" << endl;}int _name; // 姓名
};class Student : virtual public Person
{
public:virtual void func1(){cout << "Student::func1()" << endl;}
protected:int _num = 2; //学号
};
class Teacher : virtual public Person
{
public:virtual void func1(){cout << "Teacher::func1()" << endl;}
protected:int _id = 3; // 职工编号
};class Assistant :public Student, public Teacher // 多继承
{
public:virtual void func1(){cout << "Assistant::func1()" << endl;}virtual void func3(){cout << "Assistant::func3()" << endl;}virtual void func4(){cout << "Assistant::func4()" << endl;}
protected:int _majorCourse = 4; // 主修课程
};int main()
{cout << sizeof(Assistant) << endl; // 32// Student的虚表指针 + 指向_name的虚基表指针 + int成员 = 12// Teacher的虚表指针 + 指向_name的虚基表指针 + int成员 = 12// 加上Assistant自己的int成员 4// 加上真正存储_name的地址 4  因此 12 + 12 + 4 + 4 = 32;刚好是最大对齐数4的倍数Person p;Student s;Teacher t;Assistant a;a._name = 1;return 0;
}

如图所示。

image-20240913232039444

我们来打印一下公共虚表和子类自己的虚表

	// 打印Assistant类的父类公共虚表cout << "打印子类的父类公共虚表\n";PrintVFTable((VF_PTR*)(*(int*)((char*)&a + 24)));cout << "打印子类自己的虚函数的虚表\n";PrintVFTable((VF_PTR*)(*(int*)&a));

image-20240913230421432

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

6.1概念选择题

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

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

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

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

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

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

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

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

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

    **解析:**优先使用组合

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

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

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

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

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

    **解析:**B:声明纯虚函数的类是抽象类

    C:子类未必需要实现纯虚函数,只是不能实例化对象而已

    D:纯虚函数也是可以有实现的,但是除了显式调用,一般无法调用到

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

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

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

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

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

    **解析:**内联函数在编译时直接展开没有地址,怎么放到虚表里面?

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

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

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

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

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

    解析:

    A:一个类里面可以继承多个父类,对象就有多个虚表

    B:不是公用虚表,只子类的虚表内容和父类的一样

    C:不是动态生成,编译生成完成后,是动态的去虚表里找虚函数进行调用

  7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(D)

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

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

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

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

    解析:

    A:B类对象的前四个字节也是虚表指针

    B:不是虚基表,是虚函数表。如果是菱形虚拟继承情况的下的虚表要另说,这里没说是虚继承,因此不用考虑虚继承的情况

    C:不相同,B类的虚表虽然继承自A,但是是两个虚表,并且对虚函数进行了重写,因此虚函数地址也进行了覆盖

6.2程序选择题

  1. 下面程序输出结果是什么? (A)
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;
}

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

解析:

因为B C虚拟继承了A,D继承了B、C,因此D里面只有一份A,构造函数只会构造一次A类,因此class A只会出现一次。又因为,是先执行初始化列表,才执行函数体内的实现,所以classD在最后。

  1. 多继承中指针偏移问题?下面说法正确的是( C)
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

解析:

因为子类是先存第一个父类——Base1的成员 _b1, 再存第二个父类——Base2的成员_b2,又根据切片,因此p1指向的是子类对象d的Base1父类地址,p2指向的是子类对象d的Base2父类地址。p3指向d对象的地址。

子类对象第一个父类的地址和d对象的开头地址是一样的。

因此p1 == p3 != p2。

  1. 以下程序输出结果是什么()
// 面试选择题
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: 以上都不正确

// 答案是:B

为什么呢?请看下面这个图:

image-20240910121356531

6.3问答题

  1. 什么是多态?

  2. 什么是重载、重写(覆盖)、重定义(隐藏)?

  3. 多态的实现原理?

  4. inline函数可以是虚函数吗?

    答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。

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

    答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

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

    答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

  7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

    答:可以,并且最好把基类的析构函数定义成虚函数。参考2.2.1.2

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

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

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

    答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

  10. C++菱形继承的问题?虚继承的原理?

    答:这里不要把虚函数表和虚基表搞混了。

  11. 什么是抽象类?抽象类的作用?

    答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1534445.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

搜索树和Map

一.搜索树 1.概念 二叉搜索树又叫二叉排序树&#xff0c;它可以是一颗空树也可以是具有以下性质的二叉树 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值它的左子树也分别为二…

Redis 篇-深入了解使用 Redis 中的 GEO 数据结构实现查询附近店铺、BitMap 实现签到功能、HyperLogLog 实现 UV 流量统计

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 GEO 数据结构的基本用法 1.1 使用 GEO 导入数据 1.2 使用 GEO 实现查找附近店铺功能 2.0 BitMap 基本用法 2.1 使用 BitMap 实现签到功能 2.2 统计连续签到功能 3…

windows server2012 配制nginx安装为服务的时候,直接跳要安装.net框架,用自动的安装,直接失败的解决。

1、上一个已成功在安装过程中的图&#xff1a; 2、之前安装过程中错误的图&#xff1a; 3、离线安装解决&#xff1a; 下载.net framework 3.5&#xff0c;然后解压后&#xff0c;选择指定备用源路径&#xff0c;然后选择.net安装包所在目录&#xff1a; 只要指定上面全路径就…

4G模块点对点传输手把手教程!如何实现远程设备直接通信

使用4G模块进行点对点传输&#xff0c;可以实现远程设备的直接通信&#xff0c;广泛应用于工业控制、远程监控、物联网等领域。本教程将详细讲解如何通过4G模块&#xff0c;构建设备之间的点对点&#xff08;P2P&#xff09;传输系统&#xff0c;从配置设备、建立通信通道到实际…

Delphi Web和Web服务开发目前有哪些选择

Delphi Web和Web服务开发目前有哪些选择 Delphi Web和Web服务开发目前有以下几个选择&#xff1a; Delphi MVC Framework&#xff08;https://github.com/delphimvcframework/delphimvcframework&#xff09;&#xff1a;这是一个开源的Delphi Web框架&#xff0c;基于MVC&am…

【Linux】基本指令及其周边知识

1.准备阶段 在介绍Linux的基本指令之前&#xff0c;我先先向大家介绍一下我的Linux平台&#xff0c;首先我是在阿里云买了个服务器&#xff0c;然后使用Xshell来远程登录Linux&#xff0c;之后有关Linux上的操作都是在这上面进行的。如果你也买了相关的服务器并且设置了相关示…

Parallels Desktop19中文版2024九月最新

Parallels Desktop可以使轻松地在 MAC上运行成千上万款 Windows应用程序&#xff0c;如Excel&#xff0c;会计交易软件等。针对最新版 windows11和macOS Sonoma 进行优化。在 MAC虚拟机中跨多个操作系统开发和测试。包含 Parallels Toolbox – 40 多个适用于 Mac 和 PC 的一键…

ROS1录包偶现一次崩溃问题定位

现象&#xff1a;崩到了mogo_reporter里面 堆栈&#xff1a;crash里面同时存在两个主线程的堆栈 代码 #include "boost/program_options.hpp" #include <signal.h> #include <string> #include <sstream> #include <iostream> #include <…

[“1“, “2“, “3“].map(parseInt)结果

parseInt 的用法 parseInt 是 JavaScript 中的一个全局函数&#xff0c;用于将字符串转换为整数。它的基本语法如下&#xff1a; parseInt(string, radix);string&#xff1a;要解析的字符串。radix&#xff08;可选&#xff09;&#xff1a;字符串的基数&#xff0c;可以是 …

高科技企业选择跨网文件系统最容易踩坑的地方

在数字化时代&#xff0c;高科技企业频繁使用跨网文件交换系统的原因多种多样。首先&#xff0c;随着全球化的推进&#xff0c;企业需要在不同地理位置的分支机构之间传输敏感数据和重要文件。其次&#xff0c;跨网文件交换能够提高工作效率&#xff0c;确保信息的实时更新和共…

开源 TTS 模型「Fish Speech」1.4 发布;GameGen-O :生成开放世界游戏视频模型丨 RTE 开发者日报

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。 我们的社区编辑团队会整理分享 RTE&#xff08;Real-Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、…

高并发下的生存之道:如何巧妙化解热Key危机?

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! 哈喽,大家好!我是小米,29岁,喜欢分享技术的小米上线啦!今天咱们来聊聊在互联网高并发场景下,一个让大家又爱又恨的问题——热Key问题。热Key是什么…

【C++】_stack和_queue容器适配器、_deque

当别人都在关注你飞的有多高的时候&#xff0c;只有父母在关心你飞的累不累。&#x1f493;&#x1f493;&#x1f493; 目录 ✨说在前面 &#x1f34b;知识点一&#xff1a;stack •&#x1f330;1.stack介绍 •&#x1f330;2.stack的基本操作 &#x1f34b;知识点二&…

【电路笔记】-反相运算放大器

反相运算放大器 文章目录 反相运算放大器1、概述2、理想反相运算放大器3、实际反相运算放大器3.1 闭环增益3.2 输入阻抗3.3 输出阻抗4、反相运算放大器示例5、总结1、概述 上一篇关于同相运算放大器的文章中已介绍了该运算放大器配置的所有细节,该配置在同相引脚 (+) 上获取输…

LSS如何创建视锥

1 完整代码 def create_frustum(self):# 128 352, 22 8in_H

LRELHLNNN;亲水性抗肝纤维化多肽作为基础肽;I型胶原蛋白靶向肽;九肽LRELHLNNN

【LRELHLNNN 简介】 LRELHLNNN是一种多肽&#xff0c;它能够选择性地结合到I型胶原蛋白&#xff0c;具有亲和力为170 nM。LRELHLNNN是由9个氨基酸组成&#xff0c;其氨基酸序列为H-Leu-Arg-Glu-Leu-His-Leu-Asn-Asn-Asn-OH。LRELHLNNN因其与I型胶原蛋白的高亲和力而在生物医学领…

MDC日志追踪(一)介绍

一、背景 在排查问题时&#xff0c;如果只根据关键字搜索&#xff0c;可能不精准&#xff0c;比如根据userId搜索&#xff0c;但是这个userId访问的记录也很多&#xff0c;很难定位出问题的是哪一次的&#xff1b;比如根据其他关键字搜索如orderId&#xff0c;可能很多用户都访…

wifi贴码推广能赚钱吗?wifi贴码怎么跟商家沟通?

大家好&#xff0c;我是鲸天科技千千&#xff0c;大家都知道我是做开发的&#xff0c;平时会给大家分享一些互联网相关的创业项目和网络技巧&#xff0c;感兴趣的可以给我点个关注。 最近WiFi这个项目很多朋友来问我&#xff0c;我是前两年就接触过这个&#xff0c;所以比较了…

“孪舟”引擎V5.0:更有颜、更真实、更智能、更灵活、更强大

在9月6日智汇云舟2024视频孪生产品发布会上&#xff0c;我们向线上线下嘉宾展示了基于视频孪生技术的众多产品&#xff0c;以及前沿技术。我们的目标是依托自研3DGIS引擎&#xff0c;将视频、AI、IoT等多种技术深度融合&#xff0c;升级数字孪生为视频孪生&#xff0c;实时实景…

《Putty 的下载和安装步骤》

Putty 是一款免费开源的 SSH 和 Telnet 客户端,它主要用于远程登录和管理其他计算机或服务器。 1.Putty 的一些主要特点和优势: 1. 简单易用:它具有直观的用户界面,操作相对简单,即使对于不太熟悉技术的用户也能轻松上手。 2. 支持多种协议:除了 SSH(Secure Shell)…