类的多态(Polymorphism)

C++   2022-12-18 09:31   98   0  

多态的概念:

首先,我们先来看一个静态的多态的例子:

class Point {
public:
    int x;
    int y;

    Point() : x(0), y(0) {}
    Point(int _x, int _y) : x(_x), y(_y) {}

    Point operator+(Point& p) { // 重载了+运算符
        Point temp;
        temp.x = this->x + p.x;
        temp.y = this->y + p.y;
        return temp;
    }
};

int main() {
    Point p1 = Point();
    Point p2 = Point(1, 2);
    
    // 以下展现了运算符“+”的多态
    int a = 3 + 4; // 既可以直接作加法
    Point p3 = p1 + p2; // 也可以执行运算符重载定义的方法

    cout << a << endl;
    cout << p3.x << "," << p3.y << endl;

    return 0;
}

通过以上的例子,我们知道,多态其实就是同一个行为的不同的表现。

类的上转型操作:

看下面的例子,有一个Animal父类,一个Cat继承了Animal:

class Animal {
public:
    int age;

    Animal() : age(18) {}

    void shout() {
        cout << "Shout: " << age << endl;
    }
};

class Cat : public Animal {
public:
    char color;

    Cat() : color('y') {}

    void shout() {
        cout << "Meow: " << age << color << endl;
    }
};

void main() {
    Animal ap = c; // 子类上转型,操作合法
    //Cat cp = a;  // 子类下转型,操作不合法

    //Animal* ap = &c; // 合法
    //Cat* cp = &a; // 不合法

    //Animal& ap = c; // 合法
    //Cat& cp = a; // 不合法

    ap.shout();
}

也就是说,子类转换为父类是合法的,因为我们可以说“猫是动物”,但下转型必然是不合法的,我们不能说“动物是猫”。

① 对于对象的上转型:从代码层面来看,子类比父类多一个color属性,如果要把子类上转型为父类,那只需要把子类的age赋值给父类即可,color可以不管,但如果下转型,因为父类没有color,导致子类的color为空,必然会产生错误。

② 对于指针的上转型:从代码层面来看,其实就是父类的指针指向了一个子类,这个时候,指针的类型就派上用场了,虽然我们知道所有指针,无论什么类型,大小都是4字节(32位机器下),但在这个例子中,cp指针指向了一个子类,那么它指向的也只是子类里面的age,和color没有关系。

③ 对于引用的上转型:从代码层面来看,可以理解为还是给c取了个别名ap,但是这个ap只管age这个属性,不管color的属性。

对比三种上转型,我们可以发现,第一种是完全的复制一份的,子类里面的那个color已经不在了,但是后面两种,子类的color依然存在,只是“看不见”了而已

从上述例子,我们知道无论是对象、指针还是引用,对于上转型都是合法的,下转型都是不合法的

动态的多态的实现:

从上一小节我们知道,只有使用指针或者引用的上转型才能在不丢失信息的情况下实现多态,而直接使用对象会丢失信息。接下来我们使用指针和引用来实现多态。

看下面的例子,有一个Animal的父类和Dog、Cat的子类,我要实现当一个Animal是Cat的时候要Meow叫,而一个Animal是Dog的时候Woof地叫:

class Animal {
public:
    void shout() {
        cout << "Shout" << endl;
    }
};

class Cat : public Animal {
public:
    void shout() {
        cout << "Meow" << endl;
    }
};

class Dog : public Animal {
public:
    void shout() {
        cout << "Woof" << endl;
    }
};

void test(Animal* a) {
    a->shout();
}

void main() {
    Cat c;
    Dog d;

    test(&c); // 输出Shout
    test(&d); // 输出Shout
}

结果和我们想的不太一样,这是因为Animal的shout函数是“早绑定”的,如果是指针的话,会直接调用这个早绑定的函数,和对象无关。因此,我们要实现目的效果的话,只需要在Animal的shout函数前面使用virtual修饰即可:

class Animal {
public:
    virtual void shout() { // 虚拟函数
        cout << "Shout" << endl;
    }
};

这样我们的输出就是Meow和Woof了。

当然,我们使用引用来实现也是可以的:

class Animal {
public:
    virtual void shout() {
        cout << "Shout" << endl;
    }
};

class Cat : public Animal {
public:
    void shout() {
        cout << "Meow" << endl;
    }
};

class Dog : public Animal {
public:
    void shout() {
        cout << "Woof" << endl;
    }
};

void test(Animal& a) { // 注意此处传入的是一个引用,但如果没有了引用符号,那就直接是复制对象了,无论传入什么,都是输出Shout了
    a.shout();
}

int main() {
    Cat c;
    Dog d;

    test(c); // Meow
    test(d); // Woof

    return 0;
}

现在我们需要思考的是,为什么加上virtual关键字就可以实现多态了呢?

我们来看下面这段程序:

class Animal {
public:
    void shout() {
        cout << "Shout" << endl;
    }
};

void main() {
    Animal a;
    cout << sizeof(a) << endl;
}

很明显,它的输出应该是1,因为一个对象占用一个字节,而这个对象里面什么属性都没有,尽管有一个方法,但是对象的大小和方法没有任何关系

这个时候,我们再在shout前面加上virtual关键字,再运行以下程序:

class Animal {
public:
    virtual void shout() { // 加上virtual关键字
        cout << "Shout" << endl;
    }
};

void main() {
    Animal a;
    cout << sizeof(a) << endl;
}

这个时候,输出的就是4了(32位,如果是64位那应该输出8),聪明的你应该发现了,这个4(或者8)其实是一个指针的大小,C++为了实现多态,通过在类里面存储一个虚拟函数指针(Virtual Function Pointer)来让函数实现多态。简单来说,就是通过这个指针,在子类上转型为父类的时候,让这个指针指向不同的子类,比如Animal a = Dog()的时候,a的shout函数就会指向Dog的shout,而a = Cat()的时候,就会指向Cat的shout函数。当然,底层实现还会更复杂一点,C++使用了虚函数表来完成这个过程(和之前学习继承的时候解决“菱形继承”的问题很类似)。

纯虚函数(抽象类):

思考一下,上述Animal类的shout方法其实不需要真的实现出来,因为Animal是一个大的概念,shout只是一个能执行的方法罢了,具体实现应该交给它的子类来完成,因此,C++给我们提供了纯虚函数:

class Animal { // 包含纯虚函数的类称为抽象类,它的子类一定要实现抽象方法,抽象类不能被实例化
public:
    virtual void shout() = 0; // 纯虚函数
};

// class Cat : public Animal { // 虽然继承了Animal,但没有实现shout方法,因此仍然无法实例化
// };

class Dog : public Animal { // 继承了Animal抽象类
public:
    void shout() { // 实现了纯虚函数shout
        cout << "Woof" << endl;
    }
};

int main() {
    // Animal a; // 抽象类不能被实例化
    // Cat c; // 没有实现纯虚函数,不能被实例化

    Dog d; // 继承抽象类后实现了纯虚函数,可以实例化
    d.shout(); // 可以调用自己的shout
    Animal* a = &d; // 使用指针实现多态
    a->shout(); // 输出Woof

    return 0;
}

面向抽象编程是一种高级编程思维,需要多加练习来掌握。

对于纯虚函数,考虑下面这种情况:

class Animal {
public:
    Animal() {
        cout << "Animal()" << endl;
    }

    ~Animal() {
        cout << "~Animal()" << endl;
    }

    virtual void shout() = 0;
};

class Cat : public Animal {
public:
    int* age;

    Cat() : age(new int(18)) {
        cout << "Cat()" << endl;
    }

    ~Cat() {
        delete age;
        cout << "~Cat()" << endl;
    }

    void shout() {
        cout << "Meow: " << *age << endl;
    }
};

void test() {
    Animal* a = new Cat();
    a->shout();
    // 函数结束之后,会删掉栈空间的东西,那留在堆空间里的Cat和18就会造成内存泄漏
}

int main() {
    test();
    return 0;
}

运行上述程序之后,会发现子类和父类的析构函数都没有被调用,这是我们不希望的,因此,我们给test函数加上一个delete:

void test() {
    Animal* a = new Cat();
    a->shout();
    delete a;
}

这个时候运行程序会发现,程序只调用了Animal的析构函数,没有调用Cat的析构函数。因此程序发现a是一个Animal的指针,那必然就会去找Animal的析构函数,而Cat依然留在内存空间内,内存泄漏仍然存在!

为了解决这个问题,我们就要使用虚析构函数

class Animal {
public:
    Animal() {
        cout << "Animal()" << endl;
    }


    virtual ~Animal() { // 把析构函数变成虚析构函数即可
        cout << "~Animal()" << endl;
    }
    
    //virtual ~Animal() = 0; // 但是写成纯虚析构是绝对不行的!

    virtual void shout() = 0;
};

这样子,就会发现先调用子类的析构再调用父类的析构了。但是我们发现直接写成纯虚析构是不行的,因为如果是纯虚函数的话,那他的子类就要去实现了,很明显,子类怎么能实现父类的析构函数呢,这是没有意义的!因此,析构函数不能是纯虚函数

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。