牛刀小试4

本文详细介绍了C++中的多态性,包括编译时的静态多态(重载和模板)和运行时的动态多态(虚函数实现)。静态多态在编译阶段确定函数调用,而动态多态则在运行时根据对象实际类型决定调用哪个函数。通过虚函数表和虚函数指针,C++实现了动态多态,允许基类指针调用派生类的函数。此外,文章还讨论了隐藏(重定义)的概念,以及重载、覆盖和隐藏的实现原理和示例代码。
摘要由CSDN通过智能技术生成

c 语言中多态的几种实现方法,详解C++ 多态的两种形式(静态、动态)

多态(Polymorphisn)是面向对象程序设计(OOP)的一个重要特征。多态字面意思为多种状态。在面向对象语言中,一个接口,多种实现即为多态。C++中的多态性具体体现在编译和运行两个阶段。
在这里插入图片描述
(1)编译时多态是静态多态(重载,模板)

       是在编译的时候,就确定调用函数的类型。

(2)运行时多态是动态多态(覆盖,虚函数实现)

       在运行的时候,才确定调用的是哪个函数,动态绑定。运行基类指针指向派生类的对象,并调用派生类的函数。
虚函数实现原理:虚函数表和虚函数指针。纯虚函数: virtual int fun() = 0;

  • 重载
    实现方式
          重载是在同一作用域内(不管是模块内还是类内,只要是在同一作用域内),具有相同函数名不同的形参个数或者形参类型,或者形参顺序。返回值可以相同也可以不同(在函数名、形参个数、形参类型都相同而返回值类型不同的情况下无法构成重载,编译器报错。这个道理很简单,在函数调用的时候是不看返回值类型的)。

    实现原理
          重载是一种静态多态,即在编译的时候确定的。C++实现重载的方式是跟编译器有关,编译过后C++的函数名会发生改变,会带有形参个数、类型以及返回值类型的信息(虽然带有返回值类型但是返回值类型不能区分这个函数),所以编译器能够区分具有不同形参个数或者类型以及相同函数名的函数。插一句,在C语言中编译器编译过后函数名中不会带有形参个数以及类型的信息,因此C语言没有重载的特性。由此带来麻烦的一点是如果想要在C++中调用C语言的库,需要特殊的操作(extern “C”{})。库中的函数经过C编译器编译的话会生成不带有形参信息的函数名,而用C++的编译器编译过后会生成带有形参信息的函数名,因此将会找不到这个函数。extern “C”{}的作用是使在这个作用域中的语句用C编译器编译,这样就不会出错。这也是一种语言兼容性的问题。

  • 覆盖(重写)
    实现方式
          覆盖是指派生类中存在重新定义的函数,其函数名、参数列、返回值类型必须同父类中的相对应被覆盖的函数严格一致(并且父类中带有virtual关键字),覆盖函数和被覆盖函数只有函数体(花括号中的部分)不同.当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖。
    (PS:协变
    函数返回值类型可以不同但是必须是指针或者引用,并且两个虚函数的返回值之间必须要构成父子类关系。这种情况称之为协变,也是一种重写。引入协变的好处是为了避免危险的类型转换。)
    实现原理
          重写是一种动态多态,即在运行时确定的。C++实现重写的方式也跟编译器有关,编译器在实例化一个具有虚函数的类时会生成一个vptr指针(这就是为什么静态函数、友元函数不能声明为虚函数,因为它们不实例化也可以调用,而虚函数必须要实例化,这也是为什么构造函数不能声明为虚函数,因为你要调用虚函数必须得要有vptr指针,而构造函数此时还没有被调用,内存中还不存在vptr指针,逻辑上矛盾了)。vptr指针在类的内存空间中占最低地址的四字节。vptr指针指向的空间称为虚函数表,vptr指针指向其表头,在虚函数表里面按声明顺序存放了虚函数的函数指针,如果在子类中重写了,在子类的内存空间中也会产生一个vptr指针,同时会把父类的虚函数表copy一下当做自己的,然后如果在子类中重新声明了虚函数,会按声明顺序接在父类的虚函数函数指针下。而子类中重写的虚函数则会替换掉虚函数表中原先父类的虚函数函数指针。重点来了,在调用虚函数时,不管调用他的是父类的指针、引用还是子类的指针、引用,他都不管,只看他所指向或者引用的对象的类型(这也称为动态联编),如果是父类的对象,那就调用父类里面的vptr指针然后找到相应的虚函数,如果是子类的对象,那就调用子类里面的vptr指针然后找到相应的虚函数。当然这样子的过程相比静态多态而言,时间和空间上的开销都多了(这也是为什么内联函数为什么不能声明为虚函数,因为这和内联函数加快执行速度的初衷相矛盾)。

  • 隐藏(重定义)
    实现方式
          隐藏是指派生类的函数屏蔽了与其同名的基类函数,(是在不同作用域内的(一个在父类一个在子类),只要函数名相同,且不构成重写,均称之为重定义)规则如下:
    1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
    2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
    实现原理
          隐藏(重定义)的实现原理跟继承树中函数的寻找方式有关,他会从当前对象的类作用域内开始查找同名的函数,如果没有找到就一直向上查找直到基类为止。如果找到一个同名的函数就停止。这也就说明他不管函数的形参类型或者个数是不是一样,只要函数名一样,他就认为是找到了,如果这时候形参类型或者个数不一致,编译器就会报错。多重继承的查找,如果在同一层内出现一样的函数声明那么编译器会报错不知道调用哪一个函数,这类问题也叫钻石继承问题。钻石问题的解决方案可以通过虚继承来实现,这样就不会存在多个一样的函数声明。

/*****重载**********/
int add(int a,int b)
{
	return (a + b);
}
double add(double a,double b)
{
	return (a + b);
}
int main(void)
{
 int a=1;
 int b=1;
 double c=2.2,d=2.2;
 cout<<"(int)a+b = "<<add(a,b)<<endl;
 cout<<"(double)c+d = "<<add(c,d)<<endl;
 return 0;
}
/******************/
/*******模板***********/
template <class T> 
T add(T a, T b)
{
	return a + b;
}
int main(void)
{
 int a=1,b=1;
 double c=2.2,d=2.2;
 cout<<"(int)a+b = "<<add(a,b)<<endl;
 cout<<"(double)c+d = "<<add(c,d)<<endl;
 return 0;
}
/******************/
/*******重写(覆盖)***********/
class Base
{
public:
virtual void func() //虚函数作为接口
{
	cout << "Base::fun()" << endl;
}
};
class Derived : public Base
{
public:
virtual void func()
{
	cout << "Derived::fun()" << endl;
}
};
int main()
{
	Base* b=new Derived; //使用基类指针指向派生类对象
	b->func(); //动态绑定派生类成员函数func
	Base& rb=*(new Derived); //也可以使用引用指向派生类对象
	rb.func();
}
/******************/
/*******重定义(隐藏)***********/
class Animal {
	public:
	void Eat(){
	cout<<"BaseAnimal Eat()"<<endl;
	}
};
class Wolf:public Animal {
	public:
	void Eat(){
	cout<<"Wolf Eat()"<<endl;
	}
};
int main()
{
	Animal animal;
	animal.Eat();
	Wolf wolf;
	wolf.Eat();
	return 0;
}
/******************/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值