一、抽象、封装

数据抽象:是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制,是一种依赖于接口实现分离的设计技术
数据封装:是一种把数据和操作数据的函数捆绑在一起的机制

1. 好处

  • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损
  • 类实现可能随着时间的推移而发生变化,数据抽象可以更好的应对不断变化的需求

2. 策略

  • 通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。
  • 抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可

3. 接口

接口描述了类的行为和功能,而不需要完成类的特定实现。如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类

接口的好处实现了解耦合的作用。 可以将软件架构分为业务逻辑层、抽象层和实现层

二、继承

继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。类派生列表以一个或多个基类命名

派生类可以访问基类中所有的非私有成员,同时,一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数
  • 基类的重载运算符
  • 基类的友元函数

三、多态

虚函数:虚函数是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链编到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链编,或后期绑定。

  • 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数
  • 一般要将父类的析构函数设置为虚函数,如果不把父类的析构函数设置为虚函数,在 delete 父类指针时就不会调用子类的析构了
  • 类析构顺序:1)派生类本身的析构函数;2)对象成员析构函数;3)基类析构函数
  • 若在基类中不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数,在函数参数后直接加 = 0 告诉编译器,函数没有主体,这种虚函数即是纯虚函数

测试类:

// 基类 Shape
class Shape 
{
protected:
    int width, height;
public:
    Shape(int a = 0, int b = 0)
    {
       width = a;
       height = b;
    }
    virtual ~Shape()
    {
        cout << "Shape destructor" << endl;
    }
    
    // pure virtual function
    virtual int area() = 0;
};
// 基类 PaintCost
class PaintCost 
{
public:
    int getCost(int area)
    {
        auto res = area * 70;
        cout << "PaintCost: " << res << endl;
        return res;
    }
};
class Rectangle: public Shape, public PaintCost
{
public:
    Rectangle(int a = 0, int b = 0):Shape(a, b) 
    { }
    ~Rectangle()
    {    
        cout << "Rectangle destructor" << endl;
    }
    void printPro()
    {
        // 访问父类的成员变量(不能访问父类的私有成员)
        cout << "width: " << width << "\theight: " << height << endl;
    }
    int area ()
    { 
        auto area = width * height;
        cout << "Rectangle class area: " << area <<endl;
        return area; 
    }
};
class Triangle: public Shape
{
public:
    Triangle(int a = 0, int b = 0):Shape(a, b) 
    { }
    ~Triangle()
    {    
        cout << "Triangle destructor" << endl;
    }
    int area ()
    { 
       auto area = width * height / 2;
       cout << "Triangle class area: " << area <<endl;
       return area; 
    }
};

多继承:

int main()
{
    Rectangle rec(10, 20);
    rec.printPro();
    // 调用父类函数
    rec.getCost(10);
    return 0;
}

输出:

Start
width: 10	height: 20
PaintCost: 700
Rectangle destructor
Shape destructor
0
Finish

多态:

int main()
{
    Shape *shape = new Rectangle(10, 7);
    Triangle  tri(10,5);

    shape->area();          //Rectangle class area
    delete shape;
 
    shape = &tri;
    shape->area();          //Triangle class area
    // delete shape;        // 这种指针不能 delete
    return 0;
}

输出:

Start
Rectangle class area: 70
Rectangle destructor
Shape destructor
Triangle class area: 25
Triangle destructor
Shape destructor
0
Finish

实现多态的原因: 如果父类有 virtual 方法,编译器会创建一个虚函数表(在只读区),同时会在类中存储一个指向虚函数表的指针 VPTR,这个表也会被子类继承,如果子类中重写了父类的虚函数,就会在子类中覆盖所继承的虚函数表中的函数

虚函数表指针是分步初始化的,在构建父类时指向的还是父类的虚函数表

class A{
public:
    A(){
        cout << "A()..." << endl;
        print();
    }
    virtual ~A(){
        cout << "~A()..." << endl;
        print();
    }
    virtual void print(){
        cout << "A" << endl;
    }
};
class B : public A{
public:
    B(){
        cout << "B()..." << endl;
        print();
    }
    ~B(){
        cout << "~B()..." << endl;
        print();
    }
    virtual void print(){
        cout << "B" << endl;
    }
};
int main()
{
    A *a = new B;
    delete a;
    return 0;
}

输出:

A()...
A
B()...
B
~B()...
B
~A()...
A