引言
这篇博客本来在上个月就应该发出来,但是中间由于各种原因,而一拖再拖,今天也终于完成了这篇博客,
各位看官请好好欣赏吧
继承的概念与定义
继承是对代码进行复用,是类设计层次的复用,让我们的子类可以使用父类的代码,减少代码的冗余提高效率
class strudent
{
string name;
string id;
string address;
//...
//独自有的
//学院,专业,宿舍楼
}
class teacher
{
string name;
string id;
string address;
//...
//独自有的
//职称,科目
}
我们会发现这虽然这是两种不同的类,但是有很多信息是重复冗余的
所以我们完全可以定义个父类,来保存共有的信息
如:
class person
{
string name;
string id;
string address;
}
继承person的
class student:public person
{
private:
//学院,专业,宿舍楼
}
class teacher:public person
{
private:
//职称,科目
}
#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;
}
继承的模板
class student :public person
继承的定义
class默认是公有继承(但是我们最好把继承方式写上)
- 格式
class student :public person
student是派生类,public是继承方式,person是基类 - 继承关系与访问限定符
继承方式
:public继承,protect继承,private继承
访问限定符
:public访问,protect访问,private访问
- 基类的private成员在派生类中无论以什么方式继承都是不可以见到的
数据继承下来了,在那个地方但是用不上,不可以见的意思是
:继承下来之后,不管是在类的里面还是在类的外面都不可以访问他
继承:简单粗暴,直接全部继承下来,但是无论如何都永不上这个成员的
不想给子类用就定义成private,想给子类用就定义成public或者protected
-
取访问限定符和访问方式中的小的那一个
public>protect>private -
public和protect和private的区别
public想让别人可以直接使用
protect不想要让别人直接使用,但是想要让子类能够使用
private不想让任何人使用,包括子类
保护和私有在父类中没有区别
4.在实际使用的过程中,一般都是public继承,几乎很少private和protect,不提倡private和protect继承,
在类里面基本都使用protect和public,几乎不使用private
常见的继承
父类成员:公有和保护
子类继承方式:公有继承
基类和派生类对象赋值转换
(赋值兼容规则)
我们以前学过,如果是同一种类型就可以直接进行赋值,如果不是同一种类型可以实现显示类型访问,
如果我们想要子类给父类
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;
}
继承中的作用域
- 在继承体系中基类和派生类偶有独立的作用域
子类和父类出现同名成员:隐藏/重定义,子类会隐藏父类的同名成员
如果有同一名字的话,并不是构成函数重载(重载是在同一个作用域里面才可以称之为重载)
只要函数名相同就构成隐藏,参数的相同与否无所谓,不影响
最好不用定义同名的成员函数和同名成员
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;
}
派生类的默认成员函数
#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,就需要通过虚基表中的偏移量进行计算
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++的缺陷
- 没有垃圾回收器
- 多继承
继承和组合
//继承
//继承是为了复用代码
class a
{
}
class b:public a
{
}
//组合
class c
{
int_c
}
class d
{
C _obj;//这样也是一种复用,开一个C类型的变量
int _d
}
- public继承是一种is_a 的关系,也就是说每个派生类的对象都是一种基类对象,b就是一个a
Student 和Person的关系就适合用继承 ,Student is Person
- 组合是一种has_a的关系,b组合了A,那么就是说b里面有一个a对象
眼睛和头的关系,就适合用组合,头上有眼睛
车和轮胎的关系,车上有轮胎
- 如果它既是is_a 又可以是has_a,那么优先使用has_a(组合)
继承是白箱服用(能看到它的实现细节),组合是黑箱服用(看不见它的实现细节),所以黑盒测试
除了父类的私有成员,其他子类都是可以进行使用的,这样会破坏基类的封装,子类可能会调用父类的公有成员和保护成员,D只能用C 的公有,不能
类和类之间:低耦合,高内聚(跟我没关系的东西,不要设计进来),
类和类之间 的依赖程序低,方便我们维护,所以组合的耦合程度低,
组合下来,保护的成员也是无法使用的
组合关系之间依赖关系小,关联程度低,修改很方便,可维护性强
切割和切片就是继承的好处
而多态就是建立在继承的基础之上,