C++之多态

1. 综述

问题抛出: 如果子类定义了与父类中原型相同的函数时会发生什么?

函数重写:在子类中定义与父类中原型相同的函数,函数重写只发生在父类与子类之间。

父类中被重写的函数依然会继承给子类,默认情况下子类中重写的函数将隐藏父类中的函数,通过作用域分辨符::可以访问到父类中被隐藏的函数。

1.1 类型兼容性原则遇上函数重写--引出面向对象新需求

#include <iostream>
#include <stdlib.h>
using namespace std;

class Parent
{
public:
	Parent(int a)
	{
		this->a = a;
		cout << "Parent : a = " << a << endl;
	}

	void print()
	{
		cout << "Parent print: a = " << a << endl;
	}

private:
	int a;
};

class Child : public Parent
{
public:
	Child(int b) : Parent(10)
	{
		this->b = b;
		cout << "Parent : b = " << b << endl;
	}

	void print()
	{
		cout << "Child print: b = " << b << endl;
	}

private:
	int b;
};

void howtoPrint1(Parent *base)
{
	base->print();
}

void howtoPrint2(Parent &base)
{
	base.print();
}

void main()
{
	Parent *base = NULL;
	Parent p1(20);
	Child  c1(30);

	/* 面向对象新需求的提出:
	 * 如果传一个父类对象,执行父类的print函数;
	 * 如果传一个子类对象,执行子类的print函数.
	 */

	/* 如下场景始终执行的是父类的print函数 */ 
	{
		/* 场景一 : 指针 */ 
		base = &p1;
		base->print(); // 执行父类的打印函数

		base = &c1;    // 父类指针指向子类对象
		base->print(); // 发现执行的父类的打印函数--因此提出:面向对象新需求
	}

	{
		/* 场景二 : 引用 */
		Parent &base2 = p1;
		base2.print();

		Parent &base3 = c1;	// base3是c1的别名
		base3.print(); // 发现还是执行的是父类的打印函数
	}

	{
		/* 场景三 : 函数调用 */
		howtoPrint1(&p1);
		howtoPrint1(&c1);

		howtoPrint2(p1);
		howtoPrint2(c1);
	}

	system("pause");
	return;
}

1.2 面向对象新需求

编译器的做法不是我们所期望的,我们期望的是:

  • 根据实际的对象类型来判断重写函数的调用
  • 如果父类指针指向的是父类对象则调用父类中定义的函数
  • 如果父类指针指向的是子类对象则调用子类中定义的重写函数

面向对象中的多态

1.3 解决方案

  • C++ 通过 virtual 关键字对多态进行了支持
  • 使用 virtual 声明的函数被重写后即可展现多态特性

注:这才是 virtual 真正的应用场景,而不是虚继承中。

2. 多态

2.1 多态实例

#include <iostream>
#include <stdlib.h>
using namespace std;

class HzeoFighter
{
public:
	virtual int power()
	{
		return 10;
	}
};

class EnemyFighter
{
public:
	int attack()
	{
		return 15;
	}
};

class AdvHzeoFighter : public HzeoFighter
{
public:
	int power()
	{
		return 20;
	}
};

// 使用多态的方法
void playobj(HzeoFighter *hf, EnemyFighter *ef)
{
	// hf->power()将会有多态发生
	if (hf->power() > ef->attack())
	{
		cout << "主角赢" << endl;
	}
	else
	{
		cout << "主角输" << endl;
	}
}

// 使用多态的方法
void main()
{
	HzeoFighter    hf;
	AdvHzeoFighter advhf;
	EnemyFighter   ef;

	playobj(&hf, &ef);
	playobj(&advhf, &ef);
}

void main01()
{
	HzeoFighter    hf;
	AdvHzeoFighter advhf;
	EnemyFighter   ef;

    // 这不是使用多态的案例
	if (hf.power() > ef.attack())
	{
		cout << "主角赢" << endl;
	}
	else
	{
		cout << "主角输" << endl;
	}

	if (advhf.power() > ef.attack())
	{
		cout << "主角赢" << endl;
	}
	else
	{
		cout << "主角输" << endl;
	}

	system("pause");
	return;
}

2.2 多态成立条件

2.2.1 间接赋值成立的三个条件

  1. 两个变量(通常一个实参,一个形参)
  2. 建立关系,实参取地址赋给形参
  3. *p 形参去间接修改实参的值

2.2.2 多态成立的三个条件(是面向对象领域的一个标准)

  1. 要有继承
  2. 要有虚函数重写(即用 virtual 修饰)
  3. 要有父类指针(或父类引用)指向子类对象

注:多态是设计模式的基础,是框架的基础。

2.3 多态的理论基础

2.3.1 静态联编和动态联编

  1. 联编是指一个程序模块、代码之间互相关联的过程。
  2. 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
  3. 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch 和 if 语句是动态联编的例子。

2.3.2 理论联系实际

  1. C++ 和 C 相同,是静态编译型语言。
  2. 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
  3. 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象。从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

3. 多态原理探究

3.1 多态理论知识

  • 当类中声明虚函数时,编译器会在类中生成一个虚函数表。
  • 虚函数表是一个存储类成员函数指针的数据结构。
  • 虚函数表示由编译器自动生成和维护的。
  • virtual 成员函数会被编译器放入虚函数表中。
  • 当存在虚函数时,每个对象都有一个指向虚函数表的指针(C++ 编译器给父类对象、子类对象提前布局 vptr 指针;当进行 howtoPrint(Parent *base) 函数时,C++ 编译器不需要区分子类对象或者父类对象,只需要在 base 指针中,找 vptr 指针即可)。
  • vptr 一般作为类对象的第一个成员。

3.2 多态的实现原理

3.2.1 多态实现原理图例

多态实现原理图1


说明:通过虚函数表指针 vptr 调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了 应该调用的函数。在效率上,虚函数的效率要低很多。

多态实现原理图2

3.2.2 多态原理的探究例子

#include <iostream>
#include <stdlib.h>
using namespace std;

/* 多态成立的三个条件:
 * 1、要有继承
 * 2、要有虚函数重写
 * 3、要有父类指针(或父类引用)指向子类对象
 */

class Parent
{
public:
	Parent(int a = 0)
	{
		this->a = a;
		cout << "Parent 执行" << endl;
	}

	virtual void print() // 1、为实现多态,可能动手脚的地方
	{
		cout << "我是你爹" << endl;
	}

private:
	int a;
};

class Child : public Parent
{
public:
	Child(int a = 0, int b = 0) : Parent(a)
	{
		this->b = b;
		cout << "Chiild 执行" << endl;
	}

	void print()
	{
		cout << "我是儿子" << endl;
	}

private:
	int b;

};

void howtoplay(Parent *base)
{
	base->print();   // 2、动手脚
	// 效果:传来 子类对象 执行子类的 print 函数;传来父类对象 执行父类的 print 函数
	// C++编译器根本不需要区分是 子类对象 还是 父类对象
	// 父类对象和子类对象都有一个 vptr 指针,根据该指针去找 虚函数表(每个对象都一个虚函数表),
	// 最终找到函数的入口地址.
	// 因此 迟绑定(运行时,才去判断调用的函数)
}
  
void main()
{
    /* 3、动手脚  提前布局
     * 用类定义对象的时候 C++ 编译器会在对象中添加一个 vptr 指针,该指针指向虚函数表
     */
    Parent p1; 
    Child c1;  // 子类中也有一个vptr指针

    howtoplay(&p1);
    howtoplay(&c1);

    system("pause");
    return; 
} 

3.2.3 如何证明 vptr 指针存在

#include <iostream>
using namespace std;

class A
{
public:
    void printf()
    {
        cout << "aaa" << endl;
    }
protected:
private:
    int a;
};

class B
{
public:
    virtual void printf()
    {
        cout << "aaa" << endl;
    }
protected:
private:
    int a;
};

void main()
{
    // 加上 virtual 关键字 c++ 编译器会增加一个指向虚函数表的指针
    printf("sizeof(a):%d, sizeof(b):%d 
", sizeof(A), sizeof(B));
    cout << "hello..." << endl;
    system("pause");
    return;
}

3.2.4 构造函数中能调用虚函数,实现多态吗?

注:这句话的意思是:定义一个子类对象,在子类对象的父类里面调用一个虚函数,问能产生多态吗?

  1. 对象中的 vptr 指针是什么时候被初始化?
    对象在创建的时候,由编译器对 vptr 指针进行初始化,只有当对象的构造完全结束后 vptr 的指向才最终确定。父类对象的 vptr 指向父类的虚函数表,子类对象的 vptr 指向子类的虚函数表。

  2. 分析过程,如下图

构造函数中不能实现多态,如下例子:

#include <iostream>
#include <stdlib.h>
using namespace std;

/* 构造函数调用虚函数,能发生多态吗?
 * 意思是:定义子类对象时,在子类对象的父类里面能执行虚函数,能实现多态吗?
 */

class Parent
{
public:
	Parent(int a = 0)
	{
		this->a = a;
		print();	// 在父类函数中执行虚函数
		cout << "Parent 执行" << endl;
	}

	virtual void print() 
	{
		cout << "我是你爹" << endl;
	}

private:
	int a;
};

class Child : public Parent
{
public:
	Child(int a = 0, int b = 0) : Parent(a)
	{
		this->b = b;
		cout << "Chiild 执行" << endl;
	}

	void print()
	{
		cout << "我是儿子" << endl;
	}

private:
	int b;

};

void howtoplay(Parent *base)
{
	base->print();   
}

void main()
{
	Child c1;    /* 定义子类对象,子类对象在创建中会调用父类的构造函数,那么在父类对象中
			   * 调用虚函数print,能发生多态吗
			   * 运行,调试发现:虽然定义的是子类对象,但是仍然执行的是父类的print函数。
			   * 因此,在此场景下,不能发生多态。
			   */

	system("pause");
	return;
}

4. 多态相关知识

4.1 重载与重写

函数重载(属于静态联编):

  • 必须在同一个类中进行
  • 子类无法重载父类的函数,父类同名函数被将被名称覆盖
  • 重载是在编译期间根据参数类型和个数决定函数调用

函数重写:

  • 必须发生在父类和子类之间
  • 并且父类与子类中的函数必须有完全相同的原型
  • 使用 vitual 声明之后能够产生多态(如果不使用 virtual,那叫重定义)

注:多态是在运行期间根据具体对象的类型决定函数调用的。

4.2 类成员函数与虚函数

问: 是否可将类的每个成员函数都声明为虚函数,为什么?
答:虽然可以都声明为虚函数,但不建议这样做。因为通过虚函数表指针 vptr 调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能真正确定应该调用的函数。而普通成员函数在编译时就确定了调用的函数。所以在效率上,虚函数的效率要低很多。因此,出于效率的考虑,没有必要将所有的成员函数都定义为虚函数。

4.3 为什么要定义虚析构函数?

在什么情况下应当声明虚函数?

  • 构造函数不是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的虚函数。
  • 析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象。

普通析构函数在删除动态派生类对象的调用情况示例图:

问:为什么要定义虚析构函数?
答:想通过父类指针把所有子类对象的析构函数都执行一遍,释放所有的子类资源。

虚析构函数案例:

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <stdlib.h>
using namespace std;

class A
{
public:
	A()
	{
		p = new char[20];
		strcpy(p, "obja");
		cout << "A执行" << endl;
	}

	virtual ~A()
	{
		if (p != NULL)
		{
			delete[] p;
			p = NULL;
			cout << "~A执行" << endl;
		}
	}

private:
	char *p;
};

class B : public A
{
public:
	B()
	{
		p = new char[20];
		strcpy(p, "objb");
		cout << "B执行" << endl;
	}

	~B()
	{
		if (p != NULL)
		{
			delete[] p;
			p = NULL;
			cout << "~B执行" << endl;
		}
	}

private:
	char *p;
};

class C : public B
{
public:
	C()
	{
		p = new char[20];
		strcpy(p, "objc");
		cout << "C执行" << endl;
	}

	~C()
	{
		if (p != NULL)
		{
			delete[] p;
			p = NULL;
			cout << "~C执行" << endl;
		}
	}

private:
	char *p;
};

void houtodelete(A *base)
{
	delete base;
}

void main()
{
	C *myC = new C;
	houtodelete(myC);

	system("pause");
}

4.4 父类指针和子类指针的步长

  1. 铁律1: 指针也是一种数据结构,C++ 类对象的指针 p++/p--,仍然可用;
  2. 指针运算是按照指针所指的类型进行的:
    p++ 等价于 p = p + 1,即 p = (unsigned int)basep + sizeof(*p) 步长
  3. 结论:父类 p++ 与子类 p++ 步长不同,不要混搭,不要用父类指针 ++ 方式操作数组。

父类指针和子类指针的步长不一样的示例:

#include <iostream>
#include <stdlib.h>
using namespace std;

/* 构造函数调用虚函数,能发生多态吗?
* 意思是:定义子类对象时,在子类对象的父类里面能执行虚函数,能实现多态吗?
*/

class Parent
{
public:
	Parent(int a = 0)
	{
		this->a = a;
		cout << "Parent 执行" << endl;
	}

	virtual void print()
	{
		cout << "我是你爹" << endl;
	}

private:
	int a;
};

// 成功,一次偶然的成功,比必然的失败更可怕
class Child : public Parent
{
public:
	Child(int b = 0) : Parent(0)
	{
		//this->b = b;
		cout << "Chiild 执行" << endl;
	}

	virtual void print()
	{
		cout << "我是儿子" << endl;
	}

private:
	//int b;    // 把该语句的注释撤销前,则此时父类和子类的指针的步长一样,指针++/--不会
			    // 使程序出现core dump;但是撤销后,则会使子类的步长与父类的步长不一致了,
			    // 再++/--则会出现core dump现象。

};

void howtoplay(Parent *base)
{
	base->print();
}

void main()
{
	Parent *pP = NULL;
	Child  *pC = NULL;

	Child array[] = { Child(1), Child(2), Child(3) };
	pP = array;
	pC = array;

	pP->print();
	pC->print();	// 多态发生

	pP++;
	pC++;
	pP->print();
	pC->print();	// 多态发生

	pP++;
	pC++;
	pP->print();
	pC->print();	// 多态发生
	system("pause");
	return;
}

上述例子中两个类的内存分布:

原文地址:https://www.cnblogs.com/jimodetiantang/p/9049355.html