C++多态

🎉文章简介:

🎉本篇文章将 多态,多态原理以及虚函数表打印 相关知识进行分享!
💕如果您觉得文章不错,期待你的一键三连哦!!!

C++多态

1. 多态的概念

1.1什么是多态?

多态是指允许不同类的对象同一个 消息(一般是调用同一函数)做出不同 响应。即同一 操作对于不同对象 ,可以产生不同的结果。而这些不同对象间是通过继承等关系共享相同行为的。

举个栗子:
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

2. 多态的定义及实现

2.1多态的构成条件

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

2.2 虚函数

虚函数:即被virtual 修饰的类成员函数称为虚函数。(在函数前面加)

注意:
1.virtual关键字只能在类里面声明时加,在类外面定义时不能加;
2.static与virtual不能同时使用;静态成员函数不能设置为虚函数;
原因为:因为静态成员函数可以通过类名::成员和函数名直接调用,此时没有this指针,无法拿到虚表,无法实现多态,因此不能设置为虚函数;
3.友元函数不是类成员函数,不能成为虚函数;
4.

2.3虚函数的重写

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

值得注意的是:在重写基类虚函数时,派生类虚函数不加virtual 关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是不建议。

重写与重定义(隐藏)的关系:

1.都发生在继承体中;

2.重写与重定义是两码事,重写是针对多态,重定义即隐藏;

3.重写要求函数完全相同,重定义要求函数名相同即可;

经典例题一:

cpp 复制代码
 class A
   {
   public:
       virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
	   virtual void test(){ func();}
   };
   
   class B : public A
   {
   public:
       void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
   };
   
   int main(int argc ,char* argv[])
   {
       B*p = new B;
       p->test();
       return 0;
   }
 A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

答案:B
解析:
首先,p是指向B类型的指针,调用test的时候,现在B的类里面去找,没有找到,然后去继承的类里面去找,在A里面找到了,A中的func函数的this指针类型是A* ,调用func函数,func函数构成重写,p是一个指向B的指针,所以会去调用在B里重写后的func函数;注意:这里重写用的是A的声明,B的定义;所以答案是B;

经典例题二:

cpp 复制代码
以下程序输出结果是( )
class A
{
public:
  A ():m_iVal(0){test();}
  virtual void func() { std::cout<<m_iVal<<' ';}
  void test(){func();}
public:
  int m_iVal;
};
class B : public A
{
public:
  B(){test();}
  virtual void func()
  {
    ++m_iVal;
    std::cout<<m_iVal<<' ';
  }
};
int main(int argc ,char* argv[])
{
  A*p = new B;
  p->test();

  return 0;

}

解析:

分析:newB时先调用父类A的构造函数,执行test()函数,在调用func()函数,由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0,构造完父类后执行子类构造函数,又调用test函数,然后又执行func(),由于父类已经构造完毕,虚表已经生成,func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1,最终通过父类指针p->test(),也是执行子类的func,所以会增加m_iVal的值,最终打印2, 所以答案为C 0 1 2;

虚函数重写的两个例外:

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

协变的例子

cpp 复制代码
class Person
{
public:
	virtual Person& Buy_Ticket()    //返回的是本对象的引用
	{
		cout << "Person票价:原价" << endl;
	}
};
class student :public Person
{
public:
	virtual student& Buy_Ticket()   //返回的是本对象的引用
	{
		cout << "student票价:七折" << endl;
	}
};
class child :public Person
{
public:
	virtual child& Buy_Ticket()       //返回的是本对象的引用
	{
		cout << "child票价:免费" << endl;
	}
};

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

注意: : 如果构造函数不重写的话,会有内存泄漏等问题;
比如下面例子:
如果没有重写,只会调用两次Person的析构函数,就会导给student开的空间没有被释放,内存泄漏问题;
注意:这里是student继承了Person,所以Person在delete的时候,会调用Person的析构函数,但是student在delete的时候,应该先子后父,先调用stdent的析构函数,再调用Person的析构函数;

cpp 复制代码
class Person
{
public:
	 virtual ~Person()
	  //~Person()
	 {
		cout << "~Person()" << endl;
	 }
};
class student :public Person
{
public:
	virtual ~student()      //重写
	//~student()            //没有重写
	{
		cout << "~student()" << endl;
	}
};
int main()
{
	Person* pp = new Person;
	Person* ps = new student;

	delete pp;
	delete ps;
	
	return 0;
}

没有重写的运行结果:

~Person()

~Person()

重写后的运行结果:

~Person()

~student()

~Person()

结合上面的条件我们举个多态的例子

当我们调用同一个函数时func(同一个操作),传入的不同对象时,所作出的结果不同;

cpp 复制代码
class Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "票价:原价" << endl;
	}
};
class student:public Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "student票价:七折" << endl;
	}
};
class child:public Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "child票价:免费" << endl;
	}
};
void func(Person& P)       //可以是基类的引用
//void func(Person* P)    //可以是基类的指针
{
	//P->Buy_Ticket();
	P.Buy_Ticket();
}
int main()
{
	Person* P;
	child c;
	student s;
	//func(&c);
	//func(&s);    //传指针
	func(c);
	func(s);     //传引用
	
	return 0;
}

运行结果:

Person票价:原价

student票价:七折

child票价:免费

2.4 C++11新增关键字 override 和 final

2.4.1override

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

举个例子:

cpp 复制代码
class Person
{
public:
	//virtual ~Person()
	~Person()                     //把基类的virtual关键字去掉,就会重写失败
	{
		cout << "~Person()" << endl;
	}
};
class student :public Person
{
public:
	 virtual ~student() override          //重写失败,报错
	{
		cout << "~student()" << endl;
	}
};

运行报错:

2.4.1final

**作用一:**修饰类,表示该类为最终类,不能被继承(把一个类的构造函数放在私有也可以)

举个例子:

cpp 复制代码
class Person final 
{
public:
	virtual ~Person()   

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

运行结果:

**作用二:**修饰虚函数,表示该虚函数不能再被重写

cpp 复制代码
class Person 
{
public:
	virtual ~Person() final

	{
		cout << "~Person()" << endl;
	}
};
class student :public Person
{
public:
	virtual ~student() 
	{
		cout << "~student()" << endl;
	}
};
int main()
{
	return 0;
}

运行结果:

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

3. 抽象类(也叫接口类)

3.1纯虚函数:就是在虚函数后面加=0;

例如:

cpp 复制代码
class A
{
	virtual void func()=0
	{
		cout << "你好" << endl;
	}
};

就是一个类里面有纯虚函数的类,就是抽象类,抽象类不能被实例化;
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

例子:

cpp 复制代码
class A
{
	virtual void func() = 0
	{}
protected:
	int _a;
};
class B:public A
{
	B()
	{}
private:
	int _b;
};
int main()
{
	A a;    //不能实例化出对象,会报错
	B b;      //不能实例化出对象,会报错
	return 0;
}

运行结果:

如果想要继承后的派生类能够实例化出对象,则需要重写纯虚函数;

例子:

cpp 复制代码
class A
{
	virtual void func() = 0
	{}
protected:
	int _a;
};
class B:public A
{
	virtual void func()
	{}
private:
	int _b;
};
int main()
{
	B b;     

	return 0;
}

3.2 接口继承和实现继承

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

4.多态的原理

4.1虚函数表

前面学习了一个普通类的大小的计算:是成员变量的大小(要考虑内存对齐)

例如:

cpp 复制代码
class A
{
	void func()
	{}
private:
	int _a;
	int _size;
};

int main()
{
	cout << sizeof(A) << endl;    //运行结果为8
	return 0;
}

这是前面普通类的大小,到了这里,有了继承和多态后,会有所不一样;

例如:

cpp 复制代码
class A
{
	virtual void func()
	{}
private:
	int _a=10;
};

int main()
{
	A a;
	cout << sizeof(A) << endl;      //运行结果为8

	return 0;
}

思考:如果按照前面的计算方法的话,应该就只有int的大小,为4;但是运行结果为8;这是为什么呢?
从监视窗口可以看到:

从上图可以看到A中多了一个指针_vfptr(virtual function table);这个指针叫虚函数表指针,指向一个虚函数表,虚函数表是一个函数指针数组,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。虚函数表里面只有虚函数的地址,没有普通函数的地址;虚函数表在类里面的先后位置与声明顺序有关,多继承与继承顺序有关;

接下类分析派生类中这个表放了些什么呢?

cpp 复制代码
class A
{
	virtual void func1()
	{}
	virtual void func2()
	{}

protected:
	int _a=10;
};
class B:public A
{
	virtual void func1()
	{}
	void func3()
	{}
protected:
	int _b;
};
  1. 派生类对象B中也有一个虚表指针,B对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在这部分里,另一部分是自己的成员。
  2. 基类A对象和派生类B对象虚表是不一样的,这里我们发现Func1完成了重写,所以B的虚表中存的是重写的A::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表;
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr;
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的;
  7. 一个类的不同对象共享一份虚表;
  8. 虚表是在编译生成的;

4.2多态的原理

可以解释最开始买票那里为什么能传谁调用谁了。
代码:

cpp 复制代码
class Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "票价:原价" << endl;
	}
};
class student:public Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "student票价:七折" << endl;
	}
};
class child:public Person
{
public:
	virtual void Buy_Ticket()
	{
		cout << "child票价:免费" << endl;
	}
};
void func(Person* P)    
{
	P->Buy_Ticket();
}
int main()
{
	child c;
	student s;
	
	func(&c);
	func(&s);    
	
	return 0;
}

分析:

调用的时候,他们分别找到了自己对象里面继承的基类的虚函数表,这个虚函数表里面完成了对Person类里面函数Buy_Ticket()的重写,然后调用到对应的函数;
注意:
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
在程序运行的时候才知道具体指向的是哪一个对象,才然后通过虚表调用对应的虚函数,实现多态;

4.3 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载,模板属于编译时多态;
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

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

单继承和多继承关系中,下面我们去关注的是派生类对象虚表模型,因为基类的虚表模型前面我们已经看过了

5.1单继承的虚表

《单继承》

cpp 复制代码
class A
{
	virtual void func1(){}
	void func2(){}
protected:
	int _a=10;
};
class B:public A
{
	virtual void func1(){}
	virtual void func3(){}
protected:
	int _b = 10;
};
int main()
{
	A a;
	B b;

	return 0;
}

从下图发现监视窗口里面B并没有自己的虚函数表,这是编译器的一个bug,但是我们可以通过打印虚函数表来观察;

打印虚函数表:

cpp 复制代码
class A
{
public:
	virtual void func1() { cout << "  A:func1()" << endl; }
	void func2() {}
protected:
	int _a = 10;
};
class B:public A
{
public:
	virtual void func1() { cout << "  B:func1()" << endl; }
	virtual void func3() { cout << "  B:func3()" << endl; }
protected:
	int _b = 20;
};

typedef void(*_vfptr)();            //重命名函数指针为_vfptr

void Print_vftable(_vfptr vfarr[])   //void Print_vftable(_vfptr vfarr[])
{
	cout << "虚表地址" << vfarr << endl;
	for (int i = 0; vfarr[i] != nullptr; i++)    //不为空继续
	{
		printf("[%d]:%p", i, vfarr[i]);
		_vfptr p = vfarr[i];
		p();
	}
	cout << endl;
}
int main()
{
	A a;
	B b;

	_vfptr* ptra = (_vfptr*)(*(int*)&a);
	Print_vftable(ptra);

	_vfptr* ptrb = (_vfptr*)(*(int*)&b);
	Print_vftable(ptrb);

	return 0;
}
//在Vs上面,虚函数表是放在对象的最前面,我们只需要取对象的前4个字节,因为虚函数表是一个函数指针数组,VS下面表结束的位置是nullptr,然后依次取依次打印即可,遇到空结束;
//这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

运行结果:

根据运行结果可以看出,B有自己的虚函数表,为上图的第二个表;

5.2 多继承中的虚函数表

cpp 复制代码
class A
{
public:
	virtual void func1(){ cout << "  A:func1()" << endl; }
	void func2(){}
protected:
	int _a=10;
};
class B
{
public:
	virtual void func3() { cout << "  B:func3()" << endl; }
	virtual void func4(){ cout << "  B:func4()" << endl; }
protected:
	int _b = 20;
};
class C:public A, public B
{
public:
	virtual void func1() { cout << "  C:func1()" << endl; }
	virtual void func3() { cout << "  C:func3()" << endl; }
	virtual void func5() { cout << "  C:func5()" << endl; }
protected:
	int _c=30;
};
typedef void(*_vfptr)();    //重命名函数指针为_vfptr

void Print_vftable(_vfptr* vfarr)   //void Print_vftable(_vfptr vfarr[])
{
	cout << "虚表地址" << vfarr << endl;
	for (int i = 0; vfarr[i]!=nullptr; i++)    //不为空继续
	{
		printf("[%d]:%p",i, vfarr[i]);   //%d 打印第几个虚函数   %p打印虚函数地址
		_vfptr p = vfarr[i];        //拿到函数的指针
		p();                //调用函数
	}
	cout << endl;
}
int main()
{
	A a;
	B b;
	
	_vfptr* ptra = (_vfptr* )(*(int*)(&a));   //将地址强转int*,再解引用取值,拿到的就是虚表的前4个字节,在强转为函数指针类型
	Print_vftable(ptra);

	_vfptr* ptrb = (_vfptr*)(*(int*)(&b));  //注意,32位机器下是4字节,如果是64位的话就应该是8字节了,就应该强转long long;
	Print_vftable(ptrb);

	return 0;
}

上述代码中,类A中有一个虚函数,类B中有两个虚函数,类C中重写了A中func1,重写了B中func3,然后自己有一个虚函数func5,根据我们打印了C中的虚函数表,发现有两个虚函数表,由下面运行结果可以发现,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中;
运行结果:

本章完~

相关推荐
长弓聊编程8 分钟前
Linux系统使用valgrind分析C++程序内存资源使用情况
linux·c++
陌小呆^O^11 分钟前
Cmakelist.txt之win-c-udp-client
c语言·开发语言·udp
cherub.15 分钟前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
I_Am_Me_27 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
暮色_年华29 分钟前
Modern Effective C++item 9:优先考虑别名声明而非typedef
c++
重生之我是数学王子37 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手39 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z43 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹1 小时前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE1 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang