# Lecture11 - 继承、虚函数
能声明成 const 就声明成 const

# 单继承
- protected: - 如果没有继承的话,protected 和 private 是相同的
- 派生类可以访问基类中 protected 的属性的成员。(student 可以访问自己的 id)
- 派生类不可以访问基类中的对象的 protected 的属性。(Undergraduate_Student 不能访问自己引用的 student 的 protected 属性)
- 派生类含有基类的所有成员变量
 
- class 和 struct 的默认权限 
 使用 class 时,类中的成员默认都是 private 属性的;- 而使用 struct 时,结构体中的成员默认都是 public 属性的。 - class 继承默认是 private 继承,而 struct 继承默认是 public 继承 

| class Student { | |
| int id;//id 在 Undergraduated_Student 中仍然是私有的 | |
| public: | |
| char nickname[16]; | |
| void set_ID (int x) {id = x;} | |
| void SetNickName (char *s) {strcpy (nickname,s);} | |
| void showInfo () {cout << nickname << ":" << id << endl ;} | |
| void showInfo(int x){cout << x << endl;} | |
| }; | |
| class Undergraduated_Student: public Student { | |
| int dept_no;// 学院编号 | |
| public: | |
| void setDeptNo(int x){dept_no = x;} | |
| void showInfo(){cout << dept_no << ":" << nickname << endl;} | |
| void set_ID (int x) {……} | |
| void showInfo(){ | |
| cout << dept_no << ":" << nickname << endl; | |
|         } | |
| private: | |
| Student::nickname;// 这样在才能修改可见性 | |
| void SetNickName();// 新定义了一个 private 方法,父类对应方法被隐藏 | |
| }; | |
| Undergraduated_Student us; | |
| us.showInfo(10);// 可以吗?不可以,因为是新的名空间,重定义后面的名空间访问不到 | 
基类的方法,将函数指针传给派生类。
虚函数,virtual,延迟到运行的时候确定调用的函数类型,和 java 类似。
class Undergraduated_Studnet: public Student 在函数定义的时候需要写出 Student
# 继承方式

# 继承声明方式
区分声明和定义,前向声明只是告诉别人有这个东西,定义要明确需要占用多少内存空间
| // 错误声明 | |
| class Undergraduated_Student : public Student;// 声明的时候是不用声明继承的 | |
| // 正确声明 | |
| class Undergraduated_Student; | 
# 基类和继承类的方法关系
# 基类与派生类中的 showInfo 访问
隐藏 (不是重写 / 覆盖),隐藏基类的所有的 showInfo () 函数,重定义将名空间进行了覆盖
| class Student { | |
| int id;//id 在 Undergraduated_Student 中仍然是私有的 | |
| public: | |
| char nickname[16]; | |
| void showInfo () {cout << nickname << ":" << id << endl ;} | |
| void showInfo(int x){cout << x << endl;} | |
| }; | |
| class Undergraduated_Student: public Student { | |
| int dept_no;// 学院编号 | |
| public: | |
| void showInfo(){cout << dept_no << ":" << nickname << endl;} | |
| }; | |
| int main(){ | |
| Undergraduated_Student *student = new Undergraduated_Student; | |
| student->showInfo(1);// 不能够运行,在 undergraduated_student 里面设置之后,student 含参和无参的函数都没有了。 | |
| } | 
区分重载、隐藏、重写(覆盖)
https://blog.csdn.net/zx3517288/article/details/48976097
如何访问父类中被隐藏的函数?
父类中的所有的函数都不可见:但是我们可以通过指定名空间来完成访问: using Student::showInfo , 所有的版本都可以见,这时候是重写。
# 继承权限修改
- class ABC : private XYZ //private derivation
- class ABC : public XYZ //public derivation
- class ABC : protected XYZ //protected derivation
- class ABC: XYZ //private derivation by default
public 继承,protected 继承,private 继承
# 不可以被继承的部分
- 构造函数和析构函数是不可以被继承的:是对类进行初始化的,无法继承 
- 运算符重载函数也是不可以被继承的 
# 访问权限的修改方法
| private: | |
| Student::nickname;//char nickname [16]; 语法上没问题,没有将原来的 nickname 变为私有的 | |
| void SetNickName();// 新定义了一个 private 方法,父类对应方法被隐藏 | 
如果重新声明 char nickname[16]; , 并没有修改原来父类中 nickname 访问等级,语法无误,但是语义不对;
# 友元和 protected

void clobber(Sneaky &s) 通过传入派生类访问基类的 protected 成员。 void clovver(Base &b) 传入基类不能够访问。
protected 只能由派生类来访问。否则只要声明某基类的派生类,就能够访问该基类的 protected 成员,绕过保护机制,随意访问保护成员。
友元不具有传递性。不能通过成为派生类的友元而成为基类的友元。
# 继承的初始化

4
没有在成员初始化表中指出,则调用默认构造函数
| B(const B& b){ | |
| } | |
| B b1(10, 100); | |
| B b2(b1); | |
| // 有 B 的 copy 构造函数,b2 的值 copy 错误,编译器无法明确是只 copy x 还是都 copy | |
| b1 10 100; | |
| b2 0 100; | |
| A() B(&b);// 构造函数调用顺序 | |
| // 如果没有 B 的 copy 构造函数,b2 的值 copy 正确 | |
| b1 10 100; | |
| b2 10 100; | |
| // 可以 copy x 的 copy 函数 | |
| B(const B&b):A(b); | 
语法糖
using A::A; 继承 A 的构造函数
# 虚函数

类型相容:类型相同、派生类
赋值相容(不丢失信息):类型相同、派生类【发生对象切片,属于派生类的属性不复存在】
| A a; B b; a = b; | |
| B b; | |
| A a = b;// 相当于调用了 A 的拷贝构造函数,调用哪个,取决于声明的变量是什么类型的 | 

| func1(A &a){ | |
| a.f(); | |
| } | |
| func2(A *pa){ | |
| pa->f(); | |
| } | |
| // 均调用 A 的 f | 

- 前期绑定 - 在编译的时候就可以确定【func1\func2 在编译时就已经确定调用 A 的 f】 - cpp 默认前期绑定 
- 动态绑定 - java 虚拟机动态地决定调用哪个版本 
- cpp 是一门注重效率的语言 - cpp 需要通过显式指出使用后期绑定 

在方法前加 virtual 关键字动态绑定

# 限制
- 成员函数才可以时虚函数,全局函数不能是虚函数
- 静态成员函数通过类来调用,编译期间确定,相当于全局函数
- 内联成员函数编译时要展开,在编译时需要确定
- 构造函数,虚函数表需要构造函数构造,虚函数只有在声明基类指向子类的时候才会调用,构造函数不存在声明类型与构造函数的类型不一致的情况。没有实际意义。
- 析构函数(子类会声明新的变量)

| p = &a; | |
| p -> h();// 调用 a 的函数 | |
| p -> f();// 调用 a 的函数 | |
| p = &b; | |
| p -> h();// 调用 a 的函数 | |
| p -> f();// 调用 b 的函数 | 
如何确定调用哪个?传入一个虚函数表【vtable】
如何确定虚函数表的偏移?传入一个虚函数指针,vpointer,确认排序,查找函数。
如何实现查表的操作? (**((char *)p-4))(p) (char*) p -4,偏移 4 个字节,第一次解引用解出了虚函数表的指针,第二次解引用解出了 f 的函数指针。

| B b;//A 的构造函数 A 的 f B 的构造函数,B 的构造函数还未调用,vtable 还没有改变 | |
| A *p = &b; | |
| p -> f();// B::f | |
| p -> g();// A::g g 是非虚函数 | |
| p -> h();// A::h, B::f, A::g | 
构造函数不应该调用莫名其妙的东西(比如虚函数)
直到构造函数返回之后,对象方可正常使用(虚函数表才构建好)
| B b; | |
| A *p = &b; | |
| p -> f();// B::f() B::g() | 
虚函数调用非虚函数,类型变为一致。
非虚函数调用虚函数,非虚函数就是非虚函数,虚函数就是虚函数。【 非虚接口, 可以用不同的虚函数来实现这个非虚函数,非虚函数调用虚函数来获得虚函数的特性】【实现算法骨架的复用】
# final, override

override 告诉编译器是一个虚函数的重定义。
final 不能够再 override。
不能够写成 void f1(int) override , 这样更改了 f1 的类型
void f2(int) 名隐藏,不能够再调用 d.f2()

纯虚函数用于抽象类,抽象类用于框架中

为什么不能直接设置 display 的空函数,而是设置纯虚函数?
因为抽象类不能创建对象,而且会出现对象切片,出现在栈上(降低效率);抽象类不能创建对象,只能通过指针和引用的方式传递参数,保证了一定会出现多态的特性。

通过构建非虚函数 CreateButton() ,可以实现更好的代码复用
抽象工厂模式

是否需要新的析构函数,看是否声明了新的成员变量。
需要释放新的变量,与基类不同,需要新的析构函数,需要是虚函数。
调用完 D 的析构函数后,就会调用 B 的析构函数。【倒着的次序调用】

| A *p_a; | |
| B b; | |
| p_a = &b; | |
| p_a -> f(); | |
| // 打印 0 | |
| A *p_a1; | |
| C c; | |
| p_a1 = &c; | |
| p_a1 ->f(); | |
| // 打印 0 | |
| // 如果 class C:public B 仍然打印 0 | 
编译器不知道指针指向谁,默认参数值为声明的类型的默认值。
从效率的角度出发,没有动态绑定参数值,参数值相当于一个全局或者静态的变量,不被改变。
只有虚函数表示动态的,默认参数是静态的。
# 公有继承 is_a

(嘿嘿公开继承应该不考
前置条件更弱,后置条件更强。
比如 base.f (x),参数要求正整数,返回 int;derive.f (x),参数要求整数,返回 int 的子集。以上,derive.f (x) 就可以替代 base.f (x)
| //v1 | |
| class Rectangle { | |
| public: | |
| void setHeight(int); | |
| void setWidth(int); | |
| int height() const;// 不修改的声明为 const 是好习惯 | |
| int width() const; | |
| }; | |
| assert(s.width() == s.height()); | |
| class Square: public Rectangle { | |
| public: | |
| void setLength (int); | |
| private:// 设置为 private 可以避免父类的方法被单独调用 | |
| void setHeight(int); | |
| void setWidth(int); | |
| }; | |
| assert(s.width() == s.height());// 前后校验不变式 | |
| Square s(1,1); | |
| Rectangle *p = &s;// 即使 square 声明成 private 方法,改成 rectangle 指针之后,还是可以调用方法 | |
| p->setHeight(10); | |
| //v2 添加虚函数声明 | |
| class Rectangle { | |
| public: | |
| virtual void setHeight(int); | |
| virtual void setWidth(int); | |
| int height() const; | |
| int width() const; | |
| }; | |
| assert(s.width() == s.height()); | |
| class Square: public Rectangle { | |
| public: | |
| void setLength (int); | |
| public:// 注意是 public 的 | |
| void setHeight(int); | |
| void setWidth(int); | |
| }; | |
| // 问题:如下的操作如果传入正方形 | |
| void Widen(Rectangle& r, int w) | |
| { | |
| int oldHeight = r.height(); | |
| r.setWidth(r.width() + w); | |
| assert(r.height() == oldHeight);// 原来的高和现在的高不一致,长宽一起增加了 | |
| } | |
| //v3 | |
| class Rectangle { | |
| public: | |
| virtual void setHeight(int); | |
| virtual void setWidth(int); | |
| int height() const; | |
| int width() const;}; | |
| assert(s.width() == s.height()); | |
| class Square: public Rectangle { | |
| public: | |
| void setLength (int); | |
| private:// 修改为 private | |
|     // 完全无效,编译时函数即被确定 | |
| void setHeight(int); | |
| void setWidth(int ); | |
| }; | |
| assert(s.width() == s.height()); | |
| void Widen(Rectangle& r, int w) | |
| { | |
| int oldHeight = r.height(); | |
| r.setWidth(r.width() + w); | |
|     // 编译的时候检查 rectangle 是 public 的,可以通过编译。 | |
|     // 之后调用的时候发现是虚函数,然后从虚函数表能找到 private 的函数,并且调用。 | |
| assert(r.height() == oldHeight); | |
| } | 
长方形的性质和正方形的性质不兼容
程序中的基类和子类,与生活中的一般与特殊并不一致
| class B { | |
| public: | |
| void mf(); | |
| }; | |
| class D: public B { | |
| public: | |
| void mf(); | |
| }; | |
| D x; | |
| B* pB = &x; | |
| pB->mf();//B:mf | |
| D* pD = &x; | |
| pD->mf();//D:mf | 
同一个对象更换指针,就出现了不同的行为。实际上是更换了名空间。
修改:不要定义继承而来的同名函数。
# 私有继承 has_a

实际上是 Has-A 关系(is a,是这个类的一种,继承,派生类是一个基类对象;has a, 含有这个类,包含,在派生类中拥有一个基类对象)
Student 私有继承 HumanBeing,则不能够调用 b.eat。
函数参数无法从派生类隐式转换为基类:如果两个类的继承是私有的,则不能在派生类外将派生类转换成基类对象。不能在派生类外部隐式转化。
一般私有继承是很罕见的,仅是可以使用基类的 protected 成员。
# 虚函数的使用情景

纯虚函数 —— 每个派生类依赖小,自己实现自己的。
一般虚函数 —— 覆盖功能多,派生类只需要修改部分。依赖性强。
非虚函数 —— 绝对不要重定义。保证所有的行为一致。
# 多继承

# 名冲突

出现名冲突的问题,Bed 和 Sofa 同时都有 SetWeight ()
如何解决名冲突的问题:
- 都是自己设计 —— 基类分解 Base-Class Decomposition,形成格的结构


- 不是自己设计的类 —— 用名空间的调用来消除名冲突

# 虚基类

virtual public 和 public virtual 等价
虚基类什么时候构造?D 只有一个 A,不能有两个 A。D 先构造 A,BC 不再构造 A。所有对象中只有一个 A 的对象。解决了在菱形设计中名冲突的问题。
# 内存存储

多继承有多个虚函数表指针 B1、B2、B3。
B1、B2 中都有 virtual 的 f,那么 D 重写,父类中的虚函数 f 全部都被重写。
# 期末题型
读程序理解(编译是否正确,如果错误,在哪里出了错) 要么得分,要么没分
手写代码
# 1
| #include<iostream> | |
| using namespace std; | |
| class B{ | |
| int x = 0; | |
| public: | |
| void setX(int px){ | |
| x = px; | |
|   } | |
| void getX(){ | |
| cout <<x; | |
|   } | |
| }; | |
| class D: public B{ | |
| public: | |
| using B::x; | |
| void setX(int px){ | |
| x = 2 * px; | |
|   } | |
| }; | |
| int main(){ | |
| B * pb = new D; | |
| pb->setX(15); | |
| pb->getX(); | |
| return 0; | |
| } | 

只能修改原来就是可见(public)的
x 原本就没有权限
# 2
| #include<iostream> | |
| using namespace std; | |
| class B{ | |
| int x = 0; | |
| public: | |
| void setX(int px){ | |
| x = px; | |
|   } | |
| void getX(){ | |
| cout <<x; | |
|   } | |
| }; | |
| class D: public B{ | |
| public: | |
| int x = 0; | |
| void setX(){ | |
| x = 5; | |
|   } | |
| void setX(int px){ | |
| B::setX(2*px); | |
|   } | |
| }; | |
| int main(){ | |
| D * pb = new D; | |
| pb->setX(); | |
| pb->getX(); | |
| return 0; | |
| } | |
| // 打印出来为 0,调用的是 B 的 getY | 
# 3
| #include<iostream> | |
| using namespace std; | |
| class B{ | |
| int x = 0; | |
| public: | |
| void setX(int px){ | |
| x = px; | |
|   } | |
| void getX(){ | |
| cout <<x; | |
|   } | |
| }; | |
| class D: public B{ | |
| public: | |
| int x = 0; | |
| void setX(){ | |
| x = 5; | |
|   } | |
| void setX(int px){ | |
| B::setX(2*px); | |
|   } | |
| }; | |
| int main(){ | |
| D * pb = new D; | |
| pb->setX(5);// 这里多了参数 | |
| pb->getX(); | |
| return 0; | |
| } | |
| // 打印出来是 10 | 
# 4
| #include<iostream> | |
| using namespace std; | |
| class B{ | |
| int x = 0; | |
| public: | |
| void setX(int px){ | |
| x = px; | |
|   } | |
| void getX(){ | |
| cout <<x; | |
|   } | |
| }; | |
| class D: public B{ | |
| public: | |
| int x = 0; | |
| void setX(){ | |
| x = 5; | |
|   } | |
| void setX(int px){ | |
| B::setX(2*px); | |
|   } | |
| }; | |
| int main(){ | |
| B * pb = new D;// 这里声明为 B | |
| pb->setX();//B 中没有无参数的 setX 函数 | |
| pb->getX(); | |
| return 0; | |
| } | 

