C++学习笔记-多态

守給你的承諾、 提交于 2020-03-30 17:23:37

多态作为面向对象的重要概念,在如何一门面向对象编程语言中都有着举足轻重的作用,学习多态,有助于更好地理多态的行为
多态性(Polymorphism)是指一个名字,多种语义;或界面相同,多种实现。
重载函数是多态性的一种简单形式。
虚函数允许函数调用与函数体的联系在运行时才进行,称为动态联编

静态联编

联编是指一个程序模块、代码之间互相关联的过程
静态联编,是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编
动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编。switch语句和if语句是动态联编的例子
普通成员函数重载可表达为两种形式:

  • 在一个类说明中重载
Show(int , char);
Show(char * , float);
  • 基类的成员函数在派生类重载。有 3 种编译区分方法:
    • 根据参数的特征加以区分
    Show(int , char);
    Show(char * , float);
    
    • 使用::加以区分
    A :: Show();
    B :: Show();
    
    • 根据类对象加以区分
    //这其实是通过this指针区分
    Aobj.Show();//调用	A :: Show()
    Bobj.Show();//调用	B :: Show()
    

类指针的关系

基类指针和派生类指针与基类对象和派生类对象4种可能匹配:

  • 直接用基类指针引用基类对象
  • 直接用派生类指针引用派生类对象
  • 用基类指针引用一个派生类对象
  • 用派生类指针引用一个基类对象

基类指针引用派生类对象

class B : public A

A    * p ;// 指向类型 A 的对象的指针
A    A_obj ;// 类型 A 的对象
B    B_obj ;// 类型 B 的对象
p = &A_obj ;// p 指向类型 A 的对象
p = &B_obj ;// p 指向类型 B 的对象,它是 A 的派生类

利用 p,可以通过 B_obj 访问所有从 A 类继承的元素,但不能用 p访问 B 类自定义的元素(除非用了显式类型转换)

派生类指针引用基类对象

派生类指针只有经过强制类型转换之后,才能引用基类对象
class DateTime : public Data

((Date *)this) -> Print();

虚函数和动态联编

冠以关键字 virtual 的成员函数称为虚函数
实现运行时多态的关键首先是要说明虚函数,另外,必须用基类指针调用派生类的不同实现版本

虚函数和基类指针

基类指针虽然获取派生类对象地址,却只能访问派生类从基类继承的成员

#include <iostream>
using namespace std;

class Base
{
public:
	Base(char xx)
	{
		x = xx;
	}
	void who()
	{
		cout << "Base class: " << x << "\n";
	}
protected:
	char x;
};

class First_d : public Base
{
public:
	First_d(char xx, char yy) : Base(xx)
	{
		y = yy;
	}
	void who()
	{
		cout << "First derived class: " << x << ", " << y << "\n";
	}
protected:
	char y;
};

class Second_d : public First_d
{
public:
	Second_d(char xx, char yy, char zz) : First_d(xx, yy)
	{
		z = zz;
	}
	void who()
	{
		cout << "Second derived class: " << x << ", " << y << ", " << z << "\n";
	}
protected:
	char z;
};

void main()
{
	Base B_obj('A');
	First_d F_obj('T', 'O');
	Second_d S_obj('E', 'N', 'D');
	Base * p;

	p = &B_obj;
	p->who();

	p = &F_obj;
	p->who();

	p = &S_obj;
	p->who();

	F_obj.who();

	((Second_d *)p)->who();

	system("pause");
}

输出结果为

Base class: A
Base class: T
Base class: E
First derived class: T, O
Second derived class: E, N, D
请按任意键继续. . .

修改程序,定义虚函数

#include <iostream>
using namespace std;

class Base
{
public:
	Base(char xx)
	{
		x = xx;
	}
	virtual void who()
	{
		cout << "Base class: " << x << "\n";
	}
protected:
	char x;
};

class First_d : public Base
{
public:
	First_d(char xx, char yy) : Base(xx)
	{
		y = yy;
	}
	void who()
	{
		cout << "First derived class: " << x << ", " << y << "\n";
	}
protected:
	char y;
};

class Second_d : public First_d
{
public:
	Second_d(char xx, char yy, char zz) : First_d(xx, yy)
	{
		z = zz;
	}
	void who()
	{
		cout << "Second derived class: " << x << ", " << y << ", " << z << "\n";
	}
protected:
	char z;
};

void main()
{
	Base B_obj('A');
	First_d F_obj('T', 'O');
	Second_d S_obj('E', 'N', 'D');
	Base * p;

	p = &B_obj;
	p->who();

	p = &F_obj;
	p->who();

	p = &S_obj;
	p->who();

	system("pause");
}

输出结果为:

Base class: A
First derived class: T, O
Second derived class: E, N, D
请按任意键继续. . .

结论:
由于who()的虚特性随着p指向不同对象,this指针作类型转换执行不同实现版本

注意事项:

  • 一个虚函数,在派生类层界面相同的重载函数都保持虚特性
  • 虚函数必须是类的成员函数
  • 不能将友员说明为虚函数,但虚函数可以是另一个类的友员
  • 析构函数可以是虚函数,但构造函数不能是虚函数

虚函数的重载特性

  • 在派生类中重载基类的虚函数要求函数名、返回类型、参数个数、参数类型和顺序完全相同
  • 如果仅仅返回类型不同,C++认为是错误重载
  • 如果函数原型不同,仅函数名相同,丢失虚特性
class base
{
public:
	virtual void vf1();
	virtual void vf2();
	virtual void vf3();
	void f();
};

class derived : public base
{
public:
	void vf1();// 虚函数
	void vf2(int);// 重载,参数不同,虚特性丢失
	//char vf3();// error,仅返回类型不同
	void f();// 非虚函数重载
};

void  g()
{
	derived d;
	base *bp = &d;// 基类指针指向派生类对象
	bp->vf1();// 调用 deriver :: vf1()
	bp->vf2();// 调用 base :: vf2()
	bp->f();// 调用 base::f()
};

虚析构函数

  • 构造函数不能是虚函数。建立一个派生类对象时,必须从类 层次的根开始,沿着继承路径逐个调用基类的构造函数
  • 析构函数可以是虚的。虚析构函数用于指引 delete 运算符正 确析构动态对象
    普通析构函数在删除动态派生类对象的调用情况:
#include <iostream>
using namespace std;

class A
{
public:
	~A()
	{
		cout << "A::~A() is called.\n";
	}
};

class B : public A
{
public:
	~B()
	{
		cout << "B::~B() is called.\n";
	}
};

void main()
{
	A *Ap = new B;
	B *Bp2 = new B;
	cout << "delete first object:\n";
	delete Ap;
	cout << "delete second object:\n";
	delete Bp2;
	system("pause");
}

输出结果:

delete first object:
A::~A() is called.
delete second object:
B::~B() is called.
A::~A() is called.
请按任意键继续. . .

虚析构函数在删除动态派生类对象的调用情况:

#include <iostream>
using namespace std;

class A
{
public:
	~A()
	{
		cout << "A::~A() is called.\n";
	}
};

class B : public A
{
public:
	~B()
	{
		cout << "B::~B() is called.\n";
	}
};

void main()
{
	A *Ap = new B;
	B *Bp2 = new B;
	cout << "delete first object:\n";
	delete Ap;
	cout << "delete second object:\n";
	delete Bp2;
	system("pause");
}

输出结果:

delete first object:
B::~B() is called.
A::~A() is called.
delete second object:
B::~B() is called.
A::~A() is called.
请按任意键继续. . .

  • 定义了基类虚析构函数,基类指针指向的派生类动态对象也可以正确地用delete析构
  • 设计类层次结构时,提供一个虚析构函数,能够使派生类对象在不同状态下正确调用析构函数

纯虚函数和抽象类

  • 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本
  • 纯虚函数为各派生类提供一个公共界面
  • 纯虚函数说明形式:virtual 类型 函数名(参数表)= 0 ;
  • 一个具有纯虚函数的基类称为抽象类
class point
{
    /*……*/
};

class shape;// 抽象类
{
    point center;
    ···
public:
  point where()
{
    return center;
}
  void move(point p )
{
    enter = p;
    draw ();
}
  virtual void rotate(int) = 0;// 纯虚函数
  virtual void draw () = 0;// 纯虚函数
};
···
shape x;// error,抽象类不能建立对象
shape *p;// ok,可以声明抽象类的指针
shape f();// error, 抽象类不能作为返回类型
void g(shape);// error, 抽象类不能作为参数类型
shape & h(shape &);// ok,可以声明抽象类的引用

简单图形类例子:

#include <iostream>
using namespace std;

class figure
{
protected:
	double x, y;
public:
	void set_dim(double i, double j = 0)
	{
		x = i;
		y = j;
	}
	virtual void show_area() = 0;
};

class triangle : public figure
{
public:
	void show_area()
	{
		cout << "Triangle with high " << x << " and base " << y << " has an area of " << x * 0.5 * y << "\n";
	}
};

class square : public figure
{
public:
	void show_area()
	{
		cout << "Square with dimension " << x << "*" << y << " has an area of " << x * y << "\n";
	}
};

class circle : public figure
{
public:
	void show_area()
	{
		cout << "Circle with radius " << x;
		cout << " has an area of " << 3.14 * x * x << "\n";
	}
};

void main()
{
	triangle t;	//派生类对象
	square s;
	circle c;
	t.set_dim(10.0, 5.0);
	t.show_area();
	s.set_dim(10.0, 5.0);
	s.show_area();
	c.set_dim(9.0);
	c.show_area();
	system("pause");
}

输出结果:

Triangle with high 10 and base 5 has an area of 25
Square with dimension 10*5 has an area of 50
Circle with radius 9 has an area of 254.34
请按任意键继续. . .

void main()
{
	figure *p;	// 声明抽象类指针
	triangle t;
	square s;
	circle c;
	p = &t;
	p->set_dim(10.0, 5.0); 	// triangle::set_dim()
	p->show_area();
	p = &s;
	p->set_dim(10.0, 5.0);	// square::set_dim()
	p->show_area();
	p = &c;
	p->set_dim(9.0);		// circle::set_dim()
	p->show_area();
	system("pause");
}

输出结果为:

Triangle with high 10 and base 5 has an area of 25
Square with dimension 10*5 has an area of 50
Circle with radius 9 has an area of 254.34
请按任意键继续. . .

使用抽象类引用:

#include <iostream>
using namespace std;

class Number
{
public:
	Number(int i)
	{
		val = i;
	}
	virtual void Show() = 0;
protected:
	int val;
};
class Hex_type : public Number
{
public:
	Hex_type(int i) : Number(i)
	{
	}
	void Show()
	{
		cout << "Hexadecimal:" << hex << val << endl;
	}
};
class Dec_type : public Number
{
public:
	Dec_type(int i) : Number(i)
	{
	}
	void Show()
	{
		cout << "Decimal: " << dec << val << endl;
	}
};
class Oct_type : public Number
{
public:
	Oct_type(int i) : Number(i)
	{
	}
	void Show()
	{
		cout << "Octal: " << oct << val << endl;
	}
};
void fun(Number & n)// 抽象类的引用参数
{
	n.Show();
}
void main()
{
	Dec_type n1(50);
	fun(n1);	// Dec_type::Show()
	Hex_type n2(50);
	fun(n2);	// Hex_type::Show()
	Oct_type n3(50);
	fun(n3);	// Oct_type::Show()
	system("pause");
}

输出结果为:

Decimal: 50
Hexadecimal:32
Octal: 62
请按任意键继续. . .

虚函数与多态的应用

  • 虚函数和多态性使成员函数根据调用对象的类型产生不同的动作
  • 多态性特别适合于实现分层结构的软件系统,便于对问题抽象时定义共性,实现时定义区别

一个实例

计算雇员工资
Employee:

class Employee
{
public:
	Employee(const long, const char*);
	virtual ~Employee();//虚析构函数
	const char * getName() const;
	const long getNumber() const;
	virtual double earnings() const = 0;//纯虚函数,计算月薪
	virtual void print() const;//虚函数,输出编号、姓名
protected:
	long number;//编号
	char * name;//姓名
};

Manager:

class Manager : public Employee
{
public:
	Manager(const long, const char *, double = 0.0);
	~Manager() { }
	void setMonthlySalary(double);//置月薪
	virtual double earnings() const;//计算管理人员月薪
	virtual void print() const;//输出管理人员信息
private:
	double monthlySalary;//私有数据,月薪
};

HourlyWorker:

class HourlyWorker : public Employee
{
public:
	HourlyWorker(const long, const char *, double = 0.0, int = 0);
	~HourlyWorker(){}
	void setWage(double);//置时薪
	void setHours(int);//置工时
	virtual double earnings() const;//计算计时工月薪
	virtual void print() const;//输出计时工月薪
private:
	double wage;//时薪
	double hours;//工时
};

PieceWorker:

class PieceWorker : public Employee
{
public:
	PieceWorker(const long, const char *, double = 0.0, int = 0);
	~PieceWorker() { }
	void setWage(double);//置每件工件薪金
	void setQuantity(int);//置工件数
	virtual double earnings() const;//计算计件薪金
	virtual void print() const;//输出计件薪金
private:
	double wagePerPiece;//每件工件薪金
	int quantity;//工件数
};

测试用例:

void test()
{
	cout << setiosflags(ios::fixed | ios::showpoint) << setprecision(2);
	Manager m1(10135, "Cheng ShaoHua", 1200);
	Manager m2(10201, "Yan HaiFeng");
	m2.setMonthlySalary(5300);
	HourlyWorker hw1(30712, "Zhao XiaoMing", 5, 8 * 20);
	HourlyWorker hw2(30649, "Gao DongSheng");
	hw2.setWage(4.5);
	hw2.setHours(10 * 30);
	PieceWorker pw1(20382, "Xiu LiWei", 0.5, 2850);
	PieceWorker pw2(20496, "Huang DongLin");
	pw2.setWage(0.75);
	pw2.setQuantity(1850);
	// 使用抽象类指针,调用派生类版本的函数
	Employee *basePtr;

	basePtr = &m1;
	basePtr->print();

	basePtr = &m2;
	basePtr->print();

	basePtr = &hw1;
	basePtr->print();

	basePtr = &hw2;
	basePtr->print();

	basePtr = &pw1;
	basePtr->print();

	basePtr = &pw2;
	basePtr->print();

	system("pause");
}

异质链表

程序中,用基类类型指针,可以生成一个连接不同派生类对象的动态链表,即每个结点指针可以指向类层次中不同的派生类对象。这种结点类型不相同链表称为异质链表
e.g.

class Employee
{
public:
	Employee(const long, const char*);
	virtual ~Employee();
	const char * getName() const;
	const long getNumber() const;
	virtual double earnings() const = 0;
	virtual void print() const;
	Employee *next;// 增加一个指针成员
protected:
	long number;
	char * name;
};
void AddFront(Employee * &h, Employee * &t)//在表头插入结点
{
	t->next = h;
	h = t;
}
void test()//测试函数
{
	Employee * empHead = NULL, *ptr;
	ptr = new Manager(10135, "Cheng ShaoHua", 1200);//建立第一个结点
	AddFront(empHead, ptr);//插入表头
	ptr = new HourlyWorker(30712, "Zhao XiaoMing", 5, 8 * 20);//建立第二个结点
	AddFront(empHead, ptr);//插入表头
	ptr = new PieceWorker(20382, "Xiu LiWei", 0.5, 2850);//建立第三个结点
	AddFront(empHead, ptr);//插入表头
	ptr = empHead;
	while (ptr)
	{
		ptr->print();
		ptr = ptr->next;
	}//遍历链表,输出全部信息
	ptr = empHead;
	while (ptr)//遍历链表,输出姓名和工资
	{
		cout << ptr->getName() << "  " << ptr->earnings() << endl;
		ptr = ptr->next;
	}
}

多态

多态的理论基础

  • 联编是指一个程序模块、代码之间互相关联的过程。
  • 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
  • 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch 语句和 if 语句是动态联编的例子。
  • 理论联系实际
  1. C++与C相同,是静态编译型语言
  2. 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
  3. 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
    从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。

多态的c++实现

virtual关键字,告诉编译器这个函数要支持多态;不要根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用。冠以virtual关键字的函数叫虚函数;虚函数分为两类:一般虚函数、纯虚函数。

多态的实现效果

多态:同样的调用语句有多种不同的表现形态

多态实现的三个条件

有继承、有重写、有父类指针(引用)指向子类对象

重写与重载

函数重载

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

函数重写

  • 必须发生于父类与子类之间
  • 并且父类与子类中的函数必须有完全相同的原型
  • 使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
  • 多态是在运行期间根据具体对象的类型决定函数调用

编译器是如何实现多态的

  1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表
  2. 虚函数表是一个存储类成员函数指针的数据结构
  3. 虚函数表是由编译器自动生成与维护的
  4. virtual成员函数会被编译器放入虚函数表中
  5. 存在虚函数时,每个对象中都有一个指向虚函数表的指针
  6. VPTR一般作为类对象的第一个成员
  7. 虚函数表指针是在构造函数执行之前被赋值,还是在构造函数被赋值之后被赋

总结

  • 虚函数和多态性使软件设计易于扩充。
  • 派生类重载基类接口相同的虚函数其虚特性不变。
  • 如果代码关联在编译时确定,称为静态联编。代码在运行时关联称为动态联编。
  • 基类指针可以指向派生类对象、基类中拥有虚函数,是支持多态性的前提。
  • 虚析构函数可以正确释放动态派生类对象的资源。
  • 纯虚函数由派生类定义实现版本。
  • 具有纯虚函数的类称为抽象类。抽象类只能作为基类,不能建立对象。抽象类指针使得派生的具体类对象具有多态操作能力。
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!