继承概览

面向对象的继承与派生

  • 继承和派生:一个新类从现有的类继承特征(属性和方法),从现有的类产生新类的过程称为派生

  • 现有的用来派生新类的类成为基类或父类,派生出来的类成为派生类或子类

  • 派生类可以作为基类继续派生新的类,从而形成复杂的类的层次结构

  • 类继承的层次结构:下层具有上层的特征同时还加入独属于自己特生,即逐层细化具体,如此更符合人类认识世界的规律

派生类的定义

1
2
3
4
5
6
7
8
class 派生类名: 继承方式 基类名{
private:
成员声明列表
protected:
成员声明列表
public:
成员声明列表
};
  • 继承方式分为三种:公有继承、私有继承和保护继承

  • 通常在派生类类体中列出新增的数据成员和成员函数,基类一切成员都会自动的成为派生类的成员(共性)

派生类的示例

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
class Point //基类
{
double x,y;
public:
Point(){ x=0; y=0; }
Point( double a, double b) { x=a; y=b; }
void setXY( double a, double b) { x=a; y=b; }
double getX() { return x; }
double getY() { return y; }
};

class Circle : public Point //派生类
{
double radius;
public:
Circle() { radius=0; }
//先利用初始化列表调用基类构造函数初始化基类继承的数据成员,然后再给数据派生类自己的成员赋值
Circle( double a, double b,double r):Point(a,b){
radius=r;
}
void setR(double r) { radius=r; }
double getR() { return radius; }
};

int main() //主函数
{
Circle c(3,4,5);
//既可以调用从基类继承的成员函数
cout<<c.getX()<<endl;
cout<<c.getY()<<endl;
//也可以调用属于派生类自己的成员函数
cout<<c.getR()<<endl;
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
class Point
{
double x; //私有成员
protected:
double y; //保护成员
public:
Point(double a, double b){
x = a;
y = b;
}
void setX(double a) { x = a; }
double getX() { return x; }
};

class Circle : public Point
{
double radius;
public:
Circle(double a, double b, double r) : Point(a, b), radius(r){}
void setXYR(double a, double b, double r)
{
x = a; //错的,派生类内部也不能直接访问基类的私有成员
setX(a); //解决办法:通过基类提供的共有成员函数间接访问
y = b;
radius = r;
}
};

保护继承

  • 基类的公有成员和保护成员都变为派生类的保护成员
  • 基类的私有成员在派生类中不可访问
  • 一个其他继承方式没有的特点:多次保护继承派生出的类可以直接访问之前所有的父类成员
继承的基类成员 派生类内部直接访问 派生类外部类名访问
公有成员
保护成员
私有成员
自身添加的成员 派生类内部直接访问 派生类外部类名访问
公有成员
保护成员
私有成员

相当于在公有继承方式上又禁止了派生类外部通过类名访问原基类的公有成员

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
class Point
{
double x; //私有成员
protected:
double y; //保护成员
public:
Point(double a, double b){
x = a;
y = b;
}
void setX(double a) { x = a; }
int getX() { return x; }
};

class Circle : protected Point
{
double radius;
public:
Circle(double a, double b, double r) : Point(a, b){
radius = r;
}
void setXYR(double a, double b, double r){
//不可以访问基类的私有成员但是能访问基类的保护成员
x = a; //错的
setX(a); //解决办法:通过基类的公有成员函数间接访问
y = b; //对的
radius = r;
}
void setRadius(double r){
radius = r;
}
};

class Test : protected Circle
{
int color;
public:
Test(double a, double b, double r, int c) : Circle(a, b, r){
color = c;
}
void setXYRC(double a, double b, double r, int c){
setX(a);
y = b; //对的,多次保护继承其保护属性不变,继续允许基类子类的直接访问
setRadius(r);
color = c;
}
};

私有继承

  • 基类的公有成员和保护成员都成为派生类成员的私有成员
  • 基类的私有成员在派生类中不可访问
继承的基类成员 派生类内部直接访问 派生类外部类名访问
公有成员
保护成员
私有成员
自身添加的成员 派生类内部直接访问 派生类外部类名访问
公有成员
保护成员
私有成员

虽然上述列表和保护成员的列表相同,但是继承得来的公有成员和保护成员变为了派生类的私有成员(权限更加小了),所以后面派生类再从当前派生类保护继承也不能访问当前派生类继承得来的公有成员和保护成员了

注意区分保护继承和私有继承:他们限制是后代派生类访问当前派生类继承得来的公有成员和保护成员的权限

基类成员在派生类中权限对比

禁止继承

某些情况下如果不想让后序代码继承当前的类,可以借助 C++ 11 的 final 关键词修饰类,后序如果调用编译器报错

1
2
3
4
5
6
class Test final{
...
}
class Sub: public Test{ //编译器报错
...
}

构造与析构

构造基类成员和自身成员

  • 由于派生类继承了基类的所有成员,派生类的每一个对象都包含基类数据成员的值,在派生类构造函数中应该提供初始化列来初始化基类数据成员的机制(一般使用初始化列表)

非要在花括号内赋值?当然可以但是效率太低,不是真正意义上的初始化而是定义,初始化列表是真正的初始化,在创建之时进行

  • 与容器类初始化对象成员类似,派生类也必须通过初始化列表初始化基类数据成员。对本类的数据成员,可以用初始化列表,也可以放在构造函数体内赋值

构造函数的调用顺序

  • 创建派生类对象时(A 派生 B)

    • 调用 A 中对象成员(如果有的话)对应的构造函数
    • 调用基类 A 的构造函数
    • 调用 B 中对象成员(如果有的话)对应的构造函数
    • 调用派生类 B 的构造函数
  • A -> B -> C(A 派生 B,B 再派生 C)

    • 调用基类 A 的构造函数
    • 调用直接派生类 B 的构造函数
    • 调用间接派生类 C 的构造函数
  • 总而言之:

    • 原则一:父子类之间先基类(父类)构造再派生类(子类)构造
    • 原则二:同一个类中先对象成员,再本类构造函数体

析构函数的调用顺序

类似栈的操作顺序,析构函数调用顺序恰好和构造函数的调用顺序相反

  • 创建派生类对象时(A 派生 B)
    • 调动派生类 B 的析构函数
    • 调用 B 中对象成员(如果有的话)对应的析构函数
    • 调用基类 A 析构函数
    • 调用 A 中对象成员(如果有的话)对应的析构函数
  • A -> B -> C(A 派生 B,B 再派生 C)
    • 调用间接派生类 C 的析构函数
    • 调用直接派生类 B 的析构函数
    • 调用基类 A 的析构函数

具体示例

  • 在继承过程中,构造函数和析构函数不能被继承,在创建派生类对象时按照顺序由系统自动调用基类和派生类的构造函数,而不能在派生类中显示调用基类构造函数
  • 也不能在派生类中显示调用基类的析构函数

基类和派生类的定义

1
2
3
4
5
6
7
8
class Point
{
double x,y;
public:
Point(){ x=0; y=0; }
Point(double a,double b) { x=a; y=b; }
...
};

派生类构造函数定义

1
2
3
4
5
6
Circle::Circle(double a,double b,double aa,double bb,double r):Point(a,b),p(aa,bb){
radius=r;
}

Circle c(3,4,5,6,8);
//(3,4)对应Point(a,b),(5,6)对应p(aa,bb),8对应r

多重继承

多继承概念

C++ 允许使用多个基类进行继承成为多继承。派生类继承所有基类的成员,定义多继承派生类语法与单继承语法类似,但是要指定所有要继承的基类以及每个基类的继承方式

1
2
3
4
5
6
7
8
class 派生类名: 继承方式1 基类名1, 继承方式2 基类名2, ...{
private:
成员声明列表
protected:
成员声明列表
public:
成员声明列表
};
  • 在多继承派生类的构造函数中,要通过初始化列表的形式调用直接基类的构造函数
  • 构造函数的执行顺序:先执行基类构造函数,再执行派生类构造函数;多个基类构造函数按照定义派生类时的顺序进行与初始化列表中顺序无关
  • 使用多继承容易造成混乱尽量避免使用,只用了解

多继承的二义性

问题示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base1
{
public:
void draw() {...} //第一个基类函数存在draw()
};
class Base2
{
public:
void draw() {...} //第二个基类函数也存在draw()
};

class Derived: public Base1, public Base2{
... //继承时出现二义性
};

int main()
{
Derived d;
d.draw(); //调用更是二义性
return 0;
}

解决办法

  • 限定函数前缀

问题可以解决但是不好,因为对于使用的客户不友好,他需要考虑函数所在的类并确定调用前缀

1
2
3
4
5
6
7
8
9
10
11
class Derived: public Base1, public Base2{	
...
};

int main()
{
Derived d;
d.Base1::draw();
d.Base2::draw();
return 0;
}
  • 覆盖技术

派生类中提供接口,内部解决二义性方便客户使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Derived: public Base1, public Base2{	
public
void draw(){
if(...) Base1::draw();
else Base2::draw();
}
};

int main()
{
Derived d;
d. draw();
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
class A{ //基类
public:
double a;
...
};

class B : public A{ //从A派生的类1
... //初始化一个值
};

class C : public A{ //从A派生的类2
... //又初始化一个值
};

class D : public B, public C{ //从A派生出的两个派生类继续派生
public:
double getValue() {
return a; //返还谁的a呢?
}
...
};

int main()
{
D d;
cout << d.getValue(); //二义性
return 0;
}

解决办法

  • 限定对象前缀
1
2
3
4
5
6
7
8
9
class D : public B, public C
{
public:
double getValue(){
return A::a; //指明返还根基类A的a?错,隔代不能直接访问
return B::a; //允许,成功查看到a成员,但是还是容易出错
}
...
}

这种办法看似能解决燃眉之急,但是本质上还是从不同的类继承了多个相同的成员,在实际中还是容易造成混乱。例如将上述改为下列修改操作

1
2
3
4
5
6
7
8
void D::setValue() {
B::a=5;
}
int main(){
D d;
d.setValue();
return 0;
}

这次修改从类 B 继承的 a ,下次修改从 A 继承的 a ?下下次呢?借助不同前缀修改成员最后查询的时候不是你想要的最终的 a。你能保证每次都用一个前缀?那么复杂的工程类代码不把眼看花?

  • 真正解决的办法——虚基类

上述二义性问题在于如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员

二义性解决:虚基类

将总的基类定义为成虚基类,可以确保该基类通过多条路径继承时派生类仅仅继承该基类一次,避免上述由于多路径继承造成的二义性

做法:借助虚函数将基类定义为虚基类

具体虚函数使用方法下文有详细讲解

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
class A{
public:
double a;
};
//A是B和C的虚基类
class B : virtual public A {...};
class C : virtual public A {...};
//虚基类确保A的成员只有一份
class D : public B, public C {
public:
void setValue() {
a=5;
}
//完全等效,本质上都是操作一个对象
//B::a = 5;
//A::a = 5;
...
};

int main()
{
D d;  
d.setValue();
return 0;
}
  • C++ 规定,由虚基类沿不同路经进行继承时,要求最终的派生类载构造函数中通过初试化参数列表对虚基类成员直接进行初始化,而中间层次的类对虚基类成员的初始化被忽略,尽管他们提供了初始化列表,如此确保了虚基类构造函数只被调用一次
  • 进行多继承时,同时有虚基类和非虚基类是虚基类构造函数优先执行

多态概览

  • 多态指不同的对象接受到相同的消息时产生不同的行为(调用不同的方法)

相当于使用同一个函数名调用不同内容的函数,实现了“一个借口,多种方法”

  • 在 C++ 中通过覆盖、成员函数重载、运算符重载、模版、虚函数等技术,使得基类和派生类中可以出现同名的成员函数。不同的成员函数被调用时表现出不同的行为,表现出很强的灵活性
    • 成员函数覆盖
    • 成员函数重载
    • 运算符重载
    • 虚函数
    • 模板(后续讲解)

静多态性和动态多态性

  • 静态多态性:编译时的多态性,成员函数重载、成员函数覆盖、运算符重载都属于静态多态性。编译器根据实参数据类型或对象的数据类型,在编译时就确定调用哪个函数

  • 动态多态性:运行时多态性,通过虚函数来实现。通过虚函数实现的动态多态性在代码执行的过程中决定调用哪个函数

成员函数的重载和覆盖

  • 重载:同一个类中,存在名称相同但“签名不同”的成员函数(函数参数类型或个数不同),编译时根据实参类型确定调用的是哪个版本的函数

  • 覆盖:派生类和基类存在名称相同的成员函数,实现派生类方法覆盖(改造)基类方法的功能。如果要访问基类被覆盖方法,需要使用类名前缀。根据是否使用虚函数,实现隐藏或改写的效果

赋值兼容性

每一个派生类的对象都是基类的一个对象。赋值兼容规则是指在公有派生情况下,一个公有派生类的对象可以当作基类的对象使用,反之则禁止

  • 派生类的对象可以赋值给基类对象
  • 派生类的对象可以初始化基类的引用
  • 指向基类的指针也可以指向派生类
  • 通过基类对象名、指针只能使用基类继承的成员

没必要去死记硬背,根据理解就是允许派生类指向基类的操作(因为基类能做的派生类不能做),但是基类指向派生类的操作部分可以(如果派生类需要的但是基类给不了那就不行)

示例一

1
2
3
Circle c(2,3,4);
Point p;
p = c;

编译器允许用派生类赋值基类,因为基类有的成员派生类肯定有

示例二

1
2
3
Point p(2,3);  
Circle c;
c = p;

编译器报错,派生类 c 并不能根据基类 p 确定所有的成员值

示例三

1
2
Circlr c(2,3,4);  
Point &rp = c;

编译器允许基类引用派生类,但是只能访问基类成员(部分数据和函数),因为其他的成员引用类型限制了它访问

示例四

1
2
Circle c(2,3,4);
Point *pp=&c;

编译器允许基类指针指向派生类,但是只能访问基类成员(部分数据和函数),因为其他的成员指针类型限制了它访问

类型转换

static_cast

pp 和 ppc 的转换可以通过编译,但运行时会出现崩溃。static_cast 在继承体系中由基类这种转换为子类指针是不安全的

1
2
3
4
5
6
Circle c(2,3,4); //派生类
Point p(2,3); //基类
Point *pp=&c; //基类指针可指向派生类对象
Circle *pc=static_cast<Circle *>(pp);
Circle *ppc=static_cast<Circle *>(&p);
int i=static_cast<int>(3.2);

dynamic_cast

dynamic_cast 是一种运行时类型转换,可以转换指针或引用,用于继承体系中的类型转换。若转换失败,返回空指针或抛出异常(引用)

1
2
3
4
5
Circle c(2,3,4);
Point p(2,3);
Point *pp=&c; //基类指针可指向派生类对象
Circle *pc=dynamic_cast<Circle *>(pp);
Circle *ppc=dynamic_cast<Circle *>(&p);

const_cast 和 reinterpret_cast

const_cast 去除常量特性的类型转换,reinterpret_cast 执行任意类型转换且不执行任何类型检查,安全性最差

1
2
3
4
5
6
double *p=new double(3.4);
char *pc=reinterpret_cast<char *>(p);
char str[]{“hello”};
const char * cps=str;
char *ps=const_cast<char *>(cps);
ps[1]='t;

覆盖技术

在派生类中定义与基类同名的成员函数后会出现覆盖现象,实现重新定义基类成员函数,对象类型不同调用的同名函数实现也不同

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
const double PI = 3.14159;
class Point{ //基类
double x, y;
public:
Point(double i, double j){
x = i;
y = j;
}
double getArea() { return 0; }
};

class Circle : public Point //派生类
{
double radius;
public:
Circle(double a,double b,double r) : Point(a, b){
radius = r;
}
double getArea(){ //覆盖求面积函数
return PI * radius * radius;
}
};

int main()
{
Point a(1.5,6.7);
Circle c(1.5,6.7,2.5);
cout << "area of a:" << a.getArea() << endl;
cout << "area of c:" << c.getArea() << endl;
Point *p = &c; //指向的是派生类但是本质是基类,所以调用基类函数
cout << "area of c:" << p->getArea() << endl;
return 0;
}
/*output
*area of a:0
*area of c:19.6349
*area of c: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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Point
{
double x, y;
public:
Point(){
x = 0;y = 0;
}
Point(double a, double b){
x = a;y = b;
}
void setVal(double a, double b){
x = a;y = b;
}
void show(){
cout << x <<“,”<< y << endl;
}
};

class Circle : public Point
{
double radius;
public:
Circle(double a, double b, double r) : Point(a, b){
radius = r;
}
void setVal(double a, double b, double r){
Point::setVal(a, b);
radius = r;
}
void show(){
cout << radius <<",";
Point::show();
}
};

int main()
{
Circle c(3, 4, 5);
c.show();
c.setVal(5, 6, 7);
c.show();
c.Point::setVal(7, 8);
c.Point::show();
return 0;
}
/*output
*5,3,4
*7,5,6
*7,8
*/

另外要注意的是同一个类中两个重载版本的函数覆盖并不会起到想象中函数重载的效果,而是直接最后一个函数覆盖之前所有版本的同名函数

总结成一句话:父子类之间的同名函数参数签名不同不会形成重载

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
class Point
{
double x, y;
public:
Point(){
x = 0;y = 0;
}
Point(double a, double b){
x = a;y = b;
}
void setVal(double a, double b){
x = a;y = b;
}
void show(){
cout << x <<","<< y << endl;
}
};

class Circle : public Point
{
double radius;
public:
Circle(double a, double b, double r) : Point(a, b){
radius = r;
}
void setVal(double a, double b, double r){
Point::setVal(a, b);
radius = r;
}
void show(){
cout << radius <<",";
Point::show();
}
};

int main()
{
Circle c(3, 4, 5);
c.show();
c.setVal(5, 6, 7); //调用基类的setVal()
c.show();
c.setVal(7,8);c.setVal(7); //都是语法错误,子类的setVal覆盖了基类的所有版本的同名函数
c.Point::setVal(7, 8); //借助基类名前缀访问这是可以的
c.Point::show();
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Mammal
{
int age;
double weight;

public:
Mammal(int a, double w)
{
age = a;
weight = w;
}
void shout()
{
cout <<“I’m a mammal.\n”;
}
};

class Dog : public Mammal
{
public:
Dog(int a, double w) : Mammal(a, w) {}
void shout() { cout <<“woo.\n”; }
};

class Cat : public Mammal
{
public:
Cat(int a, double w) : Mammal(a, w) {}
void shout() { cout <<“meow.\n”; }
};

void shout(Mammal *p) //基类指针指向传入的对象
{
p->shout(); //调用相应的 shout()
}

int main()
{
Mammal m(3, 5);
Dog dog(4, 6);
Cat cat(4, 7);
shout(&m);
shout(&dog);
shout(&cat);
return 0;
}
/*output:
*I'm a mammal.
*I'm a mammal.
*I'm a mammal.
*/

很明显代码想做到基类指针根据指向对象的不同输出不同的结果。但是输出结果却都是 I'm a mammal.

因为定义的基类指针在编译后就成为了确定事实不会改变,调用自己的函数而不会动态的根据传入对象的结果调用不同的函数

勉强的一种解决办法就是通过对象分别调用各自的函数

1
2
3
4
5
6
7
8
9
10
int main()
{
Mammal m(3,5);
Dog dog(4,6);
Cat cat(4,7);
m.shout();
dog.shout();
cat.shout();
return 0;
}

但是当有上百种不同类型的对象的时候代码要写上百遍,真正解决的办法通过虚函数实现运行时的多态

虚函数

将想要实现运行动态的函数在基类中定义加上关键字 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
class Mammal
{
int age;
double weight;

public:
Mammal(int a, double w){
age = a;
weight = w;
}
virtual void shout(){
cout <<"I’m a mammal.\n";
}
};

//Dog类和Cat类保持不变
void shout(Mammal *p){
p->shout();
}

int main()
{
Mammal m(3, 5);
Dog dog(4, 6);
Cat cat(4, 7);
shout(&m);
shout(&dog);
shout(&cat);
return 0;
}
/*output:
* I'm a mammal.
* woo.
* meow.
*/

如此在基类指针调用函数的时候会根据不同的对象类型调用相应的类内的函数,实现了一个借口多种方法的效果,这是静态联编无法实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void shout(Mammal *p){
p->shout();
}

int main()
{
Mammal *p;
if (...)
p = new Dog(3, 4);
else
p = new Cat(5, 6);
shout(p); //生成的 p 不同输出也不同
delete p;
return 0;
}

虚函数说明

  • 虚函数实现动态性关键在于使用基类指针,当用基类指针指向不同对象时,到底调用哪个版本的成员函数取决于所指向对象的类型。如果指向 Dog 类对象,则调用 Dogshout(),反之指向 Cat 类对象就会调用 Cat 类的 shout()。如果指向 Mammal 类对象那么调用基类的 shout()
  • 用虚函数实现的多态性是代码执行过程中的多态,大大增加了程序的灵活性
  • 子类中覆盖的虚函数可以不加上 virtual 关键字,但是作为良好的习惯方便他人了解哪些是虚函数一般还是会加上 virtual 关键字

override 强制覆盖基类的方法

  • 只有当基类定义虚函数时子类中覆盖该方法才能形成多态的效果,如果子类中定义方法与基类同名但是参数不同并不能形成多态,只是子类中增加了一个新的方法
  • 为了防止忘记覆盖基类的虚函数,我们可以借助 C++ 11 的 override 关键字让编译器强制检查是否覆盖
1
2
3
4
5
class Sub : public Super
{
public :
virtual void method(int) override;
};

如果基类中没有定义 virtual void method(int) 的方法那么编译器会报错

寻根求源:静态多态性

静态联编中通过借助不同类型对象的实例来调用函数可以实现不同的输出结果,只能根据对象类型确定调用哪个函数

寻根求源:虚函数

虚函数指针(引用)能实现动态联编的效果其实是给因为编译器给每个类隐含生成了一个虚函数的虚函数表,它为虚函数的指针(引用)指明了调用函数的入口

对象访问其实相当于静态联编,下文有讲原因

以下面代码为例:B 和 C 都是 A 的派生类,其中 show()inc() 为虚函数,sub() 不是虚函数

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 A{
public:
int a;
A(int x) { a = x; }
virtual void show() { cout <<'a'<< endl; }
virtual void inc() { a++; }
void sub() { a--; } //非虚函数
};

class B : public A{
public:
int b;
B(int x, int y) : A(x) { b = y; }
virtual void show() { cout <<'b'<< endl; }
virtual void inc() { b++; }
void sub() { b--; }
};

class C : public A{
public:
int c;
C(int x, int y) : A(x) { c = y; }
virtual void show() { cout <<'c'<< endl; }
virtual void inc() { c++; }
void sub() { c--; }
};

编译器在编译的时候给虚函数创建了一个虚函数表,这个表格里面存放的是类的虚函数的入口地址

1
2
3
4
5
6
7
int main()
{
A aa(3);
B bb(4, 5);
C cc(6, 7);
return 0;
}

调用对象的函数的时候虚函数和非虚函数都相当于静态联编(虚函数通过虚函数表确定调用入口)

1
2
3
4
5
6
7
8
9
int main()
{
A aa(3);
A *p=&a;
p->show();
p->inc();
p->sub();
return 0;
}

基类指针指向派生类,虚函数通过派生类的虚函数表确定调用入口,非虚函数类调用指针类型的函数

1
2
3
4
5
6
7
8
9
int main()
{
B bb(4,5);
A *p=&bb;
p->show();
p->inc();
p->sub();
return 0;
}

基类指针指向派生类,虚函数通过派生类的虚函数表确定调用入口,非虚函数类调用指针类型的函数

1
2
3
4
5
6
7
8
9
int main()
{
B bb(4,5);
A *p=&bb;
p->show();
p->inc();
p->sub();
return 0;
}

虚析构函数

C++ 中规定某个类含有虚函数的时候,则应该将其析构函数设置为虚函数。否则容易出现内存泄漏的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Shape
{
double x,y;
public:
virtual ~Shape() {...}
virtual double getArea() {return 0; }
};

const double PI = 3.1415926;
class Circle : public Shape
{
double radius;
public:
Circle(double x, double y, double z)
: Shape(x, y) { radius = z; }
virtual double getArea(){
return PI * radius * radius;
}
virtual ~Circle() { ... }
};

纯虚函数

  • 纯虚函数是一种特殊的虚函数,在基类中声明为虚函数,但不提供实现部分,而要求各派生类提供该虚函数的不同版本实现

纯虚函数只有声明没有实现!由于基类不提供定义,所以和虚函数不同,这个派生类不定义函数编译器是会报错的

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
class Shape{
double x, y; // 基点坐标
public:
Shape(double a, double b){
x = a;
y = b;
}
virtual ~Shape() {}
virtual double getArea() = 0;
};
const double PI = 3.1415926;
class Circle : public Shape{
double radius;
public:
Circle(double x, double y, double z)
: Shape(x, y) { radius = z; }
virtual ~Circle() {}
virtual double getArea()
{
return PI * radius * radius;
}
};
class Rectangle : public Shape{
double length, width;
public:
Rectangle(double x, double y, double z, double w) : Shape(x, y){
length = z;
width = w;
}
virtual ~Rectangle() {}
virtual double getArea() { return width * length; }
};
double calArea(Shape &sh){
return sh.getArea();
}
int main()
{
Circle c(3, 4, 5);
cout <<"Circle area :"{<< calArea(c) << endl;
Rectangle r(3, 4, 5, 6);
cout <<"Rectangle area :"<< calArea(r) << endl;
}
/*output:
*Circle area:78.5398
*Rectangle area:30
*/

纯虚类

  • 凡是含有纯虚函数的类称为抽象类。抽象类往往描述的是一般抽象概念,如形状类、动物类,其中的纯虚函数如 getArea() 没有实际意义,不能提供实现代码。要求派生类如 Circle 类提供自己版本的 getArea 实现

  • C++ 规定,不能在内存中创建抽象类对象,无论是定义抽象类对象、作为形参或返回值,还是动态创建抽象类对象都是非法的。但可以定义一个抽象类指针(引用),并用该指针指向不同的派生类对象,以实现多态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
double calArea(Shape *p)   //可以用纯虚函数类型的指针
{
return p->getArea();
}

int main()
{
Circle c(3,4,5);
cout<<calArea(&c)<<endl;
Rectangle r(3,4,5,6);
cout<<calArea(&r)<<endl;
//Shape sh(3,4); 错的,不能实例化纯虚函数
return 0;
}