反爬链接
正文开始
在此之前,我们接触的复用都是函数复用,那么继承就是类设计层次的复用。
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段。它允许程序员在保持原有类(基类)特性的基础上进行扩展,增加功能,产生新的类,称派生类。
在如下代码中,子类会继承(public)父类的所有成员,包括成员变量 & 成员函数。这样,子类就可以使用父类成员 ——
#include<iostream>
#include<string>
using namespace std;
// 父类(基类)
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "you-know-who"; //姓名
int _age = 20; //年龄
};
//子类(派生类)
class Student : public Person
{
protected:
int _stuid; //学号
};
class Teacher : public Person
{
protected:
int _jobid; //工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
1. 继承的规则
1.1 继承的格式
" title="">
1.2 访问限定符 & 继承方式
在类和对象中,protected和private没有区别。它们的区别体现在继承中:在这一层没有区别,下一层private无法再继承下去(事实上,在继承体系中,很少用private)。
" title="">
这样组合下来,就一共有3*3 = 9种继承情况 ——
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类的private成员 | 子类不可见 | 子类不可见 | 子类不可见 |
但其实我们只要把握住两点,抓住重点,它并不复杂,请看下一小节。
1.3 继承父类的成员访问方式变化
:purple_heart: 1. 父类的private成员在子类中都是不可见的。不可见是指父类的私有成员被子类继承了,但是在语法上限制子类对象在类内&类外都不可以访问。这里需要区分private和不可见,区别主要在于在类里面是否可以访问,private类内可类外不可;不可见类内外都不可。父类不想给子类用的可以给私有。事实上,我们很少定义private成员。
:purple_heart: 2. 子类中继承下来的成员的访问权限 = min(成员在子类中的访问修饰限定符,继承方式),总之就是取权限小的。权限大小关系:public > protected > private.
事实证明,C++的设计过于复杂。实际上一般使用都是public继承,几乎很少使用protected/private继承(也不推荐使用保护和私有继承,因为保护和私有继承下来的成员只能子类的类内部使用,扩展维护性不强)。
注:对于访问修饰限定符,如果不写,默认struct中成员默认是public;class默认是private. 对于继承也一样,struct中默认的继承方式是public;class默认的是private,但是最好显式写出。
:heart: 总结 - 我们删繁就简,常见的使用就是 ——
- 父类成员:公有和保护
- 子类的继承方式:公有继承
这样原本复杂的3*3表格,就精简到了左上角 ——
" title="">
2. 赋值兼容规则 - 切片
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _id; // 学号
};
子类和父类的赋值转换。它在它的作用我们在4.子类的默认成员函数来感受。
" title="">
:purple_heart: 1. 子类对象可以赋值给父类的对象/父类的指针/父类的引用(引用的底层也是指针)。形象的说法叫切片或者切割。即把子类中父类那部分切来赋值过去。
这里不存在类型转换,是语法天然支持的行为。众所周知,同类型可以相互赋值(自定义类型会调用的赋值重载);不同类型之间也可以相互赋值,相关类型隐式类型转换,不相关类型显式强制类型转换。这里显然不存在类型转换,因为众所周知,类型转换中间会产生临时变量,临时变量具有常属性,必须加const,这儿不加也不报错~
" title="">
Person p;
Student s;
//1.父类 = 子类
p = s;
Person* ptr = &s;
Person& ref = s;
注意:这仅限于公有继承,私有和保护都不行。这是因为存在权限转换问题,因为只有在公有方式继承下,父子类中的成员访问权限才会统一;其他方式继承都可能引起访问权限缩小,这时再切片回去后,访问权限被放大,这是不合理的。
举个具体的反例,比如Person中有公有成员,如果以私有方式继承下来,在子类Student看来都是私有的,子类切片给父类,作指针和引用,父类却把它看作是公有的。那么我继承下来是私有的,你去用反而是公有的了,这不合理。
:purple_heart: 2. 父类对象不能赋值给子类对象。因为子类通常比父类成员多,那_id拿什么去给呢?
但是指针和引用经过强转后可以,但最好不要用,因为有越界风险。(注:那怎样才能安全呢?这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换(以后详谈).)
" title="">
//2.子类 = 父类
//s = p; //强制类型转换也不行
Student* pptr = (Student*)&p;
Student& rref = (Student&)p;
//很危险,存在越界访问的风险
//pptr->_id = 1201021318; //崩了
3. 继承中的作用域 - 隐藏
子类和父类出现同名成员,称为隐藏/重定义。
:purple_heart: 1. 在继承体系中,父类和子类具有独立的作用域。因此,同名成员可以同时存在。
:purple_heart: 2. 若子类与父类有同名成员,由局部优先原则,子类会屏蔽父类,优先访问自己类中的成员;若想访问父类成员,需要指定作用域。这种情况就叫隐藏/重定义。
注:在继承体系中,不推荐定义同名的成员(变量+函数)
#include<iostream>
#include<string>
using namespace std;
// Student的_num和Person的_num构成隐藏关系(这样代码虽然能跑,但是非常容易混淆)
class Person
{
protected:
string _name = "you-know-who"; // 姓名
int _num = 23010620; //身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "学号:" << _num << endl; //就近原则
cout << "身份证号:" << Person::_num << endl; //需要指定作用域
}
protected:
int _num = 1201021318; //学号
};
int main()
{
Student s1;
s1.Print();
return 0;
}
运行结果 ——
" title="">
来一道小题 ——
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)->" << i << endl;
}
};
思考如下两种情况 ——
" title="">
请选择:
a. A、B的func构成函数重载
b. 编译报错
c. 运行报错
d. A、B的func构成函数隐藏
它们不可能构成函数重载,函数重载要求在同一作用域中。那么1.便是函数隐藏,选d;2编译报错,因为被隐藏了根本调不到,需要指定作用域b.A::func();
. 于是我们又总结出 ——
:purple_heart: 3. 对于成员函数的隐藏,只要函数名相同就构成隐藏,参数随意。
4. 派生类的默认成员函数
关于子类的默认成员函数,我们一共要探讨两个问题 ——
- 派生类的4个重点默认成员函数,我们不写,编译器默认会做什么?
- 如果我们要写,要写些什么?什么情况下必须自己写?
:purple_heart: 1. 派生类的4个重点默认成员函数,我们不写,编译器默认会做什么?
#include<iostream>
#include<string>
using namespace std;
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; //姓名
};
class Student : public Person
{
public:
protected:
string _id; //学号
};
int main()
{
Student s1;
Student s2(s1);
return 0;
}
" title="">
由上述运行结果可以看出 ——
:star:1.1 我们不写默认生成的子类的构造&析构?
- a. 父类继承下来的:调用父类的默认构造&析构
- b. 自己的:和普通类一样(内置类型不处理;自定义类型调用自己的默认构造&析构)
再来看拷贝构造和赋值重载 ——
" title="">
:star:1.2 我们不写默认生成的子类的拷贝构造&赋值重载operator=?同上
- a. 父类继承下来的:调用父类的默认构造&析构
- b. 自己的:和普通类一样(内置类型不处理;自定义类型调用自己的默认构造&析构)
:heart: 总结:父类成员调用父类的来处理,自己的成员按普通类处理。
:purple_heart: 2. 如果我们要写,要写些什么?什么情况下必须自己写?
:star: 2.1 when?
- 父类没有默认构造函数,需要我们自己写,显式的调用构造。不能自己处理。
- 析构函数呢?如果子类有资源需要释放,就需要自己写析构。父类的不用管,会调用父类的完成(父类就那一个析构,不存在调不到问题)。比如
int ptr = new int[10];
- 如果子类存在浅拷贝问题,就需要自己实现拷贝构造和赋值重载来深拷贝。
:star: 2.2 how?
如果需要自己写,父类成员调用父类对应的构造、拷贝构造、operator=和析构来处理;自己的成员按照普通类来处理:该浅拷贝的浅拷贝,该深拷的深拷贝。
请仔细阅读代码中注释!
#include<iostream>
#include<string>
using namespace std;
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 = "you-know-who",int num = 2003000)
: Person(name) /*显式调用父类的构造函数*/ /*_name(name)这样不行(非法的成员初始化:“_name”不是基或成员)*/
, _num(num)
{}
// s2(s1)
Student(const Student& s)
: Person(s) /*显式调用父类的赋值 —— 切片:把s1父类的部分切给s2父类的那部分*/
, _num(s._num)
{
// 一些自己的深拷贝...
}
// s3 = s1
Student& operator=(const Student& s)
{
if (this != &s)
{
// 显式调用父类的赋值:需要指定作用域,否则会隐藏父类的,优先调用子类(会Stack overflow)。
Person::operator=(s); /*切片*/
_num = s._num;
// 一些自己的深拷贝...
}
return *this;
}
~Student()
{
//Person::~Person();
// 清理自己的资源... /*delete[] ptr;*/
}
protected:
int _num = 2003001; //班号
};
int main()
{
Student s1;
Student s2(s1);
Student s3;
s3 = s1;
return 0;
}
关于析构,注释中写不下了 ——
" title="">
析构时,会发现调不动,诶?!这不合理呀~ 这是因为析构函数的名字会被统一处理成destructor(),那么子类和父类的析构函数构成隐藏,需要指定作用域 (至于为什么会统一处理,下一篇文章多态详谈)。调整之后,却发现析构调用了两次,我这现在是偷懒了没写清理资源的代码,写了不就崩了?!
" title="">
难道说不需要我们显式调用父类的析构函数?!是的。子类析构函数结束时,会自动调用父类的析构函数,这样才能保证先析构子类成员,后析构父类成员。
因此,我们实现子类析构函数时,不需要显式调用父类的析构函数。
5. 继承与友元
友元关系不能继承。父类的友元,不是子类的友元。
#include<iostream>
#include<string>
using namespace std;
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; //nope~
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
6. 继承与静态成员
父类定义了static成员,则整个继承体系只有这一个公有的成员。这可以统计Person以及Person的派生类共创建了多少个对象。
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
Person() { ++_count; }
protected:
string _name; //姓名
public:
static int _count; //统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Person p;
Student s;
Graduate g;
// 用来统计父类及其派生类创建对象的个数
cout << Person::_count << endl; //3
cout << Student::_count << endl; //3
cout << Graduate::_count << endl; //3
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
return 0;
}
运行结果 ——
" title="">
7. 菱形继承 & 菱形虚拟继承
7.1 菱形继承
:yellow_heart: 单继承:一个子类只有一个直接父亲
" title="">
:yellow_heart: 多继承:一个子类有两个及两个以上的直接父亲
" title="">
多继承看起来很合理,一个类继承多个类,但其实它就是一个坑( Java等语言直接没有多继承,避开这个坑),它带来的菱形继承,一个研究生助教对象中有两份Person,会有数据冗余和二义性的问题。
二义性还可以通过指定作用域勉强解决 ——
#include<string>
using namespace std;
class Person
{
public:
string _name; //姓名
};
class Student : public Person
{
protected:
int _stuid; //学号
};
class Teacher : public Person
{
protected:
int _teacherid; //工号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};
int main()
{
//二义性:对_name的访问不明确
Assistant a;
//a._name = "peter"; //nope~ 错误示范,请勿模仿
//需要显示指定访问哪个父类的成员
a.Student::_name = "文彬";
a.Teacher::_name = "文教"; //嘘~文教超级nb,超级好!
return 0;
}
那数据冗余呢?如果我在祖先类Person中添加一个int a[10000]数组,再那就白白浪费了4万字节。
为了解决解决菱形继承的二义性和数据冗余的问题,C++付出了极大的代价,虚继承。
7.2 菱形虚拟继承
注意是在腰儿上虚继承,不要在其他地方使用virtual
关键字。
class Person
{
public:
string _name; //姓名
};
class Student : virtual public Person
{
protected:
int _stuid; //学号
};
class Teacher : virtual public Person
{
protected:
int _teacherid; //工号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; //主修课程
};
int main()
{
Assistant a;
// 访问的是同一个
a.Student::_name = "文彬";
a.Teacher::_name = "文教";
a._name = "文教yyds!"; //无需指定
return 0;
}
7.3 菱形虚拟继承的原理
为了研究虚拟继承的原理,我们写一段简单的继承关系,配合内存窗口观察。
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//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 = 3;
d._c = 4;
d._d = 5;
//d._a = 0; //不存在二义性,可以直接找
return 0;
}
先来观察一下不采用虚拟继承时,是有数据冗余 & 二义性问题的(且先继承的在前,后继承的在后) ——
" title="">
:purple_heart: 再观察虚拟继承时,可以观察到,这样A成员的确只存储了一份,在对象的最底下——
但是B和C中那是啥?推测是地址,众所周知,当前机器采取的是小端存储(低位存低地址,高位存高地址),我们再打开内存窗口来看这地址存的什么 ——
D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A(虚基类)呢?就是通过了B和C的两个指针虚基表指针),指向的虚基表(找虚基类的表)。虚基表中存的偏移量,通过偏移量可以找到下面的A。
Person关系菱形虚拟继承的原理 ——
A一般叫做虚基类,在D中,A放到一个公共位置,有时B需要找A、C需要找A,就要通过虚基表中的偏移量来计算。那为什么要找呢?考虑以下场景
D d;
B b = d; //切片,要找_a
C c = d;
B* pb = &d;
pb->_a = 10;
注意:有公共祖先类就会构成菱形继承,那么virtual
加在哪里呢?
" title="">
虚继承加在B、C上,而不是D、C上。是因为要解决的是A的数据冗余和二义性的问题。若是D、C,你把谁放在最下面?
8. 总结
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。多继承可以认为是C++的缺陷之一(另一个是没有垃圾回收器),很多后来的OOP语言都没有多继承,如Java。
:purple_heart: 继承和组合
继承和组合都是一种复用,只不过访问的方式有所不同。
//继承 - 白箱复用(white-box reuse)
class A
{
int _a;
};
class B: public class A
{
int _b;
}
//组合 - 黑箱复用(black-box reuse)
class C
{
int _c;
}
class D
{
C _obj;
int _d;
}
public继承是一种is-a的关系;组合是一种has-a的关系。
继承是白箱复用(white-box reuse),父类成员除了私有的不可见,公有和保护对子类都是透明的可以直接访问(事实上,继承中要复用很少定义私有),因此白箱复用在一定程度上破坏了封装;组合是黑箱复用(black-box reuse),D只能访问C的公有,不能访问保护,和在类外的对象一样。
另外我们希望,类、模块之间的关系最好是低耦合高内聚,方便维护。继承(A和B),耦合度高,依赖性强,任意一个成员的改变都会对我有影响;组合(C和D),耦合度低,依赖关系较弱。
结论:完全符合is-a,就用继承;完全符合has-a,就用组合;既是is-a,又是has-a,优先用组合而不是继承。
持持续更新@边通书
过两三天我估计要暂停更新了,要备考嘞哎呦,各种实验考试大作业压上来,我现在还是悠悠闲闲的,觉睡得很足~ anyway~ 最近起了一点小心思,未来慢慢靠近吧!不是少女小心思of course哈哈:) 然后会更一波儿学校的笔记大作业,因为要分儿啊,懒得开小号了,求求大家别看别点赞了,查完作业我大概率就删了,丢死人了~