继承
目录
一、封装
类的设计就是一种简单的封装
将数据与方法放在类中,直接访问的定义成公有,不想直接访问的定义为私有或保护
迭代器的设计也是一种简单的封装
vector,string,list,deque的迭代器在使用层面类似
屏蔽了底层的设计细节:原生指针,用类封装的节点指针或者更为复杂的结构...
二、继承的概念及定义
2.1.继承的概念
继承(inheritance): 面向对象程序设计使代码可以复用的最重要的手段
(复用:类似模板,将重复的程序交给编译器处理)
允许在保持原有类特性的基础上进行扩展:
增加方法(成员函数)和属性(成员变量)
这样产生新的类称为子类
示例:
对于学生类 与老师类,有很多的方法与属性是一样的
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
class Student
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
int main()
{
return 0;
}
可以将这两个类中公共的成员放到个人类
学生类与老师类都继承个人类,就可以复用这些成员,不用重复定义
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
2.2.继承的定义
定义格式:
个人类是父类,也称作基类
学生类是子类,也称作派生类

继承方式:
- 公有继承(public)
- 保护继承(protected)
- 私有继承(private)
访问限定符:
- 公有访问(public)
- 保护访问(protected)
- 私有访问(private)
父类private成员在子类中不可见
不可见:父类私有成员继承到子类,但语法上限制子类的对象不管在类里还是类外都无法访问
父类protect成员在子类中可以被访问,但在类外无法被访问
(前提:子类不是private继承)
父类public成员由子类的继承方式来决定访问权限
|-----------|------------|---------------|-------------|
| 父类成员访问限定符 | 子类public继承 | 子类protected继承 | 子类private继承 |
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可见 | 不可见 | 不可见 |
**注:**可以通过公有的成员函数间接访问私有或者保护的对象
如果不显示写继承方式
- class默认:私有继承
- struct默认:公有继承
2.3.类模板继承类模板
在类模板继承另一个类模板时
如果要在子类模板中使用父类模板的成员
必须要显示告诉编译器该名称来自于父类
(通过类域限定符Base<T>::,this->,using声明 )
本质:
父类模板是依赖型类型
编译器在模板定义阶段不会查找其成员
只有显示标记,才能触发父类的按需实例化
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <vector>
#include <list>
#include <deque>
//#define CONTAINER std::vector
//#define CONTAINER std::list
#define CONTAINER std::deque
namespace bit
{
//template<class T>
//class vector
//{};
// stack和vector的关系,既符合is-a,也符合has-a
template <class T>
class stack : public CONTAINER<T>
{
public:
void push(const T & x)
{
// 基类是类模板时,需要指定一下类域,
// 否则编译报错:error C3861: "push_back": 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
CONTAINER<T>::push_back(x);
//push_back(x);
}
void pop()
{
CONTAINER<T>::pop_back();
}
const T& top()
{
return CONTAINER<T>::back();
}
bool empty()
{
return CONTAINER<T>::empty();
}
};
}
int main()
{
bit::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
三、赋值兼容转换
3.1.切片(切割)
公有继承的子类对象可以赋值给父类的对象/指针/引用

cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
//子类对象可以赋值给父类的指针/引用
Person* pp = &sobj;
Person& rp = sobj;
//子类对象可以赋值给父类的对象是通过调用基类的拷贝构造完成的
Person pobj = sobj;
//父类对象不能赋值给子类对象,会编译报错
sobj = pobj;
return 0;
}
注:
父类对象不可以给子类对象
父类的指针/引用可以通过强制类型转换赋值给子类的指针/引用
**继承中的作用域:**在继承体系中,父类与子类都有独立的作用域
3.2.隐藏
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问
(在子类成员函数中,可以用 父类::父类成员 来显示访问)
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
};
**注:**如果是成员函数的隐藏,只要是函数名相同就构成隐藏,不会有函数重载
3.3.试题
A和B类中两个func构成什么关系(B)
A.重载
B.隐藏
**注:**函数重载要求在同一作用域
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
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();
//子类对象指定父类的作用域就可以调用父类中的成员函数
b.A::fun();
return 0;
};

四、子类的默认成员函数
4.1.构造函数
子类的构造函数必须调用父类的构造函数,先初始化父类部分的成员,再初始化子类部分的成员
**注:**如果父类没有默认的构造函数,必须在子类的构造函数的初始化列表阶段显示调用
默认构造函数行为
**内置类型:**不确定(大概率为随机值)
**自定义类型:**调用自己的默认构造函数
继承父类成员看作一个整体对象,要求调用父类的默认构造函数

可以给缺省值进行初始化

如果父类没有默认构造函数,则会发生报错

子类不允许直接初始化父类的成员,必须调用父类的初始化函数

除非在子类默认构造函数的初始化列表中显示调用父类

4.2.拷贝构造函数
拷贝构造函数行为
**内置类型:**值拷贝(浅拷贝)
**自定义类型:**调用自己的拷贝构造函数(深拷贝),没有就为浅拷贝
**继承的父类成员:**调用父类的拷贝构造
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person & p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name, int num, const char* addrss)
:Person(name)
, _num(num)
, _addrss(addrss)
{
}
protected:
int _num = 1; //学号
string _addrss = "江西省南昌市";
};
int main()
{
Student s("张三",111,"江西省");
Student s1(s);
return 0;
}

学生类的拷贝构造使用默认生成的就够用了
- 内置类型_num可以直接浅拷贝
- 自定义类型string可以调用自己的拷贝构造函数进行深拷贝
- 继承父类的成员可以调用父类中的拷贝构造函数
只有学生类自身在有需要深拷贝的资源时,才需要自己实现拷贝构造函数
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person & p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name, int num, const char* addrss)
:Person(name)
, _num(num)
, _addrss(addrss)
{
}
//如果显式写拷贝构造函数
//传子类对象给父类时会发生赋值兼容转换,赋值给父类的引用,进行拷贝构造
Student(const Student & s)
: Person(s)
, _num(s._num)
,_addrss(s._addrss)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _num = 1; //学号
string _addrss = "江西省南昌市";
};
int main()
{
Student s("张三",111,"江西省");
Student s1(s);
return 0;
}

4.3.赋值运算符重载
与拷贝构造函数一样,不用显式写也可以直接调用
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person & p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person & operator=(const Person & p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name, int num, const char* addrss)
:Person(name)
, _num(num)
, _addrss(addrss)
{
}
Student(const Student& s)
: Person(s)
, _num(s._num)
, _addrss(s._addrss)
{
cout << "Student(const Student& s)" << endl;
}
//如果显示写赋值运算符重载函数
Student& operator=(const Student& s)
{
if (this != &s)
{
//子类与父类成员同名时构成隐藏,需要显式调用
Person::operator=(s);
_num = s._num;
_addrss = s._addrss;
}
return *this;
}
protected:
int _num = 1; //学号
string _addrss = "江西省南昌市";
};
int main()
{
Student s("张三",111,"江西省");
Student s1(s);
Student s2("李四", 222, "江苏省");
s1 = s2;
return 0;
}
4.4.析构函数
与拷贝构造函数一样,不用显式写也可以直接调用
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person & p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person & operator=(const Person & p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
Student(const char* name, int num, const char* addrss)
:Person(name)
, _num(num)
, _addrss(addrss)
{
}
Student(const Student& s)
: Person(s)
, _num(s._num)
, _addrss(s._addrss)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
_addrss = s._addrss;
}
return *this;
}
//析构函数都会被特殊处理成destructor()
~Student()
{
//子类的析构与父类的析构也构成隐藏关系
//Person::~Person();
//不需要显示调用,子类析构函数
//编译器会在子类调用完析构函数后
//自动调用父类的析构函数,保证先子后父
}
protected:
int _num = 1; //学号
string _addrss = "江西省南昌市";
};
int main()
{
Student s("张三",111,"江西省");
Student s1(s);
Student s2("李四", 222, "江苏省");
s1 = s2;
return 0;
}
注:
在所有的类中,析构函数都会被特殊处理成destructor()
这会导致子类与父类发生隐藏
- 子类对象初始化先调用父类构造再调用子类构造(先父后子)
- 子类对象析构清理先调用子类析构再调用父类析构(先子后父)
但如果显示调用,则会造成先父后子,不能保证先子后父
实现一个不能被继承的类
**法1:**父类的构造函数私有
子类的构成必须调用父类的构造函数
但是父类的构成函数私有化以后
子类不可见,无法完成实例化对象
**法2:**C++11新增final关键字,定义为最终类
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的⽅法
/*Base()
{}*/
};
class Derive :public Base
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
五、继承与友元
友元关系不能被继承
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
//前置声明
class Student;
class Person
{
public:
//Person中的Display为友元函数
//在类外定义时,可以使用类中protected中的成员变量
friend void Display(const Person & p, const Student & s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person & p, const Student & s)
{
cout << p._name << endl;
//Display函数是Person的友元
//但友元无法被继承
//所以在Student中无法获取protected中的成员变量
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
// 编译报错:error C2248: "Student::_stuNum": 无法访问 protected 成员
// 解决方案:Display也变成Student 的友元即可
Display(p, s);
return 0;
}
六、继承与静态成员
父类定义了static静态成员
整个继承体系里面只有一个这样的成员
无论派生多少子类,都只有一个static成员实例
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
//非静态成员_name的地址是不一样的
//说明子类继承下来了,与父类对象各有一份
cout << &p._name << endl;
cout << &s._name << endl;
//静态成员_count的地址是一样的
//说明子类和父类共用同一份静态成员
cout << &p._count << endl;
cout << &s._count << endl;
//公有的情况下,父类指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
//还可以用对象进行指定访问
//cout << p._count << endl;
//cout << s._count << endl;
return 0;
}

七、多继承及其菱形继承问题
7.1.继承模型
**单继承:**一个子类只有一个直接父类

**多继承:**一个子类有两个或两个以上的直接父类

实现一个蔬菜类和水果类
番茄既属于蔬菜也属于水果
**菱形继承:**数据冗余与二义性

Assistant对象内会包含两份独立的Person子对象
一份来自Student继承链,一份来自Teacher继承链
二义性:
当访问Assistant对象中的_name时
编译器不知道要访问的是Student继承的_name,还是Teacher继承的_name
数据冗余:
Assistant里有两个Person子对象
在内存中会存放两个_name,一个在Student,一个在Teacher
这两个_name的空间是独立的,所以一个Assistant对象会有两中类的信息
但实际想要的是一个Assistant对象中
只能存放一个学生的信息,或者一个老师的信息
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a;
// 编译报错:error C2385: 对"_name"的访问不明确
//a._name = "peter";
//需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
cout << sizeof(a) << endl;
return 0;
}
**补充:**IO流就是一个经典的菱形继承

八、虚继承
**虚继承:**子类共享一个父类中的成员变量(内存空间相同)
**非虚继承:**子类各自都有一个父类中的成员变量(内存空间各自独立)
哪个类(公共父类)会产生数据冗余与二义性,就在继承时给父类加上virtual

cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:
string _name; // 姓名
/*int _tel;
int _age;
string _gender;
string _address;*/
// ...
};
//虚继承Person类
class Student : virtual public Person
{
protected:
int _num; //学号
};
//虚继承Person类
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
//教授助理
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};
int main()
{
//虚继承,可以解决数据冗余和二义性
Assistant a;
//直接访问
a._name = "peter";
//指定访问
a.Student::_name = "小李";
a.Teacher::_name = "老李";
//此时a对象中的名字为老李
cout << a._name << endl;
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
return 0;
}

最好不要设计菱形继承
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:
Person(const char* name)
:_name(name)
{}
string _name; // 姓名
/*int _tel;
int _age;
string _gender;
string _address;*/
// ...
};
//虚继承Person类
class Student : virtual public Person
{
public:
Student(const char* name,int num = 0)
:Person(name)
,_num(num)
{}
protected:
int _num; //学号
};
//虚继承Person类
class Teacher : virtual public Person
{
public:
Teacher(const char* name, int id = 1)
:Person(name)
, _id(id)
{}
protected:
int _id; // 职工编号
};
//教授助理
class Assistant : public Student, public Teacher
{
public:
Assistant(const char* name1,const char* name2,const char* name3)
:Student(name1)
,Teacher(name2)
,Person(name3)
{}
protected:
string _majorCourse; //主修课程
};
int main()
{
//初始化name3,name为王五,name1和name2都没有作用
Assistant a("张三","李四","王五");
return 0;
}
8.1.多继承的指针偏移问题
1.下面说法正确的是(C)
A.p1 == p2 == p3
B.p1 < p2 < p3
C.p1 == p3 != p2
D.p1 != p2 != p3

cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1 * p1 = &d;
Base2 * p2 = &d;
Derive * p3 = &d;
return 0;
}

2.下面说法正确的是(E)
A.p1 == p2 == p3
B.p1 < p2 < p3
C.p1 == p3 != p2
D.p1 != p2 != p3
E.p2 == p3 != p1
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Base1
{
public:
int _b1 = 1;
};
class Base2
{ public:
int _b2 = 2;
};
class Derive : public Base2, public Base1
{
public:
int _d = 3;
int _e = 4;
};
int main()
{
Derive d;
Base1 * p1 = &d;
Base2 * p2 = &d;
Derive * p3 = &d;
return 0;
}

8.2.IO库中的菱形虚拟继承


九、继承和组合
9.1.继承关系
public继承 是一种is-a的关系
每个子类的对象都是一个父类的对象
继承是一种白箱复用
在继承中,父类的成员一般定义为保护与公有
派生的类都可以直接使用,一定程度地破坏了父类的封装
父类的改变对子类有很大影响,父子间的依赖关系强,耦合度高

9.2.组合关系
组合 是一种has-a的关系
B组合了A,每个B对象中都有一个A对象
组合是一种黑箱复用
组合的类,它的对象内部细节是不可见的
比如栈组合了链表,链表的结构是访问不到的
只能调用它公有的成员函数
所以组合类之间依赖关系弱,耦合度低

优先使用组合,而不是继承
组合的耦合度低,代码维护性好
但不绝对:
类之间的关系就时候继承
多态的实现也必须用继承
类之间的关系既适合继承(is-a)也适合组合(has-a)时,就用组合
示例:
- 学生和人:继承
- BMW和Benz与车:继承
- 轮胎和车:组合
- stack和vector:既符合继承,也符合组合(优先组合)
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺⼨
};
class Car {
protected:
string _colour = "⽩⾊"; // 颜⾊
string _num = "陕ABIT00"; // ⻋牌号
Tire _t1; // 轮胎
Tire _t2; // 轮胎
Tire _t3; // 轮胎
Tire _t4; // 轮胎
};
class BMW : public Car {
public:
void Drive() { cout << "好开-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
template < class T>
class vector
{};
// stack和vector的关系,既符合is-a,也符合has-a
template < class T>
class stack : public vector < T>
{};
template < class T>
class stack
{
public:
vector<T> _v;
};
int main()
{
return 0;
}
补充:
**黑盒测试:**不了解底层实现,从功能角度测试
**白盒测试:**了解底层实现,从代码运行逻辑角度测试
**覆盖度:**测试的指标
假设有if else if else三段代码,至少设计三个测试用例来覆盖三个分支
**UT(单元测试):**对软件中最小可测试单元进行验证,确保单个模块正确
**CI(自动化测试系统):**检查测试用例的覆盖度
**低耦合:**模块与模块的关系越弱越好
(类开放的成员越少越好)
**高内聚:**一个模块内的关系越紧密越好,确保模块只做一件事