天天看点

C++入门系列:第八章 多态性和虚函数

C++入门系列:第一章 认识C++的对象

C++入门系列:第二章 从结构到类的演变

C++入门系列:第三章 函数和函数模板

C++入门系列:第四章 类和对象

C++入门系列:第五章 特殊函数和成员

C++入门系列:第六章 继承和派生

C++入门系列:第七章 类模板和向量

C++入门系列:第八章 多态性和虚函数

C++入门系列:第九章 流类库

文章目录

    • 1 多态性
    • 2 虚函数
      • 2.1 虚函数实现多态性的条件
      • 2.2 构造函数和析构函数调用虚函数
      • 2.3 纯虚函数与抽象类
      • 2.4 类成员函数的指针与多态性

1 多态性

派生一个类的原因并非总是为了添加新的数据成员或成员函数,有时是为了重新定义基类的成员函数。

#include <iostream>
using namespace std;
const double PI = 3.14159;
class Point {
	private:
		double x, y;
	public:
		Point(double x, double y) {
			this->x = x;
			this->y = y;
		}
		double area() {
			return 0;
		}
};
class Circle:public Point {
	private:
		double radius;
	public:
		Circle(double a, double b, double r):Point(a, b) {
			radius = r;
		}
		double area() {	
			return PI * radius * radius;
		}
};
void main() {
	Point a(1.5, 6.7);
	Circle c(1.5, 6.7, 2.5);
	cout << "area of Point is" << a.area() << endl;		// 调用Point的area()
	cout << "area of Circle is" << c.area() << endl;	// 调用Circle的area()
	Point* p = &c;										// 虽然指向Circle的地址,但是指针是Point
	cout << "area of Circle is" << p->area() << endl;	// 调用Point的area()
	Point& rc = c;										// 与指针同理
	cout << "area of Circle is" << rc.area() << endl;	// 调用Point的area()
}
           

使用关键字

virtual

声明Point类的area()函数,将这种函数称为虚函数:

当编译系统编译含有虚函数的类时,将为它建立一个虚函数表,表中的每一个元素都指向虚函数的地址。此外,编译器也为类增加一个数据成员,这个数据成员是一个指向该虚函数表的指针,通常称为vptr。Point类只有一个虚函数,所以虚函数表里也只有一项。

虚函数的地址翻译取决于对象的内存地址。编译器为含有虚函数类的对象首先建立一个入口地址,这个地址用来存放指向虚函数表的指针vptr,然后按照类中虚函数的声明次序,一一填入函数指针。当调用虚函数时,先通过vptr找到虚函数表,然后再找出虚函数的真正地址。

派生类能继承基类的虚函数表,而且只要是和基类同名(参数也相同)的成员函数,无论是否使用virtual声明,它们都自动成为虚函数。如果派生类没有改写继承基类的虚函数,则函数指针调用基类的虚函数。如果派生类改写了基类的虚函数,编译器将重新为派生类的虚函数建立地址,函数指针会调用改写过的虚函数。

虚函数的调用规则是:根据当前对象,优先调用对象本身的虚成员函数。这和名字支配规律类似,不过虚函数是动态联编的,是在执行期间“间接”调用实际上欲联编的函数。

(简单说:基类有声明了virtual的虚函数,继承这个基类的派生类重载了这个虚函数,都默认成为虚函数,可以使用多态调用,多态与java相同)

C++入门系列:第八章 多态性和虚函数

2 虚函数

一旦基类定义了虚函数,该基类的派生类中的同名函数也自动成为虚函数。

虚函数只能是类中的一个成员函数,但不能是静态成员,关键字virtual用于类中该函数的声明中。

class A {
	public:
		virtual void fun();	// 声明虚函数
};
void A::fun();	// 定义虚函数
           

当派生类中定义了一个同名的成员函数时,只要该成员函数的参数个数和相应类型以及它的返回类型与基类中同名的虚函数完全一样,则无论是否为该成员函数使用virtual,它都将成为一个虚函数。

2.1 虚函数实现多态性的条件

关键字virtual指示C++编译器对调用虚函数进行动态联编。这种多态性是程序运行到需要的语句处才动态确定的,所以称为运行时的多态性。不过,使用虚函数并不一定产生多态性,也不一定使用动态联编。

产生运行时的多态性有如下3个前提:

  • 类之间的继承关系满足赋值兼容性规则
  • 改写了同名虚函数
  • 根据赋值兼容性规则使用指针(或引用)

满足前两条并不一定产生动态联编,必须同时满足第3条才能保证实现动态联编。第3条又分为两种情况:第1种是已经演示过的按赋值兼容性规则使用基类指针(或引用)访问虚函数;第2种是把指针(或引用)作为函数参数,即这个函数不一定是类的成员函数,可以是普通函数,而且可以重载。

#include <iostream>
using namespace std;
const double PI = 3.14159;
class Point {
	private:
		double x, y;
	public:
		Point(double x, double y) {
			this->x = x;
			this->y = y;
		}
		virtual double area() { return 0; } 	// 声明虚函数
};
class Circle:public Point {
	private:
		double radius;
	public:
		Circle(double a, double b, double r):Point(a, b) {
			radius = r;
		}
		double area() { return PI * radius * radius; }	// 重载了Point基类的虚函数
};
void display(Point* p) {
	cout << p->area() << endl;
}
void display(Point& a) {
	cout << a.area() << endl;
}
void main() {
	Point a(1.5, 6.7);
	Circle c(1.5, 6.7, 2.5);
	Point* p = &c;
	Point& rc = c;
	display(a);		// 调用Point的area()
	display(p);		// 调用Circle的area()
	display(rc);	// 调用Circle的area()
}
           

由于动态联编是在运行时进行的,相对于静态联编,它的运行效率比较低,但它可以使程序员对程序进行高度抽象,设计出可扩充性好的程序。

2.2 构造函数和析构函数调用虚函数

在构造函数和析构函数中调用虚函数采用静态联编,即它们所调用的虚函数是自己的类或基类中定义的函数,但不是任何在派生类中重定义的虚函数。

#include <iostream>
using namespace std;
class A {
	public:
		A() {}
		virtual void func() { cout << "Constructing A" << endl; }
		~A() {}
		virtual void fund() { cout << "Destructor A" << endl; }
};
class B:public A {
	public:
		B() { func(); }
		void fun() { cout << "Come here and go..."; func(); }
		~B() { fund(); }
};
class C:public B {
	public:
		C() {}
		void func() { cout << "Class C" << endl; }	// 重写了基类A的func()
		~C() { fund(); }
		void fund() { cout << "Destructor C" << endl; }	// 重写了基类A的fund()
};
void main() {
	C c;
	c.fun();
}

输出结果:
Constructing A	// 构造函数调用了基类
Come here and go...Class C	// 调用了基类B的fun(),因为当前对象是C,所以fun()中的func()会调用类C的func()
Destructor C	// 析构函数从派生类开始析构销毁
Destrcutor A
           

目前推荐的C++标准不支持虚构造函数。由于析构函数不允许有参数,因此一个类只能有一个虚析构函数。虚析构函数使用virtual说明。只要基类的析构函数被说明为虚函数,则派生类的析构函数,无论是否使用virtual进行说明,都自动地称为虚函数。

一般说来,如果一个类中定义了虚函数,析构函数也应说明为虚函数。

如果基类的析构函数为虚函数,则在派生类未定义析构函数时,编译器所生成的析构函数也称为虚函数。

2.3 纯虚函数与抽象类

在许多情况下,不能在基类中为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数,将其定义留给派生类去做。(简单说,纯虚函数就是java的抽象方法,同样也是抽象类,虚函数就是可以被子类重载的虚函数):

class 类名 {
	virtual 函数类型 函数名(参数列表) = 0;
};
           

一个类可以说明多个纯虚函数,包含有纯虚函数的类称为抽象类。一个抽象类只能作为基类来派生新类,不能说明抽象类的对象。

从一个抽象类派生的类必须提供纯虚函数的实现代码,或在该派生类中仍将它说明为纯虚函数,否则编译器将给出错误信息。这说明了纯虚函数的派生类仍是抽象类。如果派生类中给出了基类所有纯虚函数的实现,则该派生类不再是抽象类。

抽象类至少含有一个虚函数,而且至少有一个虚函数是纯虚函数,以便将它与空的虚函数区分开来。

virtual void area() = 0;	// 纯虚函数
virtual void area() {}		// 空的虚函数
           

在成员函数内可以调用纯虚函数。因为没有为纯虚函数定义代码,所以在构造函数或析构函数内调用一个纯虚函数将导致程序运行错误。

class Shape {
	public:
		virtual double area() = 0;	// 纯虚函数,Shape为抽象类
};
class Square:public Shape {
	protected:
		double H;
	public:
		Square(double i) { H = i; }
		double area() { return H * H; }	// 实现纯虚函数
};
class Circle:public Square {
	public:
		Circle(double r):Square(r) {}
		double area() { return H * H * 3.14159; }	// 派生类各自实现自己的area()
};
class Triangle:public Square {
	protected:
		double W;
	public:
		Triangle(double h, double w):Square(h) { W = w; }
		double area() { return H * W * 0.5; }
};
class Rectangle:public Triangle {
	public:
		Rectangle(double h, double w):Triangle(h, w) {}
		double area() { return H * W; }
};
double total(Shape* s[], int n) {
	double sum = 0.0;
	for (int i = 0; i < n; i++)
		sum += s[i]->area();
	return sum;
}

#include <iosteram>
using namespace std;
void main() {
	Shape* s[5];
	s[0] = new Square(4);
	s[1] = new Triangle(3, 6);
	s[2] = new Rectangle(3, 6);
	s[3] = new Square(6);
	s[4] = new Circle(10);
	for (int i = 0; i < 5; i++)
		cout << "s[" << i << "]=" << s[i]->area() << endl;
	double sum = total(s, 5);
	cout << "The total area is:" << sum << endl;
}
           

2.4 类成员函数的指针与多态性

在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,仍发生多态性。