C++多态机制详解(多态实现原理,单继承和多继承时虚函数表,菱形继承时的虚函数表原理)

news2024/9/24 15:23:31

文章目录

    • 多态的定义
    • 多态的实现
      • 1.多态实现的两个必要条件
      • 2.什么是虚函数
      • 3.重写的条件
      • 4.多态实现代码
      • 5.重写的两个例外
    • C++11引入的final和override关键字
    • 重载,重写(覆盖),隐藏(重定义)
    • 抽象类
    • 接口继承和实现继承
    • 多态的原理
      • 1.虚函数表
      • 2.多态的实现原理
    • 关于动态绑定与静态绑定
    • 单继承和多继承时的虚函数表
      • 单继承的虚函数表
      • 多继承的虚函数表
    • 菱形继承的虚函数表
    • 菱形虚拟继承的虚函数表
    • inline函数可以是虚函数
    • 静态成员函数不可以是虚函数
    • 构造函数不可以是虚函数
    • 析构函数作为虚函数的场景

在这里插入图片描述

C++面向对象三大特性之一,多态

多态的定义

多态就是多种形态。就是不同对象在完成同一种行为的时候结果不同。
一种情况是不同的对象,调用同一个函数,但是出现了不同的结果。

多态分为两种:
1.静态多态:函数重载(看起来调用的是一个函数但是传递不同的参数有不同的行为,比如cout的自动识别类型)
2.动态多态:用一个基类对象的引用或者指针去调用重写完成的虚函数,更具引用或指向的对象不同,出现不同的行为
静态:是指在编译阶段实现
动态:是指在程序运行阶段实现

多态的实现

1.多态实现的两个必要条件

1.必须是基类的引用或者指针调用
2.被调用的函数必须是虚函数,并且派生类对其完成了重写。

2.什么是虚函数

被virtual修饰的函数就是虚函数

class Student
{
public:
	virtual void func1()
	{
		cout << "Student::virtual void func1()" << endl;
	}
};

这里的func1就是虚函数

静态成员函数不能成为虚函数(理解多态原理后再看)
解释静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表(虚表的指针存放在对象里面),就无法实现多态,因此不能设置为虚函数

3.重写的条件

派生类和基类中的虚函数满足三同(函数名相同,参数相同,返回值相同)就构成重写,派生类函数完成了对基类函数的重写(又叫做覆盖)

class Person
{
public:
	virtual void func1()
	{
		cout << "Person::virtual void func1()" << endl;
	}
};
class Student : public Person
{
public:
	virtual void func1()
	{
		cout << "Student::virtual void func1()" << endl;
	}
};

这段代码派生类就完成了对于基类的重写。

4.多态实现代码

class Person
{
public:
	virtual void func1()
	{
		cout << "Person::virtual void func1()" << endl;
	}
};
class Student : public Person
{
public:
	virtual void func1()
	{
		cout << "Student::virtual void func1()" << endl;
	}
};

void test1(Person& p)
{
	p.func1();
}

int main()
{
	Person p1;
	Student s1;
	test1(s1);
	test1(p1);

	return 0;
}

这里使用的是引用接收,然后引用调用,传递了不同的对象,调用了同一个函数(看似是同一个,实际底层调用的并不是一个函数)出现了不同的结果。

在这里插入图片描述

关于多态这里的函数调用,主要是看满不满足多态条件

  1. 多态调用,调用的函数与引用或者指向的对象有关。
  2. 普通调用,调用的函数与调用该函数的对象有关。

5.重写的两个例外

1.协变(基类与派生类的虚函数返回值不同)
重写条件是虚函数且满足三同,但是协变例外,协变情况下返回值可以不同。
返回值必须是父子关系的指针或者引用。可以是其他的继承类的父子指针也可以是自己这里的父子关系的指针或引用。
注意:基类的虚函数必须返回基类的指针或者引用,派生类返回的引用或指针可以是子类也可以是基类。

class Person
{
public:
	virtual Person* func1()
	{
		cout << "Person::virtual void func1()" << endl;
		return nullptr;
	}
};
class Student : public Person
{
public:
	virtual Student* func1()
	{
		cout << "Student::virtual void func1()" << endl;
		return nullptr;
	}
};

2.析构函数的重写
只要基类的析构时virtual,基类的析构和派生类的析构就构成重写

class Person
{
public:
	virtual Person* func1()
	{
		cout << "Person::virtual void func1()" << endl;
		return nullptr;
	}

	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual Student* func1()
	{
		cout << "Student::virtual void func1()" << endl;
		return nullptr;
	}

	virtual ~Student()
	{
		cout << "~Student" << endl;
	}
};
void test2()
{
	Person* pp = new Person;
	Person* ps = new Student;

	delete pp;
	delete ps;
}

析构函数在编译的时候都会被编译器替换成destructor(),因此满足了重写的三同条件。

析构函数设计成重写为了应对上述代码的情况:
new出来的基类对象和派生类对象都交给基类的指针来管理。这时如果派生类没有对基类的析构函数进行重写,那么delete的时候两次调用的都是基类的析构函数,那么子类对象就会有一部分资源没有清理,会造成内存泄漏。

派生类里面进行重写的虚函数不用加virtual也是可以的。
因为派生类在继承的时候先继承了基类的虚函数属性然后完成了重写。所以派生类的析构函数不加virtual也算是虚函数。重写是接口继承,派生类只重写了基类虚函数的实现。

这种设计的初衷是为了方便进行代码分工,如果基类函数定义成立虚函数,那么派生类继承了基类之后就算忘记加virtual也是可以完成重写的。不至于因为派生类没有重写而造成内存泄漏的错误(特别是派生类的析构函数要完成重写防止内存泄漏)

所以在写父类的时候,析构函数最好设置为虚函数,不管子类析构有没有加virtual都构成重载,避免出现内存泄漏。

C++11引入的final和override关键字

C++11为了规范多态的使用加入了final和override关键字。

final

final关键字的作用是添加在虚函数的后面使得这个虚函数不可被覆盖(重写)。
final还可以放在类名后面进行修饰使得这个类不可被派生(继承)


class Person
{
public:
	virtual Person* func1() final
	{
		cout << "Person::virtual void func1()" << endl;
		return nullptr;
	}

	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual Student* func1()
	{
		cout << "Student::virtual void func1()" << endl;
		return nullptr;
	}

	virtual ~Student() 
	{
		cout << "~Student" << endl;
	}
};

image.png

override

override关键字的作用是放在派生类虚函数的后面检查该函数是否完成了对基类虚函数的重写,没有重写就会报错。如果这个函数不是虚函数那也会报错。

class Person
{
public:
	virtual Person* func1()
	{
		cout << "Person::virtual void func1()" << endl;
		return nullptr;
	}

	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual Student* func2() override
	{
		cout << "Student::virtual void func1()" << endl;
		return nullptr;
	}

	virtual ~Student() 
	{
		cout << "~Student" << endl;
	}
};

image.png

如何让一个类不能被继承?

  1. 可以用final修饰基类,使基类不可被继承,这个类叫做最终类。(直接限制)
  2. 将构造函数设置成为私有,派生类继承基类后调不动基类的构造函数无法实例化出对象。(间接限制)

重载,重写(覆盖),隐藏(重定义)

重载条件:

  1. 必须是同一作用域
  2. 必须函数名相同,并且参数不同(类型不同或者个数不同或者顺序不同)

重写(覆盖)条件:

  1. 必须是虚函数
  2. 必须满足函数名相同,参数相同,返回值相同(协变除外)

隐藏(重定义)的条件

  1. 必须是分别在基类和派生类的作用域
  2. 必须是函数名相同

基类和派生类的同名函数不是重写就是隐藏。

抽象类

抽象的含义就是没有实体,或者说现实世界中不存在实物的东西。
一个类如果含有纯虚函数(在虚函数后面加上 = 0)那么这个类就是抽象类。抽象类不能实例化出对象,派生类继承抽象类之后也不可以实例化出对象,只有派生类对抽象类的纯虚函数完成重写之后,派生类才可以实例化出对象。

image.png
抽象类可以强制子类重写虚函数,抽象类不能实例化出对象所以抽象类内部的函数不需要写出实现(可以写,但是写了也没用)。因此派生类继承了抽象类相当于继承了抽象类的接口,抽象类又被叫做接口类。体现了接口继承。

class A
{
public:
	virtual void test() = 0;//纯虚函数
};

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


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

int main()
{
	A* pb = new B;
	A* pc = new C;
	pb->test();
	pc->test();

	return 0;
}

派生类对抽象类中的纯虚函数进行重写之后,派生类就可以正常的创建对象了。

接口继承和实现继承

实现继承就是普通函数的继承。普通函数在派生类中可以直接调用,相当于是将函数的实现继承了过来。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,实现多态。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

1.虚函数表

有虚函数的类实例化出来的对象里面都会存在一个虚函数表指针(一般是在最前面),这个指针指向了一个虚函数表(是一个虚函数指针数组)简称虚表,虚表里面存放是虚函数的地址,也包括继承下来的虚函数。

image.png

a对象里面有一个成员变量_a,理论上说大小应该是4字节,但是算出来是8个字节也就是印证了对象里面还包含了一个指针变量(虚函数表指针,指针在32位机4个字节,64位机8个字节)

在这里插入图片描述

a对象里面的第一个成员是一个__vfptr的指针,这个指针就是虚函数表指针。现在虚函数表内有一个虚函数,所以只有一个地址。vs中虚函数表最后以nullptr结尾(不同的编译器可能不同,还有的用-1作为结尾)。

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

class B : public A
{
public:
	
private:
	int _a;
};

int main()
{
	A a;
	B b;
	return 0;
}

这里B类继承了A类,自然也是继承了A类的虚函数表。
image.png
b对象的虚函数表是A类的虚函数表的拷贝
如果B类中对于func1进行重写,然后增加一个B类自己的虚函数func3再来看这个虚函数表的变化。
image.png
经过重写后,b对象的虚函数表内的第一个函数指针的内容已经变了。也即是重写后新的派生类的虚函数将原来的那个A类的虚函数覆盖掉了。这就是重写又叫覆盖的原因,重写是语法的叫法,覆盖是原理层的含义。

从监视窗口看不到func3出现在虚表中,是因为vs对于监视窗口进行了优化。这里可以通过内存窗口来观察func3的地址是不是真的在虚函数表内。
在这里插入图片描述

这个内存里面,在func1和func2地址下面的那个地址就是func3的地址。

虚函数表这里也验证了为什么多态的第一个条件,必须是基类的指针或者引用调用。因为子类对象传给父类的指针或引用会进行切片。保留基类的那一部分包括虚函数表。
如果是赋值,就是将子类的里面的基类部分拷贝给基类对象,拷贝的时候是不会拷贝虚表的。

这里的规则就是父类对象里面一定是父类的虚表,子类对象里面一定是子类的虚表。
如果父类对象经过子类对象的一次赋值,父类虚表就变成了子类的虚表那么后续使用父类对象的时候就不能确定此时对象里面是父类虚表还是子类虚表了。

image.png
同类型的对象共用一个虚表。

虚表是在编译阶段生成的,在构造函数的初始化列表中初始化给对象。虚表存在代码段或者叫常量区。

2.多态的实现原理

image.png
B类继承了A类。
实参分别是基类和派生类的指针,派生类传过去发生了切片。所以虽然都是转换成了A*类型但是两次调用的虚函数表指针是不同的,所以虚函数表也不是同一个。去虚函数表里面找func1的时候找到的函数也就不同,这就实现了多态。

这时我们也可以理解为什么需要子类虚函数覆盖(重写)父类的虚函数。
因为子类的虚表是拷贝的父类虚表。重写之后才可以将虚表内的函数覆盖为子类自己的虚函数地址。

为什么要用父类的指针或者引用来调用?
因为不会发生拷贝构造,子类传过去发生了切片,但是虚函数表指针还是子类的虚函数表指针。由此才可以调用到子类的虚函数。

通过下面的汇编代码分析,满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到
对象的虚函数表中去找的。不满足多态的函数调用时编译时确认好的。** **
image.png
前面的汇编代码是取出pa指向的对象的前四个字节的虚函数表指针,然后找到虚函数表,再找到func1,将func1的地址放到eax寄存器里面,最后通过call指令调用。其实这里的地址并不是func1函数真正的地址。call之后会跳到真正的func1函数的地址处。
image.png
image.png
vs这里封装了一层,所以第一次call之后并不是直接跳到func1函数体处开始执行,而是跳到中间层然后再次jmp跳到真正的func1函数处开始执行。

vs这里做多一层的封装,实际是为了中间层控制this指针的偏移。

总结多态原理:
满足多态的条件调用函数的时候才去虚函数表里面找,不满足多态,编译的时候就已经确定了调用函数的地址。

如果派生类重写基类的函数被放在派生类的私有或限定域内,在满足多态的条件的时候也是可以调用的。因为可以通过虚函数表里面的虚函数地址来找到函数,直接调用。也就是说访问限定符的限定作用并不是绝对的。

关于动态绑定与静态绑定

动态绑定和静态绑定就是动态多态和静态多态的实现原理方面。
动态绑定是指程序运行期间确定了程序的具体行为(调用什么函数)所以也称为动态多态。
静态绑定是指在程序编译阶段确定了程序的具体行为,将调用的函数地址填入调用的地方。因此称为静态多态。比如函数重载

单继承和多继承时的虚函数表

单继承的虚函数表

因为vs编译器将虚函数表内的有些虚函数进行了隐藏,下面我们就打印出虚函数表来直接观察虚函数表内的函数地址。

如何打印虚函数表?
需要拿到派生类对象的前四个字节(也就是虚函数表指针)

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

class B : public A
{
public:
	virtual void func1()
	{
		cout << "class B : public A :: func1()" << endl;
	}
	virtual void func3()
	{
		cout << "class B : public A :: func3()" << endl;
	}
	virtual void func4()
	{
		cout<<"class B : public A :: func4()" << endl;
	}
private:
	int _b;
};
/
typedef void(*_vfptr)();//将函数指针重命名成_vfptr

void VfuncPrint(_vfptr vfunc[])
{
	for (int i = 0; vfunc[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址:%p\n", i + 1, vfunc[i]);
	}
}
int main()
{
	A a;
	B b;
	VfuncPrint((_vfptr*) *((void**)&a));
	puts("");
	VfuncPrint((_vfptr*) *((void**)&b));
}

如何拿到对象的虚函数表的指针
先取出对象的地址然后强转成为void**,当然也可以是其他的二级指针,比如int**,强转成二级指针的原因是二级指针里面存放的是一级指针的地址,一定是四个或者八字节,我们对象里面的前四个字节或者八个字节就是一个指针(指向虚函数指针数组的指针,又叫虚函数表指针)所以完全吻合,不论是32位机还是64位机。

对二级指针解引用刚好拿到前四个字节,也即是虚函数表指针。强转成函数数组指针然后传参给Print函数,依次向后直到nullptr即可打印所有的虚函数地址。

这里的强转也可以将a的地址转成int*,然后解引用拿到前四个字节(只适合32位机,64位要使用long long,不可以用double因为会有精度损失使得二进制错误)
强制类型转换的时候也要注意,不可以随便转,必须是相关类型才可以互相转换。什么是相关类型,比如i整形和指针就是。

打印结果:

image.png
第一个和第二个虚函数是派生类继承下来的,派生类对于第一个虚函数进行了重写,所以重写后的虚函数的地址覆盖了原来的地址,因此第一个不一样。后面第三个和第四个虚函数地址是派生类自己的虚函数。

多继承的虚函数表

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 VfuncPrint(_vfptr vfunc[])
{
	printf("虚表地址:%p\n", vfunc);
	for (int i = 0; vfunc[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址:%p\n", i + 1, vfunc[i]);
	}
}
int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;
	VfuncPrint((_vfptr*) *((void**)&b1));
	puts("");
	VfuncPrint((_vfptr*)*((void**)&b2));
	puts("");
	VfuncPrint((_vfptr*)*((void**)&d));
}

image.png
Derive继承了Base1和Base2,所以d对象里面存在着两个虚表。分别是Base1和Base2虚表的拷贝,如果派生类没有进行重写,虚函数表内就还是原来的地址,如果进行了重写,对于两个虚表内满足条件的虚函数都会被重写。

这段代码对func1进行了重写,就是对继承的两个类都完成了重写,所以这里的两个虚表的第一个地址应该是一样的。但是这里监视窗口看到的是不一样的。因为这里vs进行了封装,实际上他们调用的是同一个函数。

image.png

创建d对象,用派生类对象d的地址切片给Base1的指针和Base2的指针。此时满足多态条件,访问func1是在子类的两个虚表中查找。

下面看汇编代码演示Base1*的调用过程:
image.png

eax里存放的是虚函数表里面func1的地址
image.png
汇编中不是直接跳到了func1而是到一层中转层。
在这里插入图片描述

再次jmp才是真正调用到了func1.

下面是Base2*调用的func1函数的汇编代码,来分析一下,为什么这两个虚表内第一个func1的地址是不一样的。

image.png
第一步取eax寄存器内的地址进行跳转。可以看到这里是和Base1调用时候eax的地址不一样的。
image.png
再次跳转,在Base1调用的时候,应该就到func1函数了。
image.png
这次跳转到了一句汇编代码,将ecx寄存器里面的值减掉了8,然后下一句jmp跳到了Base1*调用jmp func1的指令地址处,再次跳转。
image.png
再次跳转
image.png
到了func1函数。

为什么ecx寄存器要先减掉8个字节呢?因为切片造成的this指针偏移。
首先说明ecx里面存放的是this指针。
image.png
这是Derive的对象模型,Derive先继承了Base1所以在d对象里Base1是在前面的。因此在切片的时候直接将d的地址给Base1*是没有问题的,但是在Base2这里切片的时候就不是d的地址了。栈向下增长,地址是减小的。所以d的地址减掉Base1的大小字节数,就能到Base2部分的起始地址处了。因此中间需要加一层移动ecx寄存器的指令所以监视看到的虚函数表中第一个func1函数的地址是不相同的。
但是在汇编看到实际最后调用到的还是相同的函数func1。

下面打印一下Derive的d对象的两个虚函数表,来看看Derive自己的虚函数是放在那个虚函数表的。

image.png
Derive自己的func3是存放在第一个虚函数表的。(默认将派生类自己的虚函数存放在第一个继承的类的那个虚函数表里面)
总结多继承的虚函数表,继承了几个类,就有几个虚函数表,自己的虚函数总是存放在第一个虚函数表内。

菱形继承的虚函数表

该模块总结内容来自刘皓前辈的博客:https://coolshell.cn/articles/12176.html我做了很大成度的简化,尽量使之容易理解。
菱形继承时重复继承(不使用虚拟继承)虚函数表存在数据冗余和二义性
image.pngimage.png
image.png
D类对象d里面存在两个虚函数表,一个是B1类的一个是B2类的,B1和B2类的虚函数表内都存在一份B类的虚函数,D类重写了B类的f( )(必须重写f( )),B1类的f1( ) ,B2类的f2( ) ,D类中还添加了一个没有参与任何重写的虚函数Df( )存在了B1部分的虚函数表内。
因为f1和f2分别重写了B1类和B2类中的f1,f2,所以他们的位置是确定的。需要覆盖B1和B2继承下来的虚函数。

菱形继承存在数据冗余,D类对象内部,B1类虚函数表后面跟着B类和B1类的成员变量。B2类的虚函数表后面先跟着B类的成员变量然后是B2类的成员变量。最后存的是D类的成员变量。

菱形虚拟继承的虚函数表

菱形虚拟继承在继承的时候加上了virtual关键字,解决了菱形继承时候造成的数据冗余和二义性。
image.png
菱形虚拟继承的时候,两个子类B1和B2虚继承了同一个父类B,下面是他们的内存模型
image.png
内存分布:B1类的虚表指针,B1类的成员变量,然后是B类的虚表指针,B类的成员变量。
VC++进一步的解决了虚函数表内的重复,因为B1类重写了B类的f()所以只在B1类虚函数表内存放没有重写的虚函数,重写的虚函数f( )一同放到公共的B类的虚函数表内部。
注意区分:如果是普通的单继承,那么只有一个虚函数表,子类重写覆盖父类的虚函数,子类自己的虚函数放在父类虚函数的下面。这里是虚拟单继承,所以存在两个虚函数表(为了解决菱形继承),这就是虚拟继承对内存模型的改变。

下面就是菱形虚拟继承中D类对象的内存模型
image.png
GCC环境下B1类的虚函数表里存放的是B类和B1类和D类中的所有虚函数,如果完成了重写的虚函数就覆盖。与普通菱形继承的时候不同,f2( )也出现在了B1的虚函数表内。GCC下B2类中存放了B2类原有的虚函数,D类中完成重写的虚函数会覆盖B2类原有的虚函数。公共部分B类里面存放的是B类经过D类覆盖后的虚函数表。这些虚函数表内还是存在着重复的虚函数。比如三个部分都有的f()函数。
但是条理清晰,D类自己的虚函数放在了第一个继承的B1类部分的虚函数表内。B2类部分只放B2类自己的虚函数。B类部分的虚函数表只放B类自己的虚函数。
GCC下的成员变量部分,B1虚表指针后跟着B1的成员变量。B2虚表指针后面是B2类的成员变量。D类的成员变量。最后是公共部分B的虚表指针和成员变量。

VC++的情况与GCC有所不同,B1类虚表内保存了B1的虚函数,f1( )和Bf1( )还有D类中没有重写的Df( )函数,在B2类的虚表里面存放的也是B2的虚函数,f2( )和B2( )。在公共部分B类的虚表内,存放了公共的f( )和没被重写的Bf( )函数。从此方面来看VC++这里的重复处理的是比GCC更好的
VC++的成员变量部分,就与GCC部分一样了。
表内标准红色的语段是不知道为何这里是-4.

VC++和GCC都把公共部分B放在最下面,而VC++有一个NULL作为B和B1和B2的虚表的结束标志。GCC则是在B1虚表结束用了1,B2和B虚表结束使用了0。
下面这段话截取自开头提到的博客。
VC++中的内存布局有两个地址我有些不是很明白,在其中我用红色标出了。取其内容是-4。接道理来说,这个指针应该是指向B类实例的内存地址(这个做法就是为了保证重复的父类只有一个实例的技术)。但取值后却不是。这点我目前还并不太清楚,还向大家请教。
**GCC的内存布局中在B1和B2中则没有指向B的指针。这点可以理解,编译器可以通过计算B1和B2的size而得出B的偏移量。这个偏移量就存放在虚基表中(虚基表就是D类对象的B1部分和B2部分都有一个指针,这个指针指向的就是虚基表,**虚基表内存放的是当前B1分区到公共的虚函数表([0]到[8])和公共的成员变量的两个偏移量([0]到[9]))。所以虚基表里面存在两个偏移量的问题就解决了。

inline函数可以是虚函数

inline函数可以是虚函数,在调用这个虚函数的时候如果不构成多态,那么这个虚函数显的是inline属性,就会被原地展开。
构成多态的时候调用,就没有inline属性了。因为多态调用的时候会去对象的虚函数表里面找虚函数的地址,此时inline属性就被编译器忽略了。

静态成员函数不可以是虚函数

静态成员函数不可定义成虚函数,因为没有价值。多态就是为了让虚函数实现多态调用,但是静态函数可以直接使用类型::函数名调用。多态对此来说就没有意义了了。

构造函数不可以是虚函数

虚函数的意义就是为了构成多态,调用时要去虚表里面找虚函数,对象中的虚表指针是在初始化列表初始化的。这也就注定了构造函数不可以是虚函数。

析构函数作为虚函数的场景

析构函数做虚函数只有一种场景,就是基类和派生类都开辟了对象,都是交给了基类的指针管理,那么在delete的时候如果析构函数不是虚函数没有完成重写,就会造成派生类的一部分成员资源没有清理干净,可能会造成内存泄漏。

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

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

相关文章

关于JAVA8的Lambda表达式

1. 水在前面 这个礼拜忽然心血来潮把Lambda表达式学习了一遍&#xff0c;发现这玩意跟原来想象的好像不是一个东西&#xff0c;写个学习心得供以后复习用。还是那句话&#xff0c;这篇水文不能让你完全掌握&#xff0c;只是用来给我自己温习用的&#xff0c;或者也可以作为小伙…

DXP学习2- 绘制电气图【实验】

目录 一、实验目的 二、实验原理 1、创建一个新的项目文件。 2、新建原理图文件 3、设置原理图选项 4、放置元器件 5、其他电路元素的放置 6、对所有电路元素属性参数值的修改 三、实验设备 四、实验内容 1、绘制实验图2-1 元器件所在位置&#xff1a; 1&#xff0c;…

四、Elasticsearch 进阶

自定义目录 4.1 核心概念4.1.1 索引&#xff08;Index&#xff09;4.1.2 类型&#xff08;Type&#xff09;4.1.3 文档&#xff08;Document&#xff09;4.1.3 字段&#xff08;Field&#xff09;4.1.5 映射&#xff08;Mapping&#xff09;4.1.6 分片&#xff08;Shards&#…

基于java+springboot+vue实现的游戏账号估价交易平台(文末源码+Lw+ppt)23-555

摘 要 系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对游戏账号估价交易的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自…

【计算机网络篇】数据链路层(1)数据链路层的地位,问题

文章目录 &#x1f354;数据链路层在网络体系结构中的地位&#x1f354;链路&#xff0c;数据链路&#xff0c;帧&#x1f354;数据链路层的三个重要问题&#x1f95a;封装成帧和透明传输&#x1f95a;差错检测&#x1f95a;可靠传输 &#x1f354;数据链路层在网络体系结构中的…

Tableau学习——范围-线图、倾斜图

1范围-线图&#xff08;人工接听数据&#xff09; 范围-线图&#xff1a;将整体及个体数据特征&#xff08;均值、最值等&#xff09;都展示出来了 筛选出某个员工 &#xff08;1&#xff09;创建计算字段来表示均值、最大值、最小值 &#xff08;2&#xff09;数据处理好后&…

Day61:WEB攻防-PHP反序列化原生类TIPSCVE绕过漏洞属性类型特征

知识点&#xff1a; 1、PHP-反序列化-属性类型&显示特征 2、PHP-反序列化-CVE绕过&字符串逃逸 3、PHP-反序列化-原生类生成&利用&配合 补充&#xff1a;如果在 PHP 类中没有实现某个魔术方法&#xff0c;那么该魔术方法在相应的情况下不会被自动触发。PHP 的魔…

路桥公司知识竞赛活动方案

一、参赛对象 龙建路桥股份有限公司权属企业 二、组织单位 主办单位&#xff1a;龙建路桥股份有限公司委员会 承办单位&#xff1a;黑龙江省龙建路桥第二工程有限公司委员会 三、活动时间 11月&#xff08;具体时间另行通知&#xff09; 四、活动地点 龙建松北综合经营生产中心…

基于Java中的SSM框架实现图书仓储管理系统项目【项目源码+论文说明】计算机毕业设计

基于Java中的SSM框架实现图书仓储管理系统演示 摘要 随着社会经济的迅速发展和科学技术的全面进步&#xff0c;计算机事业的飞速发展&#xff0c;以计算机与通信技术为基础的信息系统正处于蓬勃发展的时期&#xff0c;随着经济文化水平的显著提高&#xff0c;人们对生活质量及…

Token的详解

Token的详解 文章目录 Token的详解前言:简介:使用token&#xff1a; 前言: 为什么会用到Token&#xff0c;因为cookie和session一些自身的缺点&#xff0c;限制了一些功能的实现&#xff0c;比如&#xff1a; cookie&#xff1a;优点是节省服务器空间&#xff0c;缺点不安全。…

如何监控企业微信聊天记录内容

假如说老板可以查看到你的微信聊天记录&#xff0c;那么此时此刻的你&#xff0c;会不会瑟瑟发抖&#xff1f; 其实不用啦&#xff0c;监控企业微信聊天记录&#xff0c;也是需要员工个人同意的。 下面我介绍两种方法&#xff0c;看看你属于哪种 方法一 企业微信自带功能 …

倒计时3天!2024“智衡杯”智能感知挑战赛即将启幕!

为推动智能感知算法领域的发展&#xff0c;发掘高品质的智能感知算法&#xff0c;并促进其在实际应用中落地&#xff0c;“2024‘智衡杯’智能感知挑战赛”将于3月26日盛大启幕&#xff01; 本次大赛由全国人工智能计量技术委员会、先进技术成果长三角转化中心、北京航天计量测…

深入浅出Reactor和Proactor模式

Reactor模式和Proactor模式是两种常见的设计模式&#xff0c;用于处理事件驱动的并发编程。它们在处理IO操作时有着不同的工作方式和特点。 对于到来的IO事件&#xff08;或是其他的信号/定时事件&#xff09;&#xff0c;又有两种事件处理模式&#xff1a; Reactor模式&…

阿里云服务器“镜像”操作系统选择方法(超详细)

阿里云服务器镜像怎么选择&#xff1f;云服务器操作系统镜像分为Linux和Windows两大类&#xff0c;Linux可以选择Alibaba Cloud Linux&#xff0c;Windows可以选择Windows Server 2022数据中心版64位中文版&#xff0c;阿里云服务器网aliyunfuwuqi.com来详细说下阿里云服务器操…

CAN总线位时序

一. 简介 前面文章学习了 CAN总线协议&#xff0c;即五种数据帧格式。 I.MX6ULL 带有 CAN 控制器外设&#xff0c;叫做 FlexCAN&#xff0c;FlexCAN 符合 CAN2.0B 协议。 本文来学习一下 CAN总线的位时序。 二. CAN总线位时序 CAN 总线以帧的形式发送数据&#xff0c;但是…

Teable——强大的在线数据电子表格

公众号&#xff1a;【可乐前端】&#xff0c;每天3分钟学习一个优秀的开源项目&#xff0c;分享web面试与实战知识&#xff0c;也有全栈交流学习摸鱼群&#xff0c;期待您的关注! 每天3分钟开源 hi&#xff0c;这里是每天3分钟开源&#xff0c;很高兴又跟大家见面了&#xff0…

C++ Thread 源码 观后 自我感悟 整理

Thread的主要数据成员为_Thr 里面存储的是线程句柄和线程ID 先看看赋值运算符的移动构造 最开始判断线程的ID是否不为0 _STD就是使用std的域 如果线程ID不为0&#xff0c;那么就抛出异常 这里_New_val使用了完美转发&#xff0c;交换_Val和_New_val的值 _Thr _STD exchange(_…

BRAM底层原理详细解释(1)

目录 一、原语 二、端口简述 2.1 端口简介 2.2 SDP端口映射 三、端口信号含义补充说明 3.1 字节写使能&#xff08;Byte-Write Enable&#xff09;- WEA and WEBWE&#xff1a; 3.2 地址总线—ADDRARDADDR and ADDRBWRADDR 3.3 数据总线—DIADI, DIPADIP, DIBDI, and D…

Pycharm小妙招之Anaconda离线配环境

Pycharm小妙招之Anaconda离线配环境———如何给无法联网的电脑配python环境&#xff1f; 1. 预备工作2. 电脑1导出包2.1 环境路径2.2 压缩py38导出至U盘 3. 电脑2导入包4. 验证是否导入成功4.1 conda查看是否导入4.2 pycharm查看能否使用 1. 预备工作 WINDOWS系统电脑1(在线)…

AI跟踪报道第34期-新加坡内哥谈技术-AI新闻快报:世界即将改变

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…