首页 > 编程语言 >C++多态与虚函数

C++多态与虚函数

时间:2023-10-15 18:22:27浏览次数:52  
标签:调用 函数 多态 C++ 基类 重写 构造函数

多态与虚函数

1. 什么是多态

所谓多态,就是同一个函数名具有多种状态,或者说一个接口具有不同的行为;C++的多态分为编译时多态和运行时多态,编译时多态也称为为静态联编,通过重载和模板来实现,运行时多态称为动态联编,通过继承和虚函数来实现。

1.1 编译时多态

重载(Overloading)

是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。注意区分重写(是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。)

  • 静态绑定:在重载中,方法的选择是在编译时确定的,因此它被称为静态绑定或早期绑定。编译器会根据方法调用中提供的参数类型来决定要调用哪个方法。
  • 方法签名:方法的重载是根据方法的签名来区分的,方法签名包括方法的名称、参数的数量和参数的类型。编译器使用方法签名来决定要调用的方法。
  • 无需运行时类型信息:由于重载是在编译时解决的,因此不需要运行时类型信息或动态分派(与运行时多态相反)。这降低了运行时的开销,使代码更加高效。

尝试用重载的方式来模拟LOL中英雄的互相攻击:

先创建一个英雄基类,然后创建三个英雄类,分别是盖伦,伊泽瑞尔和瑞兹

#include<iostream>

class Ezreal;
class Ryze;

class Hero 
{
};

class Garen : public Hero
{
public:
	void Attack(Ezreal* pEzreal)
	{
		std::cout << name << "Garen attack Ezreal" << std::endl;
	}

	void Attack(Ryze* pRyze)
	{
		std::cout << "Garen attack Ryze" << std::endl;
	}
};

class Ezreal : public Hero
{
public:
	void Attack(Garen* pGaren)
	{
		std::cout << "Ezreal attack Garen" << std::endl;
	}

	void Attack(Ryze* pRyze)
	{
		std::cout << "Ezreal attack Ryze" << std::endl;
	}
};

class Ryze : public Hero
{
public:
	void Attack(Garen* pGaren)
	{
		std::cout << "Ryze attack Garen" << std::endl;
	}

	void Attack(Ezreal* pEzreal)
	{
		std::cout << "Ryze attack Ezreal" << std::endl;
	}
};



int main()
{
	Garen* garen = new Garen();
	Ezreal* ezreal = new Ezreal();
	Ryze* ryze = new Ryze();

	garen->Attack(ezreal);
	garen->Attack(ryze);
}

输出结果:

Garen attack Ezreal
Garen attack Ryze

如果用重载的方法写英雄类,工作量十分巨大,而且每次增加英雄都要修改所以英雄类的 Attack方法。而使用运行时多态,也就是虚函数的方法则可以大大减小工作量。

1.2 运行时多态

虚函数与函数重写(Override)

重写是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰(不加virtual则是在子类中重定义)。

如果我们不使用 virtual设置虚函数,

#include<iostream>

class Hero 
{
public:
	void Attack()
	{
		std::cout << "hero attack" << std::endl;
	}
};

class Garen : public Hero
{
public:
	void Attack()
	{
		std::cout << "Garen attack" << std::endl;
	}
};

class Ezreal : public Hero
{
public:
	void Attack()
	{
		std::cout << "Ezreal attack" << std::endl;
	}

};

class Ryze : public Hero
{
public:
	void Attack()
	{
		std::cout << "Ryze attack" << std::endl;
	}
};



int main()
{
	Hero* hero = new Ryze();
	hero->Attack();

}

输出结果是

hero attack

调用了基类的方法

如果设置虚函数,修改 Hero

class Hero 
{
public:
	virtual void Attack()
	{
		std::cout << "hero attack" << std::endl;
	}
};

输出结果是

Ryze attack

可以用基类指针调用派生类的重写的函数。

我们再用虚函数实现最开始的功能

//Hero基类
class Hero
{
public:
	virtual void Hurted() = 0;

};


//Ryze类
class Ryze : public Hero
{
public:
	void Attack(Hero* pHero)
	{
		pHero->Hurted();
	}

	void Hurted()
	{
		std::cout << "Ryze is Hurted" << std::endl;
	}
};

//Ezreal类
class Ezreal : public Hero
{
public:
	void Hurted()
	{
		std::cout << "Ezreal is Hurted" << std::endl;
	}
};

//Garen类
class Garen : public Hero
{
public:
	void Hurted()
	{
		std::cout << "Garen is Hurted" << std::endl;
	}
};

int main()
{
	Ryze* ryze = new Ryze();
	Garen* garen = new Garen();
	Ezreal* ez = new Ezreal();
	ryze->Attack(garen);
	ryze->Attack(ez);
	ryze->Attack(ryze);

}

输出结果

Garen is Hurted
Ezreal is Hurted
Ryze is Hurted

2. 虚函数指针与虚函数表

参考文章:

3. 关于virtual

某个方法在基类中声明为虚方法,一旦一个方法被声明为虚方法,它在后续继承过程中将永远是一个虚方法,不管重写的时候是否使用 virtual关键字,在父类中声明了虚方法,子类中重写的方法可以不使用关键字 virtual

class Base
{
public:
	virtual void Print()
	{
		std::cout << "I am Base" << std::endl;
	}
};

class Derived1 : public Base
{
public:
	void Print()
	{
		std::cout << "I am Derived1" << std::endl;
	}
};

class Derived2 : public Derived1
{
public:
	void Print()
	{
		std::cout << "I am Derived2" << std::endl;
	}
};

int main()
{

	Base* p1 = new Derived1();
	p1->Print();

	Base* p2 = new Derived2();
	p2->Print();

	Derived1* p3 = new Derived2();
	p3->Print();
}

输出结果:

I am Derived1
I am Derived2
I am Derived2

3.1 override & final

C++11关键字:override 和 final

  • override:保证在派生类中声明的重载函数,与基类的虚函数有相同的签名;
  • final:阻止类的进一步派生 和 虚函数的进一步重写。

加了override,明确表示派生类的这个虚函数是重写基类的,如果派生类与基类虚函数的签名不一致,编译器就会报错。

如果不希望某个类被继承,或不希望某个虚函数被重写,则可以在类名和虚函数后加上 final 关键字,加上 final 关键字后,再被继承或重写,编译器就会报错。

1696918805901

3.2 为什么C++的构造函数不可以是虚函数,而析构函数可以是虚函数

简言之:构造函数不能是虚函数,因为虚函数是基于对象的,构造函数是用来产生对象的,若构造函数是虚函数,则需要对象来调用,但是此时构造函数没有执行,就没有对象存在,产生矛盾,所以构造函数不能是虚函数。

析构函数可以为虚函数,而且当要使用基类指针或引用调用子类时,最好将基类的析构函数声明为虚函数,否则会存在内存泄露的问题。【内存泄漏:析构函数是虚函数,因为若有父类指针指向子类对象存在,需要析构的是子类对象,但父类析构函数不是虚函数,则只析构了父类,造成子类对象没有及时释放,引起内存泄漏。】

4. 多态面经

  • inline可以是虚函数吗?
    可以。inline需要 展开 ,编译时不存在地址,但是虚函数需要将其地址存入虚表中,表现上来说,两者是互斥的。但是需要注意,inline只是一个建议性关键字,关键取决于编译器,不会强制性执行。两者关键字存在的时候,如果是多态调用,编译器会自动忽略inline这个建议,因为没法将这个虚函数直接展开,这个建议无了。不是多态就可以利用此建议。
  • static函数可以是虚函数吗?
    不可以。静态成员函数没有this指针,直接利用类域指定的方式调用。虚函数都是为多态服务的。多态是运行时决议,而静态成员函数都是编译性决议。
  • 构造函数可以是虚函数吗?
    不可以。构造函数之前,虚表没有进行 初始化 。virtual函数是为了实现多态,运行时去虚表找对应虚函数进行调用。对象的虚表也是在构造函数的初始化列表进行初始化的。
  • 析构函数可以是虚函数。
  • 拷贝构造和赋值可不可以是虚函数?
    拷贝构造不可以,拷贝构造同样也是构造函数。
    赋值可以,但是没有意义。
    (但是可以简单实现一下父类给给子 ... ... )但是,赋值一般是同类对象之间数据进行拷贝,这样就不存在实际价值。
  • 6.对象访问普通函数快还是虚函数快?
    不构成多态一样快,否则普通函数快。
  • 虚函数表是在什么阶段生成的,存在哪里的?
    构造函数初始化列表初始化的是虚函数表指针,对象中也是存的指针。
    存在代码区--利用验证法,和只读常量或者静态变量的地址进行验证。
  • 在(基类的)构造函数和析构函数中调用虚函数会怎么样
    从语法上讲,调用没有问题,但是从效果上看,往往不能达到需要的目的(不能实现多态);因为调用构造函数的时候,是先进行父类成分的构造,再进行子类的构造。在父类构造期间,子类的特有成分还没有被初始化,此时下降到调用子类的虚函数,使用这些尚未初始化的数据一定会出错;同理,调用析构函数的时候,先对子类的成分进行析构,当进入父类的析构函数的时候,子类的特有成分已经销毁,此时是无法再调用虚函数实现多态的。

Reference

标签:调用,函数,多态,C++,基类,重写,构造函数
From: https://www.cnblogs.com/dogwealth/p/17765934.html

相关文章

  • cpu亲和性相关函数和宏 基础讲解[cpu_set_t]
    cpu亲和性相关函数和宏讲解:写在前面:我在查找关于linuxcpu宏函数没看到有对宏函数基础的、详细的讲解,笔者便通过官方文档入手,对次进行的翻译和理解希望能帮到对这方面宏有疑惑的读者explain:/elem/表示为elem变量,这样子便于区分P.S:#include<sched.h>动态范围cpu设置......
  • 前端every()函数
    前端中的every函数是用于验证数组中的每个元素是否都满足某个条件。它接受一个回调函数作为参数,该回调函数会依次遍历数组中的每个元素,并返回一个布尔值来表示该元素是否满足条件。如果数组中的所有元素都满足条件,every函数将返回true;否则,返回false。下面是一个示例代码,展示了如何......
  • kotlin的函数关于可变参数使用vararg
    前提:kotlin在编译的时候会转换成对应的java一、java的可变参数类型:java类型的类似:voidfunc(Integer...values){}   那么对应的kotlin的类型类似:funfunc(varargvalues:Int?){}  注意:这里我使用的是Int?是可空的意思,那么到java层的时候会转换成Integer,......
  • string类构造函数与析构函数
    string类构造函数与析构函数构造函数构造函数作用strings构造一个空字符串strings(s1)生成一个和s1相同的空字符串sstrings(s1,5)将s1[5]以后的部分作为s的初始部分strings(s1,5,5)将始于s1[5],长度为5的部分作为s的初始值strings(cstr)以C_strin......
  • 终于知道如何利用hive的日期转换函数进行日期格式的清洗啦~(之前用的外部数据清洗)
    1、创建合适格式的表result10createtableresult10(ipString,time1String,dayString,trafficString,typeString,idString)rowformatdelimitedfieldsterminatedby','storedastextfile;2、将txt文件的数据插入到表中:loaddatalocalinpath'/data/resul......
  • 【gdb】向上或向下切换函数堆栈帧
    向上或向下切换函数堆栈帧1.例子:#include<stdio.h>intfunc1(inta){return2*a;}intfunc2(inta){intc=0;c=2*func1(a);returnc;}intfunc3(inta){intc=0;c=2*func2(a);retur......
  • C语言 strdup函数把字符串复制到新空间
    头文件是string.h。根据传入的字符串参数,malloc分配空间并复制,返回首地址,该地址通过free来释放。#include<stdio.h>#include<malloc.h>#include<string.h>intmain(){chara[20]="123";char*b=strdup(a);printf("%s\n",b);free(b);......
  • c++ 线段树模板
    洛谷模板:P3372【线段树1】 #include<bits/stdc++.h>#defineintlonglongusingnamespacestd;constintN=1e5+10;inta[N],d[N<<2],b[N<<2];intn,q;inlinevoidbuild(intl,intr,intp){if(l==r){d[p]=a[l];......
  • linux shell中创建函数
     001、[root@pc1test]#cattest.sh##函数脚本#!/bin/bashfunctiondb1##function关键字来定义函数,db1是函数名{read-p"请输入:"valuereturn$[$value*2]##return返回函数值}db1#......
  • C++ const 在函数中的使用
    C++中的const在函数中的用法有三种:修饰形参此时写法如下:voidfun(constClassA&a);目的为防止传入的原始参数被修改;修饰返回值此时写法为constint&getAge();目的为防止函数返回值作为左值被修改;修饰函数此时的写法为typeNamefun()const();当const修饰函数时,所有......