课堂笔记| 第七章:多态

本节课要点:

  • 继承
  • 特性
  • 多态
  • 虚函数

目录

一、多继承 

二、继承的前提:正确的分类 

三、多态

1. 虚函数

2. 确保覆盖和终止覆盖

3. 虚函数的实现原理

4. 虚析构函数 

四、纯虚函数和抽象类

1. 纯虚函数 

2. 抽象类 


一、多继承 

在之前的课程中,我们已经构建了自己的双向链表类。双向链表是一种线性表,线性表的作用是存储数据,故又称容器。学习了第六章:继承之后,我们会发现两者之间存在着Is-a关系,即双向链表是一种容器。因此,双向链表类应继承自容器类,即容器类是双向链表类的父类。 

下面考察 container 类(容器类) :

// container.h

#pragma once

#include "value_type.h"

struct container {
    using value_type = ::value_type;
    using pointer = value_type*;
    using reference = value_type&;
};

// traits
struct is_traversible {
    enum : bool {value = true}; // 枚举常量取的是bool值

    bool operator()() { // 这个重载使得结构体成为了可调用对象
        return value;
    }

    // 为了使用reference,把这段copy过来
    using value_type = ::value_type;
    using pointer = value_type*;
    using reference = value_type&;

    using callback = void (reference);
};

is_traversible 可遍历的,是所有容器共有的一种特性。

双向链表类继承容器类和特性类:

// dlist.h

...

class dlist : public container, public is_traversible { // 多继承
public:
    // typename container::value_type表明类型来自父类,typename指明这是一种类型
    using value_type = typename container::value_type;
    using pointer = typename container::pointer;
    using reference = typename container::reference;

private:
    // 定义回调函数类型
    // using callback = void (reference);
    using is_traversible::callback;

    ...

};

特性介于继承和混入(mix-in)之间,但在C++中采用继承的方式来实现。

 

二、继承的前提:正确的分类 

考虑雇员、经理、销售员三者之间的关系,以下是一种错误的分类:

// employee.h

#pragma once

#include <iostream>
#include <string>

class employee {
protected:
    job * j; // 赋值兼容原则

public:
    const std::string name;

    // 只能在初始化列表初始化常量,否则就变成了赋值
    employee(job * j, const std::string& n) : j(j), name(n) {}
    ~employee() {}

    void aboutme() {
        std::cout << name << ':' << j->title << std::endl;
    }
};

class manager : public employee {
    using employee::employee; // 继承构造函数
};

class salesperson : public employee {
    using employee::employee;
};

 修正后:

//employee.h

#pragma once

#include <iostream>
#include <string>

struct job {
    std::string title;
    int salary;
    job(const std::string & t, int s) : title(t), salary(s) {}
};

struct manager : public job {
    manager(int s = 10000) : job("manager", s) {}
};

struct salesperson : public job {
    salesperson(int s = 5000) : job("salesperson", s) {}
};

class employee {
protected:
    job * j;

public:
    const std::string name;

    employee(job * j, const std::string& n) : j(j), name(n) {}
    ~employee() {}

    void aboutme() {
        std::cout << name << ':' << j->title << std::endl;
    }
};

雇主和雇员是按照身份来分的,销售员和经理是按照职位来分的。

销售员和经理是一种角色(role),是可以互相转换的。而我们之前谈到的双向链表和数组是不会互相转换的,它们的关系铁铁的。

 

三、多态

回到之前的多边形类,我们试图求取各多边形的面积:

// poly.cpp

#include <iostream>

class poly {
protected:
    //共性
    std::string ids;
    size_t edge;

public:
    poly(std::string _ids = "poly", size_t e = 0) : ids(_ids), edge(e) {}
    ~poly() {}

    std::string what() const {
        return ids;
    }

    double area() const {
        return 0.0;
    }
};

class quad : public poly {
public:
    quad(std::string _ids = "quad") : poly(_ids, 4) {}
    ~quad() {}
};

class para : public quad {
protected:
    double width, height;

public:
    para(double w = 1, double h = 1, std::string _ids = "para") : quad(_ids), width(w), height(h) {}
    ~para() {}

    double area() const override {
        return width * height;
    }
};

class rect : public para {
public:
    rect(double w = 1, double h = 1, std::string _ids = "rect") : para(w, h, _ids) {}
    ~rect() {}
};

class square final : public rect {
public:
    square(double w = 1) : rect(w, w, "square") {}
    ~square() {}
};

int main() {
    quad *quads[] = { new para(7, 4), new rect(10, 5), new square(8) };

    for (auto q : quads) {
        std::cout << q->what() << ' ' << q->area() << std::endl;
        delete q;
    }

    return 0;
}

输出结果:

para 0
rect 0
square 0

据分析,这里的三个形体使用的都是其父类 quad 的 area() 方法。原因是:我们使用的是父类 quad 的指针指向的子类对象,导致内存被重解释了。quad 认为自己指向的就是一个 quad 对象,因此便调用了父类子对象的 area() 方法。

因此,我们需要使用子类的方法覆盖父类的同原型方法。 

1. 虚函数

若要使子类的成员函数能够覆盖父类的同原型成员,则必须将父类的该成员说明是虚函数。关键字 virtual 明确地告诉编译器:这是一个虚函数;该类子类中的同名版本将覆盖这个版本。 

class poly {
protected:
    std::string ids;
    size_t edge;

public:
    poly(std::string _ids = "poly", size_t e = 0) : ids(_ids), edge(e) {}
    ~poly() {}

    std::string what() const {
        return ids;
    }

    virtual double area() const {
        return 0.0;
    }
};

虚特性能够被继承。如果子类原型一致地重载了父类的某个虚函数,那么即使在子类中没有将这个函数显式说明成是虚的,它也会被编译器认为是虚函数。 

 

2. 确保覆盖和终止覆盖

被 override 描述符修饰的函数明确地告诉编译器,自己是一个覆盖版本。 

class para : public quad {
protected:
    double width, height;

public:
    para(double w = 1, double h = 1, std::string _ids = "para") : quad(_ids), width(w), height(h) {}
    ~para() {}

    double area() const override {
        return width * height;
    }
};

final 描述符能有效终止虚函数的覆盖,或者后继继承的发生。 

class square final : public rect {
public:
    square(double w = 1) : rect(w, w, "square") {}
    ~square() {}
};

 

3. 虚函数的实现原理

考察下述代码:

// sizeof-class-with-virtual.cpp

#include <iostream>

// alignof(类型) 向该类型对齐
class alignas(8) noVirtual { // align对齐 alignas(8)八字节对齐
    char a;
    void f() {}
};

// 按道理来说,函数是不占类对象大小的
class alignas(8) oneVirtual {
    char a;
    virtual void f() {}
};

class alignas(8) manyVirtual {
    char a;
    virtual void f() {}
    virtual int g() {
        return 0;
    }
    virtual double h(double) {
        return 1.0;
    }
};

int main() {
    std::cout << "size of noVirtual: " << sizeof(noVirtual) << std::endl;
    std::cout << "size of oneVirtual: " << sizeof(oneVirtual) << std::endl;
    std::cout << "size of manyVirtual: " << sizeof(manyVirtual) << std::endl;
    std::cout << "ref: size of pointer: " << sizeof(void *) << std::endl;
    return 0;
}

输出结果:

size of noVirtual: 8
size of oneVirtual: 16 # 这多出的8个字节是编译器干的
size of manyVirtual: 16
ref: size of pointer: 8 # 给出参考:指针的大小

为了实现多态,编译器首先要为每个多态类创建一张虚表(VTABLE),表中记录了这个类的所有虚函数的入口地址。此外,编译器还在每一个多态类的对象中设置了一个虚指针(Virtual Pointer/VPTR),它指向了该类的虚表(VTABLE)。

 

4. 虚析构函数 

考察下述代码:

//normal-destructor.cpp

#include <iostream>

class X {
public:
    X() {
        std::cout << "X()" << std::endl;
    }
    ~X() {
        std::cout << "~X()" << std::endl;
    }
};

class Y : public X {
public:
    Y() {
        std::cout << "Y()" << std::endl;
    }
    ~Y() {
        std::cout << "~Y()" << std::endl;
    }
};

int main() {
    X *p = new Y;
    delete p;
    return 0;
}

输出结果:

X()
Y()
~X()

问题:这个对象没有完全被拆除

解决:设置父类的析构函数为虚函数

因为子类和父类占据的内存不一致,因此父类的析构函数不能被继承。

修正后:

class X {
public:
    X() {
        std::cout << "X()" << std::endl;
    }
    virtual ~X() {
        std::cout << "~X()" << std::endl;
    }
};

 

四、纯虚函数和抽象类

1. 纯虚函数 

public:
    poly(std::string _ids = "poly", size_t e = 0) : ids(_ids), edge(e) {}
    ~poly() {}

    std::string what() const {
        return ids;
    }

    virtual double area() const = 0;
};

2. 抽象类 

// 狭义:只有类型定义和纯虚函数,广义:凡是拥有纯虚函数的类
struct container {
    using value_type = ::value_type;
    using pointer = value_type*; 
    using reference = value_type&;

    virtual bool empty() const = 0;
};
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值