
◆博主名称:少司府
欢迎来到少司府的博客☆*: .。. o(≧▽≦)o .。.:*☆
⭐数据结构系列个人专栏:
⭐C++基础个人专栏:
⭐水滴石穿非一日,功不唐捐终可期
目录
[1.1 概念](#1.1 概念)
[1.2 定义](#1.2 定义)
[1.2.1 定义格式](#1.2.1 定义格式)
[1.2.2 继承基类成员访问方式的变化](#1.2.2 继承基类成员访问方式的变化)
[1.3 类模板的继承](#1.3 类模板的继承)
[3.1 隐藏规则](#3.1 隐藏规则)
[3.2 隐藏相关的题目](#3.2 隐藏相关的题目)
[4.1 4个常见的默认成员函数](#4.1 4个常见的默认成员函数)
[4.2 实现不能被继承的类](#4.2 实现不能被继承的类)
[7.1 继承模型](#7.1 继承模型)
[7.2 虚继承](#7.2 虚继承)
[7.3 多继承中指针偏移问题](#7.3 多继承中指针偏移问题)
一、继承的概念及定义
1.1 概念
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称子类 。继承呈现了⾯向对象程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
cpp
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;
}
如图,Student和Teacher都继承了Person类,可以使用identity方法。
1.2 定义
1.2.1 定义格式
下⾯我们看到Person是基类,也称作父类 。Student是派⽣类,也称作子类。(因为翻译的原因,所以 既叫基类/派⽣类,也叫⽗类/⼦类)



1.2.2 继承基类成员访问方式的变化

1)、父类private成员在子类中任何方式继承都是不可见的(不能访问,但继承下来了)
2)、父类private成员在子类、类外面中不能访问 ,父类protected成员在子类中可以访问,类外面不能访问。
3)、父类私有成员(private)在子类中不可见,父类其他成员在子类的访问方式==Min(父类的访问限定符,继承方式),public > protected > private。
4)、不写继承方式时,使用class时默认继承方式是private,struct的默认继承方式是public
5)、实践上一般使用public继承
1.3 类模板的继承
cpp
namespace cxr
{
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
vector<T>::push_back(x);
//push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
}
int main()
{
cxr::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
1)、父类是类模板时,需要指定⼀下类域。
2)、stack<int>实例化时,也实例化vector<int>了,但是模版是按需实例化,vector中push_back等成员函数未实例化,所以找不到。因此需要指定类域。
二、基类和派生类的转换
public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

注意:
1)、子类对象可以赋值给父类对象/指针/引用
2)、父类对象不能赋值给子类对象
3)、父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用
cpp
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
// 1.派⽣类对象可以赋值给基类的指针/引⽤
Person* pp = &sobj;
Person& rp = sobj;
// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
Person pobj = sobj;
//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
sobj = pobj;
//3.父类指针/引用可以通过强转给子类指针/引用
Student* ps = (Student*)pp;
Student& rs = (Student&)rp;
return 0;
}

三、继承中的作用域
3.1 隐藏规则
1)、在继承中子类和父类都有独立的作用域
2)、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。
3)、在子类成员函数中,可以使⽤父类::父类成员显⽰访问
4)、注意,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

如图,Student中的_num和Person中的_num构成隐藏关系,就近原则_num访问的是Student中的。
3.2 隐藏相关的题目
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;
};
问题:
1.两个func构成什么关系?
隐藏
2.程序编译运行结果是什么?
编译报错,两个func构成隐藏,需要指定父类使用func。
cpp
b.A::func();
四、子类的默认成员函数
4.1 4个常见的默认成员函数
cpp
class Person
{
public:
Person(const char* name = "peter")
: _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; // 姓名
};
1)、子类的构造函数必须调用父类的构造函数 初始化父类的那部分成员,如果父类没有默认构造函数,则必须在子类构造函数的初始化列表显示调用。

如图,在子类Student中显示调用父类构造函数初始化name。
2)、子类的拷贝构造必须调用父类的拷贝构造完成父类的拷贝初始化
cpp
Student(const Student& s)
:_num(s._num)
,_address(s._address)
,Person(s) // 这里是子类对象传给父类的引用,赋值兼容转换
{ }
3)、子类的operator=必须要调用父类的operator= ,完成父类的赋值。注意:子类的operator=隐藏了父类的operator=,所以必须显示调用。
cpp
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
_address = s._address;
}
return *this;
}
4)、子类的析构会在被调用完后自动调用父类的析构清理父类成员,不需要显示调用父类析构
5)、子类对象初始化优先初始化父类成员,子类对象析构优先析构子类成员
6)、编译器会对析构函数名进行特殊处理,处理成 destructor(),所以父类析构函数不加virtual的情况下,子类析构和父类析构构成隐藏关系。
cpp
~Student()
{}

4.2 实现不能被继承的类
我们用C++98的方式来实现:
cpp
class Base
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的⽅法
Base()
{}
};
如图,私有化默认构造,继承就不能访问默认构造,就无法被继承。不过,有一个缺点就是,如果不实例化对象的话就不会报错。
我们用C++11来改良:
cpp
// C++11的⽅法
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
如图,final 关键字跟在类名后面表示该类无法被继承。
五、继承与友元
友元关系不能被继承,即父类友元不能访问子类私有和保护成员。
cpp
// 前置声明
class Student;
class Person
{
public:
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;
cout << s._stuNum << endl;
}
如图,假如没有一开始的前置声明,在Person的友元声明里面不知道Student类是什么,而Student继承Person因此不能放在前面,两个类构造相互依赖关系,所以需要前置声明。
但是还是会报错,无法访问 _stuNum,原因就是Person的友元关系没有被继承下来,解决方法就是,在Student里加上友元声明:
cpp
friend void Display(const Person& p, const Student& s);
六、继承与静态成员
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。
简单点来说,父类和子类共用同一份静态成员。
注意,公有情况下,静态成员访问方式:
1)、指定类域访问
2)、通过对象访问
七、单继承,多继承和菱形继承
7.1 继承模型
1)、单继承 :子类继承的只有一个直接父类

2)、多继承 :子类继承的有两个及以上父类。在内存中,先继承的在前面,后继承的在后面,子类成员在最后。

3)、菱形继承 :继承关系构造菱形,有数据冗余和二义性的问题。

cpp
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; // 主修课程
};
如图,Assistant 构成菱形继承关系。
cpp
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
如图,访问的时候指定类域进行访问可以避免二义性的问题,但是 Assistant 中仍然有两份 Person 成员,数据冗余的问题没有解决。
7.2 虚继承
cpp
// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:
int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:
int _id; // 职⼯编号
};
如图,虚继承Person就能解决数据冗余和二义性的问题。
7.3 多继承中指针偏移问题
cpp
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;
}


如图,子类指针给父类指针的时候会方式切片,把子类中父类的那一部分切出来。
八、继承和组合
1)、public 继承是一种is-a的关系,也就是说每个子类对象都是一个父类对象
2)、组合是一种has-a的关系,假设B组合了A,每个B对象中都有一个A对象
cpp
// 继承
class stack : public list
{};
// 组合
class stack
{
list _lt;
};
3)、继承允许你根据父类的实现来定义子类的实现 。这种通过⽣成子类的复⽤通常被称为⽩箱复⽤(white-box reuse)。术语"⽩箱"是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对子类可⻅。继承⼀定程度破坏了父类的封装,父类的改变,对子类有很⼤的影响。子类和父类间的依赖关系很强,耦合度⾼。

4)、对象组合 是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。对象只以"⿊箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
5)、优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。保证模块低耦合高内聚。
本期的分享就到这里,如果觉得博主的文章比较对胃口的话,可以点一个小小的关注~
您的三连是我持续更新的动力~