C++多态

文章目录

  • [🐵1. 什么是多态](#🐵1. 什么是多态)
  • [🐶2. 构成多态的条件](#🐶2. 构成多态的条件)
    • [🐩2.1 虚函数](#🐩2.1 虚函数)
    • [🐩2.2 虚函数的重写](#🐩2.2 虚函数的重写)
    • [🐩2.3 final 和 override关键字](#🐩2.3 final 和 override关键字)
    • [🐩2.4 重载、重写、重定义对比](#🐩2.4 重载、重写、重定义对比)
  • [🐱3. 虚函数表](#🐱3. 虚函数表)
  • [🐯4. 多态的原理](#🐯4. 多态的原理)
  • [🐎5. 多继承的虚表关系](#🐎5. 多继承的虚表关系)
  • [🦬6. 抽象类](#🦬6. 抽象类)

🐵1. 什么是多态

当下网络有个热门词汇叫"双标",意思就是用不同的标准来衡量人或事,这是一个贬义词。而在编程世界中,这种"双标",我们称之为多态,当然了这里的多态并不是贬义词,而是一种技术实现。

比如说某种商城有会员机制,将用户分为普通用户、普通会员、尊贵会员等

那买同种东西的时候,不同的用户等级会有着不同的价格,这就是一种多态行为

🐶2. 构成多态的条件

实现多态性的主要构成条件是使用虚函数继承

  • 必须通过基类的指针或引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对虚函数进行重写

🐩2.1 虚函数

只有类的成员函数才能被定义为虚函数,格式如下:

cpp 复制代码
class A
{
    //函数前面加上virtual 表面该成员函数为虚函数
	virtual void func() {}
};

🐩2.2 虚函数的重写

当派生类中有一个和基类完全相同的虚函数时,我们称这为虚函数的重写/覆盖

重写有三同,即:返回值类型、函数名、参数列表完全相同

cpp 复制代码
class A
{
public:
	//虚函数
	virtual void func() const
	{
		cout << "A->func()" << endl;
	}
};
class B :public A
{
public:
	//虚函数重写
	virtual void func() const
	{
		cout << "B->func()" << endl;
	}
};
//多态调用传引用过去
void Print(const A& p)
{
	p.func();
}
int main()
{
	Print(A());	//A->func()
	Print(B());	//B->func()
	return 0;
}

多态调用中,看的是指向的对象;而普通的函数调用,看的是当前的类型

虚函数的重写,还需注意几点:

  1. 虚函数父类必须加上 virtual修饰,子类虚函数重写前面可以不加virtual,但在实际中,还是建议加上

  2. 对于虚函数的重写,我们规定三同,但是有例外------协变

    即基类与虚函数返回值类型不同,但是返回值类型必须是构成父子关系指针或者引用(同时是指针 或 同时是引用)

    cpp 复制代码
    class A
    {
    public:
    	//虚函数
    	virtual A* func() const
    	{
    		cout << "A->func()" << endl;
    		return 0;
    	}
    };
    class B :public A
    {
    public:
    	//虚函数重写 B和A是父子关系
    	virtual B* func() const
    	{
    		cout << "B->func()" << endl;
    		return 0;
    	}
    };
    void Print(const A& p)
    {
    	p.func();
    }
    int main()
    {
    	Print(A());
    	Print(B());
    	return 0;
    }
  3. 析构函数的重写,基类和派生类的析构函数名不同

    cpp 复制代码
    class A
    {
    public:
    	//虚函数
    	virtual ~A()
    	{
    		cout << "~A()" << endl;
    	}
    };
    class B :public A
    {
    public:
    	//虚函数重写
    	virtual ~B()
    	{
    		cout << "~B()" << endl;
    	}
    };
    int main()
    {
    	A* a1 = new A;
    	A* a2 = new B;
    	delete a1;
    	delete a2;
    	return 0;
    }

    输出:

    这里的原因是因为编译器对析构函数的名字做了处理,编译后名称统一处理为destructor,那为什么要将析构函数统一处理称destructor呢?因为这里要让他们构成重写。如果不构成重写,就好出现类似这样的情况:

    cpp 复制代码
    class A
    {
    public:
    	~A()
    	{
    		cout << "~A()" << endl;
    	}
    };
    class B :public A
    {
    public:
    	~B()
    	{
    		delete ptr;
    		cout << "~B()" << endl;
    	}
    protected:
    	int* ptr;
    };
    int main()
    {
    	A* a1 = new A;
    	delete a1;
    	a1 = new B;
    	delete a1;
    	return 0;
    }

    输出发现,我们这里new了一个B对象,但是每次都是调用A的析构函数,这显然与我们的意愿不符,我们期望的是这个a1->destructor形成的是多态调用,所以这样统一处理之后,就可以让他们构成重写

🐩2.3 final 和 override关键字

如果不想让这个虚函数被重写,可加上final关键字修饰

当然了,final也可以修饰类,让这个类不被继承,一般用于最终的类

如果要检查某个派生类是否重写了基类的某个虚函数,可用override关键字修饰,如果没有重写,则编译报错

🐩2.4 重载、重写、重定义对比

🐱3. 虚函数表

cpp 复制代码
class A
{
public:
	virtual void func()
	{
		cout << "func()" << endl;
	}
protected:
	int _a;
};
int main()
{
	cout << sizeof(A) << endl;
}

这段代码如果不加上virtual,则输出的是4;但是加上virtual之后,输出的是16(64位下,指针是8字节,然后内存对齐)

这是因为有了虚函数,这个类里面会多一个虚函数表的指针,这些表里面存的是虚函数的地址

但如果将这个虚函数没有被重写,那么派生类的虚函数表还是指向基类的虚函数;如果重写了,则指向重写的虚函数。

所以多态调用的时候,不管我们传的是基类和派生类,在内存里看到的都是父类;普通调用是在编译 的时候就确定了地址,而多态调用时,运行时会到指向对象的虚表找函数的地址

动态绑定与静态绑定:

  • 静态绑定:在编译时确定调用哪个函数或方法。这是在编译器根据变量的静态类型(声明类型)来决定调用哪个函数
  • 动态绑定:在运行时根据对象的实际类型来确定调用哪个函数或方法。这是通过虚函数(在基类中声明为虚函数,子类进行重写)实现的。动态绑定适用于通过基类指针或引用调用虚函数的情况,确保调用正确的派生类函数

在这里虚表的地址,是存储在哪里的呢?我们通过这段代码来验证

cpp 复制代码
class A
{
public:
	virtual void func()
	{
		cout << "A->func()" << endl;
	}
	virtual void Func()
	{
		cout << "A->Func()" << endl;
	}
	int _a;
};
class B :public A
{ 
public:
	virtual void func()
	{
		cout << "B->func()" << endl;
	}
};
void Print(A a)
{
	a.func();
}
int main()
{
	A aa;
	B bb;

	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";
	printf("常量区:%p\n", str);
	//前四个字节,一定是虚表的地址
	printf("虚表a:%p\n", *((int*)&aa));
	printf("虚表b:%p\n", *((int*)&bb));
}

输出发现虚表的地址和常量区的地址隔的较近 ,所以我们可以得出结论:虚表的地址存储在常量区

另外,我们在Vs的监视窗口只能查看3个虚函数的地址,但这不代表这,内存里面只有三个虚函数的地址,我们可通过这段代码进行验证:

cpp 复制代码
class A
{
public:
	virtual void func1()
	{
		cout << "A->func1()" << endl;
	}
	virtual void func2()
	{
		cout << "A->func2()" << endl;
	}
	virtual void func3()
	{
		cout << "A->func3()" << endl;
	}
};
class B :public A
{
	virtual void func3()
	{
		cout << "B->func3()" << endl;
	}
	virtual void func4()
	{
		cout << "B->func4()" << endl;
	}
};
//函数指针命名
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{
	for (size_t i= 0; table[i]!=nullptr ; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		Func_Ptr f = table[i];
		f();
	}
	printf("\n");
}
int main()
{
	A a;
	B b;
	int vft1 = *((int*)&a);
	PrintVFT((Func_Ptr*)vft1);
	int vft2 = *((int*)&b);
	PrintVFT((Func_Ptr*)vft2);
	return 0;
}

🐯4. 多态的原理

有了虚表的概念,这我们就能理解,为什么构成多必须是通过基类的指针或引用调用虚函数。因为只有父类的虚表才能既能指向父类,又能指向子类。

那这里还有一个问题就是,为什么必须是指针或引用呢?

cpp 复制代码
class A
{
public:
	virtual void func()
	{
		cout << "A->func()" << endl;
	}
	virtual void Func()
	{
		cout << "A->Func()" << endl;
	}
	int _a;
};
class B :public A
{
public:
	virtual void func()
	{
		cout << "B->func()" << endl;
	}
};
void Print(A a)
{
	a.func();
}
int main()
{
	A a;
	a._a = 1;
	B b;
	b._a = 10;
	a = b;
	A* pa = &b;
	A& ref = b;
}

这段代码调试发现,子类赋值给父类,父类会进行切片,这里值会拷贝过去,但是虚表并不会拷贝;因为如果拷贝了虚表的话,这样父类对象中的虚表指向的是父类还是子类就混淆了

🐎5. 多继承的虚表关系

上面讲的内容,包括举得例子都是单继承的,所以就不再赘述。这里我们看一下多继承里面的虚表是怎样的

cpp 复制代码
class A
{
public:
	virtual void func1()
	{
		cout << "A->func1()" << endl;
	}
	virtual void func2()
	{
		cout << "A->func2()" << endl;
	}
protected:
	int _a;
};
class B
{
public:
	virtual void func1()
	{
		cout << "B->func1()" << endl;
	}
	virtual void func2()
	{
		cout << "B->func2()" << endl;
	}
protected:
	int _b;
};
class C :public A, public B
{
public:
	virtual void func1()
	{
		cout << "C->func1()" << endl;
	}
	virtual void funcC()
	{
		cout << "C->funcC()" << endl;
	}
protected:
	int _c;
};
typedef void (*Func_Ptr)();
//打印函数指针数组
void PrintVFT(Func_Ptr table[])
{
	for (size_t i= 0; table[i]!=nullptr ; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		Func_Ptr f = table[i];
		f();
	}
	printf("\n");
}
int main()
{
	C c;
	cout<<sizeof(c)<<endl;
	int vft1 = *((int*)&c);
	//int vft2 = *((int*)(char*)&c + sizeof(A));
	B* ptr = &c;
	int vft2 = *((int*)ptr);
	PrintVFT((Func_Ptr*)vft1);
	PrintVFT((Func_Ptr*)vft2);
}

通过验证,我们可以发现,C类 里面有两张虚表,一张是A的,一张是B的。而C里面的虚函数funcC()的虚表,是存放在第一张虚表里面

但是,我们这里发现,重写的func1()函数,明明是一样的,但是地址却不一样,我们这段代码转到汇编代码查看

cpp 复制代码
int main()
{
	C c;
	A* ptr1 = &c;
	B* ptr2 = &c;
	ptr1->func1();
	ptr2->func1();
	return 0;
}

我们发现,ptr1是直接调用找个func1(),而ptr2最终调用的地址和ptr1是一样的,但是在jump的,寄存器减了一个8,这个减8正好是c的地址。ptr1不用修改是因为正好指向了c的起始地址,内存不看类型,只看地址

菱形继承这里就不讲了,很混乱~

🦬6. 抽象类

虚函数后面加上=0,则这个函数为纯虚函数 ,包含了纯虚函数的类,叫做抽象类

抽象类不能实例化出对象,之后继承的派生类也不能实例化对象,只能重写虚函数,派生类才能实例化出对象。这里规定了派生类必须重新虚函数,所以抽象类也叫接口类

cpp 复制代码
class A
{
public:
	virtual void func() = 0;
};
class B :public A
{
public:
	virtual void func()
	{
		cout << "B->func()" << endl;
	}
};
class C :public A
{
public:
	virtual void func()
	{
		cout << "C->func()" << endl;
	}
};
void Func(A*a)
{
	a->func();
}
int main()
{
	Func(new B);
	Func(new C);
	return 0;
}

那么本期的分享就到这里咯,我们下期再见,如果还有下期的话。

相关推荐
远望清一色2 分钟前
基于MATLAB的实现垃圾分类Matlab源码
开发语言·matlab
confiself11 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq041516 分钟前
J2EE平台
java·java-ee
凌云行者21 分钟前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
XiaoLeisj23 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
凌云行者25 分钟前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
杜杜的man26 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*27 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
半桶水专家28 分钟前
go语言中package详解
开发语言·golang·xcode
llllinuuu29 分钟前
Go语言结构体、方法与接口
开发语言·后端·golang