一. 继承

C++ 面向对象设计有三个基本要点:封装,继承,多态

通过 继承 联系在一起的类构成一种层次关系,通常在层次关系的根部有一个 基类 ,其它类则直接或间接地从基类继承而来,这些继承得到的类称为 派生类

继承连接可以是 public、protected、private

1.1 public 继承

public 继承意味着 “is-a” (是一种)的关系,比如:“学生”是一个“人”,“老师”是一个“人”,“学生”和“老师”便可以通过 public 继承成为 “人” 的派生类

#include <cstdio>
#include <string>

class Person{
private:
    int age;
    std::string name;

public:
    explicit Person(const std::string &name, int age)
    :age(age), name(name){}

public:
    void describe(void){
        printf("my name is %s, my age is %d years old.\n", this->name.c_str(), this->age);
    } 
};

class Student : public Person{
public:
    explicit Student(const std::string &name, int age)
    :Person(name, age){}
};

class Teacher : public Person{
public:
    explicit Teacher(const std::string &name, int age)
    :Person(name, age){}
};

int main(){
    Student you("Alice", 18);
    you.describe();

    Teacher me("Jack", 30);
    me.describe();
    return 0;
}

public 继承可以拿到基类中访问权限为 public、protect 的属性和方法,并且访问权限与父类一致

public 继承意味着适用于“基类”身上的每一件事情一定也适用于“派生类”身上,因为每一个“派生类”对象也都是一个“基类”对象

protect 继承意义不明确,不用学

private 继承虽然有意义的 implemented-in-terms-of (根据某物实现出),即派生类的实现需要基类的某些特性。然而这种关系也可以由复合实现,并尽可能使用复合实现,必要时才使用 private 继承

二. 多态

多态的实现依靠基类的指针或引用调用虚函数或纯虚函数实现

2.1 虚函数、纯虚函数

在 public 继承中

(1)有时候你会希望“派生类”只继承“基类”成员函数的接口

(2)有时候你会希望“派生类”同时继承“基类”成员函数的接口和实现,并且能够覆写所继承的实现

(3)有时候你会希望“派生类”同时继承"基类"成员函数的接口和实现,并且不允许覆写任何东西

(1)的实现依靠继承纯虚函数 (2)的实现依靠继承虚函数 (3)的实现依靠继承普通函数就可以了

声明虚函数

virtual void test(void){
    printf("This is a test.\n");
}

声明纯虚函数

virtual void test(void) = 0;
#include <cstdio>
#include <string>

class Person{
private:
    int age;
    std::string name;

public:
    explicit Person(const std::string &name, int age)
    :age(age), name(name){}

    virtual ~Person() = default;

public:
    void describe(void) const{
        printf("my name is %s, my age is %d years old.\n", this->name.c_str(), this->age);
    }

    virtual void work(void) const = 0;
};

class Student : public Person{
public:
    explicit Student(const std::string &name, int age)
    :Person(name, age){}

public:
    void work(void) const override{
        printf("I'm a student, my work is studying.\n");
    }                                                                                                                                          
};

class Teacher : public Person{
public:
    explicit Teacher(const std::string &name, int age)
    :Person(name, age){}

public:
    void work(void) const override{
        printf("I'm a teacher, my work is teaching.\n");
    }
};

void yourWork(const Person &person){
    person.work();
    return;
}

int main(){
    Student you("Alice", 18);
    Teacher me("Jack", 30);
    yourWork(you);
    yourWork(me);
    return 0;
}

含有纯虚函数的类被称为抽象基类

带有多态性质的基类应该声明一个 virtual 析构函数。原因在于当”派生类“对象被一个”基类“指针删除,而该基类带着一个 non-virtual 析构函数时,其结果是未定义的,通常是对象的”派生“成分未被销毁,于是造成一个诡异的”局部销毁“对象

当派生类覆写它继承的某个成员函数时,应在形参列表后添加一个关键字 override

三. auto 关键字

auto 可以在声明变量时根据初始化表达式自动推断该变量的类型

auto num_1 = 5;               // 推导为 int
auto num_2 = 5.0;             // 推导为 double
auto str = "hello, world";    // 推导为 const char *

如果我们编写的代码让编译器无法进行推导,那么使用 auto 会导致编译失败

auto i;
i = 5;

3.1 decltype

decltype 可以获取对象或表达式的类型

int num_1 = 1;
decltype(num_1) num_2 = 2; 

3.2 如何查看推导的类型?

利用编译器诊断,要让编译器显示出它推导的类型,一个有效的方法就是,错误地使用那种类型,从而导致编译报错,错误报告里面会很清楚地显示出那个出错类型

首先,声明一个未定义的类模板

template <typename Type>
class Test;

然后尝试去实例化这个模板,这将会产生一个错误信息,因为没有模板定义可以实例化,我们想看哪个类型,就用哪个类型去实例化这个类模板即可

Test<decltype(num_1)> test;
Test<decltype(num_2)> test;
Test<decltype(str)> test;

作业

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库的sort函数的情况下解决这个问题。

示例 1: 输入:nums = [2,0,2,1,1,0]

输出:[0,0,1,1,2,2]

示例 2: 输入:nums = [2,0,1]

输出:[0,1,2]