【C++】面向对象---多态(万字详解)

这篇具有很好参考价值的文章主要介绍了【C++】面向对象---多态(万字详解)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

       🔥🔥 欢迎来到小林的博客!!
      🛰️博客主页:✈️小林爱敲代码
      🛰️文章专栏:✈️小林的C++之路
      🛰️欢迎关注:👍点赞🙌收藏✍️留言
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言

      今天给大家讲解多态,多态是面向对象的一个重要内容。也非常的抽象,所以今天尽我所能为大家分享自己对C++中多态的一些理解。



         每日一句: 世界上只有想不通的人,没有走不通的路。

大纲:
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言

💖1. 多态的概念

    多态的意思就是多种形态,简而言之就是 : 不同的事物做同一种行为,产生了不同的结果。
打个比方,学生和普通人买票,学生优惠7折,而普通人没有优惠。这类现象就符合多态,不同的事物(普通人,学生)做同一种行为(买票)产生了不同的结果(学生七折,普通人全款)。
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
而普通人和学生之间还有另外一种关系,那就是继承关系。因为学生也是人,所以构成多态的前提是不同的事物之间构成继承关系。


💖2. 多态的定义及实现

🌺2.2 多态的构成条件

    想要知道多态如何定义,那么我们必须知道多态的定义条件。构成多态的两个条件:

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


🌺2.3 虚函数

虚函数:被virtual 修饰的函数即为虚函数。

class Person
{
public:
	virtual void BuyTicket()//被virtual修饰,是虚函数
	{
		
	}
};

🌺2.4 虚函数的重写

虚函数的重写: 派生类必须有一个和基类一样(三同)的虚函数,才能构成重写。构成重写的条件:
1.派生类被重写的函数也得是虚函数(虽然不是也可以,因为会直接继承父类的虚函数)。
2.派生类被重写的函数和基类的虚函数一样 (函数名,返回值,参数都相同,协变除外)。

下面是一个虚函数构成重写的案例:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成年人买票" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票" << endl;
	}
};

以上代码构成以下关系:
基类 : Person
派生类 : Student
基类的BuyTicket函数被virtual修饰,所以是虚函数。
派生类有一个和基类虚函数一模一样的虚函数。
所以派生类BuyTicket构成重写(覆盖)。

🌺2.5 协变

上面说过,被重写的函数必须与其基类对应的虚函数保持三同(返回值,函数名,参数),而协变是个例外,协变支持返回值是父子类的指针或引用。

代码案例如下:

class A{};
class B:public A
{};

class Person
{
public:
	virtual A* BuyTicket()
	{
		new A();
	}
};
class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		new B();
	}
};

返回值是父子类的指针或引用(也就是协变),一样会重写。

🌺2.6析构函数的重写

当我们在通过父类指针接收一个子类对象时,并期望释放掉这个对象。那么我们必须要让子类重写析构函数。也就是让析构函数变成虚函数,析构函数变成虚函数之后。子类会自动重写析构,这是因为在编译时析构函数的函数名会被统一处理为destructor。所以析构函数的函数名看起来不同,但实际上却是相同的。

下面是一个重写析构函数的例子:
不重写析构函数的代码:


class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成年人买票" << endl;
	}
	 ~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票" << endl;
	}
	 ~Student()
	{
		cout << "~Student" << endl;
	}

};

void a(Person* p)
{
	delete p;
}
int main()
{
	Person* p = new Person();
	Student* s = new Student();
	a(s);	
	return 0;
}

我们这个代码是没有没有重写析构函数的,因为析构函数不是虚函数,我们看看结果。
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言

我们会发现,问题很严!因为我传过去的是一个Student,也就是基类对象。但是我们用父类指针接收,那么 指针pp的使用范围 就是Person的范围。所以无法调用子类的析构函数,只能调用自己的析构函数。也就是说!释放不彻底,因为传过去的对象是s对象的指针,而delete它时,它却只调用了父类的析构函数,没有调用自己本身的析构函数,如果此时s对象有动态开辟的空间,那么就造成了内存泄露,这是很严重的。这是因为指针是Person类型的,所以只能访问Person的那一部分。想要解决这个问题,我们就需要重写析构函数。以至于传子类对象指针,父类指针接收也能调到子类的析构函数。


正确的写法:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成年人买票" << endl;
	}
	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票" << endl;
	}
	virtual ~Student()
	{
		cout << "~Student" << endl;
	}
	 int _a;

};

void a(Person* pp)
{
	delete pp;
}
int main()
{
	Person* p = new Person();
	Student* s = new Student();
	a(s);	
	return 0;
}

这时候我们可以看到Student的析构函数也被调用了,这就意味着s对象被真正析构。所以析构函数还是很有必要被重写的。
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言

🌺2.7 重载,重写(覆盖),重定义(隐藏)之间的区别

一张图概括

【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言


💖3. override 和 final(c++11)

override 和 final 在c++11才被引用,2个关键字的作用也很简单。
final:修饰虚函数,表示虚函数不能被重写。
override:检查派生类是否重写了虚函数,如果没重写,会报错。

final的使用:
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言

override的使用:
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言


💖4.抽象类

在虚函数的后面加上一个 = 0,这个函数就是纯虚,这就代表这是一个抽象类,也叫接口类,抽象类不能被实例化,派生类继承后也不能实例化出对象。除非重写其基类的纯虚函数。

代码样例:


class Person
{
public:
	virtual void Eat() = 0
	{
	}
};
class Student : public Person
{
public:

};

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

【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
如果想使用,我们必须重写纯虚函数。
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
但是p依然不能实例化,想要p对象,我们可以通过指针或者引用的方式。

class Person
{
public:
	virtual void Eat() = 0
	{
	}
};
class Student : public Person
{
public:
	virtual void Eat()
	{
		cout << "吃饭" << endl;
	}
};

int main()
{

	Student s;
	Person& p = s;
	p.Eat();
	return 0;
}

这种方法已经构成了多态,因为s通过了基类的指针调用其纯虚函数。


💖 5.多态的原理

那么多态是怎么实现的呢?我们先来监视一下,非多态时,子类对象和父类对象。

class Person
{
public:
	 void BuyTicket()
	{
		 cout << "成年人买票" << endl;
	}
	 int _p;
};
class Student : public Person
{
public:
	 void BuyTicket()
	{
		cout << "学生买票" << endl;
	}
	 int _s;
};

int main()
{

	Student s;
	Person p;

	return 0;
}

这是父类对象
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言


这是子类对象
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
接下来我们看看实现多态时的样子。

class Person
{
public:
	virtual void BuyTicket()
	{
		 cout << "成年人买票" << endl;
	}
	 int _p;
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生买票" << endl;
	}
	 int _s;
};

int main()
{

	Student s;
	Person p;

	return 0;
}

父类对象:
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
子类对象:
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
我们可以很清楚的发现,构成多态后,对象里面会多一个__vfptr的参数。而这个参数是虚函数表指针(简称虚表指针),它指向一个数组,数组的每个元素是一个函数指针。而这个数组,叫做虚函数表。而虚函数表里面存的就是虚函数的地址。当子类进行重写的时候,就会去虚函数表里面把存储的父类虚函数的地址覆盖成自己的虚函数地址。所以进行切片时,虚函数表里的虚函数地址还是自己的。调用虚函数时,去自己的虚函数表里面找到对应的虚函数。

所以多态的实现原理,简单来说就是以下几个步骤:

  1. 看父类有没有虚函数,如果有虚函数,则会在父类生成一个虚函数表。
    【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言

  2. 子类继承父类时,会把父类的虚函数表也继承下来。
    【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言

  3. 随后子类查找有没有符合重写条件的函数(三同,且是虚函数),符合重写条件则到继承的虚函数表里,覆盖掉父类的虚函数表。

【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
此时如果构成多态,就会进Student的虚函数表里面找对应的虚函数调用,因为父类虚函数的地址被替换了。


💖 6.单继承和多继承的虚函数表

🌺 6.2 打印虚函数表

以下这段代码可以直接打印虚函数表,其原理取对象的地址,随后强制转换成一个指针。因为指针在32平台是4字节,在64平台是8字节。所以把对象强制转换成指针类型,访问的第一个元素就是一个指针的大小。因为虚表指针就是在对象的最前面4个或8个字节。然后强制转换成函数指针。

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

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

	 int _p;
};
class Student : public Person
{
public:
	virtual void fun1()
	{
		cout << "Student::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Student::fun2()" << endl;
	}
	virtual void fun3()
	{
		cout << "Student::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Student::fun4()" << endl;
	}
	 int _s;
};
void a(Person& p)
{
	p.fun1();
}

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 << endl;
}
int main()
{
	Person p;
	Student s;

	VFPTR * vTableb = (VFPTR*)(*(void**)&p);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(void**)&s);
	PrintVTable(vTabled);
	return 0;
}

我们看看打印结果
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
这样我们就可以看到虚函数表的打印结果了。

🌺 6.3 单继承的虚函数表

上面说过的情况就是单继承时的情况,子类继承父类时会继承它的虚表。随后在虚表里面覆盖重写的函数。那么如果此时子类自己的函数也是虚函数,那么也会添加至虚函数表中。

两个类的关系如下所示

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

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

	 int _p;
};
class Student : public Person
{
public:
	virtual void fun1()
	{
		cout << "Student::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Student::fun2()" << endl;
	}
	virtual void fun3()
	{
		cout << "Student::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Student::fun4()" << endl;
	}
	 int _s;
};

我们可以发现,子类的fun1和fun2与父类构成多态。可是fun3和fun4并没有构成多态,但是它们依然会被添加进子类的虚函数表。
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
所以虚函数表也会添加自身的虚函数。

🌺 6.4 多继承的虚函数表

那么如果是多继承呢?

以下代码实现了多继承,me继承了Base1和Base2。因此,Base1的虚函数表在m的前4/8个字节的位置。但是Base2的虚函数表可不在后面。所以要想知道Base2的虚函数表位置。我们需要m的地址在原有的基础上加一个Base1大小,这样就到达了Base2对象的首地址,再取前4/8个字节就是Base2的虚函数表。

class Base1
{
public:
	virtual void fun1()
	{
		cout << "Base::fun1()" << endl;
	}

	virtual void fun2()
	{
		cout << "Base::fun2()" << endl;
	}

	 int _p;
};
class Base2
{
public:
	virtual void fun3()
	{
		cout << "Base2::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Base2::fun4()" << endl;
	}

};

class me :public Base1,public Base2
{
	virtual void fun1()
	{
		cout << "me::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "me::fun2()" << endl;
	}
	virtual void fun5()
	{
		cout << "me::fun5()" << endl;
	}
	virtual void fun6()
	{
		cout << "me::fun6()" << endl;
	}
};


typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i+1, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl << endl;
}
int main()
{
	me m;

	VFPTR * vTableb = (VFPTR*)(*(void**)&m);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(void**)((char*)&m+sizeof(Base1)));
	PrintVTable(vTabled);
	return 0;
}

我们看看代码结果:
【C++】面向对象---多态(万字详解),小林的C++之路,c++,java,开发语言
我们可以看到,当有一个类继承了多个类时,那么会产生多张虚函数表。而自己的虚函数(非重写) 将会被默认放在第一张函数表中。

💖 7.多态面试问答题

  1. 什么是多态?
    2答:不同的事物做同一行为产生不同的结果。

  2. 什么是重载、重写(覆盖)、重定义(隐藏)?
    答:重载要在同一作用域下,且函数名相同,但参数的顺序,个数,类型不同。
    重写是当基类和派生类有一模一样的虚函数时,子类虚函数表中的父类虚函数地址会被覆盖。
    重定义,从父类继承下来,且没有重写的就是重定义,重定义函数名,参数相同。

  3. 多态的实现原理?
    答:父类的所有虚函数都会存在虚函数表中,而虚函数表存储在常量区。当子类继承了父类时,也会继承父类的虚函数表,如果此时子类又符合重写要求的函数。则会去自己的虚函数表中替换掉父类的虚函数地址,换成自己的虚函数地址。

  4. inline函数可以是虚函数吗?
    答:可以,不过编译器就忽略inline属性,这个函数就不再是
    inline,因为虚函数要放到虚表中去。

  5. 静态成员可以是虚函数吗?
    答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

  6. 构造函数可以是虚函数吗?
    答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

  7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
    可以,并且最好把基类的析构函数定义成虚函数,否则当子类和父类构成多态时。delete释放对象可能会导致内存泄漏,具体上面有讲解。

  8. 对象访问普通函数快还是虚函数更快?
    答:构成多态,普通函数快。不构成多态,一样快。

  9. 虚函数表是在什么阶段生成的,存在哪的?
    答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

  10. 什么是抽象类?抽象类的作用?
    答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

总结🥳:

💦💦如果有写的有什么不好的地方,希望大家指证出来,我会不断的改正自己的错误。💯💯如果感觉写的还可以,可以点赞三连一波哦~🍸🍸后续会持续为大家更新C/C++,数据结构,Linux相关的知识

🔥🔥你们的支持是我最大的动力,希望在往后的日子里,我们大家一起进步!!!
🔥🔥
文章来源地址https://www.toymoban.com/news/detail-789837.html

到了这里,关于【C++】面向对象---多态(万字详解)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 面向对象(类/继承/封装/多态)详解

    面向对象编程(Object-Oriented Programming,OOP)是一种广泛应用于软件开发的编程范式。它基于一系列核心概念,包括类、继承、封装和多态。在这篇详细的解释中,我们将探讨这些概念,并说明它们如何在PHP等编程语言中实现。 类是OOP的基础。它是一种用于创建对象的蓝图或模

    2024年02月08日
    浏览(50)
  • 【Java基础教程】(十四)面向对象篇 · 第八讲:多态性详解——向上及向下转型、关键字 final与 instanceof的作用~

    掌握final 的主要作用及使用; 掌握对象多态性的概念以及对象转型的操作; 掌握instanceof 的主要作用及使用; 在Java 中 final称为终结器,在Java 里面可以使用 final定义类、方法和属性,用于表示不可变性 。 final 类:当一个类被声明为 final 时,意味着该类不能被

    2024年02月16日
    浏览(32)
  • c++面向对象之封装、继承、和多态

    把客观事物封装成类,而且可以把自己的数据和方法设置为只能让可信的类或者对象操作,对不可信的信息进行隐藏(利用public,private,protected,friend)实现 has-a :描述一个类由多个部件类构成,一个类的成员属性是另一个已经定义好的类。 use-a:一个类使用另一个类,通过类之间

    2024年02月02日
    浏览(37)
  • C++ 面向对象核心(继承、权限、多态、抽象类)

    继承(Inheritance)是面向对象编程中的一个重要概念,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和方法。继承是实现类之间的关系,通过继承,子类可以重用父类的代码,并且可以在此基础上添加新的功能或修改已有的功能。 在C++中,继承

    2024年02月08日
    浏览(36)
  • Java面向对象多态

    目录 多态概述 Java 多态包括以下三种方式 方法重写(Override) 向上转型(Upcasting) 实现多态 Java 多态是指同一种类型的对象,在不同的情况下有着不同的状态和行为。它是基于继承、重写和向上转型等特性实现的,多态是面向对象编程的三大特征之一,其他两个分别是封装

    2023年04月13日
    浏览(34)
  • 【JavaSE】面向对象编程思想之多态(图文详解)

    目录 1. 多态的概念 2. 多态实现条件 3. 重写 4. 向上转型和向下转型 4.1 向上转型 4.2 向下转型 5. 多态的优缺点 6. 避免在构造方法中调用重写的方法 多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。  总的来说

    2024年02月14日
    浏览(32)
  • Java面向对象 - 封装、继承和多态

    目录 第1关:什么是封装,如何使用封装 第2关:什么是继承,怎样使用继承 第3关:super的使用 第4关:方法的重写与重载 第5关:抽象类 第6关:final的理解与使用 第7关:接口 第8关:什么是多态,怎么使用多态 Java_Educoder

    2024年02月07日
    浏览(69)
  • 【java】面向对象三大特性之多态

            俗话说的好,“一龙生九子,九子各不同”,这句话就蕴含了面向对象三大特性之一的多态的思想。那么多态具体有什么特点呢,就由博主来带大家梳理一下吧🤔 目录 一、什么是多态 二、重写 三、向上转型和向下转型 1、向上转型 2、向下转型 四、多态的优缺点

    2024年03月15日
    浏览(57)
  • C++:多态究竟是什么?为何能成为面向对象的重要手段之一?

    本篇博客基于VS2019X86环境下,后续关于多态原理相关验证都是基于vsX86环境,而虚表本质上是一个虚函数指针数组,在X86环境下VS编译器会在数组最后放一个unllptr!! 多态的概念:通俗来说,就是多种形态,具体点就是去 完成某个行为,当不同的对象去完成时会产生出不同的

    2024年02月02日
    浏览(38)
  • Educoder/头歌JAVA——JAVA面向对象:封装、继承和多态的综合练习

    目录 第1关:封装、继承和多态进阶(一) 相关知识 面向对象思想 封装 继承 组合和继承 构造函数 super()和this() 编程要求 第2关:封装、继承和多态进阶(二) 相关知识 重写和重载 abstract(抽象类)和interface(接口) final static static的作用 多态 编程要求 第

    2024年02月04日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包