C++杂谈之一二三-多态

前言

  经过前几章c++基础语法的铺垫后,我们后面可以研究一些稍微深入的知识了。c++的三大特性-封装、继承、多态。这三大特性在我的理解更多的是表明C++中OOP(面向对象编程)思想的一个总结,其中封装性代表的是一个思想,即:应暴露尽可能少的实现细节给外部。其中涉及到的点有访问限定符、提供读写数据接口而非直接提供数据等,具体的细节本文不再展开。而继承和多态二者是相辅相成的,也就是多态依赖于继承,而继承的一大亮点也是可以实现多态。下面我们就继承和多态来引入c++最核心的知识点~

什么是多态

  多态,从语义上理解就是多种形态,在c++中它代表的就是调用一个函数能够产生多种形态。分为静态多态和动态多态,其中静态多态就是重载,函数名相同,参数个数或者类型不同,这个很容易理解,不多说,我们主要说一下动态多态。

动态多态

  动态多态是基于c++的RTTI机制结合c++中的继承和虚函数所表现出来的一种强大特性,他的释义是:将函数绑定推迟至运行期,根据实际对象的类型来确定调用哪种版本的函数。听起来很抽象是吧,我尝试用我的理解来解释一下:某一天村长(编译器)里要集中盖房子,每家都必须出人,老黄家也必须出人,问到老黄家的时候,回复说:那天不知道谁空,等到了那天再说(运行期),我家谁有空就谁去,然后村长就记着:老黄家不确定派谁干活,等到那天再说(多态)。例子举得不太恰当,意思就是告诉编译器这个函数(虚函数)你先不要编译,等到运行期间的话,再根据调用对象的实际类型来调用我。

如何使用多态

  多态 = 继承+虚函数,继承就不多说了,我们直接上代码

class Base
{
public:
	virtual void function()
	{
		std::cout << "base createproduct" << std::endl;
	}
};
class Deived:public Base
{
public:
	void function()
	{
		std::cout << "deived createproduct" << std::endl;
	}
};
int main()
{
	Base * base = nullptr;
	base = new Base();
	base->function();
	delete base;
	base = new Deived();
	base->function();
	delete base;
}

  结果如下:
  我们来分析一下,我首先定义了一个指针置为nullptr(C++11出的空指针),然后将指针指向了一个base对象的内存空间,调用function,然后将指针指向了一个deived对象的内存空间,再次调用function函数。其中我的调用方式是一模一样的,区别是不是只在于base这个指针实际指向内存地址的不同。而这就是动态多态。

注意点:
  1.只有虚函数才有多态性,普通函数没有;
  2.只有有继承关系的类才会根据实际对象类型而呈现多态;
  3.虚函数的声明需要加上virtrual标志符,定义不需要加,子类的虚函数重写也可以不加;

多态的重要性

  一定要学习多态吗?我一直搞不懂多态是干嘛的,能不使用多态就可以写代码吗?答案是肯定的,不用多态你是个码农,但是你肯定写不出好的代码,用了多态,你就踏上了进阶之路,有了多态的基础你才能理解很多设计模式(工厂模式、观察者模式等等)和优秀框架设计,所以多态非常重要。

多态的实现机制

  为啥继承+虚函数就可以实现多态了呢?没有为啥,c++就是这样规定的,既然用了人家的语言,就遵守人家的语法。但是我们可以尝试稍微深入看一下c++是如何实现多态的。
  “我知道很多道理,却依旧过不好这一生”这句话也适用于我们的学习之路,我们在很多的博客中见过太多介绍多态实现的文章了,说的都很好,但是学习的最好方法就是实践,对于多态的实现机制我们先介绍其理论,再写代码验证之~
  首先多态的实现是由于编译器在编译期间为函数分配入口地址,但是有一个例外,那就是虚函数,如果编译器检测到这个函数是虚函数的话,那么就不会为这个函数分配地址,会将这个过程推迟到运行期,换句话说,也还是我们上面那句话,这个虚函数的地址会在运行到调用的时候,才会临时分配地址。
  而编译器是如何在运行期间找到对应版本的虚函数地址的呢?bingo,都知道,每个对象的内存在初始化的时候,如果类中有虚函数的话,那么他会维护一个虚函数表指针,这个虚函数表里面存放了虚函数的地址。
  咱们写段代码验证一下这个东西

#pragma once
#include <iostream>

class base
{
public:
	int privateValue;
public:
	void Func()
	{
		std::cout << "非虚函数base :: func 被调用" << std::endl;
	}
	virtual void virFunc()
	{
		std::cout << "虚函数base :: virFunc被调用" << std::endl;
	}
};

然后子类deived重写virFunc函数

#pragma once
#include <iostream>
class deived :public base
{
public:
	mutable int a;
public:
	void virFunc()
	{
		std::cout << "虚函数deived :: virFunc被调用" << std::endl;
	}
};
#include <stdio.h>
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>
#include "deived.h"

using namespace std;

int main()
{
	base* ptr = nullptr;
	ptr = new base();
	delete ptr;
	ptr = new deived();
	delete ptr;
}

  很简单的代码,我们设断点看一下两个对象内存里面到底存放了什么东西
在这里插入图片描述
  没错,base构造出来的内存中,我们看到在内存的首位就维护了一个虚表指针,确实指向了一个虚函数地址,而在deived中同样也维护了一个虚表指针,而它指向的确实自己重写过的虚函数的地址。
  那么我们得出以下结论:
  1.如果一个类中存在虚函数,那么它所构造的内存空间中首部就会维护一个虚表指针,指向一个虚函数表,这个虚函数表存放的是虚函数的地址;
  2.如果子类重写了父类的虚函数,那么虚表中存放的地址是子类版本的虚函数地址,而非父类;

  那么问题又来了,那既然子类重写了父类的虚函数,那么虚表中存放的地址是子类版本的虚函数地址,那Base *ptr = new deived();中ptr是不是无法访问父类版本的虚函数了呢?
  答案是可以访问,但是需要强行加上作用域符ptr->base::virFunc();即可访问,我擦,这是为啥,那是因为编译器如果直接遇到这种用法的话,会直接访问虚函数的地址,而不再通过虚表指针了~

小结

  1.多态,从语义上理解就是多种形态,在c++中它代表的就是调用一个函数能够产生多种形态。分为静态多态和动态多态,其中静态多态就是重载,函数名相同,参数个数或者类型不同,动态多态就是通过父类指针指向父类或者子类对象内存,相同调用代码能够呈现出不同的表现形式。
  2.有了多态的基础才能理解很多设计模式(工厂模式、观察者模式等等)和优秀框架设计,所以多态非常重要。
  3.多态的实现机制就是通过虚表指针来实现的,多态性就是通过父类指针指向不同的对象内存地址(父类或者子类),而不同的对象内存中会维护不同的虚表指针,指向不同的虚表,从而能够呈现出多态性。
  好了,我是Double beans,一个希望通过简单代码能够说明一件事的c++程序员,学海无涯,我们后面再见。

### 面向对象编程中的多态概念解释 多态是指方法或对象能够表现出多种形式的能力,这是面向对象编程的第三大特性[^1]。它允许程序在不同情况下执行不同的行为,增强了灵活性和扩展性。 #### 多态的具体表现形式 多态可以分为两种主要类型: - **编译时多态(静态多态)** - 主要通过函数重载实现,在编译阶段就决定了具体调用的方法版本[^2]。 - **运行时多态(动态多态)** - 通常由子类覆盖父类的方法来达成,具体的实现取决于实例的实际类型,只有到运行时刻才能确定。 #### 示例说明 下面是一个展示Java中多态特性的简单例子,其中包含了方法重写以及向上转型的概念: ```java // 定义一个基类 Animal class Animal { public void makeSound() { System.out.println("Some generic animal sound"); } } // 创建 Dog 类作为 Animal 的子类并重写了 makeSound 方法 class Dog extends Animal { @Override public void makeSound() { System.out.println("Bark!"); } } public class Main { public static void main(String[] args) { // 向上转型:将子类的对象赋给超类类型的变量 Animal myDog = new Dog(); // 调用的是 Dog 中被重写的 makeSound 方法 myDog.makeSound(); // 输出 "Bark!" } } ``` 在这个例子中,`myDog` 是 `Animal` 类型的一个引用,但它指向了一个 `Dog` 对象。当调用 `makeSound()` 方法时,实际上执行的是 `Dog` 类里定义的行为而不是 `Animal` 类里的默认行为,这体现了运行时多态的特点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值