【C++】多态(万字详解) —— 条件 | 虚函数重写 | 抽象类 | 多态的原理

news2025/4/8 15:47:57

🌈欢迎来到C++专栏~~多态


  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    在这里插入图片描述

请添加图片描述

多态

  • 🌈欢迎来到C++专栏~~多态
    • 一. 多态的概念
    • 二. 多态的定义及实现
      • 🌈多态的条件
      • 🌈虚函数重写的两个特例
        • 🥑协变
        • 🥑析构函数的重写
      • 🌈只有父类带 virtual 的情况
      • 🌈C++11 final & override
        • 🥑final
        • 🥑override
    • 三. 重载 vs 重写 vs 隐藏
    • 四. 抽象类
    • 五. 多态的原理
      • 🔥虚函数表
      • 🔥多态的原理
      • 🔥小细节
      • 🔥虚函数表在哪
    • 六. 单继承和多继承关系中的虚函数表
      • 💦打印虚函数表
      • 💦单继承的虚函数表
      • 💦多继承的虚函数表
      • 💦菱形继承的虚函数表
    • 七. 常见考点总结
    • 八. 经典题型讲解
      • 1️⃣杀手题目
      • 2️⃣杀手题目
  • 📢写在最后

请添加图片描述

一. 多态的概念

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

比如买票,我们想让不同身份的人,买票的价格不同,就可以借助多态实现
在这里插入图片描述

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 Func(Person& p)//父类的指针/引用
{
	p.BuyTicket();//虚函数重写
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);//传父类对象 —— 调父类的
	Func(st);//传子类对象 —— 调子类的
	Func(sd);//传子类对象 —— 调子类的

	return 0;
}

其中子类的函数满足 三同(返回值类型、函数名、参数列表完全相同)的虚函数这两个条件,叫做重写(覆盖)

ps:此时的函数名相同,但是不构成隐藏,不满足三同的才叫做隐藏
这样就可以什么人对应什么政策

在这里插入图片描述

二. 多态的定义及实现

🌈多态的条件

🥑多态有两个条件,缺一不可:

  • 必须通过父类的 指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    • 虚函数:被virtual修饰的类成员函数
    • 重写要求虚函数 + 三同(父类和子类的返回值类型函数名字参数列表完全相同)(形参名和缺省参数名不一样不影响
  • 构成多态,传的哪个类型的对象,调用的就是哪个类型的虚函数 - 跟对象有关
  • 不构成多态,调用的就是p类型函数 - 跟类型有关

下面进行验证:如果用对象来调用,够不够构成多态

void Func(Person p)
{
	p.BuyTicket();
}

在这里插入图片描述

我们思考为什么一定要是父类的指针或引用呢?为什么是父类?为什么是指针和引用

  1. 因为只有指针引用访问才能实现晚绑定,如果使用的是对象的话,在编译期间就已经绑定完毕了(也就是已经确定call好了地址了),也就不能实现多态
  2. 我们知道一个子类的第一个成员是父类成员,父类下的第一个就是虚表指针,我们是要通过父类指针来找到这个虚表的!如果是通过子类去访问就是静态绑定,不能达到动态调节

没有重写的话,是编译时决定还是运行时决定地址?

在这里插入图片描述

调试打开反汇编可以看见是运行时决定
在这里插入图片描述

此处的编译器并没有完全检查你是否重写,只是初略的检查是否是虚函数以及父类指针调用,但是调用的还是同一个虚函数,因为没有完成覆盖

🌈虚函数重写的两个特例

🥑协变

协变返回值可以不同,但要求必须是父子关系的指针或者引用

在这里插入图片描述
实际上用的不多

🥑析构函数的重写

如果析构函数构是虚函数,这里构成重写吗?yes!但是他们的函数名不相同啊,因为析构函数名被特殊处理了,都处理成了destructor(),至于为什么要特殊处理,就是源于多态

//建议在继承中析构函数定义成虚函数
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person {
public:
	//析构函数名会被处理成destructor,所以完成了重写
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Person p;
	Student s;
	
	return 0;
}

在这里插入图片描述

普通的场景下是没有出现问题的,但是有特殊的场景!要记住面试高频考点

✨ 那什么场景下,析构函数要是虚函数呢?

	Person* ptr1 = new Person;
	delete ptr1;

	Person* ptr2 = new Student;
	delete ptr1;

如果不是虚函数,那也就不构成多态,那与类型有关,都会去调用父类的析构函数,但是这样会导致子类对象可能有资源未被清理,我们希望的是父类调用父类,指向子类调用子类的(完了再调用父类),这样是不是就符合我们多态的理念

在这里插入图片描述
析构函数的重写很简单,因为函数名“相同”,没有参数,加一个virtual就可以

在这里插入图片描述
在其他场景,析构函数是不是虚函数都可以

🌈只有父类带 virtual 的情况

虚函数,允许父子类两个都是虚函数 或 只有父类是虚函数也行。这其实是C++不是很规范的地方,建议两个都写上virtual

这是因为虽然子类没带virtual,但是它 继承了父类的虚函数属性,重写是实现

在这里插入图片描述

🌈C++11 final & override

🥑final

final有两个功能

  • 修饰一个类,这个类不能被继承
  • 修饰虚函数,限制它不能被子类中的虚函数重写

C++11中final还可以限制重写
修饰虚函数,限制它不能被子类中的虚函数重写

在这里插入图片描述

🥑override

override放在子类重写的虚函数后面,帮助检查是否完成重写,没有重写会报错

类似于核酸检测,,没有做就报错(做核酸魔怔了)
在这里插入图片描述

三. 重载 vs 重写 vs 隐藏

在这里插入图片描述

四. 抽象类

💛 包含纯虚函数的类叫做抽象类(接口类)。在虚函数的后面写上=0 ,则这个函数为纯虚函数

纯虚函数一般只声明,不实现,抽象类不能实例化出对象;相当于间接强制你重写!

在这里插入图片描述

即使我们创造了一个子类对象,其派生类继承后也不能实例化出对象,因为继承了抽象类后,这个派生类就继承了纯虚函数,那它同样也是一个抽象类!

只有重写纯虚函数,派生类才能实例化出对象。所以呀,抽象类本质上强制继承它的子类完成虚函数重写

class Car
{
public:
	virtual void Drive() = 0;
};

class BMW: public Car
{
public:
	virtual void Drive()
	{
		cout << "操控——好开" << endl;
	}
};

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

int main()
{
	//BMW b;
	Car* ptr = new BMW;
	ptr->Drive();

	Car* ptr = new Benz;
	ptr->Drive();

	return 0;
}

在这里插入图片描述

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

五. 多态的原理

🔥虚函数表

⚡引入

// 其中sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

private:
	int _b = 1;
	char _ch = 'A';
};

int main()
{
	cout << sizeof(Base) << endl;
}

如果我只考虑到了内存对齐的话,答案就是8

在这里插入图片描述

但此处的考点不仅仅只有内存对齐,真正考察的是多态,那究竟是什么东西的存在多了4个字节
通过监视窗口,发现这个对象多了一个成员,虚函数表指针_vfptr(virtual function table)(简称虚表指针) ,所谓的虚函数表就是一个指针数组,里面存放的是函数指针(放的是虚函数地址),一般这个数组的最后面放了一个nullptr——

虚函数等等的函数都是放在代码段的!

在这里插入图片描述

记住对象里面没有虚表,只有指向虚表的指针;

🔥多态的原理

虚函数表是理解多态原理的关键,下面将在底层剖析

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

class Student : public Person 
{
public:
	virtual void BuyTicket() 
	{ cout << "买票-半价" << endl; }
protected:
	int _b = 0;
};

void Func(Person& p) {
	p.BuyTicket();
}

int main()
{
	Person Scort;
	Func(Scort);

	Student Durant;
	Func(Durant);

	return 0;
}

虚函数的“重写”也叫“覆盖”,重写是语法上的概念,覆盖是原理层的概念;子类继承父类的虚函数,可以认为深拷贝了一份虚函数表,没有重写时,子类与父类虚表完全相同;若重写了,便会用新地址覆盖

🍂转到反汇编可以发现:

  • 对于普通成员函数的调用,是在编译后就已经确定了调用地址(橙色的);
  • 给父类/子类对象,调用虚函数p.BuyTichet()的汇编代码却是相同的,那就说明此时调用函数时,不再是直接确定地址,而是借助了eax这个寄存器,这是多态原理的关键

在这里插入图片描述
(汇编不要求全部看懂,懂大概意思就可)

🟢多态的本质原理,基类的指针/引用指向谁,就去谁的虚函数表中找到对应位置的虚函数进行调用,这是在运行中确定的,所以叫动态的多态

在这里插入图片描述

而普通函数,在编译链接的时候已经确定了函数运行地址,直接调用即可
在这里插入图片描述

🔥小细节

p1和p2是共用一个虚表吗?

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

int main()
{
	Person p1;
	Person p2;

	return 0;
}

在这里插入图片描述

结论是:同一类型的对象共用一个虚表

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

class Student :public Person {
public:
	//virtual void BuyTicket() { cout << "买票——半价" << endl; }
};

int main()
{
	Person p1;
	Person p2;

	Student s1;
	Student s2;

	return 0;
}

在这里插入图片描述

结论:vs下,不管是否完成重写,子类虚表和父类的虚表不是同一个

🔥虚函数表在哪

虚表在哪里呢?我们就铺垫过虚函数表不能修改,所以我猜测是在常量区的
我们写一段代码来验证一下 ——

在这里插入图片描述

所以,虚函数表是存在“常量区”的

int main()
{
	int* ptr = (int*)malloc(4); 
	printf("heap: %p\n", ptr);

	int a = 0;
	printf("stack: %p\n", &a);

	static int s = 0;
	printf("数据段:%p\n", &s);

	const char* p = "always";
	printf("常量区:%p\n", p);

	printf("代码段:%p\n", &Base::func1);

	Base b;
	// 取对象头4/8个字节 —— 强转(Base* -> int*) —— 再解引用拿到_vfptr
	printf("虚函数表: %p\n", *((int*)&b));

	return 0;
}

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

首先我们要再来观察如下代码在监视窗中的状况,这儿vs起到了很好的误导作用,我们来一一揭秘

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

	virtual void func1()
	{}
};

class Student :public Person {
public:
	virtual void BuyTicket() { cout << "买票——半价" << endl; }

	virtual void func2()
	{}
};

int main()
{
	Person p1;
	Person p2;

	Student s1;
	Student s2;

	return 0;
}

ps:子类的虚函数在监视窗口是看不见的
但是func2还是要进虚表的

在这里插入图片描述

💦打印虚函数表

有时虚函数地址被隐藏掉了,之前我们只能在内存窗口中观察,现在我们来学习打印虚函数表

在这里插入图片描述

虚函数表中都是函数指针,函数指针如何定义变量还记得吗,不是函数名不是定义在最后的,而是混杂其中的,我们在typedef时,依然保留了这个原则

//typedef void(*)() VFPTR; 这样定义是编译不过的,要像函数指针定义一样
typedef void(*VFPTR)();

//打印虚函数表           vs下虚表最后才有nullptr,linux下没有
void PrintfVFTable(VFPTR table[])
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("vtf[%d]:%p", i, table[i]);
		VFPTR pf = table[i];
		pf();
	}
}

那么在调用这个函数的时候,就需要传入虚函数表的地址,即指针数组的(首元素)地址,即对象中的虚表指针_vfptr

问题就转化成了如何取到对象头4/8个字节呢?

两个没有关系的类型(int 和 student),是没办法直接强转成int,那取&个地址,变成student* 再转成 int*,可是传入的参数类型还不匹配,那就再(VF_PTR*)强转一下 ——

    //取对象头部虚函数表指针传递过去
	PrintfVFTable((VFPTR *)(*(int*)&s1));

一步步理顺:先&s1,变成student*,再强转成(int*)解引用就是开头的4个字节了,形参是VF_PTR[],类型是(VF_PTR*),最后再强转一下即可

出现打印不全的情况,只需要重新生成一下解决方案即可

在这里插入图片描述

    PrintVFTable((VF_PTR*)(*(int*)&b));			// 32位
	PrintVFTable((VF_PTR*)(*(long long*)&b));	// 64位
	PrintVFTable((VF_PTR*)(*(void**)&b));		// 32/64位
  • 32位平台,用(int*)强转
  • 64位平台,用(long long*)强转

此处为什么就是void**(int*) 解引用看一个int的大小,(long long*)解引用看的是long long的大小,void* 不能解引用,这(void**)解引用看的是void* 的大小,void* 的大小就和平台相关

记住我们要取的是对象头四个子杰中的内容而不是地址,这就是为什么要*解引用的作用
实际传的是二级指针

在这里插入图片描述

Linux下的写法与调用:

//Linux下:
void PrintfVFTable(VFPTR* table, size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		printf("vtf[%d]:%p ->", i, table[i]);
		VFPTR pf = table[i];
		pf();
	}
}
PrintfVFTable((VFPTR *)(*(int*)&s1, 3);

💦单继承的虚函数表

class Person
{
private:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int _a = 0;
};

class Student :public Person
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	void fun4() { cout << "func4()" << endl; }
private:
	int _b = 1;
};


//void(*ptr)();//函数指针

//typedef void(*)() VFPTR; 这样定义是编译不过的,要像函数指针定义一样
typedef void(*VFPTR)();

//打印虚函数表           vs下虚表最后才有nullptr,linux下没有
//void PrintfVFTable(VFPTR table[])
//Linux下:
void PrintfVFTable(VFPTR* table, size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		printf("vtf[%d]:%p ->", i, table[i]);
		VFPTR pf = table[i];
		pf();
	}
}

int main()
{
	Person p1;
	PrintfVFTable((VFPTR*)(*(int*)&p1), 2);
	cout << endl;

	Student s1;
	PrintfVFTable((VFPTR *)(*(int*)&s1),3);

	return 0;
}

在这里插入图片描述

💦多继承的虚函数表

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

我们首先来算一下Derive的大小

在这里插入图片描述

还是那句话:vs下无法检测到子类新增的虚函数,虚函数总会放进虚表的嘛,那究竟是放进第一个虚表还是第二个虚表呢

我们打印一下虚表可以发现:func3在第一个虚表中

在这里插入图片描述
那我们怎么样打印Base2的虚表呢?

    //正常方法
	PrintVTable((VFPTR*)*((int*)((char*)&d + sizeof(Base1))));

	//切片方法
	Base2* ptr = &d;
	PrintVTable((VFPTR*)*((int*)(ptr)));

在这里插入图片描述

在这里插入图片描述

可见func3()放进了第一个虚表中

在这里插入图片描述

并且即使子类重写了func1后,你发现这对象虚表中,Base1和Base2的虚函数func1的地址不一样,你早就不应该感到惊奇,因为这时jmp跳转指令的地址,最终会一跳到同一位置执行函数Derive::func1的 ——

在这里插入图片描述

发现p2->func1()调用函数时,还跳了好多层。这是为了做准备工作ecx-8 ,修正this指针(eax),为什么呢?调用虚函数时,要传递this指针,-8由指向Base1到指向Base2,从而看到对应类型视角下的那部分

💦菱形继承的虚函数表

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

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

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

还是简简单单的说一下吧,继续来上一段老代码

class A
{
public:
	virtual void f()
	{}
public:
	int _a;
};

class B : virtual public A 
{
public:
	int _b;
};

class C : virtual public A 
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	//d._a = 0; //不存在二义性,可以直接找
	return 0;
}

在这里插入图片描述

在,菱形继承中,如果B和C都重写了A的虚函数func1,那么D必须重写func1,否则会报错“D”:“void A::f1(void)”的不明确继承,因为这儿是虚继承,共用一个虚表,不知道用哪个重写

public:
	virtual void f1() {}
public:
	int _a;
};

class B : virtual public A 
{
public:
	virtual void f1() {}
	virtual void f2() {}
public:
	int _b;
};

class C : virtual public A 
{
public:
	virtual void f1() {}
	virtual void f2() {}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void f1() {}
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

我们说过虚基表中,曾经内容是00000000是为其他东西预留的,那它究竟是什么呢?这是找虚表的偏移量。

在这里插入图片描述

虚基表存的是偏移量,用来解决数据冗余和二义性

虚函数表是存放虚函数的,解决多态

所以通俗一点的说,菱形继承谁用谁…

七. 常见考点总结

  1. 什么是多态?
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?
  3. 多态的实现原理?
  4. inline函数可以是虚函数吗?
    答:不可以,因为内联函数没有地址,但是虚函数地址要被填入到虚表中。不过是可以编译通过的,因为inline只是个建议,到底有没有展开要视情况而定:若调用时不构成多态,保持inline属性;若构成多态,则没有inline属性。
  5. 静态成员可以是虚函数吗?
    答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  6. 构造函数可以是虚函数吗?
    答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  7. 析构函数可以是虚函数吗? 什么场景下析构函数是虚函数?
    答:可以,并且最好把基类的析构函数定义成虚函数。
  8. 构造函数可以是虚函数吗?
    答:拷贝构造也是构造函数,也有初始化列表,答案和构造一样
  9. 赋值函数可以是虚函数吗?
    答:语法上可以,但是没有意义
  10. 对象访问普通函数快还是虚函数更快?
    答:虚函数不构成多态就一样快,虚函数构成多态的调用,普通函数快,因为多态调用时运行时决议。
  11. 虚函数表是在什么阶段生成的,存在哪的?
    答:虚函数表是在编译阶段就生成的,一般情况下存在**代码段(常量区)**的。
  12. C++菱形继承的问题?虚继承的原理?
    答:注意这里不要把虚函数表和虚基表搞混了。
  13. 什么是抽象类?抽象类的作用?
    答:查上面的。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系

八. 经典题型讲解

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: 以上都不正确

本题很老六,不仅仅考了1️⃣多态还考了2️⃣接口继承的概念

  1. 首先test函数不满足多态,func函数满足多态();
  2. p是指向子类B的,指针p传给test中的this指针是发生了切片(因为是子类指针p传给父类指针this
  3. this 传给func函数,this指向的是子类B,所以func函数调用的就是子类B的func
    在这里插入图片描述到这里答案是不是要选D,但是还有一个细节
  4. 此处的value值是1,为什么呢?因为重写接口继承,普通函数是实现继承。接口继承就是函数主体架子都不变,直接拿去使用(加不加virtual、缺省值相不相同无所谓),重写了是实现的部分
    在这里插入图片描述

答案选B

改编:这样的话答案是选什么呢?

int main(int argc, char* argv[])
{
	A* p = new B;
	p->test();
	return 0;
}
  • 答案没变还是选B,只是切片的地方发生了改变,A* p = new B,此处就发生了切片,this指针还是指向子类B的,所以调用func函数也会调用子类的!
  • 正好符合多态的原理:基类的指针/引用指向谁,就去谁的虚函数表中找到对应位置的虚函数进行调用
  • 如果是A* p = new A,就是调用父类A的

2️⃣杀手题目

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

  • 首先A不会重复的初始化,而且是按照声明顺序来的,所以先初始化A
  • A不会在B和C中初始化,因为D中只有一份A,在其他两个中初始化都不合适,只能在D中

所以答案选A

📢写在最后

恭喜球王梅西圆梦!

请添加图片描述

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

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

相关文章

Logoist - 适用于设计师以及初次使用者,快速制作精美 logo

Logoist - 适用于设计师以及初次使用者的快速制作精美 logo 工具 从简单的标识到设计开发。它只需要一点时间来创建令人印象深刻的图像和矢量图形与Logoist。 我们的一体化应用程序为您提供了您需要的一切&#xff0c;将您的创意付诸实践或寻找新的灵感!它适合专业设计师和插画…

阿里云将加速与伙伴合作 促进Web3.0生态发展

12 月 15 日&#xff0c;在Web3.0 Cloud Day Singapore 2022 活动上&#xff0c;阿里云新加坡、南亚和泰国总经理 Dr Derek Wang 表示&#xff0c;阿里云将加速和伙伴的合作以促进创新。“我们正在与我们的合作伙伴合作以实现创新。我们仍然处于 Web 3.0 的早期阶段。我们仍然需…

【蓝桥杯选拔赛真题53】Scratch破解保险柜 少儿编程scratch图形化编程 蓝桥杯选拔赛真题讲解

目录 scratch破解保险柜 一、题目要求 编程实现 二、案例分析 1、角色分析

大数据处理之ClickHouse概述及架构参考(未完)

一、概述 中移某业务拨测系统基于业务数据拨测指标及日志的分析需要&#xff0c;随着Clickhouse在OLAP领域的快速崛起&#xff0c;以及一些特性考虑&#xff0c;比如&#xff1a; 数据量会很大&#xff0c;最好需要分布式&#xff1b; 支持实时写入&#xff0c;支持快速计算&a…

数据库管理-第四十九期 Exadata的存储节点管理(20221223)

数据库管理 2022-12-23第四十九期 Exadata的存储节点管理1 咋个查看数据是否被缓存到闪存卡了没2 EM13.5的Exadata监控3 存储降级总结第四十九期 Exadata的存储节点管理 本周二&#xff0c;抗原终于阴性了&#xff0c;星期三开始就回到现场开始办公。上周既然说了Exadata关于存…

[C++: 引用】

To shine,not be illuminated. 目录 1 引用概念 2 引用特性 3 常引用 4 使用场景 4.1 引用做参数 4.2 做返回值 5 传值、传引用效率比较 6 引用和指针的区别 7 总结 1 引用概念 引用不是新定义一个变量&#xff0c;而是给已存在变量取了一个别名&#xff0c;编译器不会为…

Java+Swing+mysql会员卡管理系统

JavaSwingmysql会员卡管理系统一、系统介绍二、功能展示1.主页页面2.会员信息查询3.会员信息删除三、系统实现1.members.java四、其它1.其他系统实现一、系统介绍 使用 Java 技术开发一个会员卡管理系统&#xff0c;具体实现功能如下&#xff1a; 1. 程序启动显示主界面&#…

Vue:从组件开始学习

文章目录Vue组件生命周期根据官网文档&#xff0c;我们可以快速使用vue3创建自己的应用代码&#xff1a;npm init vuelatest&#xff0c;然后根据自己的需要来选择对应的配置&#xff1a; 默认使用vite来配置项目的。 在main.ts入口文件中&#xff0c;我们可以看到&#xff0…

非零基础自学Golang 第17章 HTTP编程(上) 17.5 知识拓展

非零基础自学Golang 文章目录非零基础自学Golang第17章 HTTP编程(上)17.5 知识拓展17.5.1 curl工具详解第17章 HTTP编程(上) 17.5 知识拓展 17.5.1 curl工具详解 【1】curl 简介 curl是一个利用URL语法在命令行下工作的文件传输工具&#xff0c;于1997年首次发行。它支持文…

Java+MySQL基于ssm的互助救援车队管理系统

此项目能够更全面的为社会贡献爱心&#xff0c;更及时的帮助求助人&#xff0c;然后在后台完成整个过程最后在通过广播好人好事&#xff0c;在高考爱心送子、养老院奉献爱心等活动更能做到统一化&#xff0c;更有序让注册的私家车主有的放矢&#xff0c;供献爱心。现在社会的信…

MyBatis-Plus(实用篇)

文章目录一、基础组件&#xff08;接口和实现类&#xff09;1、BaseMapper\<T>2、IService\<T>3、链式查询与修改4、调用Service层操作数据二、常用注解1、TableName2、TableId3、TbaleField4、TableLogic三、条件构造器1、wapper介绍2、构造器常用方法3、组装条件…

编程艺术之变成原则

编程有一个原则&#xff0c;就是尽可能去避免重复的代码。 类的开闭原则&#xff0c;类做好后&#xff0c;就尽量不要再在类上面在修改代码&#xff0c; 耦合度&#xff1a;简单的一个例子&#xff0c;活字印刷术在之前是整版印刷&#xff0c;导致&#xff0c;如果有一个字要…

作一回白嫖怪:写一个脚本自动获取ST官网积分,用积分领取奖品

环境&#xff1a;Python、selenium、ubuntu22.04 网址&#xff1a;STMCU中文官网 chrome: 版本 108.0.5359.124&#xff08;正式版本&#xff09; &#xff08;64 位&#xff09; chromeDriver: CNPM Binaries Mirror 这两个文件我打包起来了&#xff0c;0积分方便大家…

【表格合并与底纹】vue-elementul表格简单实现合并单元格,与列和行给底纹颜色

前言 这是一个很常见的需求啊&#xff0c;因为很多公司都会涉及到写表格展示数据 那么在某些公司内就会出现为了让数据更直观的感受到 而让你给某些行和列用颜色标出来。使得看起来更方便 那么这里就汇总一下常用的横竖合并以及横竖颜色底纹如何实现 效果图 这是写的一个dem…

小米便签维护过程记录——可能出现问题解决方案

在对小米便签开源代码进行研究和维护及新功能开发的同时&#xff0c;会遇到很多问题&#xff0c;将以本文进行记录总结。 导入项目 若不导入直接Open会导致默认为Project&#xff0c;不为Android。 小米标签菜单栏不显示的问题 先来看NotesListActivity类&#xff0c;这个类创…

Zookeeper 4 Zookeeper JavaAPI 操作 4.5 Curator API 常用操作【修改节点】

Zookeeper 【黑马程序员Zookeeper视频教程&#xff0c;快速入门zookeeper技术】 文章目录Zookeeper4 Zookeeper JavaAPI 操作4.5 Curator API 常用操作4.5.1 修改节点4 Zookeeper JavaAPI 操作 4.5 Curator API 常用操作 4.5.1 修改节点 直接开干 修改之前app1 的数据是本机…

PS1文件执行

PS1文件执行目录概述需求&#xff1a;设计思路实现思路分析1.power shell 管理2.shell 管理执行参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for ch…

软件需求说明书(GB856T——88)基于协同的在线表格forture-sheet

软件需求说明书的编写提示 1引言 1.1产品目的 制造一个功能丰富&#xff0c;配置简单的在线表格组件&#xff0c;开箱即用&#xff0c;产品对位所有办公用户&#xff0c;可以大量用户同时在线进行协同合作办公&#xff0c;可以大大的提高工作效率。 1.2产品范畴 产品为软件…

Javaweb会话跟踪技术(Cookie和Session)

目录 一、会话跟踪技术概述 二、Cookie 1.Cookie基本原理 2.Cookie基本使用 3.Cookie存活时间 4.cookie存储中文 三、Session 1.Session钝化、活化 2.Session基本使用 3.sesion销毁 四Cookie和Sesion区别 一、会话跟踪技术概述 1.会话&#xff1a;用户打开浏览器&…

Java和vue开发的橱柜定制系统家具定制系统

简介 橱柜定制家具定制系统。用户查看家具样本&#xff0c;进行预约下单&#xff0c;后台人员根据用户的要求给出效果图和方案&#xff0c;以及报价&#xff0c;填写上门时间。管理员上门测量和用户确定好需求后上传协议&#xff0c;此时订单变成待开工订单&#xff0c;后台人…