【C++修炼之路】多态

news2024/11/13 15:17:20

👑作者主页:@安 度 因
🏠学习社区:StackFrame
📖专栏链接:C++修炼之路

文章目录

  • 一、概念
  • 二、定义和实现
    • 1、虚函数
    • 2、虚函数的重写
    • 3、多态的构成条件
    • 4、重写的例外
    • 5、C++11 override 和 final
    • 6、不能被继承的类
    • 7、重载、重写(覆盖)、重定义(隐藏)的对比
  • 三、抽象类
  • 四、原理
    • 1、虚函数表
    • 2、疑问拓展
    • 3、动态绑定与静态绑定
  • 五、单继承和多继承关系的虚函数表
    • 1、单继承虚表
    • 2、多继承虚表
    • 3、菱形继承、菱形虚拟继承的虚表
  • 六、题

如果无聊的话,就来逛逛 我的博客栈 吧! 🌹

一、概念

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

例如,不同人买票的价格不同;支付宝抢红包等。

二、定义和实现

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

1、虚函数

虚函数:被virtual修饰的类成员函数称为虚函数。

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

2、虚函数的重写

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

重写条件简称:虚函数(两个函数) + 三同

例如:

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

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

虚函数的重写,重写的是实现,前面的壳子可以认为并没有改变,如果父类参数列表给了缺省值,在子类调用不传参的情况下,使用的是父类的缺省参数。

重写的形参名可以不同,但是类型必须相同。重写是语法层的概念,覆盖是原理层的概念。

父类虚函数必须加 virtual,子类可以不加 virtual ,因为编译器只检查三同;父类不加不是多态;子类已经继承了虚函数,认为子类也是虚函数,重写就可以只重写实现,但是建议加上。

3、多态的构成条件

  1. 必须通过基类的指针或者引用调用虚函数

  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

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

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

// 基类指针
// 进行过切片 派生类 --> 基类
void func(const Person* p)
{
	p->BuyTicket();
}

int main()
{
	Person pp;
	func(&pp);

	Student st;
	func(&st);

	return 0;
}

多态,不同对象传递过去,调用不同函数;多态调用看的是指向的对象。

若不构成多态,则为普通调用,看当前调用者的类型,例如 func 参数为 const Person p ,则调用两次Person类的函数,此刻不构成多态:

image-20230724181537259

4、重写的例外

  1. 派生类的重写虚函数可以不加 virtual – 建议写上。
  2. 协变,返回的值可以不同,但是要求返回值必须是父子关系指针和引用(不常用)。

协变:

正常返回值不同:

image-20230724172523953

协变:

// 基类虚函数和派生类虚函数返回的参数类型只要构成父子指针即可
// 1. 基类为父,派生类为子,否则关系不对会报错
// 2. 必须同时是指针或者引用
class A
{};

class B : public A
{};

class Person {
public:
	virtual	A* BuyTicket() const { 
		cout << "买票-全价" << endl;
		return 0;
	}
};

class Student : public Person {
public:
	virtual B* BuyTicket() const { 
		cout << "买票-半价" << endl;
		return 0;
	}
};

析构函数的重写

析构函数可以是虚函数吗?为什么需要是虚函数?

析构函数加 virtual 是虚函数重写,因为类析构函数都被处理成 destructor 这个统一名字,而处理就是为了让它们构成重写。

普通状况下,析构函数不重写可以:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual ~Person() { cout << "~Person()" << endl; }
     // ~Person() { cout << "~Person()" << endl; } 也可以
};

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

int main()
{
	Person p;
	Student s;

	return 0;
}

因为无论重写成虚函数,还是不写为虚函数,这里都是调用一次 Student 的析构,两次 Person 的析构。

但是这种情况就不行:

image-20230724191655581

没有调用到派生类的析构函数,内存泄漏了。

delete 是两部分构成的:p->destructor() + operator delete(p),由于没有重写虚函数,所以这里不构成多态,所以普通调用。普通调用只会看当前调用者的类型。

当前 p 为 Person 的指针,所以只会调用 Person 的析构函数,此刻,Student 类的析构没有调用,造成了内存泄漏。

期望指向谁调用谁,所以需要借助多态,让 p->destructor() 为多态调用,那么就要进行虚函数的重写

但是对于析构函数,由于三同中不满足名字相同,所以需要把名字统一处理为 destructor ,这样加了 virtual 就构成重写。

image-20230724192809260

这时就正确了。上面的问题也都回答了。

建议:基类写析构函数把 virtual 加上,应对特殊情况。比如这里,基类的析构加上 virtual 就没有这种情况了。那么可以认为子类不加 virtual 就是为这个地方准备的,这样写子类的析构也更加正常。虽然基类加上会产生虚表有一些代价,但是利大于弊。

5、C++11 override 和 final

final:修饰虚函数,表示该虚函数不能再被重写

image-20230724195920205

无法重写。

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

image-20230724200045784

image-20230724200105666

6、不能被继承的类

方法1:基类构造函数私有 (C++98)

class A
{
public:
	// 设置为静态成员函数,否则没有构造函数,无法创建对象,那么也无法间接调用构造函数
	static A CreateObj()
	{
		return A(); // 私有后,A 调用构造函数
	}
private:
	A() {}
};

class B : public A
{};

int main()
{
	A::CreateObj(); // 访问静态成员函数,间接调用构造

	return 0;
}

================================================================
    
class A
{
public:
private:
	~A() {}
};

class B : public A
{};

int main()
{
	A* p = new A; // new 调用构造函数,创建 A 对象,创建普通对象会因为无法调用析构报错
    // 如果要释放,则写个 destory 即可
	// B bb; // err 无法调用 A 的析构,所以无法继承
	return 0;
}

方法2:基类加一个final (C++11)

加 final 的类叫做最终类,表示该类不能被继承:

image-20230724202431429

7、重载、重写(覆盖)、重定义(隐藏)的对比

image-20230724203406560

三、抽象类

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

注:纯虚函数不用实现,只要声明。

image-20230731100818028

同理,包含纯虚函数的派生类也无法实例化出对象:

image-20230731100724433

派生类继承了基类,继承了基类的纯虚函数。

实例化出对象需要重写纯虚函数:

image-20230731101000864

抽象类 Car 没有虚表,因为 Car 无法实例化出对象;Car 的派生类有虚表,因为派生类中有 Car 。

抽象类简介强制了派生类虚函数的重写

四、原理

1、虚函数表

sizeof(Base)是多少?

image-20230724210025093

由于内存对齐和虚函数带来的额外东西的原因,大小为 8

image-20230724210749992

除了 _b 还有个 _vfptr 为虚函数表指针。

虚函数本质放在公共代码区的,虚表(虚函数表)中存着虚函数的地址;如果是虚函数,就把虚函数的地址存在虚表,不是虚函数,就不存在虚表:

但是如果对虚函数重写后呢?虚表里存的是什么?

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

	int _a = 1;
};

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

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

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

	Student Johnson;
	Func(Johnson);

	return 0;
}

image-20230724214036671

子类的虚函数表中,存的是重写后的虚函数的地址,所以这里体现出了重写的别称:覆盖,覆盖表示在原理层,当虚函数重写后,子类虚表中存放的是覆盖后的虚函数的地址。

从理解上,可以认为子类的虚函数表是先把父类的表拷贝过来,再把重写的虚函数的地址在表中覆盖

image-20230724214549833

从这里,可以知道是如何实现指向父类调用父类,指向子类调用子类虚函数的原理了:

若指向父类,看到的是父类;若指向子类,则将子类的父类部分切片,看到的也是父类。所以无论传父类或者子类的引用,看到的都是父类。

若调用时,不符合多态,则为普通调用,在编译时确定地址,根据函数名修饰规则找到函数,去调用:

image-20230724215208475

若调用时,符合多态,则为多态调用;此刻并不知道 p 指向父类还是子类,因为看到的都是父类部分;运行到指向对象的虚函数表中找调用函数的地址。

image-20230724215429934

前面在虚表中找虚函数的地址 call eax 就是调用虚函数。

2、疑问拓展

针对构成多态的条件:

  1. 必须通过基类的指针或者引用调用虚函数

  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

提出以下问题:

  1. 为什么不能是子类的指针或者引用?

因为父类可以指向父类或者子类(子类中父类的部分),但是子类只能指向子类,子类要指向父类中的完整子类对象,可是父类中并没有子类对象,所以子类无法指向父类,只能指向子类。(目前是这样,学到后面子类指向父类可以支持)

去虚表里找只有子类的虚函数,无法实现多态。

  1. 为什么不能是父类的对象?

image-20230726152539541

对象之间的切片,是将对象拷贝过去,对于虚表,不会拷贝。若拷贝虚表,则父类对象的虚表中是父类虚函数还是子类虚函数就不确定了,乱套了。

若把子类对象赋值给父类对象,需要调用子类对象的虚函数,但是这里由于没有拷贝虚表,调用的仍然是父类对象的虚函数,没有完成多态。

补充:

  1. 普通函数是一种实现继承;多态为接口继承:把接口继承,重写实现;若不重写,则不会对虚表中的函数地址进行覆盖,则是直接继承了接口。
  2. 一般来说,虚表结尾会放上 ‘\0’,vs 有,g++ 没有(编译器会有 bug ,对编译好过后的代码进行修改,可能不显示\0,甚至可能虚表的地址都不变。清理解决方案,再次生成即可)

image-20230726154617277

  1. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后,观察可能看不见。

image-20230726155205032

image-20230726170541020

image-20230726155345112

image-20230726155435827

  1. 虚表存在的位置?如何验证?

栈 堆 数据段(静态区) 代码段(常量区)

首先排除堆,堆上是动态开辟的空间;再排除栈,同类型对象共用一个虚表,栈是伴随着栈帧走的 ,照这个说法,则每次开辟栈帧,那么就额外开辟一个虚表?函数结束栈帧销毁再把虚表销毁?显然不实际。

虽然分析了,但是还是有疑点,毕竟虚表也有可能存在 main 函数栈帧,main函数栈帧创建开辟,销毁则表也销毁;大多资料也是存在于静态区,到底是不是,验证一下就 ok :

int main()
{
	Person ps;
	Student st;

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

	static int b = 0;
	printf("静态区:%p\n", &b);

	int* p = new int;
	printf("堆:%p\n", p);

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

	printf("虚表1:%p\n", *((int*)&ps)); // 取前四个字节打印虚表地址
	printf("虚表2:%p\n", *((int*)&st));

	return 0;
}

image-20230726161518238

数据上看,是存在常量区。放在常量区很合理,因为虚表不能被修改;至于说的覆盖是从理解层面,实际上编译器直接使用重写后的函数地址,并不是真正拷贝过去再修改。

但是不同的平台可能不同,不确定就验证。

3、动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态,比如:继承,虚函数重写,实现的多态。

五、单继承和多继承关系的虚函数表

1、单继承虚表

先前,观察单继承关系的虚函数表时,内存中 ‘\0’ 看到一串地址,怀疑是 func3 函数的地址,但是地址并没有出现在虚函数表中,能不能证明一下?

虚函数表本质是函数指针数组,可以通过函数指针数组来对虚表中内容进行打印:

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

	virtual void Func1() 
	{
		cout << "Person::Func1()" << endl;
	}

	virtual void Func2() 
	{
		cout << "Person::Func2()" << endl;
	}

//protected:
	int _a = 0;
};

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

private:
	virtual void Func3()
	{
		//_b++;
		cout << "Student::Func3()" << endl; // 调用时打印,看地址是否和内存中一样
	}
protected:
	int _b = 1;
};

typedef void(*FUNC_PTR) ();

// 打印函数指针数组
void PrintVFT(FUNC_PTR* table)
{
	// 结尾时 \0
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
         FUNC_PTR f = table[i]; // 正常情况下,成员函数应该通过对象调用

		f(); // 但是这里是直接调用,没走常规路,突破语法层的限制,拿到地址直接调了;问题:没有 this 指针,所以成员函数中有对成员变量的访问有可能就会崩
	}
	cout << endl;
}

int main()
{
	Person ps;
	Student st;

	// 取前四个字节
	int vft1 = *(int*)&ps;
	PrintVFT((FUNC_PTR*)vft1);

	int vft2 = *(int*)&st;
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

image-20230726165921625

image-20230726170505590

2、多继承虚表

typedef void(*FUNC_PTR) ();

// 打印函数指针数组
void PrintVFT(FUNC_PTR* table)
{
	// 结尾时 \0
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		FUNC_PTR f = table[i];

		f();
	}
	cout << endl;
}

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

int main()
{
	Derive d;
	cout << sizeof(d) << endl;

	return 0;
}

image-20230726173602327

派生类一般不会单独产生虚表,继承父类,父类中包含虚表(完成了重写)。虚表可以认为属于派生类。

image-20230726174904983

在 Derive 类中,func3 应该被放在虚表中,在监视窗口并没有观察到,那么 func3 应该被放在哪里?Base1 or Base2 或者两个都放?

通过打印,获取虚表里的地址:

int main()
{
	Derive d;
	cout << sizeof(d) << endl;

	int vft1 = *((int*)&d);
	//int vft2 = *((int*)((char*)&d+sizeof(Base1))); // &d + Base1 大小为 Base2 
	Base2* ptr = &d; // 切片,指向 d 中 base2 开头
	int vft2 = *((int*)ptr);

	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

image-20230726193919532

Derive 类中的 func3 被放到了第一张虚表。

为什么重写 func1 ,但是 Base1 和 Base2 的虚表中的func1 不一样?

看汇编:

image-20230726194221144

Base1 和 Base2 虚表中的 func1 不一样,但是结果是同一个。

image-20230726210202753

ptr2调用函数 func1 让 ecx(this 指针) - 8,为什么?为什么 ptr1 不用减,ptr2 要减?

ptr1/ptr2 为基类的指针,func1 完成了虚函数重写,故调用 func1 时会到指向对象的虚函数表中,找到虚函数的地址;func1 是 Derive 的成员函数,调用时,需要让 this 指针指向 Derive 对象,所以 ptr2 - 8,指向 Derive 对象,ptr1 所在位置恰好在对象的开始(重叠)。修正好 this 指针,调用才是正确的。

在 call eax 之前,就会把 ptr1/ptr2 放到 ecx 寄存器中;但是 ptr2 的 this 指针指向不对,所以在 call eax 后,先去修正 this 指针的位置,再调用和 ptr1 一样的 func1。

3、菱形继承、菱形虚拟继承的虚表

菱形继承和多继承的虚表无区别。

菱形虚拟继承:

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1" << endl;
	}
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;
}

当 B 和 C 类对 A 类进行菱形虚拟继承,并将 func1 全重写时,会报错:

image-20230731082920018

因为菱形虚拟继承后,只有一份 A 类单独在对象最下面保存;B 和 C 共享 A ,当两个类同时对 A 类的虚函数重写时,此刻只有 A 一张虚表,虚表中放哪个类重写的虚函数的地址都不合适。

image-20230731083707418

当 B 和 C 对 func1 进行重写时,在 D 中重写 func1 :

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1" << endl;
	}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{
		cout << "D::func1" << endl;
	}
public:
	int _d;
};

对象模型和上方一样;在使用 D 对象时,重写的 B 和 C 没有意义;使用 B 和 C 对象时,符合多态条件,会根据情况调用 A/b/c 的虚函数,此刻有意义。

当 B 和 C 有不是重写 A 的虚函数,D 有自己单独的虚函数:

//class B : public A
class B : virtual public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}

	virtual void func2()
	{
		cout << "B::func2" << endl;
	}
public:
	int _b;
};

//class C : public A
class C : virtual public A
{
public:
	virtual void func1()
	{
		cout << "C::func1" << endl;
	}

	virtual void func2()
	{
		cout << "C::func2" << endl;
	}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{
		cout << "D::func1" << endl;
	}

	virtual void func3()
	{
		cout << "D::func3" << endl;
	}
public:
	int _d;
};

image-20230731090129505

image-20230731090233369

菱形虚拟继承时,说过虚基表指向空间的开头会预留一块空间,原先存的是 0 ;在这里就不是 0 。

第一章虚基表处的内容,fcffffff 是 -4,算的是虚表的偏移量,虚表在上方,四个字节。

B, C 因为有自己的虚函数,A 不属于 B, C,两个类共享一份 A ,所以B, C 的虚函数放到 A 中就要不合适了,所以 B, C 要建立自己的虚表。

d 没有自己的虚表,自己的虚函数也会被记录在被继承的虚表。

根据打印虚表函数,得出,func3 会被放在 B 的虚表中:

image-20230731092345929

六、题

选择:

  1. 下面哪种面向对象的方法可以让你变得富有( A )
    A: 继承 B: 封装 C: 多态 D: 抽象
  2. ( D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
  3. 面向对象设计中的继承和组合,下面说法错误的是?(C)
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
  4. 以下关于纯虚函数的说法,正确的是( A )
    A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
    C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
  5. 关于虚函数的描述正确的是( B )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
    C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
  6. 关于虚表说法正确的是( D )
    A:一个类只能有一张虚表
    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
    C:虚表是在运行期间动态生成的
    D:一个类的不同对象共享该类的虚表
  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类使用的不是同一张虚表

问答:

  1. 什么是多态?答:静态多态,函数重载;动态多态,满足基类指针和引用,虚函数重写的,多态调用。
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考本节课件内容
  3. 多态的实现原理?答:函数名修饰规则;虚函数表
  4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
  5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数可以使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表;无法实现出多态,也就没有意义,所以语法会强制检查。
  6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表最前面才初始化的。
  7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容
  8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
  10. C++菱形继承的问题?虚继承的原理?答:参考继承课件。注意这里不要把虚函数表和虚基表搞混了。
  11. 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

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

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

相关文章

RxJava异步编程初探

RxJava 其实就是提供一套异步编程的 API&#xff0c;这套 API 是基于观察者模式的&#xff0c;而且是链式调用的&#xff0c;所以使用 RxJava 编写的代码的逻辑会非常简洁。 RxJava 有以下三个基本的元素&#xff1a; 被观察者&#xff08;Observable&#xff09;观察者&…

prometheus+grafana进行服务器资源监控

在性能测试中&#xff0c;服务器资源是值得关注一项内容&#xff0c;目前&#xff0c;市面上已经有很多的服务器资 源监控方法和各种不同的监控工具&#xff0c;方便在各个项目中使用。 但是&#xff0c;在性能测试中&#xff0c;究竟哪些指标值得被关注呢&#xff1f; 监控有…

SqlSugar、Freesql、Dos.ORM、EF、四种ORM框架的对比

SqlSugar、Freesql、Dos.ORM、EF、四种ORM框架的对比 一、默认情况下,导航属性是延迟查询; 答:ORM(Object-relational mapping)即对象关系映射,是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。也就是说,ORM是通过使用描述对象和数据库之间映射的元数据…

线程状态

从卖包子的案例学习进程间的通信 public class Test {public static void main(String[] args) {Object objnew Object();Thread th1new Thread(){Overridepublic void run() {synchronized (obj){System.out.println("来三个包子&#xff01;");try {obj.wait(); /…

IDEA删除本地git仓库、创建本地git仓库、关联其他仓库并上传

IDEA删除本地git仓库、创建本地git仓库、关联其他仓库并上传 删除本地Git仓库 创建本地Git仓库 关联其他仓库并上传 要在IntelliJ IDEA中删除本地Git仓库并创建新的本地Git仓库&#xff0c;以及关联其他仓库并上传&#xff0c;请按照以下步骤进行操作&#xff1a; 删除本地G…

【笔记】数据结构与算法 python-03-列表查找

列表查找 在一个数据结构中&#xff0c;通过一定的方法找出与给定关键字相同的数据元素的过程。 列表查找&#xff08;线性表查找&#xff09;&#xff1a;从列表&#xff08;一种线性数据结构&#xff0c;元素按照一定的顺序存储&#xff0c;每个元素都有一个唯一的位置索引…

网络出口技术中的单一出口网络结构,你会用吗?

我们在设计一个园区网络的时候&#xff0c;园区网络的出口需要和运营商的网络进行对接&#xff0c;从而提供internet服务。 在和运营商网络对接的时候&#xff0c;一般采用如下3终方式&#xff1a; 单一出口网络结构 1、网络拓扑 终端用户接入到交换机&#xff0c;交换机直…

干货 ,ChatGPT 4.0插件Review Reader,秒杀一切选品神器

Hi! 大家好&#xff0c;我是专注于AI项目实战的赤辰&#xff0c;今天继续跟大家介绍另外一款GPT4.0插件Review Reader&#xff08;评论阅读器&#xff09;。 做电商领域的小伙伴们&#xff0c;都知道选品分析至关重要&#xff0c;可以说选品决定成败&#xff0c;它直接关系到产…

MySQL用通配符过滤数据

简单的不使用通配符过滤数据的方式使用的值都是已知的&#xff0c;但是当搜索产品名中包含ashui的所有产品时&#xff0c;用简单的比较操作符肯定不行&#xff0c;必须使用通配符。利用通配符可以创建比较特定数据的搜索模式。 通配符&#xff1a;用来匹配值的一部分的特殊字符…

【数据分享】1999—2021年地级市市政公用事业和邮政、电信业发展情况相关指标(Excel/Shp格式)

在之前的文章中&#xff0c;我们分享过基于2000-2022年《中国城市统计年鉴》整理的1999-2021年地级市的人口相关数据、各类用地面积数据、污染物排放和环境治理相关数据、房地产投资情况和商品房销售面积、社会消费品零售总额和年末金融机构存贷款余额、地方一般公共预算收支状…

减轻 PWM 的滤波要求

经典脉宽调制器 (PWM) 发出 H 个连续逻辑高电平&#xff08;1&#xff09;&#xff0c;后跟 L 个连续逻辑低电平&#xff08;0&#xff09;的重复序列。每个高电平和低电平持续一个时钟周期 T 1/F (Hz)。结果的占空比可定义为 H/N&#xff0c;其中 N HL 时钟周期。N 通常是 2…

keil固件库的安装 库函数的配置

文章目录&#xff1a; 第一步&#xff1a;下载固件库文件 第二步&#xff1a;创建一个新的文件夹&#xff0c;用来保存固件库文件。在该文件夹下新建文件夹&#xff1a;CMSIS、Lib、Startup、User 第三步&#xff1a;把库文件中文件放入我们建立对应的文件中 1.CMSIS模块 …

【MIPI协议 C-PHY详解】

MIPI协议 C-PHY详解 前言一、C-PHY介绍1.1 C-PHY 与 D-PHY wire的区别1.2 3 wire的状态&#xff08;states&#xff09;变化1.3 C-PHY Data Mapping Between 7 Symbols and a 16-Bit Data1.3 C-PHY Lane States and Line Levels ~ LP Mode 二、C-PHY LP Package Format2.1 C-PH…

SQLI_LABS攻击

目录 Less1 首先来爆字段 联合注入 判断注入点 爆数据库名 爆破表名 information_schema information_schmea.tables group_concat() 爆破列名 information_schema.columns 爆值 SQLMAP Less-2 -4 Less -5 布尔 数据库 表名 字段名 爆破值 SQLMAP Less-6 …

​LeetCode解法汇总142. 环形链表 II

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a; 力扣 描述&#xff1a; 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如…

ssl单向证书和双向证书校验测试及搭建流程

零、前提准备 说明&#xff1a; 50.50.1.118作为服务端&#xff0c;系统是 linux&#xff0c;openssl版本是&#xff1a;OpenSSL 1.1.1f 31 Mar 2020。 50.50.1.116是客户端&#xff0c;系统是Windows&#xff0c;openssl版本是&#xff1a;OpenSSL 3.0.5 5 Jul 2022 (Library…

Day07-作业(MySQL查询设计)

作业1: 根据如下需求完成SQL语句的编写 【仔细阅读题目需求】 数据准备&#xff1a; 创建一个数据库 db02_homework 执行如下SQL语句&#xff0c;准备测试数据 -- 员工管理(带约束) create table tb_emp (id int unsigned primary key auto_increment comment ID,username …

这款轻量级规则引擎,真香!

大家好&#xff0c;我是老三&#xff0c;之前同事用了一款轻量级的规则引擎脚本AviatorScript&#xff0c;老三也跟着用了起来&#xff0c;真的挺香&#xff0c;能少写很多代码。这期就给大家介绍一下这款规则引擎。 简介 AviatorScript 是一门高性能、轻量级寄宿于 JVM &…

阿里云率先荣获容器集群稳定性先进级认证

7 月 25 日&#xff0c;由中国信通院发起的“2023 稳保体系”评估结果在可信云大会现场公布&#xff0c;阿里云容器服务 ACK 成为首批通过“云服务稳定运行能力-容器集群稳定性”评估的产品&#xff0c;并荣获“先进级”认证。 云原生技术正在激活应用构建新范式&#xff0c;构…

数据结构初阶--树和二叉树的概念与结构

目录 一.树 1.1.树的概念 1.2.树的相关概念 1.3.树的表示 1.4.树在实际中的运用 二.二叉树 2.1.二叉树的概念 2.2.特殊的二叉树 满二叉树 完全二叉树 2.3.二叉树的性质 2.4.二叉树的存储结构 顺序存储 链式存储 一.树 1.1.树的概念 树是一种非线性的数据结构&am…