父类指针、子类指针

父类指针可以指向子类对象,是安全的。(继承方式必须是public

子类指针指向父类对象是不安全的。

struct A
{
    int a;
};

struct B : A
{
    int b;
};

int main()
{
    // 父类指针指向子类对象
    A *a = new B();
    delete a;
    return 0;
}

多态

多态是面向对象非常重要的一个特性。

同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

在运行时,可以识别出真正的对象类型,调用对应子类中的函数。


使用函数重载来实现传入不同类型的对象,调用同名的函数的,做不同的事情。

struct Dog
{
    void speak()
    {
        printf("Dog::speack\n");
    }

    void run()
    {
        printf("Dog::run\n");
    }
};

struct Cat
{
    void speak()
    {
        printf("Cat::speack\n");
    }

    void run()
    {
        printf("Cat::run\n");
    }
};

struct Pig
{
    void speak()
    {
        printf("Pig::speack\n");
    }

    void run()
    {
        printf("Pig::run\n");
    }
};

void liu(Dog *p)
{
    p->speak();
    p->run();
}

void liu(Cat *p)
{
    p->speak();
    p->run();
}

void liu(Pig *p)
{
    p->speak();
    p->run();
}

int main()
{
    liu(new Dog());
    liu(new Cat());
    liu(new Pig());

    return 0;
}

不过这样,每添加一个对象,就要添加对应的函数。(函数里的内容还都是一样的)。

可以让他们都继承同一个父类,通过传入不同的子类对象,产生不同的执行结果。

struct Animal 
{
    void speak()
    {
        printf("Animal::speack\n");
    }

    void run()
    {
        printf("Animal::run\n");
    }
};

struct Dog : Animal
{
    void speak()
    {
        printf("Dog::speack\n");
    }

    void run()
    {
        printf("Dog::run\n");
    }
};

struct Cat : Animal
{
    void speak()
    {
        printf("Cat::speack\n");
    }

    void run()
    {
        printf("Cat::run\n");
    }
};

struct Pig : Animal
{
    void speak()
    {
        printf("Pig::speack\n");
    }

    void run()
    {
        printf("Pig::run\n");
    }
};

void liu(Animal *p)
{
    p->speak();
    p->run();
}

int main()
{
    liu(new Dog());
    liu(new Cat());
    liu(new Pig());

    return 0;
}
// 输出
// Animal::speack
// Animal::run
// Animal::speack
// Animal::run
// Animal::speack
// Animal::run

但是,却没有达到想要的效果。

上面用到了函数重写(Override),子类的函数和父类的函数,函数名、返回值和参数都一致,构成函数重写。

默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态。

虚函数

virtual修饰的成员函数。

C++中的多态通过虚函数(virtual function)来实现。

只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual )。

struct Animal 
{
    virtual void speak()
    {
        printf("Animal::speack\n");
    }

    virtual void run()
    {
        printf("Animal::run\n");
    }
};

struct Dog : Animal
{
    void speak()
    {
        printf("Dog::speack\n");
    }

    void run()
    {
        printf("Dog::run\n");
    }
};

struct Cat : Animal
{
    void speak()
    {
        printf("Cat::speack\n");
    }

    void run()
    {
        printf("Cat::run\n");
    }
};

struct Pig : Animal
{
    void speak()
    {
        printf("Pig::speack\n");
    }

    void run()
    {
        printf("Pig::run\n");
    }
};

void liu(Animal *p)
{
    p->speak();
    p->run();
}

int main()
{
    liu(new Dog());
    liu(new Cat());
    liu(new Pig());

    return 0;
}
// 输出
// Dog::speack
// Dog::run
// Cat::speack
// Cat::run
// Pig::speack
// Pig::run

虚表

虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的虚函数地址,这个虚表也叫虚函数表。

struct Animal 
{
    int age;
    virtual void speak()
    {
        printf("Animal::speack\n");
    }

    virtual void run()
    {
        printf("Animal::run\n");
    }
};

struct Cat : Animal
{
    int id;
    void speak()
    {
        printf("Cat::speack\n");
    }

    void run()
    {
        printf("Cat::run\n");
    }
};

int main()
{
    printf("%d\n", sizeof(Cat)); // x86输出12,虚表指针占4字节,两个int类型的成员 3 * 4 = 12
    Animal *p = new Cat();
    p->age = 20;

    return 0;
}

image-20210720143002014

子类继承带有虚函数的父类后,在创建子类对象的时候,会在这个对象开头加入一个指向虚表的指针。在调用某个函数的时候,首先通过指向虚表的指针找到对应的函数地址,再调用具体的函数。

不同类型的虚表是分开的。所有相同类型的对象,共用一张虚表。

父类也是有虚表的。有虚函数就会有虚表。

虚析构函数

当父类指针指向子类对象的时候,应该将父类析构函数声明为虚函数(虚析构函数)。

delete父类指针时,才会调用子类的析构函数,保证析构的完整性。

纯虚函数

没有函数体且初始化为0的虚函数,用来定义接口规范。

struct Animal 
{
    virtual void speak() = 0;
    virtual void run() = 0;
};

struct Dog : Animal
{
    void speak()
    {
        printf("Dog::speack\n");
    }

    void run()
    {
        printf("Dog::run\n");
    }
};

抽象类(Abstract Class)

含有纯虚函数的类,不可以实例化(不可以创建对象)。

抽象类也可以包含非纯虚函数、成员变量。

struct Animal 
{
    int id;
    virtual void speak() = 0;
    virtual void run() = 0;

    void test()
    {
        // ...
    }
};

如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类。

要素

子类重写父类的成员函数(override)。

父类指针指向子类对象。

利用父类指针调用(重写的)成员函数。

调用父类的成员函数

简单粗暴:父类类名::成员函数

struct Animal 
{
    void speak()
    {
        printf("Animal::speack\n");
    }

    void run()
    {
        printf("Animal::run\n");
    }
};

struct Dog : Animal
{
    void speak()
    {
        Animal::speak();
        printf("Dog::speack\n");
    }

    void run()
    {
        printf("Dog::run\n");
    }
};