C++多态详解

本篇博客中的编译环境:vs2017,x86程序。

1.多态概念

同一动作的不同状态,当去完成某个行为时,由不同对象去完成时会产生不同的状态。

例如:

C++多态详解_第1张图片

2.多态的实现

2.1多态的构成条件

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

构成多态时,这两个条件缺一不可。

2.2 虚函数及虚函数重写

virtual修饰的函数成为虚函数。

虚函数的重写:

  1. 前提:必须是在继承体系中,派生类对基类的虚函数进行重写;
  2. 派生类重写基类虚函数:派生类虚函数原型必须要与基类虚函数原型保持一致(参数列表、返回值、函数名字);
  3. 派生类虚函数可以不加virtual关键字,一般情况下都加上;
  4. 派生类和基类虚函数的访问权限可以不同。

例如:

class People//基类
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买全票" << endl;
	}
};

class Student : public People//派生类,学生
{
     
	virtual void BuyNote()
	{
     
		cout << "购买半票" << endl;
	}
};

class Child : public People//派生类,child
{
     
	virtual void BuyNote()
	{
     
		cout << "免费" << endl;
	}
};

void Test(People& D)//测试
{
     
	D.BuyNote();
}

int main()
{
     
	People A;
	Student B;
	Child C;
	Test(A);
	Test(B);
	Test(C);
	return 0;
}

上述代码的测试条件:

  1. 采用基类引用调用虚函数
  2. 调用的函数都为虚函数
  3. 派生类和基类的虚函数访问权限不同,基类为public,派生类为private

结果:

C++多态详解_第2张图片

虚函数重写的两个例外:

  1. 协变(基类与派生类虚函数返回值类型不同)
    基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。

例:

class A
{
     };

class B : public A
{
     };

class C
{
     
public:
	virtual A* func()
	{
     
		return new A;
	}
};

class D
{
     
public:
	virtual B* func()
	{
     
		return new B;
	}
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类析构函数为虚函数,派生类析构函数给出之后,无论是否增加virtual关键字,都对基类析构函数进行了重写;

注意:在继承体系中,最好将基类中的析构函数设计为虚函数,如果子类涉及到资源管理,一定要将基类析构函数设计为虚函数。

2.3C++11中 override和final

  1. override:检查派生类虚函数是否重写了基类的虚函数,如果没有则编译报错;
  2. final:修饰虚函数,表明该虚函数不能被继承(不能被重写)

2.4 函数重载、重写、同名隐藏(重定义)的对比

C++多态详解_第3张图片

3.抽象类

在虚函数后面加上 = 0,该虚函数就为纯虚函数。包含纯虚函数的类叫做抽象类(也称为接口类),抽象类不能实例化出对象。 派生类继承之后也不能实例化出对象,只有重写虚函数,派生类才能实例化为对象。纯虚函数规范了派生类必须重写,接口继承。

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

4.多态原理

4.1对象模型

如果类中包含虚函数,用户没有显式定义构造函数,编译器会给用户生成一份默认的构造函数,在生成构造函数中会填充对象前4个字节的内容;如果用户显式定义了构造函数,编译器会修改用户定义的构造函数,增加一条给对象前4个字节赋值的语句

什么情况下编译器会给类生成默认的构造函数?

  1. A类中包含B类的对象,A类没有显式定义构造函数,B类带有无参或者全缺省的构造函数;
  2. B类继承A类,A类没有显式定义构造函数,B类带有无参或者全缺省的构造函数;
  3. 在虚拟继承中,编译器一定会给子类生成构造函数;
  4. 类中如果包含虚函数,编译器一定会给该类生成构造函数。

编译器填充的前4个字节为虚表指针。

例:

class People
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买全票" << endl;
	}
private:
	int _b;
};

int main()
{
     
	People A;
	return 0;
}

vs2017中,观察其对象模型为:

C++多态详解_第4张图片

即:

C++多态详解_第5张图片

在派生类中:

  1. 如果派生类没有对基类的虚函数进行重写,也没有新增派生类的虚函数,则派生类和基类虚表中内容完全一致;但是派生类和基类使用的不是同一张虚表。
class People
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买全票" << endl;
	}
private:
	int _b;
};

class Student : public People
{
     
private:
	int _c;
};
int main()
{
     
	People A;
	Student B;
	return 0;
}

C++多态详解_第6张图片

C++多态详解_第7张图片
2. 如果派生类重写了基类某个虚函数,编译器就会将派生类虚函数替换相同偏移量位置的基类虚函数。

class People
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买全票" << endl;
	}
	virtual void BuyAirTicket()
	{
     
		cout << "购买全票" << endl;
	}
	virtual void BuyFerryTicket()
	{
     
		cout << "购买全票" << endl;
	}
private:
	int _b;
};

class Student : public People
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买半票" << endl;
	}
	virtual void BuyAirTicket()
	{
     
		cout << "少量优惠" << endl;
	}
private:
	int _c;
};
int main()
{
     
	People A;
	Student B;
	return 0;
}

C++多态详解_第8张图片
C++多态详解_第9张图片
3. 若派生类中有新增虚函数,vs2017的监视窗口中,虚表不会显示新增的虚函数。

class People
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买全票" << endl;
	}
	virtual void BuyAirTicket()
	{
     
		cout << "购买全票" << endl;
	}
	virtual void BuyFerryTicket()
	{
     
		cout << "购买全票" << endl;
	}
private:
	int _b;
};

class Student : public People
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买半票" << endl;
	}
	virtual void BuyAirTicket()
	{
     
		cout << "少量优惠" << endl;
	}
	virtual void BuyAmusementParkTicket()
	{
     
		cout << "游乐园购买半票" << endl;
	}
private:
	int _c;
};

C++多态详解_第10张图片
其实新增的虚函数也在该虚表中,只是编译器的监视窗口隐藏了该新增的虚函数。

打印虚表中的函数:

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vfptr[])
{
     
	for (int i = 0; vfptr[i] != nullptr; ++i)
	{
     
		cout << vfptr[i] << " ";
		vfptr[i]();
	}
}
int main()
{
     
	People A;
	Student B;

	VFPTR* vftable = (VFPTR*)(*(int*)(&B));//取到了前4个字节中的内容,整形内容,强转为VFPTR*
	PrintVTable(vftable);
	return 0;
}

C++多态详解_第11张图片

4.2虚表

虚表中放置的是虚函数的入口地址,如果能拿到虚表中的内容,即拿到了虚函数的入口地址,则可以将该函数调用起来。

基类虚表构建过程:编译期间在编译期间,按照虚函数在类中声明的先后次序依次添加到虚表中;
派生类虚表的构建过程

  1. 将基类虚表中内容拷贝一份放置到子类虚表中;
  2. 如果子类重写了基类的某个虚函数,则使用子类自己的虚函数替换相同偏移量位置的基类虚函数地址;
  3. 对于子类新增的虚函数,按照在类中声明次序,依次放在虚表的后面。

注意:

  1. 虚表是在编译期间生成的;
  2. 派生类和基类不会共享同一张虚表,即使虚表中放在相同的虚函数地址;
  3. 相同类的不同对象共享同一张虚表;
  4. 虚表中的内容不可修改。(只读)

4.3虚函数调用原理

虚函数调用

  1. 获取对象地址,然后从对象中获取虚表地址;
  2. 传递参数&this指针;
  3. 从虚表中获取对应虚函数的入口地址;
  4. 调用该虚函数。

满足多态以后的函数调用,不是在编译期间确定的,而是在运行起来以后到对象中去找的,不满足多态的函数调用是在编译时确定的。

5.多继承下多态

class People
{
     
public:
	virtual void BuyNote()
	{
     
		cout << "购买全票" << endl;
	}
	virtual void BuyAirTicket()
	{
     
		cout << "购买全票" << endl;
	}
	virtual void BuyFerryTicket()
	{
     
		cout << "购买全票" << endl;
	}
private:
	int _b;
};

class Student
{
     
public:
	virtual void BuyAirTicket()
	{
     
		cout << "少量优惠" << endl;
	}
	virtual void BuyAmusementParkTicket()
	{
     
		cout << "游乐园购买半票" << endl;
	}
private:
	int _c;
};

class Child : public People, public Student
{
     
public:
	virtual void BuyTrainTicket()
	{
     
		cout << "免费" << endl;
	}
private:
	int _d;
};

int main()
{
     
	People A;
	Student B;
	Child C;
	return 0;
}

C++多态详解_第12张图片
派生类新增的虚函数放在了第一个继承基类的虚函数表中。其余规则,遵守单继承时虚表规则。

6.菱形继承下多态

class A
{
     
public:
	virtual void fun1()
	{
     }
	virtual void fun2()
	{
     }
};

class B : public A
{
     
public:
	virtual void fun1()
	{
     }
	virtual void fun3()
	{
     }
};

class C : public A
{
     
public:
	virtual void fun2()
	{
     }
	virtual void fun4()
	{
     }
};

class D : public B, public C
{
     
public:
	virtual void fun4()
	{
     }
	virtual void fun5()
	{
     }
};

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vfptr[])
{
     
	for (int i = 0; vfptr[i] != nullptr; ++i)
	{
     
		cout << vfptr[i] << endl;
	}
}

int main()
{
     
	D d;//那么在第一个虚表中,应该存在4个虚函数
	VFPTR* vftable = (VFPTR*)(*(int*)(&d));
	PrintVTable(vftable);
	return 0;
}

C++多态详解_第13张图片
派生类菱形继承下的多态,派生类的新增虚函数,存在与第一个继承的虚表下面。菱形继承在多态中,仍会产生二义性问题。

7.菱形虚拟继承下多态

class A
{
     
public:
	virtual void fun1()
	{
     }
	virtual void fun2()
	{
     }
};

class B : virtual public A
{
     
public:
	virtual void fun1()
	{
     }
	virtual void fun3()
	{
     }
};

class C : virtual public A
{
     
public:
	virtual void fun2()
	{
     }
	virtual void fun4()
	{
     }
};

class D : public B, public C
{
     
public:
	virtual void fun4()
	{
     }
	virtual void fun5()
	{
     }
};

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vfptr[])
{
     
	for (int i = 0; vfptr[i] != nullptr; ++i)
	{
     
		cout << vfptr[i] << endl;
	}
}

int main()
{
     
	D d;//那么在第一个虚表中,应该存在2个虚函数地址
	VFPTR* vftable = (VFPTR*)(*(int*)(&d));
	PrintVTable(vftable);
	return 0;
}

.C++多态详解_第14张图片
派生类菱形继承下的多态,派生类的新增虚函数,存在与第一个继承的虚表下面。菱形虚拟继承在多态中,不会有二义性问题。

静态成员函数不能是虚函数。

你可能感兴趣的:(C/C++学习,多态,指针,编译器,抽象类,C++)