C++之多态

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

静态联编

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

  • 在一个类说明中重载

    1
    2
    Show(int , char);
    Show(char * , float);
  • 基类的成员函数在派生类重载。有 3 种编译区分方法:

    • 根据参数的特征加以区分

      1
      2
         Show(int , char);
      Show(char * , float);
    • 使用::加以区分

      1
      2
         A :: Show();
      B :: Show();
    • 根据类对象加以区分

      1
      2
      3
         //这其实是通过this指针区分
      Aobj.Show();//调用 A :: Show()
      Bobj.Show();//调用 B :: Show()

类指针的关系

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

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

基类指针引用派生类对象

class B : public A

1
2
3
4
5
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

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

虚函数和动态联编

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

虚函数和基类指针

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

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#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
请按任意键继续. . .

修改程序,定义虚函数

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#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++认为是错误重载
  • 如果函数原型不同,仅函数名相同,丢失虚特性
    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
    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 运算符正 确析构动态对象
    普通析构函数在删除动态派生类对象的调用情况:
    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
    #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.
请按任意键继续. . .

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

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
#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 ;
  • 一个具有纯虚函数的基类称为抽象类
    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
    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,可以声明抽象类的引用

简单图形类例子:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#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
请按任意键继续. . .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
请按任意键继续. . .

使用抽象类引用:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;//工件数
};

测试用例:

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
33
34
35
36
37
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
};

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
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. 虚函数表指针是在构造函数执行之前被赋值,还是在构造函数被赋值之后被赋

总结

  • 虚函数和多态性使软件设计易于扩充。
  • 派生类重载基类接口相同的虚函数其虚特性不变。
  • 如果代码关联在编译时确定,称为静态联编。代码在运行时关联称为动态联编。
  • 基类指针可以指向派生类对象、基类中拥有虚函数,是支持多态性的前提。
  • 虚析构函数可以正确释放动态派生类对象的资源。
  • 纯虚函数由派生类定义实现版本。
  • 具有纯虚函数的类称为抽象类。抽象类只能作为基类,不能建立对象。抽象类指针使得派生的具体类对象具有多态操作能力。
Donate comment here