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

posted @ 2023-10-15 18:13  DogWealth~  阅读(5)  评论(0编辑  收藏  举报