C++---多态

Posted qnbk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++---多态相关的知识,希望对你有一定的参考价值。

多态:多种形态,具体是完成某种行为,当不同的对象去完成时会产生不同的状态

多态的定义及实现

int main()
{
	int i;
	char c;
	cin >> i;
	cin >> c;
	cout << i << endl;
	cout << c << endl;
	//看起来用的是一个函数,但实际不是,这就是多态实现的
	return 0;
}

多态的构成条件

多态是在不同的继承关系的类对象去调用同一函数,产生了不同的行为
在继承中要构成多态有两个条件:

  • 必须通过基类的指针或引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "买票--全票" << endl;
	}
};

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类的虚函数的返回值类型,函数名字,参数列表完全相同),称子类的虚函数重写了基类的虚函数

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "买票--全票" << endl;
	}
};
//virtuel void f() 
//只有类的非静态成员函数可以是虚函数
class Old :public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void Buyticket()
	{
		cout << "买票--免费" << endl;
	}
};
class Stu:public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void Buyticket()
	{
		cout << "买票--半票" << endl;
	}
};

void f(Person& p)
{
	//传不同类型的对象,调用的是不同函数,实现了调用的多种形态
	p.Buyticket();
}
void f(Person*  p)
{
	
	p->Buyticket();
}
int main()
{
	Person p;
	Stu s;
	Old o;
	f(p);
	f(s);
	f(o);
	cout << endl;
	f(&p);
	f(&s);
	f(&o);
	return 0;
}

虚函数重写的三个例外

协变

基类与派生类虚函数返回值类型不同
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

class A{};
class B:public A{};
class Person
{
public:
	virtual A* f(){ 
		cout << "A* Person::f()" << endl;
		return new A;
	};
};

class Stu:public Person
{
public:
	virtual B* f(){
		cout << "B* Stu::f()" << endl;
		return new B;
	};
};
int main()
{
	Person p;
	Stu s;
	Person* ptr;
	ptr = &p;
	ptr->f();
	ptr = &s;
	ptr->f();
	return 0;
}

析构函数的重写

基类与派生类析构函数的名字不同
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Person
{
public:
	//建议把父类析构函数定义为虚函数
	//这样子类的虚函数方便重写父类的虚函数
	virtual ~Person()
	{
		cout << "~Person()" << endl;

	}

};
class Stu:public Person
{
public:
	virtual ~Stu()
	{
		//Stu和Person析构函数的函数名看起来不相同,但是构成虚函数重写
		cout << "~Stun()" << endl;

	}

};
int main()
{
	Person p;
	Stu s;

	return 0;
}

class Person
{
public:
	//建议把父类析构函数定义为虚函数
	//这样子类的虚函数方便重写父类的虚函数
	 ~Person()
	{
		cout << "~Person()" << endl;

	}
	 //子函数没有重写父类
};
class Stu:public Person
{
public:
	 ~Stu()
	{
		//Stu和Person析构函数的函数名看起来不相同,但是构成虚函数重写
		cout << "~Stu()" << endl;

	}

};
int main()
{
	//在普通场景下,父子类的析构函数是否构成重写,不重要,没影响
	//Person p;
	//Stu s;
	Person* p1 = new Person;
	Person* p2 = new Stu;
	//多态行为
	//这里p2指向子类对象,应该调用子类析构函数,但是没有调用
	//那么肯内存泄漏(如果子类函数没做什么清理那就没什么事)
	//如果子类函数有清理那就存在资源泄漏
	delete p1;//p1->析构函数()+ operator delete(p1)
	delete p2;//p2->析构函数()+ operator delete(p2)

	return 0;
}


子类虚函数不加virtual

子类虚函数可以不加virtual 也算完成重写,但是父类虚函数不行。
子类是先继承父类的虚函数,继承后就有virtual属性了,子类只是重写了virtual函数。这样子类加不加virtual都一定完成了重写,保证了delete时一定能实现多态的正常调用析构函数

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "买票--全票" << endl;
	}
};
class Stu: public Person 
{
public:
	//子类虚函数,不写virtual也认为是虚函数,完成重写
	 void Buyticket()
	{
		cout << "买票--半票" << endl;
	}
};
void f(Person& p)
{
	p.Buyticket();
}
int main()
{
	Person p;
	Stu s;
	f(p);
	f(s);

	return 0;
}

C++11 override和final

final

修饰虚函数,表示该虚函数不能被重写

override

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

class car
{
public:
	virtual void Drive(){}
};
class Benz :public car
{
public:
	virtual void Drive()override{ cout << "Benz-快" << endl; }
};
int main()
{

	return 0;
}

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

重载:

  • 两个函数在同一作用域,函数名/参数不同
    重写(覆盖):
  • 两个函数分别在基类,派生类的作用域,函数名/参数/返回值必须相同(协变除外);两个函数必须是虚函数
    重定义(隐藏):两个函数分别在基类和派生类作用域,函数名相同,两个基类和派生类的同名函数不构成重写就是重定义

抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数。**包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。**派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

//抽象类-》不能实例化出对象
//作用:可以更好的表示现实世界中,没有实例对象对应的抽象类型 eg:植物,人,动物...
class car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};
class Benz :public car
{
public:
	//不能实例化出对象,得重写
	virtual void Drive()
	{ 
		cout << "Benz-快" << endl; 
	}
};
class BMW :public car
{
public:
	//不能实例化出对象,得重写
	virtual void Drive()
	{
		cout << "BWM--舒适" << endl;
	}
};
int main()
{
	car* pBen = new Benz;
	pBen->Drive();
	car* pBMW = new BMW;
	pBMW->Drive();

	return 0;
}

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

class Base
{
public:
	virtual void f1()
	{
		cout << "f1()" << endl;
	}
	virtual void f2()
	{
		cout << "f2()" << endl;
	}
	void f3()
	{
		cout << "f3()" << endl;
	}
private:
	int _i = 1;
	char _ch = '\\0';

};
int main()
{
	cout << sizeof(Base) << endl;
	Base bs;
	return 0;
}

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "买票--全票" << endl;
	}

};
class Stu :public Person
{
public:
	virtual void Buyticket()
	{
		cout << "买票--半票" << endl;
	}
};
void f(Person& p)
{
	p.Buyticket();
}
int main()
{
	Person L;
	f(L);
	Stu s;
	f(s);
	return 0;
}

动态绑定与静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
  • 汇编代码能解释什么是静态(编译器)绑定和动态(运行时)绑定。


单继承和多继承关系的虚函数表

单继承中的虚函数表

class Base
{
public:
	virtual void f1()
	{
		cout << "Base::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "Base::f2()" << endl;
	}
private:
	int _b;
};
class Dr :public Base
{
public:
	virtual void f1()
	{
		cout << "Dr::f1()" << endl;
	}
	virtual void f3()
	{
		cout << "Dr::f3()" << endl;
	}
	virtual void f4()
	{
		cout << "Dr::f4()" << endl;
	}
private:
	int _d;
};
int main()
{
	Base ba;
	Dr dr;
	return 0;
}

class Base
{
public:
	virtual void f1()
	{
		cout << "Base::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "Base::f2()" << endl;
	}
private:
	int _b;
};
class Dr :public Base
{
public:
	virtual void f1()
	{
		cout << "Dr::f1()" << endl;
	}
	virtual void f3()
	{
		cout << "Dr::f3()" << endl;
	}
	virtual void f4()
	{
		cout << "Dr::f4()" << endl;
	}
private:
	int _d;
};
//写一个程序打印一个虚表,确认虚表中调用的函数
typedef void(*VF)();
//void PrintVF(VF ptr[])
void PrintVF(VF* ptr)
{
	printf("虚表地址:%p\\n" ,ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("VF[%d]:%p->\\n", i,ptr[i]);
		ptr[i]();
	}
	printf("\\n");
}
int main()
{
	Base ba;
	PrintVF((VF*)(*(int*)&ba));
	Dr dr;
	PrintVF((VF*)(*(int*)&dr));
	return 0;
}

多继承中的虚函数表

class Base1
{
public:
	virtual void f1()
	{
		cout << "Base1::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "Base1::f2()" << endl;
	}
private:
	int _b1;
};
class Base2
{
public:
	virtual void f1()
	{
		cout << "Base2::f1()" << endl;
	}
	virtual void f2()
	{
		cout << "Base2::f2()" << endl;
	}
private:
	int _b2;
};
class Dr :public Base1,public Base2
{
public:
	virtual void f1()
	{
		cout << "Dr::f1()" << endl;
	}
	virtual void f3()
	{
		cout << "Dr::f3()" << endl;
	}

private:
	int _d;
};
typedef void(*VF)();
void PrintVF(VF* ptr)
{
	printf("虚表地址:%p\\n" ,ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("VF[%d]:%p->\\n", i,ptr[i]);
		ptr[i]();
	}
	printf("\\n");
}
int main()
{
	Base1 b1;
	Base2 b2;
	Dr dr;
	PrintVF((VF*)(*(int*)&b1));
	PrintVF((VF*)(*(int*)&b2));
	PrintVF((VF*)(*(int*)&dr));
	PrintVF((VF*)(*(int*)((char*)&dr + sizeof(Base1))));
	return 0;
}

总结

1.inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
2.静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3.构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
4.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:

以上是关于C++---多态的主要内容,如果未能解决你的问题,请参考以下文章

java中封装,继承,多态,接口学习总结

多态性和动态铸造

C/C++编程笔记:C++中的函数重载和浮动

09.面向对象多态的概述及其代码体现

C 中的共享内存代码片段

C 语言实现多态的原理:函数指针