
个人主页:小则又沐风
个人专栏:<数据结构>
<竞赛专栏>
<Linux>
座右铭
路虽远,行则将至;事虽难,做则必成
目录
[2. 拷贝构造函数](#2. 拷贝构造函数)
前言
我们的C++学习了类和对象,但是在当时我们限定词仅仅讲解了privat和public并没有讲解protect
今天我们将会通过学习这个继承来了解这个知识
继承的概念
我们在生活中由两个事物是拥有相同的特性的,但是又有一些特殊的地方,当然在我们的代码中也是存在这种的情况的,就比如我们设计一个学生的类和老师的类
cpp
#include<iostream>
#include<cstring>
using namespace std;
class student
{
public:
void study()
{
//学习;
}
private:
string name;
int age;
string tel;
string address;
string studentid;//学号
};
class teacher
{
public:
void teaching()
{
//授课
}
private:
string name;
int age;
string tel;
string address;
string title;//职务
};
int main()
{
return 0;
}
现在我们可以发现这两个类的代码由高度相似的东西
这就是因为他们都是有人的特性啊,所以我们就可以让这个两个类继承一个名为人的类
cpp
class person
{
public:
void identy()
{
cout << "void identy()" << name << endl;
}
private:
string name="张三";
int age=18;
string tel;
string address;
};
class student : public person
{
void study()
{
}
private:
string studentid;
};
class teacher :public person
{
void teaching()
{
}
private:
string title;
};

这样的代码的设计就避免了我们写了许多的重复的代码了
但是这个继承到底是怎么使用的?怎么定义的?
下面我们来仔细了解一下
继承定义

现在我们就以这个继承的类作为代表来了解一下
这个teacher叫做派生类
然后冒号这个public叫做继承的方法(在这里是public继承)
然后这个person就是基类
现在我们的脑子中出现了新的代名词了什么是派生类,什么是基类,这个继承的方法有什么影响?
派生类:
简单的来说就是由一个现有的类,因为新定义的类和这个类有了许多的类似的特性
这个类可以把他的代码继承下来,这样生成的类就是派生类
基类:基类就是只要由一个新的类是我们的派生类的话,这个类就成为了这个类的基类
继承方法:继承的方法有三种
public
private
protected
不同的继承的方法将会影响派生类的相关的权限
这个基类和派生类不需要进行再过多的讲解就是一个关系
需要详细讲解的是继承的关系之间的不同
继承关系
我们知道在我们的基类中本来就有访问限定符
那么三种的访问的限定符再加上三种继承关系那么就会有9种组合了
我们来看下面的这一表

这样看起来有一点的复杂没关系我来拆解
我们可以看到访问限定符和这个继承关系组合的话最后在基类中得到的最后的结果是有规律的
-----就是最后的结果是限定最大的那一个
现在我们来总结一下
- 基类中的private的成员无论如何在派生类中都是不可见的(这一种的不可见不是派生类没有继承,只是我们没有权限来查看这些成员)
- 但是我们如果真的想在派生类中访问我们的private成员但是又不能把这个改成public(不想让外界访问)我们就可以把这些的成员改成protected这样在派生类中就可以访问但是在外界不能访问了
- 如果我们不写继承方法,默认的继承方法是private
cpp
class person
{
public:
void identy()
{
cout << "void identy()" << name << endl;
}
protected:
string name="张三";
int age=18;
string tel;
string address;
};
class student : person
{
void study()
{
}
private:
string studentid;
};
class teacher :public person
{
void teaching()
{
}
private:
string title;
};
int main()
{
teacher t;
student s;
t.identy();
s.identy();
return 0;

- 在平时代码的编写的时候我们尽量使用的是public继承的方法,尽量避免使用其他的继承方法
基类和派生类的转化
public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
但是基类是不可以给派生类复制的
cpp
class person
{
public:
void identy()
{
cout << "void identy()" << name << endl;
}
protected:
string name="张三";
int age=18;
string tel;
string address;
};
class student : public person
{
void study()
{
}
private:
string studentid;
};
class teacher :public person
{
void teaching()
{
}
private:
string title;
};
int main()
{
teacher t;
student s;
t.identy();
s.identy();
person p;
//派生类赋值基类
p = t;
//基类赋值派生类
t = p;
return 0;
}


cpp
int main()
{
teacher t;
student s;
t.identy();
s.identy();
person p;
//派生类赋值基类
p = t;
//基类赋值派生类
/*t = p;*/
person* ptr = &t;
person& pobj = s;
return 0;
}
当然派生类的指针和引用也是可以赋值给基类的
继承中的作⽤域
在继承体系中基类和派⽣类都有独⽴的作⽤域
派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)
cpp
class person
{
protected:
string _name = "张三";
int num = 100;
};
class student :public person
{
public:
void print()
{
cout << _name << endl;
cout << person::num << endl;
cout << num << endl;
}
private:
int num = 110;
};
int main()
{
student stu;
stu.print();
return 0;
}
在这里这个num就是构成隐藏的,因为他是在基类和派生类同时出现的成员
我们如果不去指定他的作用域的话,我们默认访问的就是在当前的类域中的数值
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
在相同的作用域的同名的函数是构成重载的但是在基类和派生类中出现相同名字的函数构成的就是隐藏了
cpp
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
}; class B : public A {
public:
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main() {
B b;
b.fun(10);
b.fun();
return 0;
}
在这里的两个fun函数构成的就是隐藏
这个代码运行起来是什么样子的呢????
我们来看看

在这里我们定义的是B类的对象但是在B类中的fun的函数时要求传一个参数的但是我们想要调用的是A的函数
这样就需要我们指定一下类域
b.A::fun();
这样就可以通过编译了
派⽣类的默认成员函数
我们知道的是每一个类都是有6个默认的成员函数的'
但是我们主要了解的其中的4个
1.默认构造函数:
派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤
cpp
class person
{
public:
person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}
protected:
string _name;
};
class student :public person
{
public:
student()
{
id = 10;
cout << "派生类构造函数" << endl;
}
private:
int id;
};
int main()
{
student s;
return 0;
}
在这里就是我们的基类的构造函数是一个默认构造
在上面说我们的基类的成员必须调用的是基类的构造函数来初始化现在我们来验证一下
cpp
class person
{
public:
person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}
protected:
string _name;
};
class student :public person
{
public:
student()
:id(12)
,_name("123")
{
id = 10;
cout << "派生类构造函数" << endl;
}
private:
int id;
};
int main()
{
student s;
return 0;
}
我们在派生类中的初始化列表中尝试把这个基类的对象进行初始化

编译是不通过的
当我们的基类的构造函数不是一个默认构造函数的时候我们应该这样来初始化基类的成员
cpp
class person
{
public:
/*person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}*/
person(string name)
{
_name = name;
}
protected:
string _name;
};
class student :public person
{
public:
student(string name,int id)
:person(name)
{
_id = id;
cout << "派生类构造函数" << endl;
}
private:
int _id;
};
int main()
{
student s("张三",12);
return 0;
}
必须在初始化列表中显示调用基类的构造函数
2. 拷贝构造函数
派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
cs
class person
{
public:
/*person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}*/
person(string name)
{
_name = name;
}
person(const person& per)
{
_name = per._name;
}
protected:
string _name;
};
class student :public person
{
public:
student(string name,int id)
:person(name)
{
_id = id;
cout << "派生类构造函数" << endl;
}
student(const student& stu)
:person(stu._name)
, _id(stu._id)
{
}
private:
int _id;
};
int main()
{
student s("张三",12);
student s2(s);
return 0;
}
3.赋值函数的重载
派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
cpp
class person
{
public:
/*person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}*/
person(string name)
{
_name = name;
}
person(const person& per)
{
_name = per._name;
}
person& operator=(const person& per)
{
if(this!=&per)
_name = per._name;
}
protected:
string _name;
};
class student :public person
{
public:
student(string name,int id)
:person(name)
{
_id = id;
cout << "派生类构造函数" << endl;
}
student(const student& stu)
:person(stu._name)
, _id(stu._id)
{
}
student& operator=(const student& stu)
{
if (this != &stu)
{
//指定类域
person::operator=(stu);
_id = stu._id;
}
}
private:
int _id;
};
int main()
{
student s("张三",12);
student s2(s);
return 0;
}
4.析构函数
派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派 ⽣类对象先清理派⽣类成员再清理基类成员的顺序
派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构
cpp
class person
{
public:
/*person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}*/
person(string name)
{
_name = name;
}
person(const person& per)
{
_name = per._name;
}
person& operator=(const person& per)
{
if(this!=&per)
_name = per._name;
}
~person()
{
cout << "~person" << endl;
}
protected:
string _name;
};
class student :public person
{
public:
student(string name,int id)
:person(name)
{
_id = id;
cout << "派生类构造函数" << endl;
}
student(const student& stu)
:person(stu._name)
, _id(stu._id)
{
}
student& operator=(const student& stu)
{
if (this != &stu)
{
person::operator=(stu);
_id = stu._id;
}
}
~student()
{
cout << "~student" << endl;
person::~person();
}
private:
int _id;
};
int main()
{
student s("张三",12);
/*tudent s2(s);*/
return 0;
}
注意在派生类中的析构函数中我调用了基类的析构函数

但是我们运行起来的话我们的基类进行了两次的析构函数
上面的执行结果告诉我们这个基类的析构函数会自己调用的
还有显示调用派生类的析构函数在调用基类的析构函数
我们不需要在基类中的析构函数中调用基类的析构函数
那么现在我们了解完了上面的知识后我们怎么才能创建出一个不可以被继承的类呢?
很简单就是我们知道的是每一个类都需要构造函数,但是派生类的基类的成员是必须调用基类的构造函数初始化的,利用这一点我们只需要把基类的构造函数设置成private派生类就不可以实现出构造函数了
继承与友元
在基类中的友元是不可以访问到派生类的私有和保护的
cpp
class person
{
public:
/*person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}*/
friend void print();
person(string name)
{
_name = name;
}
person(const person& per)
{
_name = per._name;
}
person& operator=(const person& per)
{
if(this!=&per)
_name = per._name;
}
~person()
{
cout << "~person" << endl;
}
protected:
string _name;
};
class student :public person
{
public:
student(string name,int id)
:person(name)
{
_id = id;
cout << "派生类构造函数" << endl;
}
student(const student& stu)
:person(stu._name)
, _id(stu._id)
{
}
student& operator=(const student& stu)
{
if (this != &stu)
{
person::operator=(stu);
_id = stu._id;
}
}
~student()
{
cout << "~student" << endl;
person::~person();
}
private:
int _id;
};
void print(student stu)
{
cout << stu._name << endl;
}
int main()
{
student s("张三",12);
/*tudent s2(s);*/
return 0;
}

基类的朋友不是派生类的朋友
继承与静态成员
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个static成员实例
cpp
class person
{
public:
/*person()
{
_name = "张三";
cout << "person(),默认构造函数基类" << endl;
}*/
/*friend void print();*/
person(string name)
{
_name = name;
}
person(const person& per)
{
_name = per._name;
}
person& operator=(const person& per)
{
if(this!=&per)
_name = per._name;
}
~person()
{
cout << "~person" << endl;
}
static int st;
protected:
string _name;
};
int person::st = 0;
class student :public person
{
public:
student(string name,int id)
:person(name)
{
_id = id;
cout << "派生类构造函数" << endl;
}
student(const student& stu)
:person(stu._name)
, _id(stu._id)
{
}
student& operator=(const student& stu)
{
if (this != &stu)
{
person::operator=(stu);
_id = stu._id;
}
}
~student()
{
cout << "~student" << endl;
}
private:
int _id;
};
//void print(student stu)
//{
// cout << stu._name << endl;
//}
int main()
{
student s("张三",12);
person pre("z11");
cout << &pre.st << endl;
cout << &s.st << endl;
/*tudent s2(s);*/
return 0;
}

可以看到在基类和派生类中的静态的成员的地址是一样的,所以他们是共享一个静态成员的
多继承和菱形继承
多继承
刚才我们所讲的继承都是单继承的例子,也就是我们的派生类只继承了一个基类
现在我们定义了一个蔬菜类和一个水果类
现在我们需要定义出一个西红柿类(属于水果又属于蔬菜)
现在这个类就需要拥有蔬菜的特性还需拥有水果的特性,这就是涉及到了多继承的范畴了
cpp
#include<iostream>
using namespace std;
class vagetable
{
public:
vagetable()
{
cout << "我是一个蔬菜" << endl;
}
string _name="蔬菜";
int _size;
};
class fruit
{
public:
fruit()
{
cout << "我是一个水果" << endl;
}
int price;
};
class tomato :public fruit, public vagetable
{
public:
tomato()
{
cout << "我是一个西红柿" << endl;
}
int age=18;
};
int main()
{
tomato x1;
cout << x1.age << endl;
cout << x1._name << endl;
return 0;
}
总而言之多继承就是一个派生类对应了多个基类

多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。

多继承一般不会造成数据的冗余和二义性但是菱形继承一定会有
菱形继承
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议 设计出菱形继承这样的模型的。

我们直接先来写一份菱形继承
cpp
#include<iostream>
using namespace std;
class person
{
public:
string _name;
};
class student: public person
{
protected:
int _id;
};
class teacher:public person
{
protected:
string tel;
};
class assistant :public student,public teacher
{
protected:
string _majorCourse;
};
int main()
{
assistant a;
a._name = "张三";
return 0;
}
当我们想要直接访问这个_name的时候就会报错,因为我们继承了两份person所以不知道要访问那一个_name就会出现错误

要想解决这个问题我们需要指定访问
a.student::_name = "张三";
但是这也没有解决我们的派生类中有两份person基类,无法避免数据的冗余
虚继承
虚继承就是为了解决多继承的问题出现的,但是这个的底层十分的复杂
在这里只做简单的讲解
在会继承了两份的基类中的继承之前我们添加上virtual
cpp
#include<iostream>
using namespace std;
class person
{
public:
string _name;
};
class student: virtual public person
{
protected:
int _id;
};
class teacher: virtual public person
{
protected:
string tel;
};
class assistant :public student,public teacher
{
protected:
string _majorCourse;
};
int main()
{
assistant a;
a._name = "张三";
return 0;
}
这样就会解决了数据的访问的二义性了和数据的冗余,但是会造成使用的复杂
尽量避免设计出菱形继承
继承和组合
继承就是上面讲的关系
组合就是把一个类当作自己的成员.
组合不能直接访父类的私有和保护,但是和父类的耦合度低,更加的灵活
继承可以访问父类的私有和保护但是耦合度高,容易多继承造成数据的冗杂
组合的关系就是 has a 我包含有你 比如 轮胎和车
继承的关系就是 is a 我是你 比如 奔驰和车
总的来说 能用组合就不使用继承
总结
这篇文章从基础概念、定义、关系,到基类与派生类的转化、作用域,再到派生类默认成员函数(构造、拷贝、赋值、析构)的行为;接着讲解继承与友元、静态成员的交互;核心难点是多继承、菱形继承及其数据冗余与二义性问题,以及虚继承的解决方案;最后对比继承与组合的设计选择。
谢谢大家的观看!!!
之后将会讲解多态的知识