# Lecture11 - 继承、虚函数

能声明成 const 就声明成 const

image-20221115142752636

# 单继承

  1. protected:

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

    而使用 struct 时,结构体中的成员默认都是 public 属性的。

    class 继承默认是 private 继承,而 struct 继承默认是 public 继承

image-20221115144636871

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

# 继承方式

image-20221115143338570

# 继承声明方式

区分声明和定义,前向声明只是告诉别人有这个东西,定义要明确需要占用多少内存空间

// 错误声明
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 , 所有的版本都可以见,这时候是重写。

# 继承权限修改

  1. class ABC : private XYZ //private derivation
  2. class ABC : public XYZ //public derivation
  3. class ABC : protected XYZ //protected derivation
  4. class ABC: XYZ //private derivation by default

public 继承,protected 继承,private 继承

# 不可以被继承的部分

  1. 构造函数和析构函数是不可以被继承的:是对类进行初始化的,无法继承

  2. 运算符重载函数也是不可以被继承的

# 访问权限的修改方法

private:
    Student::nickname;//char nickname [16]; 语法上没问题,没有将原来的 nickname 变为私有的
    void SetNickName();// 新定义了一个 private 方法,父类对应方法被隐藏

如果重新声明 char nickname[16]; , 并没有修改原来父类中 nickname 访问等级,语法无误,但是语义不对;

# 友元和 protected

image-20221115153353920

void clobber(Sneaky &s) 通过传入派生类访问基类的 protected 成员。 void clovver(Base &b) 传入基类不能够访问。

protected 只能由派生类来访问。否则只要声明某基类的派生类,就能够访问该基类的 protected 成员,绕过保护机制,随意访问保护成员。

友元不具有传递性。不能通过成为派生类的友元而成为基类的友元。

# 继承的初始化

image-20221115153818892

4image-20221115154011142

没有在成员初始化表中指出,则调用默认构造函数

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 的构造函数

# 虚函数

image-20221122101802410

类型相容:类型相同、派生类

赋值相容(不丢失信息):类型相同、派生类【发生对象切片,属于派生类的属性不复存在】

A a; B b; a = b;
B b;
A a = b;// 相当于调用了 A 的拷贝构造函数,调用哪个,取决于声明的变量是什么类型的

image-20221122103030879

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

image-20221122103419160

  1. 前期绑定

    在编译的时候就可以确定【func1\func2 在编译时就已经确定调用 A 的 f】

    cpp 默认前期绑定

  2. 动态绑定

    java 虚拟机动态地决定调用哪个版本

  3. cpp 是一门注重效率的语言

    cpp 需要通过显式指出使用后期绑定

image-20221122103808406

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

image-20221122103859475

# 限制

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

image-20221122104329491

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 的函数指针。

image-20221122111714347

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

image-20221122113523933

override 告诉编译器是一个虚函数的重定义。

final 不能够再 override。

不能够写成 void f1(int) override , 这样更改了 f1 的类型

void f2(int) 名隐藏,不能够再调用 d.f2()

image-20221122114428240

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

image-20221122114804082

为什么不能直接设置 display 的空函数,而是设置纯虚函数?

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

image-20221122115451339

通过构建非虚函数 CreateButton() ,可以实现更好的代码复用

抽象工厂模式

image-20221124141423470

是否需要新的析构函数,看是否声明了新的成员变量。

需要释放新的变量,与基类不同,需要新的析构函数,需要是虚函数。

调用完 D 的析构函数后,就会调用 B 的析构函数。【倒着的次序调用】

image-20221124142320235

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

image-20221124143344316

(嘿嘿公开继承应该不考

前置条件更弱,后置条件更强。

比如 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

image-20221124151137410

实际上是 Has-A 关系(is a,是这个类的一种,继承,派生类是一个基类对象;has a, 含有这个类,包含,在派生类中拥有一个基类对象)

Student 私有继承 HumanBeing,则不能够调用 b.eat。

函数参数无法从派生类隐式转换为基类:如果两个类的继承是私有的,则不能在派生类外将派生类转换成基类对象。不能在派生类外部隐式转化。

一般私有继承是很罕见的,仅是可以使用基类的 protected 成员。

# 虚函数的使用情景

image-20221124152246426

纯虚函数 —— 每个派生类依赖小,自己实现自己的。

一般虚函数 —— 覆盖功能多,派生类只需要修改部分。依赖性强。

非虚函数 —— 绝对不要重定义。保证所有的行为一致。

# 多继承

image-20221124152817915

# 名冲突

image-20221124153222563

出现名冲突的问题,Bed 和 Sofa 同时都有 SetWeight ()

如何解决名冲突的问题:

  1. 都是自己设计 —— 基类分解 Base-Class Decomposition,形成的结构

image-20221124153341543

image-20221124153456359

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

image-20221124153711861

# 虚基类

image-20221124154018657

virtual public 和 public virtual 等价

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

# 内存存储

image-20221124154455909

多继承有多个虚函数表指针 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;
}

image-20221115152301500

只能修改原来就是可见(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;
}

image-20221115153218615