c++—多态【万字文章】

news2024/9/18 15:41:41

目录

  • 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/2134263.html

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

相关文章

组件上的v-model(数据传递),props验证,自定义事件,计算属性

一.props验证 在封装组件时对外界传递过来的props数据进行合法性校验&#xff0c;从而防止数据不合法问题。 1.基础类型检查 String,Number,Boolean,Array,Object,Date,Function,Symbol 2.多个可能的类型 3.必须项校验 4.属性默认值 5.自定义验证函数 <template>&…

CAD2020安装方法

文章目录 下载安装包打开压缩包打开文件夹打开CAD2020文件夹双击运行Setup.exe点击安装选择我接受 点击下一步路径默认点击安装等待加载完成安装完成点击立即启动点击OK点击输入序列号点击我同意点击激活输入序列号和 产品钥密点击下一步选择我具有 Autodesk 提供的激活码以管理…

【北京迅为】《STM32MP157开发板使用手册》- 第二十八章Cortex-M4外部中断实验

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

SRT3D: A Sparse Region-Based 3D Object Tracking Approach for the Real World

基于区域的方法在基于模型的单目3D跟踪无纹理物体的复杂场景中变得越来越流行。然而&#xff0c;尽管它们能够实现最先进的结果&#xff0c;大多数方法的计算开销很大&#xff0c;需要大量资源来实时运行。在下文中&#xff0c;我们基于之前的工作&#xff0c;开发了SRT3D&…

一、轻松部署的大模型开发平台dify.ai

一、轻松部署的大模型开发平台dify.ai 今天学习了大模型&#xff0c;顺便介绍的是一个名为dify.ai的神奇平台&#xff0c;它能让你轻松部署和使用大模型&#xff0c;即使你是编程小白也不用担心。 官网&#xff1a;https://dify.ai/zh 什么是大模型&#xff1f; 首先&#…

C++ nullptr 和NULL的区别

个人主页&#xff1a;Jason_from_China-CSDN博客 所属栏目&#xff1a;C系统性学习_Jason_from_China的博客-CSDN博客 概念概述&#xff1a; 在C中&#xff0c;nullptr 和 NULL 都是用来表示空指针&#xff0c;但它们之间有一些重要的区别&#xff1a; nullptr和NULL之间的区分…

ceph简介

ceph存储简要概述&#xff1a; 通过将文件分解成固定大小对象&#xff0c;然后存放于pool中&#xff0c;每个pool中 可包含多个pg&#xff0c;每个pg中又可包含多个osd 通过crush算法 最终数据落盘到osd中去。 一、ceph 删除osd 步骤1 修改osd数据操作权重值 ceph osd crush r…

【贪心算法】贪心算法

贪心算法简介 1.什么是贪心算法2.贪心算法的特点3.学习贪心的方向 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.什么是贪心算法 与其说是…

C++ | Leetcode C++题解之第401题二进制手表

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<string> readBinaryWatch(int turnedOn) {vector<string> ans;for (int i 0; i < 1024; i) {int h i >> 6, m i & 63; // 用位运算取出高 4 位和低 6 位if (h < 12 &&a…

如何在算家云搭建MindSearch(智能搜索)

一、MindSearch简介 MindSearch是一款由上海人工智能实验室书生浦语团队提出了 MindSearch&#xff08;思索&#xff09;框架&#xff0c;旨在提供高效、精准的信息检索服务&#xff0c;能够在 3 分钟内主动从 300 网页中搜集整理有效信息&#xff0c;总结归纳&#xff0c;解决…

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

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

初级练习[2]:Hive SQL查询汇总分析

目录 SQL查询汇总分析 成绩查询 查询编号为“02”的课程的总成绩 查询参加考试的学生个数 分组查询 查询各科成绩最高和最低的分 查询每门课程有多少学生参加了考试(有考试成绩) 查询男生、女生人数 分组结果的条件 查询平均成绩大于60分的学生的学号和平均成绩 查询至少…

学习笔记 韩顺平 零基础30天学会Java(2024.9.13)

P545 TreeMap源码解读 TreeSet的k-v其中的v是一个静态的对象&#xff0c;但是TreeMap的v是可以变化的 TreeMap使用默认构造器取出的顺序和添加的顺序是不一样的&#xff0c;但是有构造器实现了Comparator接口的匿名内部类&#xff0c;可以按顺序排序 P546 Collections工具类1 P…

不入耳蓝牙耳机排行榜第一名是哪个品牌?解密最值得购买的五大品牌!

​到了2024年&#xff0c;开放式耳机无疑成为了耳机市场的宠儿。它们的优势在于&#xff0c;不仅佩戴舒适&#xff0c;还能在保护听力的同时&#xff0c;让你保持对周围环境的警觉&#xff0c;这对于爱好户外探险的朋友来说&#xff0c;无疑是一个巨大的安全加分项。作为一名资…

【初识Linux】Linux下基本指令

01. ls 指令 语法&#xff1a; ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。 常用选项&#xff1a; -a 列出目录下的所有文件&#xff0c;包括以 . 开头的隐含文件。 -…

AI与艺术的碰撞:当机器开始创作,创造力何在?

一、引言 艺术与创造力的定义及重要性 艺术&#xff0c;作为人类情感和思想的表达形式&#xff0c;涵盖了绘画、音乐、文学等多种领域。它不仅是文化传承的载体&#xff0c;更是人类想象力和创造力的结晶。创造力&#xff0c;则是推动艺术发展和社会进步的关键力量&#xff0…

高性能编程:无锁队列----MsgQueue代码实践

目录 概述 代码结构 1. 头文件解析 (msgqueue.h) 2. 实现文件解析 (msgqueue.c) 核心功能解析 2.1 创建队列 (msgqueue_create) 2.2 放入消息 (msgqueue_put) 2.3 获取消息 (msgqueue_get) 2.4 交换队列 (__msgqueue_swap) 2.5 阻塞与非阻塞模式 2.6 销毁队列 (msgq…

GPT Prompt

Reference https://help.openai.com/en/articles/6654000-best-practices-for-prompt-engineering-with-the-openai-apihttps://platform.openai.com/docs/guides/prompt-engineeringbilibili 8分钟系统学习提示工程,别再说大模型还不够聪明!Prompt Engineering,提示词,Few…

深兰科技董事长陈海波应出席“香港大学国际科创大赛”

近日&#xff0c;以“人工智能与智能制造”为主题的“香港大学国际科创大赛——知识转化论坛(沪港场)”在上海市普陀区隆重举行&#xff0c;众多来自人工智能与智能制造领域的专家学者与企业高管齐聚一堂&#xff0c;共话人工智能、智能制造与新质生产力的深度融合、最新进展以…

第 11篇 Helm 部署 RabbitMQ

文章目录 RabbitMQ 简介Helm ChartChart 版本选择自定义 values.yaml部署效果 参考相关博文 &#x1f680; 本文内容&#xff1a;在 Helm 中部署 RabbitMQ。 RabbitMQ 简介 ⭐ RabbitMQ&#xff1a;开源、通用消息代理&#xff0c;为一致性、高可用消息场景设计&#xff0c;包…