继承的概念和意义

继承的概念

面向对象中的继承指类之间的父子关系

  • 子类拥有父类的所有属性和行为
  • 子类就是一种特殊的父类
  • 子类对象可以当作父类对象使用
  • 子类中可以添加父类没有的方法和属性
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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
int mv;
public:
Parent()
{
cout << "Parent()" << endl;
mv = 100;
}
void method()
{
cout << "mv = " << mv << endl;
}
};

class Child : public Parent
{
public:
void hello()
{
cout << "I'm Child calss!" << endl;
}
};

int main()
{
Child c;

c.hello();
c.method();

return 0;
}
1
2
3
4
5
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
Parent()
I'm Child calss!
mv = 100

重 要 规 则

  • 子类就是一个特殊的父类
  • 子类对象可以直接初始化父类对象
  • 子类对象可以直接赋值给父类对象

继承的意义

继承是 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
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <iostream>
#include <string>

using namespace std;

class Memory
{
public:
Memory()
{
cout << "Memory()" << endl;
}
~Memory()
{
cout << "~Memory()" << endl;
}
};

class Disk
{
public:
Disk()
{
cout << "Disk()" << endl;
}
~Disk()
{
cout << "~Disk()" << endl;
}
};

class CPU
{
public:
CPU()
{
cout << "CPU()" << endl;
}
~CPU()
{
cout << "~CPU()" << endl;
}
};

class MainBoard
{
public:
MainBoard()
{
cout << "MainBoard()" << endl;
}
~MainBoard()
{
cout << "~MainBoard()" << endl;
}
};

class Computer
{
Memory mMem;
Disk mDisk;
CPU mCPU;
MainBoard mMainBoard;
public:
Computer()
{
cout << "Computer()" << endl;
}
void power()
{
cout << "power()" << endl;
}
void reset()
{
cout << "reset()" << endl;
}
~Computer()
{
cout << "~Computer()" << endl;
}
};

class HPBook : public Computer
{
string mOS;
public:
HPBook()
{
mOS = "Windows 8";
}
void install(string os)
{
mOS = os;
}
void OS()
{
cout << mOS << endl;
}
};

class MacBook : public Computer
{
public:
void OS()
{
cout << "Mac OS" << endl;
}
};

int main()
{
HPBook hp;

hp.power();
hp.install("Ubuntu 16.04 LTS");
hp.OS();

cout << endl;

MacBook mac;

mac.OS();

return 0;
}

组合和继承的混合使用

小结

  • 继承:是面向对象中类之间的一种关系
  • 子类拥有父类的所有属性和行为
  • 子类对象可以当作父类对象使用
  • 子类中可以添加父类没有的方法和属性
  • 继承是面向对象中代码复用的重要手段

继承的访问级别

子类是否可以直接访问父类的私有成员?

根据面向对象理论 :

image-20220226132132526

根 据 语 法 :

image-20220226132149106

  • 面向对象中的访问级别不只是 public 和 private
  • 可以定义 protected 访问级别
  • 关 键 字 protected 的意义
    修饰的成员不能被外界直接访问
    修饰的成员可以被子类直接访问
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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
protected:
int mv;
public:
Parent()
{
mv = 100;
}

int value()
{
return mv;
}
};

class Child : public Parent
{
public:
int addValue(int v)
{
mv = mv + v;
}
};

int main()
{
Parent p;

cout << "p.mv = " << p.value() << endl;

// p.mv = 1000; // error

Child c;

cout << "c.mv = " << c.value() << endl;

c.addValue(50);

cout << "c.mv = " << c.value() << endl;

// c.mv = 10000; // error

return 0;
}

为什么面向对象需要protected?
image-20220226133402642

不同的继承方式

C+ + 中支持三种不同的继承方式
1.public 继承
父类成员在子类中保持原有访问级别

2.private 继承
父类成员在子类中变为私有成员

3.protected继承
父类中的公有成员变为保护成员 , 其它成员保持不变

image-20220226141141606

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
71
72
73
74
75
76
#include <iostream>
#include <string>

using namespace std;

class Parent
{
protected:
int m_a;
protected:
int m_b;
public:
int m_c;

void set(int a, int b, int c)
{
m_a = a;
m_b = b;
m_c = c;
}
};

class Child_A : public Parent
{
public:
void print()
{
cout << "m_a" << m_a << endl;
cout << "m_b" << m_b << endl;
cout << "m_c" << m_c << endl;
}
};

class Child_B : protected Parent
{
public:
void print()
{
cout << "m_a" << m_a << endl;
cout << "m_b" << m_b << endl;
cout << "m_c" << m_c << endl;
}
};

class Child_C : private Parent
{
public:
void print()
{
cout << "m_a" << m_a << endl;
cout << "m_b" << m_b << endl;
cout << "m_c" << m_c << endl;
}
};

int main()
{
Child_A a;
Child_B b;
Child_C c;

a.m_c = 100;
// b.m_c = 100; // Child_B 保护继承自 Parent, 所以所有的 public 成员全部变成了 protected 成员, 因此外界无法访问
// c.m_c = 100; // Child_C 私有继承自 Parent, 所以所有的成员全部变成了 private 成员, 因此外界无法访问

a.set(1, 1, 1);
// b.set(2, 2, 2);
// c.set(3, 3, 3);

a.print();
b.print();
c.print();

return 0;
}

遗憾的事实

  • 一般而言 , C++工程项目中只使用 public 继承。
  • C + + 的派生语言只支持一种继承方式( public 继承 )
  • protected 和 private 继承带来的复杂性远大于实用性(太过鸡肋,鸡肋尚且食之有味,)

继承中的构造和析构

  • 子类中可以定义构造函数
  • 子类构造函数
    必须对继承而来的成员进行初始化
       1.直接通过初始化列表或者赋值的方式进行初始化
       2.调用父类构造函数进行初始化  
    

父类构造函数在子类中的调用方式

  • 默认调用
    适用于无参构造函数和使用默认参数的构造函数
  • 显示调用
    通过初始化列表进行调用
    适用于所有父类构造函数
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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
Parent(string s)
{
cout << "Parent(string s) : " << s << endl;
}
};

class Child : public Parent
{
public:
Child()
{
cout << "Child()" << endl;
}
Child(string s)
{
cout << "Child(string s) : " << s << endl;
}
};

int main()
{
Child c;
Child cc("cc");

return 0;
}

先观察这个程序代码,子类Child中有两个构造函数,但是Child()和Child(string s)都没有显示调用父类的任何构造函数,因此这两个子类的构造函数将会隐式的调用父类无参构造函数和使用默认参数的构造函数
而观察父类并没有无参构造函数或使用默认参数的构造函数。因此编译器将会报错。
解决方法是1.Parent类中添加一个无参构造函数或者2.给Parent(string s)提供一个默认参数,比如Parent(string s = "parent")

而如果子类中Child(string s) : Parent(s)构造函数显示的调用父类的这个 Parent(s)构造函数,那么父类这个 Parent(s)将会执行。

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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
Parent()
{
cout << "Parent()" << endl;
}
Parent(string s)
{
cout << "Parent(string s) : " << s << endl;
}
};

class Child : public Parent
{
public:
Child()
{
cout << "Child()" << endl;
}
Child(string s) : Parent(s)
{
cout << "Child(string s) : " << s << endl;
}
};

int main()
{
Child c;
Child cc("cc");

return 0;
}
1
2
3
4
5
fengyun@ubuntu:~/share$ ./test
Parent()
Child()
Parent(string s) : cc
Child(string s) : cc

构造规则

  • 子类对象在创建时会首先调用父类的构造函数
  • 先执行父类构造函数再执行子类的构造函数
  • 父类构造函数可以被隐式调用或者显示调用

对象创建时构造函数的调用顺序

  1. 调用父类的构造函数
  2. 调用成员变量的构造函数
  3. 调用类自身的构造函数

口诀心法 :先父母 , 后客人 , 再自己。

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
#include <iostream>
#include <string>

using namespace std;

class Object
{
public:
Object(string s)
{
cout << "Object(string s) : " << s << endl;
}
};

class Parent : public Object
{
public:
Parent() : Object("Default")
{
cout << "Parent()" << endl;
}
Parent(string s) : Object(s)
{
cout << "Parent(string s) : " << s << endl;
}
};

class Child : public Parent
{
Object mO1;
Object mO2;
public:
Child() : mO1("Default 1"), mO2("Default 2")
{
cout << "Child()" << endl;
}
Child(string s) : Parent(s), mO1(s + " 1"), mO2(s + " 2")
{
cout << "Child(string s) : " << s << endl;
}
};

int main()
{
Child cc("cc");

return 0;
}

在前述代码的基础上我们添加一个Object类作为Parent的父类,并且子类Child中有两个成员变量是Object类型,那么为了代码能够正常运行
1.我们必须首先在parent类中处理好显示或隐式的调用Object类的构造函数
2.处理完parent类后又必须在child类中处理好Obejct类成员变量的初始化
3.main函数中调用Child构造函数,查看各个类的构造顺序

1
2
3
4
5
6
7
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
Object(string s) : cc
Parent(string s) : cc
Object(string s) : cc 1
Object(string s) : cc 2
Child(string s) : cc

结果分析:
Child cc(“cc”)首先处理父类的构造函数Parent(string s) : Object(s),而parent也先要处理parent的父类Object的构造函数Object(string s)。
处理完父母后,处理两个成员变量mO1,mO2的构造函数Object(string s)。
处理完客人后,处理自己,调用Child(string s)

析构顺序

析构函数的调用顺序与构造函数相反

  1. 执行自身的析构函数
  2. 执行成员变量的析构函数
  3. 执行父类的析构函数
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>
#include <string>

using namespace std;

class Object
{
string ms;
public:
Object(string s)
{
cout << "Object(string s) : " << s << endl;
ms = s;
}
~Object()
{
cout << "~Object() : " << ms << endl;
}
};

class Parent : public Object
{
string ms;
public:
Parent() : Object("Default")
{
cout << "Parent()" << endl;
ms = "Default";
}
Parent(string s) : Object(s)
{
cout << "Parent(string s) : " << s << endl;
ms = s;
}
~Parent()
{
cout << "~Parent() : " << ms << endl;
}
};

class Child : public Parent
{
Object mO1;
Object mO2;
string ms;
public:
Child() : mO1("Default 1"), mO2("Default 2")
{
cout << "Child()" << endl;
ms = "Default";
}
Child(string s) : Parent(s), mO1(s + " 1"), mO2(s + " 2")
{
cout << "Child(string s) : " << s << endl;
ms = s;
}
~Child()
{
cout << "~Child() " << ms << endl;
}
};

int main()
{
Child cc("cc");

cout << endl;

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
fengyun@ubuntu:~/share$ ./test
Object(string s) : cc
Parent(string s) : cc
Object(string s) : cc 1
Object(string s) : cc 2
Child(string s) : cc

~Child() cc
~Object() : cc 2
~Object() : cc 1
~Parent() : cc
~Object() : cc

小结

  • 子类对象在创建时需要调用父类构造函数进行初始化
  • 先执行父类构造函数然后执行成员的构造函数
  • 父类构造函数显示调用需要在初始化列表中进行
  • 子类对象在销毁时需要调用父类析构函数进行清理
  • 析构顺序与构造顺序对称相反

同名覆盖

子类中是否可以定义父类中的同名成员?如果可以,如何区分?如果不可以,为什么?

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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
int mi;
};

class Child : public Parent
{
public:
int mi;
};

int main()
{
Child c;

c.mi = 100; // mi 是从父类继承得到的还是子类自己定义的

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
#include <iostream>
#include <string>
using namespace std;

class Parent
{
public:
int mi;

Parent()
{
cout << "Parent() : " << "&mi = " << &mi << endl;
}
};

class Child : public Parent
{
public:
int mi;

Child()
{
cout << "Child() : " << "&mi = " << &mi << endl;
}
};

int main()
{
Child c;

c.mi = 100;

c.Parent::mi = 1000;

cout << "&c.mi = " << &c.mi << endl;
cout << "c.mi = " << c.mi << endl;

cout << "&c.Parent::mi = " << &c.Parent::mi << endl;
cout << "c.Parent::mi = " << c.Parent::mi << endl;

return 0;
}

C++允许同名变量的存在的原因是因为两个同名变量的作用域不同

1
2
3
4
5
6
7
8
fengyun@ubuntu:~/share$ g++ test.cpp -o test
fengyun@ubuntu:~/share$ ./test
Parent() : &mi = 0x7ffd055d0f10
Child() : &mi = 0x7ffd055d0f14
&c.mi = 0x7ffd055d0f14
c.mi = 100
&c.Parent::mi = 0x7ffd055d0f10
c.Parent::mi = 1000

观察结果得知,c.mi默认访问子类中的mi;只有通过作用域访问符::才可访问父类的mi,c.parent::mi

函数命名冲突

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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
int mi;

void add(int v)
{
mi += v;
}

void add(int a, int b)
{
mi += (a + b);
}
};

class Child : public Parent
{
public:
int mi;

};

int main()
{
Child c;

c.mi = 100;

c.Parent::mi = 1000;

cout << "c.mi = " << c.mi << endl;

cout << "c.Parent::mi = " << c.Parent::mi << endl;

c.add(1);
c.add(2, 3);

cout << "c.mi = " << c.mi << endl;

cout << "c.Parent::mi = " << c.Parent::mi << endl;

return 0;
}
1
2
3
4
5
fengyun@ubuntu:~/share$ ./test
c.mi = 100
c.Parent::mi = 1000
c.mi = 100
c.Parent::mi = 1006

结果分析:子类中并没有定义add函数,但是c.add()调用了父类中的add函数,并且最后父类中的mi完成了累加add()函数,子类中的mi的值并没有发生变化。
这是因为父类的成员变量mi作用域两个大括号之间,父类add看到的mi只能是父类的mi,

接着我继续在子类中添加一个add(int a,int b,int c)试图实现重载,main函数中添加语句c.add(1,2,3)

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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
int mi;

void add(int v)
{
mi += v;
}
void add(int a, int b)
{
mi += (a + b);
}
};

class Child : public Parent
{
public:
int mi;
void add(int a, int b,int c)
{
mi += (a + b + c);
}
};

int main()
{
Child c;

c.mi = 100;

c.Parent::mi = 1000;

cout << "c.mi = " << c.mi << endl;

cout << "c.Parent::mi = " << c.Parent::mi << endl;

c.add(1);
c.add(2, 3);
c.add(1, 2, 3);

cout << "c.mi = " << c.mi << endl;

cout << "c.Parent::mi = " << c.Parent::mi << endl;

return 0;
}

然而编译器报错

image-20220227112421892

我的试图重载失败了,这是因为不符合重载的条件—–函数重载必须发生在同一作用域中

类中的成员函数可以进行重载

  1. 重载函数的本质为多个不同的函数
  2. 函数名和参数列表是唯一的标识
  3. 函数重载必须发生在同一作用域中

解决方案是用作用域访问符::,main函数中进行修改

1
2
3
4
5
6
7
c.add(1);
c.add(2, 3);
c.add(1, 2, 3);
//修改为
c.Parent::add(1);
c.Parent::add(2, 3);
c.add(1, 2, 3);

结论:

  1. 子类中的函数将隐藏父类的同名函数
  2. 子类无法重载父类中的成员函数
  3. 使用作用域分辨符访问父类中的同名函数
  4. 子类可以定义父类中完全相同的成员函数

同名覆盖引发的问题

子类对象可以当作父类对象使用( 兼容性 )

  • - 子类对象可以直接赋值给父类对象
  • - 子类对象可以直接初始化父类对象
  • - 父类指针可以直接指向子类对象
  • - 父类引用可以直接引用子类对象
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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
int mi;

void add(int i)
{
mi += i;
}

void add(int a, int b)
{
mi += (a + b);
}
};

class Child : public Parent
{
public:
int mv;

void add(int x, int y, int z)
{
mv += (x + y + z);
}
};

int main()
{
Parent p;
Child c;

p = c;

Parent p1(c);


Parent& rp = c;
Parent* pp = &c;

rp.mi = 100;
rp.add(5); // 没有发生同名覆盖?
rp.add(10, 10); // 没有发生同名覆盖?

/* 为什么编译不过? */
// pp->mv = 1000;
// pp->add(1, 10, 100);

return 0;
}

观察这一段程序,父类Parent中定义了两个add函数void add(int i)void add(int a, int b),子类中定义了一个add函数void add(int x, int y, int z)和一个新变量int mv;
根据之前的知识,我们定义一个子类Child c;c.add()是只能访问子类的中的add函数无法访问父类中的add函数。

而编译运行却发现47行48行同名覆盖似乎不见了?
51行,52行pp指针竟然找不到子类的mv成员和add函数?

当使用父类指针( 引用 ) 指向子类对象时

  • - 子类对象退化为父类对象
  • - 只能访问父类中定义的成员
  • - 可以直接访问被子类覆盖的同名成员

特殊的同名函数-重写

  • 子类中可以重定义父类中已经存在的成员函数
  • 这种重定义发生在继承中 , 叫做函数重写
  • 函数重写是同名覆盖的一种特殊情况

image-20220227121149772

当函数重写遇上赋值兼容会发生什么?

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
#include <iostream>
#include <string>

using namespace std;

class Parent
{
public:
void print()
{
cout << "I'm Parent." << endl;
}
};

class Child : public Parent
{
public:
void print()
{
cout << "I'm Child." << endl;
}
};

void how_to_print(Parent* p)
{
p->print();
}

int main()
{
Parent p;
Child c;

p.print();
c.print();
cout << "----------------------------------" <<endl;

how_to_print(&p); // Expected to print: I'm Parent.
how_to_print(&c); // Expected to print: I'm Child.

return 0;
}
1
2
3
4
5
6
fengyun@ubuntu:~/share$ ./test
I'm Parent.
I'm Child.
----------------------------------
I'm Parent.
I'm Parent.

问题分析

  • - 编译期间,编译器只能根据指针的类型判断所指向的对象
  • - 根据赋值兼容,编译器认为父类指针指向的是父类对象
  • - 因此,编译结果只可能是调用父类中定义的同名函数

在编译这个函数的时候,编译器不可能知道指针 P 究竟指向了什么。但是编译器没有理由报错。 于是,编译器认为最安全的做法是调用父类的 print 函数,因为父类和子类肯定都有相同的 print 函数。