本节课要点:
- 继承
- 特性
- 多态
- 虚函数
目录
一、多继承
二、继承的前提:正确的分类
三、多态
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;
};