C++多态

目录

1、多态的概念

2、多态的定义与实现

多态的构成条件

 虚函数

 虚函数的重写

虚函数的重写有两个特殊情况:

C++11中的override和final

重载、隐藏(重定义)、重写(覆盖)的区别

3、抽象类

概念

 接口继承和实现继承

4、多态的原理

虚函数表

多态原理

动态绑定和静态绑定


1、多态的概念

多态意思就是一个行为的多种状态。具体讲就是当不同对象去完成同一行为时呈现不同结果。

举例说明:动物园买门票,大人、小孩的票价时不同的。还有在手机软件上订房时,不同人通过同一软件订同一种房间的时候价格可能是不同的。买票和订房这一行为就是一种多态性为。

2、多态的定义与实现

多态的构成条件

多态是在继承关系下的类对象去调用同一个函数,产生了不同的行为。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
};

class Child :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "儿童票" << endl;
	}
};

//必须通过父类的指针或者引用才能去调用虚函数
void Func(Person& p)
{
	p.BuyTicket();
}
void Func(Person* p)
{
	p->BuyTicket();
}

int main()
{
	Person p;
	Child c;
	Person* ptr;
	ptr = &p;//指向父类对象,调的就是父类虚函数
	Func(ptr);//通过指针去调用

	ptr = &c;//指向子类对象,调的就是子类重写的虚函数
	Func(ptr);//通过指针去调用

	Func(p);//通过引用去调用
	Func(c);//通过引用去调用

	return 0;
}

 虚函数

虚函数:被virtual修饰的成员函数就是虚函数

class Person
{
public:
    //BuyTicket就是虚函数
	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
};

 虚函数的重写

虚函数的重写也叫做覆盖,注意和重载、继承里的隐藏区分开来,派生类中会有一个和基类完全一样的虚函数,派生类的虚函数的函数名、返回值、参数列表与基类虚函数一致,函数的实现部分需要派生类自己写,这种行为就称作重写了基类的虚函数。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
};

class Child :public Person
{
public:
    //不加virtual也可以,不过不建议这样写,不易于区分
    //void BuyTicket()
	virtual void BuyTicket()
	{
		cout << "儿童票" << endl;
	}
};

虚函数的重写有两个特殊情况:

1、协变(基类和派生类的虚函数返回值不同)

class Person
{
public:
	//virtual Person* BuyTicket()
	virtual Person& BuyTicket()
	{
		cout << "成人票" << endl;
		return *this;
	}
};

class Child :public Person
{
public:
	//virtual Person* BuyTicket()
	//virtual Child* BuyTicket()
	//virtual Person& BuyTicket()
	virtual Child& BuyTicket()
	{
		cout << "儿童票" << endl;
		return *this;
	}
};

派生类重写基类虚函数时,返回值可以是指针或者引用。父类虚函数返回值是父类指针的话,子类重写后返回值可以是子类指针或父类指针;父类虚函数返回值是父类引用时,子类重写后返回值可以是子类引用或者父类引用注意指针和引用返回值不同

2、析构函数的重写

如果父类的析构函数被设置成虚函数,那么子类的虚函数无论加不加virtual,都与父类的析构函数构成重写。因为编译器最后都对析构函数的名称做了特殊处理,编译后的析构函数名都是destructor

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

class Student : public Person 
{
public:
	virtual ~Student() 
	{ 
		cout << "~Student()" << endl; 
	}
};

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	//p2指向子类对象,子类对象析构自己的时候
	//会先调用自己的析构函数再去调用父类的析构函数
	delete p2;

	return 0;
}

C++11中的override和final

1override,检查派生类是否重写了基类的虚函数,如果没有重写就会报错

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
};

class Child :public Person
{
public:
	//不加override可以看到函数名写错了,但是编译不报错
	//加上override写错后编译会不通过
	//virtual void BuyTicketl()//override
	virtual void BuyTicket()override
	{
		cout << "儿童票" << endl;
	}
};

2final,修饰虚函数,标识该虚函数不能被继承

class Person
{
public:
	//加上final后派生类重写虚函数会报错无法重写
	virtual void BuyTicket()final
	{
		cout << "成人票" << endl;
	}
};

class Child :public Person
{
public:
	virtual void BuyTicket()//err
	{
		cout << "儿童票" << endl;
	}
};

重载、隐藏(重定义)、重写(覆盖)的区别

重载:

  • 重载前提是指函数名相同,参数列表的个数或者参数顺序不同或参数类型不同,与返回值无关。
  • 重载必须在同一作用域。

隐藏:

  • 通常是派生类和基类有同名成员,继承后构成隐藏。
  • 不在同一作用域,分别位于派生类和基类中。
  • 只要名字相同就构成隐藏。
  • 参数不同,不论有无virtual,基类的函数都会被隐藏。
  • 参数相同,基类函数没有virtual,基类函数被隐藏;有virtual就是多态了。

重写:

  • 通常是派生类重写基类虚函数发生的。
  • 不在同一作用域,分别位于派生类和基类中。
  • 参数相同,函数名相同,返回值相同(协变例外)。
  • 基类虚函数必须用virtual修饰,虚函数不能是静态的,所以不能用static修饰。
  • 重写函数的访问权限可以不同,例如基类的虚函数是private,派生类重写的时候写成public也是可以的。

3、抽象类

概念

在虚函数后面加上=0,这个函数就变成了纯虚函数,纯虚函数没有实现部分。含有纯虚函数的类称为抽象类,抽象类不能实例化对象。派生类如果继承后不重写纯虚函数的话,也同样会成为抽象类。纯 虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Person
{
public:
	virtual void BuyTicket() = 0;
};
class Child :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "儿童票" << endl;
	}
};

int main()
{
	//Person p;//err
	Child ch;

	return 0;
}

 接口继承和实现继承

普通函数的继承就是实现继承,派生类继承了基类的函数,且只继承了函数的实现。

虚函数的继承就是接口继承,派生类继承的只是基类虚函数的接口,目的为了重写实现多态,体现的就是接口继承。

4、多态的原理

虚函数表

上一篇博客中提到的虚基表主要是菱形继承里出现的,不要和这里的虚函数表混淆。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
private:
	int _pAge = 18;
};

class Child :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "儿童票" << endl;
	}
private:
	int _cAge = 12;
};

int main()
{
	Person p;
	Child ch;

	return 0;
}

 可以看到基类对象中不只有_pAge这个成员变量,还有一个_vfptr,这里的v标识virtual,f标识function,所以_vfptr是虚函数表指针,实际就是一个函数指针数组,里面每个数据都是函数指针,指向虚函数。每个含有虚函数的类中至少都有一个虚函数表指针。

说明:

  1. 派生类的虚表生成:①先将基类中的虚表内容拷贝一份到派生类虚表中②如果派生类重写了基类的某个虚函数,那么就用重写的覆盖虚表中基类的那个虚函数③如果派生类自己也有一个新增加的虚函数,那么派生类自己的虚函数按声明顺序存入继承的虚函数表后面。
  2. 假设基类有一个Func()函数,该函数不是虚函数,派生类继承后不会将该函数放在虚函数表中。
  3. 虚函数表本质是函数指针数组,在内存中最后面放了一个nullptr。

多态原理

前面说到多态的函数调用必须通过父类的指针或者引用才能调用,那么为什么呢?

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
private:
	int _pAge = 18;
};

class Child :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "儿童票" << endl;
	}
private:
	int _cAge = 12;
};

void Func(Person* p)
{
	p->BuyTicket();
}

int main()
{
	Person p;
	Child ch;
	Func(&p);
	Func(&ch);

	return 0;
}
  1. 通过学习上面的虚函数表,大致就能知道,当Func传过去的是Person对象p时,会去p对象里存的虚函数表里找BuyTicket()函数,所以调用的就是基类的ButTicket(),子类同理。
  2. 之所以Func()参数不用子类指针或者引用,也是因为之前说过的切割、切片,父类不能给子类赋值,而子类可以给父类赋值。所以不能用子类做参数。
  3. Func函数不能使用父类对象做参数是因为实参传给形参会发生拷贝,这样就找不到原有的虚表,通过指针或者引用就没有这种问题了。
  4. 通过上面的分析可以知道,多态的函数调用是一种运行时调用,而不是编译时就确定的。

动态绑定和静态绑定

  1. 静态绑定意味着在编译时就确定了程序的行为,又称为静态多态;函数重载就是的静态绑定。
  2. 动态绑定就是在程序运行过程中确定程序的行为,又称为动态多态。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值