C++继承

文章目录

引言

这篇博客本来在上个月就应该发出来,但是中间由于各种原因,而一拖再拖,今天也终于完成了这篇博客,
各位看官请好好欣赏吧

继承的概念与定义

继承是对代码进行复用,是类设计层次的复用,让我们的子类可以使用父类的代码,减少代码的冗余提高效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class strudent
{
string name;
string id;
string address;
//...
//独自有的
//学院,专业,宿舍楼
}

class teacher
{
string name;
string id;
string address;
//...
//独自有的
//职称,科目
}

我们会发现这虽然这是两种不同的类,但是有很多信息是重复冗余的
所以我们完全可以定义个父类,来保存共有的信息

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class person
{
string name;
string id;
string address;
}
继承person的
class student:public person
{
private:
//学院,专业,宿舍楼
}
class teacher:public person
{
private:
//职称,科目
}

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
#include<iostream>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和
//Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。
//调用Print可以看到成员函数的复用。
//Person里的成员变量和成员函数都被包含了进去
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();//因为没有二义性,所以这里的print就是父类的
t.Print();//如果有二义性,只会是用子类的,否则要显示调用父类
return 0;
}

继承的模板

1
class student :public person

继承的定义

class默认是公有继承(但是我们最好把继承方式写上)

  1. 格式
    class student :public person
    student是派生类,public是继承方式,person是基类
  2. 继承关系与访问限定符
    继承方式
    :public继承,protect继承,private继承
    访问限定符
    :public访问,protect访问,private访问

在这里插入图片描述

  1. 基类的private成员在派生类中无论以什么方式继承都是不可以见到的

数据继承下来了,在那个地方但是用不上,不可以见的意思是
:继承下来之后,不管是在类的里面还是在类的外面都不可以访问他
继承:简单粗暴,直接全部继承下来,但是无论如何都永不上这个成员的

不想给子类用就定义成private,想给子类用就定义成public或者protected

  1. 取访问限定符和访问方式中的小的那一个
    public>protect>private

  2. public和protect和private的区别
    public想让别人可以直接使用
    protect不想要让别人直接使用,但是想要让子类能够使用
    private不想让任何人使用,包括子类

保护和私有在父类中没有区别

4.在实际使用的过程中,一般都是public继承,几乎很少private和protect,不提倡private和protect继承,
在类里面基本都使用protect和public,几乎不使用private

常见的继承
父类成员:公有和保护
子类继承方式:公有继承

基类和派生类对象赋值转换

(赋值兼容规则)
我们以前学过,如果是同一种类型就可以直接进行赋值,如果不是同一种类型可以实现显示类型访问,

如果我们想要子类给父类

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
class person
{
protected:
string _sex;
string _name;
int _age;
};

class student : public person
{
public:
int _no;
};

int main()
{
person p;
student s;
//父类=子类赋值兼任-》切割,切片
//只有public继承可以
//把子类里面父类的那一部分切割过去,给父类
//父类的指针和父类的引用
//这里不存在类型转换,是语法天然支持的行为
p=s;
//父类不能给子类,因为
person*ptr=&s;//指针只能得到其中的父类的部分
person& ref=s;//变成子类对象当中父类对象的别名
return 0;
}

继承中的作用域

  1. 在继承体系中基类和派生类偶有独立的作用域

子类和父类出现同名成员:隐藏/重定义,子类会隐藏父类的同名成员
如果有同一名字的话,并不是构成函数重载(重载是在同一个作用域里面才可以称之为重载)
只要函数名相同就构成隐藏,参数的相同与否无所谓,不影响
最好不用定义同名的成员函数和同名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

class student : public person
{
public:
int _no = 1; //ton
void Print()
{
cout << _no; //默认访问自己的
//现在想要访问父类的
cout << person::_no << endl; //访问了父亲的
}
};

int main()
{
person p;
student s;
s.Print(); //同名变量就近原则,会打印出Student
s.person::Print(); //指定调用person类域的,这样就可以用了

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
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
126
127
128
129
130
131
132
133
134

#include <string>
#include<iostream>
using namespace std;

class Person //父类
{
public:
Person(const char *name /*="peter"*/) //构造函数
: _name(name) //初始化列表,对值进行初始化
{
cout << "person()" << endl;
}
Person(const Person &s) //拷贝构造
: _name(s._name)
{
cout << "person(const person& s)" << endl;
}
Person &operator=(const Person &s) //赋值相等
{
cout << "Person=" << endl;
if (this != &s)
{
_name = s._name;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}

protected:
string _name;
};

class Student : public Person //公有继承,子类
{
public:
//父类没有默认构造函数,我们自己写
Student(const char *s = "zhans", int num = 1)//给一个缺省值
: Person(s) //调用默认构造,父类也是我们调用,不同于析构
,
_num(num)
{}

Student(const Student &s)
//把s当中父类的那一部分取出来
: Person(s) //我们可以直接传,他会切片,所以不用担心,这是一个天然的部分
//这个之后会变成父类当中的别名
,
_num(s._num)
{
}

// s2=s1;
Student &operator=(Student &s)
{
//赋值和拷贝构造类似,只不过他们是已经存在的对象进行赋值
if (this != &s)
{
_num = s._num;

// operator=(s);//调用父类的赋值运算,所以这里父类切的就是父类的那一部分,没有什么问题
//上面的操作会无限递归下去,死循环
Person::operator=(s); //我们指定一下就可以了
}
}

//析构函数的名字,会被统一处理成destructor()(至于为什么会这样,多态的时候我们就会讲解)
//所以就会被构成隐藏
#if 0
//这段代码在vscode里面无法通过
~Student()
{
//Person::~Person();//这里要指定作用域才可以调用,,
//我们指定了父类
//调用父类的
//delete[] p;
//我们这里会发现调用两次析构,
//我们子类先执行
}
//子类的析构函数不需要我们去显示调用,因为会在子类析构的时候自动调用父类的析构
//我们初始化的时候,父类先构造,子类再构造,析构的时候子类先析构,父类再析构
//所以我们实现子类析构函数的时候,不需要显示调用父类的析构函数,



#endif


private:
int _num = 1; //这里不是初始化,只是给了一个缺省值
// string _s="dasd";
// int *p=new int[10];
};

//派生类的重点4个默认成员函数,我们不写,编译器会默认生成会干些什么呢
//如果我们要写,要做些什么呢

// 我们不写,默认生成的派生类的构造和析构
// a。父类继承下来的 (调用父类的默认构造和析构处理) b。自己的(内置类型和自定义类型成员)(跟普通类是一样的)

//我们不写默认生成的拷贝构造和operator =
// a。父类继承下来的(调用父类的拷贝构造和operator=),完成值拷贝和浅拷贝 b。自己的(内置类型和自定义类型成员)(跟普通类是一样的)
/*

总结:
继承下来的调用父类处理,自己的按普通类基本规则
如果要我们自己处理呢,该如何去写,什么情况要自己写

父类的成员调用父类的对应构造,拷贝构造,operator=和析构处理
自己的成员按需求处理(普通类处理)

1.父类没有默认构造,我们要自己写
2.如果自己的子类有资源要进行释放,就需要我们自己写析构
3.如果子类有存在深拷贝问题,就需要我们自己实现拷贝构造和赋值



4.友元关系不能继承,父类的朋友,不一定是子类的朋友,所以不能访问他的私有成员,
5.继承与静态成员,静态成员无论继承怎么样,静态成员都是一样的,static count,继承都是一样样的,地址也是一样的,只有一份,


*/
int main()
{
//一个类如果我们不写,我们会不会自动生成一个
Student s;
// Student s1(s); //拷贝构造
// Student s3("jack", 18);
// s1 = s3;
return 0;
}

复杂的菱形继承与

多继承就是一个坑,

单继承:一个类只有一个直接父类

多继承,一个类有多个直接父类
在这里插入图片描述
菱形继承是多继承的一种特殊情况,多继承没问题,只不过要避免菱形继承
在这里插入图片描述
存在数据冗余和二义性
里面有两份person

先继承前面的,再继承后面的

在这里插入图片描述
A一般叫做虚基类
再D里面,A放到一个公共的位置,那么有时候B需要找A,C 需要找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
26
27
28
29
30
31
32
33
34
35
class A
{
public:
int _a;
};

class B :virtual public A
{
public:
int _b;
};

class C :virtual public A
{
public:
int _c;
};

class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;//虚继承就弄成一起的地方了,
d.C::_a = 2;
d._b = 4;
d._c = 5;
d._d = 6;

return 0;
}

我们尽量不要定义出菱形继承,虚继承

继承的总结和反思

c++的缺陷

  1. 没有垃圾回收器
  2. 多继承

继承和组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//继承
//继承是为了复用代码
class a
{}
class b:public a
{}
//组合

class c
{
int_c
}

class d
{
C _obj;//这样也是一种复用,开一个C类型的变量
int _d
}
  1. public继承是一种is_a 的关系,也就是说每个派生类的对象都是一种基类对象,b就是一个a

Student 和Person的关系就适合用继承 ,Student is Person

  1. 组合是一种has_a的关系,b组合了A,那么就是说b里面有一个a对象

眼睛和头的关系,就适合用组合,头上有眼睛
车和轮胎的关系,车上有轮胎

  1. 如果它既是is_a 又可以是has_a,那么优先使用has_a(组合)

继承是白箱服用(能看到它的实现细节),组合是黑箱服用(看不见它的实现细节),所以黑盒测试
除了父类的私有成员,其他子类都是可以进行使用的,这样会破坏基类的封装,子类可能会调用父类的公有成员和保护成员,D只能用C 的公有,不能

类和类之间:低耦合,高内聚(跟我没关系的东西,不要设计进来),
类和类之间 的依赖程序低,方便我们维护,所以组合的耦合程度低,
组合下来,保护的成员也是无法使用的

组合关系之间依赖关系小,关联程度低,修改很方便,可维护性强

切割和切片就是继承的好处
而多态就是建立在继承的基础之上,


C++继承
http://example.com/2022/05/05/C++继承/
作者
Zevin
发布于
2022年5月5日
许可协议