0%

Cpp基础(11)虚函数和多态

1.基本概念

1.1.虚函数

  • 在类的定义中,前面有virtual 关键字的成员函数就是虚函数。
1
2
3
4
class base{
virtual int get();
};
int base::get() {...}
  • virtual 关键字只用在类定义里的函数声明中,写函数体时不用。
  • 构造函数和静态成员函数不能是虚函数。
  • 虚函数和普通函数的本质区别在于虚函数可以参与多态,而普通成员函数不能。
  • 派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数。

1.2.多态的表现形式一

  • 派生类的指针可以赋给基类指针
  • 通过基类指针调用基类和派生类中的同名虚函数时:
    • 若该指针指向一个基类的对象,那么被调用的是基类的虚函数;
    • 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。
  • 这种机制就叫作多态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
public:
virtual void sayHi() { cout << "Hi" << endl; }
};
class Dog : public Animal {
public:
void sayHi() { cout << "Woof!" << endl; }
};
void func(Animal *pa) { pa->sayHi(); } // 使用基类指针调用函数
int main() {
Animal a; Dog d;
func(&a); // 指向基类对象,则输出 Hi
func(&d); // 指向派生类对象,则输出 Woof!
return 0;
}

1.3.多态的表现形式二

  • 派生类的指针可以赋给基类引用
  • 通过基类引用调用基类和派生类中的同名虚函数时:
    • 若该引用引用的是一个基类的对象,那么被调用的是基类的虚函数;
    • 若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。

不加virtual,就根据指针,引用的类型来决定调用基类还是派生类的函数;加virtual,就是虚函数,就根据指针所指,引用所引的类型来决定调用基类还是派生类的函数。派生类的函数与基类的虚函数同名且参数列表完全相同时才能体现多态性,因此一般应该禁止重新定义继承而来的非虚函数

在多层继承的情况下,从定义virtual开始的派生类中同名函数均为虚函数,无论在这些派生类的同名函数中是否显示加virtual。如果派生类将基类中的某个非虚函数声明为虚函数,使用基类指针调用该函数时,不能体现多态。

当基类指针指向派生类对象时,会优先选择符合多态的派生类成员函数。

2.多态程序实例

2.1.例1几何形体处理程序

几何形体处理程序:输入若干个几何形体的参数,要求按面积排序输出。输出时要指明形状。

输入Input:第一行是几何形体数目n(不超过100),下面有n行,每行以一个字母c开头

若 c 是 ‘R’,则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高;

若 c 是 ‘C’,则代表一个圆,本行后面跟着一个整数代表其半径;

若 c 是 ‘T’,则代表一个三角形,本行后面跟着三个整数,代表三条边的长度。

输出Output:按面积从小到大依次输出每个几何形体的种类及面积,每行一个几何形体,输出格式为:形体名称:面积

类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<iostream>
#include<stdlib.h>
#include<math.h>
using namespace std;
class Shape {
public:
virtual double Area() = 0; //纯虚函数
virtual void PrintInfo() = 0;
};

class Rectangle :public Shape {
public:
int w, h;
virtual double Area();
virtual void PrintInfo();
};

class Circle :public Shape {
public:
int r;
virtual double Area();
virtual void PrintInfo();
};

class Triangle :public Shape {
public:
int a,b,c;
virtual double Area();
virtual void PrintInfo();
};

各成员函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
double Rectangle::Area() {
return w * h;
}

void Rectangle::PrintInfo() {
cout << "Rectangle: " << Area() << endl;
}

double Circle::Area() {
return 3.14 * r * r;
}

void Circle::PrintInfo() {
cout << "Circle: " << Area() << endl;
}

double Triangle::Area() {
double p = (a + b + c) / 2.0;
return sqrt(p*(p - a)*(p - b)*(p - c));
}

void Triangle::PrintInfo() {
cout << "Triangle: " << Area() << endl;
}

用一个指向Shape基类的指针数组pShapes来存放各个几何形体,数组中的每一个元素都是基类指针,因此它可以指向不同派生类的对象。MyCompare函数比较两个几何形体面积的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Shape * pShapes[100];
int MyCompare(const void * s1, const void * s2) {
double a1, a2;
Shape **p1; //s1,s2是void*,不可写 *s1 来取得s1指向的内容
Shape **p2;
p1 = (Shape**)s1; //s1,s2指向Shapes数组中的元素,数组元素的类型是Shape*
p2 = (Shape**)s2; //故p1,p2都是指向指针的指针,类型为Shae**
a1 = (*p1)->Area(); //*p1的类型是Shape*,是基类指针,故此句为多态
a2 = (*p2)->Area();
if (a1 < a2)
return -1;
else if (a2 < a1)
return 1;
else
return 0;
}

主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main() {
int i; int n;
Rectangle *pr; Circle *pc; Triangle *pt;
cin >> n;
for (i = 0; i < n; i++) {
char c;
cin >> c;
switch (c) {
case'R':
pr = new Rectangle();
cin >> pr->w >> pr->h;
pShapes[i] = pr;
break;
case'C':
pc = new Circle();
cin >> pc->r ;
pShapes[i] = pc;
break;
case'T':
pt = new Triangle();
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt;
break;
}
}
qsort(pShapes, n, sizeof(Shape*), MyCompare); //按指针指向的几何形体的面积从小到大排序
for (i = 0; i < n; i++)
pShapes[i]->PrintInfo(); //多态,根据pShapes[i]指向的对象调用其对应的成员函数
return 0;
}

运用多态来实现这个问题所带来的好处就是提高了程序的可扩充性,如果要添加新的几何形体如五边形,则只需要从Shape里派生出Pentagon,以及在main函数中的swtich语句中增加一个case,其余部分不变。

  • 用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常见的做法。

2.2.例2

再看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
class Base {
public:
void fun1() { fun2(); } //等价于 this->fun2,fun2是虚函数,所以这句是多态
virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derived:public Base {
public:
virtual void fun2() { cout << "Derived::fun2()" << endl; }
};
int main() {
Derived d;
Base * pBase = &d;
pBase->fun1();
return 0;
}

上面程序运行的结果是Derived::fun2(),而不是Base::fun2()。这是因为pBase->fun1指至pbase是指向一个派生类的对象d的,那进到fun1里面,this指针指向的东西自然也就是这个d,所以此时this指针指向的是一个派生类的对象,那么根据多态的原则这条语句就会调用派生类的fun2,也就是Derieved类的fun2,所以会输出Derievedfun2,即Derived::fun2()

成员函数中调用虚函数有这样的规则:

  • 在非构造函数,非析构函数的成员函数中调用虚函数,是多态

  • 在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
using namespace std;

class myclass {
public:
virtual void hello() { cout << "hello from myclass" << endl; }
virtual void bye() { cout << "bye from myclass" << endl; }
};

class son :public myclass {
public:
//派生类中和基类中虚函数同名同参数表的函数,不加`virtual`也自动成为虚函数
void hello() { cout << "hello from son" << endl; }
son() { hello(); };
~son() { bye(); };
};

class grandson :public son {
public:
virtual void hello() { cout << "hello from grandson" << endl; }
virtual void bye() { cout << "bye from grandson" << endl; }
grandson() { cout << "constructing grandson" << endl; }
~grandson() { cout << "destructing grandson" << endl; }
};

int main() {
grandson gson;
son *pson;
pson = &gson;
pson->hello(); //多态
return 0;
}

程序运行结果:

3.多态实现原理

“多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定——这叫“动态联编”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
class Base {
public:
int i;
virtual void Print() { cout << "Base:Print"; }
};
class Derived:public Base {
public:
int n;
virtual void Print() { cout << "Derived:Print"; }
};
int main() {
Derived d;
cout << sizeof(Base) << "." << sizeof(Derived);
return 0;
}

程序运行输出结果为:8,12。那么为什么这个对象的大小都多出了4个字节呢?

多态实现的关键——虚函数表

每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址,多出来的4个字节就是用来放虚函数表的地址的。

多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令。

多态在提高程序可扩充性时也会有一定的代价:多态程序在运行期间会有额外的时间和空间上的开销,即时间上编译时会查询虚函数表,空间上每个有虚函数的类的对象里都会多出4个字节来存放虚函数表的地址。

需要注意的是,在构造函数和析构函数中调用虚函数时,调用的是自己的类或基类中定义的函数,不会等到运行时才决定,因此不是动态联编。在普通成员函数中调用虚函数,才是动态联编,是多态。

4.虚析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class son {
public:
~son() { cout << "bye from son" << endl; }
};

class grandson :public son {
public:
~grandson() { cout << "bye from grandson" << endl; }
};

int main() {
son *pson;
pson = new grandson;
delete pson;
return 0;
}

上面的程序运行会输出bye from sonpson这个基类的指针指向了派生类的对象,当delete pson后会引起一些问题,直观上看因为delete掉的是一个基类的指针,所以会去调用基类的析构函数。但是逻辑上讲这个指针本身又指向的是一个派生类的对象,那么分配的也是一个派生类对应的这样的一个内存空间,那么这时它应该调用的还有派生类的析构函数,但是目前的程序设计角度上来看编译器是不会知道它需要调用派生类的机构函数的,这样可能导致一些问题。

我们希望做的是:

  • 通过基类的指针删除一个派生类的对象时,先调用派生类的析构函数,再调用基类的析构函数。

解决方法是:

  • 把基类的析构函数声明为virtual,派生类的析构函数virtual可以不进行声明。
  • 类如果定义了虚函数,则最好将析构函数也定义成虚函数。
  • 注意:不允许以虚函数作为构造函数

一般来说,如果一个类中定义了虚函数,则应该将析构函数也定义成虚函数;同理,若一个类没有定义虚函数,但需要通过基类的指针销毁派生类对象,也应该将析构函数声明为虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class son {
public:
virtual ~son() { cout << "bye from son" << endl; }
};

class grandson :public son {
public:
~grandson() { cout << "bye from grandson" << endl; }
};

int main() {
son *pson;
pson = new grandson;
delete pson;
return 0;
}

5.纯虚函数和抽象类

  • 纯虚函数:没有函数体的虚函数
1
2
3
4
5
6
7
8
9
10
11
12
class A{
private:
int a;
public:
virtual void Print() = 0; //纯虚函数
void fun(){ cout << "fun"; }
};
int main(){
A a; //错误,A是抽象类,不能创建对象
A *pa; //正确,可以定义抽象类的指针和引用
pa = new A; //错误,A是抽象类,不能创建对象
}
  • 抽象类:包含纯虚函数的类

    • 抽象类只能作为基类来派生新类使用;
    • 不能创建抽象类的对象
    • 抽象类的指针和引用 $\to$ 由抽象类派生出来的类的对象。
  • 在抽象类中:

    • 在成员函数内可以调用纯需函数;
    • 在构造函数/析构函数内部不能调用纯虚函数
  • 如果一个类从抽象类派生而来 $\to$ 它实现了基类中的所有纯需函数,才能成为非抽象类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A{
public:
virtual void f() = 0; //纯虚函数
void g(){ this->f(); } //ok
A() { f();} //错误
};
class B:public A{
public:
void f(){ cout << "B:f()" << endl;} //将虚函数“实例化”
};
int main(){
B b;
b.g();
return 0;
}

输出结果:B:f()