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

news2024/9/20 2:40:24

目录

  • 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函数不是虚函数,那么输出的是16
	Base 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:00D89B34

	int i = 0;
	int* pi = &i;
	int* p2 = new int;
	const char* p3 = "hello";
	printf("栈变量地址:%p\n", pi); //栈变量地址:00FDF710
	printf("堆变量地址:%p\n", p2); // 堆变量地址:0148E438
	printf("代码段常量地址:%p\n", p3); // 代码段常量地址:00D89B80
	printf("代码段类中函数地址:%p\n", &Base::Func3);// 代码段类中函数地址:00D811E0
	printf("代码段函数地址:%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/16
	cout << sizeof(b2) << endl;// 8/16
	cout << 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.coloradmin.cn/o/2142957.html

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

相关文章

5款录屏软件电脑版,哪一款更适合你?

身边不少做行政的小伙伴&#xff0c;经常需要制作一些培训视频、会议记录或是演示文稿。这就要求他们必须掌握一款好用的录屏软件。作为一个经常搜索各种办公软件的人&#xff0c;今天&#xff0c;我就来分享一下我使用过的五款录屏软件在录制电脑屏幕时的表现。 1、福昕录屏大…

枚举类题目练习心得

两数之和 题目如下&#xff1a; 一点思路&#xff1a;该题目仅限于数据量少的情况使用枚举&#xff0c;从题目分析来看&#xff0c;需求是给定一个数字&#xff0c;要求在给定数组中找到两个数字并使这两个数字和为给定数字且返回目标数字下标。参考题解思路结合本身思路代码…

Leetcode—环形链表||

题目描述 思路 快慢指针 结论 我们需要用到一个重要的结论&#xff1a;让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。 画图解释 1.利用快慢指针找到相遇点 2. 定义两个…

java138-异常处理_java 138错误

//异常 public class test79 { //定义方法声明定义异常&#xff0c;在满足条件时抛出异常对象&#xff0c;程序转向异常处理 public double count(double n,double m)throws Exception { if (m 0) {//如果除数等于0.则抛出异常实例 throw new Ex…

day03 - Java集合和常用类

第一章 Collection集合 1. Collection概述 集合&#xff1a;java中提供的一种容器&#xff0c;可以用来存储多个数据 集合和数组既然都是容器&#xff0c;它们有啥区别呢&#xff1f; 数组的长度是固定的。集合的长度是不固定的。集合可以随时增加元素&#xff0c;其大小也随…

kubeadm方式安装k8s+基础命令的使用

一、安装环境 二、前期准备 1.设置免密登录 [rootk8s-master ~]# ssh-keygen [rootk8s-master ~]# ssh-copy-id root192.168.2.77 [rootk8s-master ~]# ssh-copy-id root192.168.2.88 2.yum源配置 3.清空创建缓存 4.主机映射&#xff08;三台主机都要设置&#xff09; 5.安装…

vivado中选中bd文件后generate output product是什么用,create HDL wrapper是什么用

vivado中选中bd文件后generate output product是什么用 在Vivado中&#xff0c;“Generate Output Products” 是一个重要的步骤&#xff0c;它用于生成IP核的输出产品&#xff0c;这些产品是将IP核集成到设计中所需的文件。这些输出产品包括&#xff1a; 综合文件&#xff…

多线程下的共享变量访问数据竞争的问题

多线程下对共享变量的写存在数据竞问题可导致数据与预期不一致。最近在研究race conditions漏洞&#xff0c;用以下python 代码记录一下&#xff0c;以此论证&#xff0c;如下&#xff1a; from concurrent.futures import ThreadPoolExecutor globalNum 5 def test():global…

微积分-反函数6.1(反函数)

表1提供了一项实验的数据&#xff0c;其中细菌培养物在有限营养基中以100个细菌开始&#xff1b;在定时记录下细菌数量随时间的变化。细菌数量 N N N 是时间 t t t 的函数&#xff1a; N f ( t ) N f(t) Nf(t)。 然而&#xff0c;假设生物学家改变了她的观点&#xff0c;开…

京东App秒级百G日志传输存储架构设计与实战

本文作者&#xff1a;平台业务研发部-武伟峰&#xff0c;数据与智能部-李阳 背景 在日常工作中&#xff0c;我们通常需要存储一些日志&#xff0c;譬如用户请求的出入参、系统运行时打印的一些info、error之类的日志&#xff0c;从而对系统在运行时出现的问题有排查的依据。 …

作为研发部门的负责人,如何助力产品在市场竞争中胜出?浅谈 CTQ

在激烈的市场竞争中&#xff0c;产品研发团队如何帮助企业的产品脱颖而出&#xff1f;成功的产品往往不仅依赖于强大的功能和技术创新&#xff0c;还需要通过高效的研发效能&#xff0c;包括效率、质量和创新&#xff0c;来提升产品的市场竞争力。在本文中&#xff0c;我们将探…

文档内容识别系统源码分享

文档内容识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

一款源码阅读的插件

文章目录 进度汇报功能预览添加高亮标记高亮风格设置笔记颜色设置数据概览高亮数据详情 结尾 进度汇报 之前提到最近有在开发一个源码阅读的IDEA插件&#xff0c;第一版已经开发完上传插件市场了&#xff0c;等官方审批通过就可以尝鲜了。插件名称&#xff1a;Mark source cod…

基于STM32F407ZGT6——看门狗

独立看门狗 独立看门狗的时钟由独立的RC 振荡器LSI 提供&#xff0c;即使主时钟发生故障它仍然有效&#xff0c;非常独立。 LSI 的频率一般在30~60KHZ 之间&#xff0c;根据温度和工作场合会有一定的漂移&#xff0c; 所以独立看门狗的定时时间并不一定非常精确&#xff0c;只适…

格式化u盘选择FAT还是NTFS U盘和硬盘格式化两者选谁

Mac用户在将U盘或硬盘进行格式化时&#xff0c;选择FAT还是NTFS往往是一个让人纠结的问题。很多用户不知道这两个格式之间有什么区别&#xff0c;更不知道在格式化时如何做出选择。本文将为大家介绍Mac选择FAT还是NTFS&#xff0c;并为大家推荐U盘和硬盘格式化两者选谁。 一、…

36.贪心算法3

1.坏了的计算器&#xff08;medium&#xff09; . - 力扣&#xff08;LeetCode&#xff09; 题目解析 算法原理 代码 class Solution {public int brokenCalc(int startValue, int target) {// 正难则反 贪⼼int ret 0;while (target > startValue) {if (target % 2 0…

第159天:安全开发-Python-协议库爆破FTPSSHRedisSMTPMYSQL等

案例一: Python-文件传输爆破-ftplib 库操作 ftp 协议 开一个ftp 利用ftp正确登录与失败登录都会有不同的回显 使用ftplib库进行测试 from ftplib import FTP # FTP服务器地址 ftp_server 192.168.172.132 # FTP服务器端口&#xff08;默认为21&#xff09; ftp_po…

Base 社区见面会 | 新加坡站

活动信息 备受期待的 Base 社区见面会将于 Token2049 期间在新加坡举行&#xff0c;为 Base 爱好者和生态系统建设者提供一个独特的交流机会。本次活动由 DAOBase 组织&#xff0c;Base 和 Coinbase 提供支持&#xff0c;并得到了以下合作伙伴的大力支持&#xff1a; The Sand…

Python 课程15-PyTorch

前言 PyTorch 是一个开源的深度学习框架&#xff0c;由 Facebook 开发&#xff0c;广泛应用于学术研究和工业领域。与 TensorFlow 类似&#xff0c;PyTorch 提供了强大的工具用于构建和训练深度学习模型。PyTorch 的动态计算图和灵活的 API 使得它特别适合研究和实验。它还支持…

GetMaterialApp组件的用法

文章目录 1. 知识回顾2. 使用方法2.1 源码分析2.2 常用属性 3. 示例代码4. 内容总结 我们在上一章回中介绍了"Get包简介"相关的内容&#xff0c;本章回中将介绍GetMaterialApp组件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 知识回顾 我们在上一章回中已经…