虚函数和多态

虚函数和多态

成员函数重写

子类继承了父类的所有成员函数,但是父类的成员函数不一定适合子类,子类可以重载或者重写(覆盖)父类的成员函数。

重载我们在之前的章节中已有介绍,即函数名相同,参数不同。
重写(覆盖)的意思是函数名和参数表完全一样,即函数的原型完全一样。

比如,void Human::introduce()函数的功能是输出人类的各成员变量的值。但是它并不合适Student对象,因为该函数没有输出Student类增加的年级成员变量grade的值。因此Student类可以新增一个更适合自己的版本,即重写(覆盖)该函数。

class Student : public Human
{
     public:
         void set_grade(int g){ grade = g; }
         int get_grade(){ return grade; }

         int grade;    

         void introduce();  // 新增introduce成员函数
};

void Student::introduce()
{
    // 与Human::introduce()函数实现相同的功能
    cout << "大家好,我是" << name << endl;
    if(is_male){
         cout << "男性" << endl;
    }else{
         cout << "女性" << endl;
    }
    cout << age << "岁" << endl;
    if(id.length() == 0){            
        cout << "身份证号未知" << endl;
    }else{
        cout << "身份证号:" << id << endl;
    }  
    // 新增grade成员的输出
    cout << "年级:" << grade << endl;
}

注意,与重载不同,重写(覆盖)只能发生在继承关系中。单独的一个类中的两个成员函数,或者类外部的两个函数是不能有相同的函数原型的。

class foo
{
public:
    void member(){};
    void member(){}; // 错误,一个类中不能出现原型完全一样的成员函数
};

void fun(int x){}
void fun(int x){}  // 错误,不能出现原型完全一样的函数

重写(覆盖)的意义在于子类可以增加一些成员函数,这些成员函数与父类的成员函数有相同的接口(函数原型),不同的实现。使它们更适合子类。换句话说即成员函数功能的细化。

重写(覆盖)的意义在于子类的”新版本“的成员函数优化了/细化了父类的一些”老版本“成员函数。

注意,父类的”老版本“的被重写的函数也被子类继承了,在子类中存在”老版本“和”新版本“的两种版本。在子类中要使用”老版本“的函数需要使用范围运算符::。否则,默认是”新版本“的。即重写和重载一样,在子类中都会自动隐藏父类的版本的函数。

下面是void Student::introduce()另一种正确的写法。

void Student::introduce()
{
    // 与Human::introduce()函数实现相同的功能,因此可以调用它
    Human::introduce();  
    // 新增grade成员的输出
    cout << "年级:" << grade << endl;
}

而下面的void Student::introduce()的实现会造成void Student::introduce()递归调用。

void Student::introduce()
{
    introduce();  // 错误,调用了Student::introduce自己,形成了递归调用,函数无法结束
    // 新增grade成员的输出
    cout << "年级:" << grade << endl;
}

继承遇到函数重写

前面说过,继承反映的是子类与父类之间“是一个”的关系,比如说对象赋值:

Human p;
Student s;
s.init("周润发", true, 30, "123456", 1);
p = s; // 把Student类对象s中包含的Human部分的各成员的值赋给p的各成员

再比如说,指针操作

Human *p_human = NULL;
Student s;
s.init("周润发", true, 30, "123456", 1);
p_human = &s;
p_human->introduce();

因为,定义p_human时指定它的类型是Human *,因此它认为自己指向的是Human对象。于是,p_human->introduce()调用的是void Human::introduce()方法。

不过,等等,这里我们是不是忽略了什么。

显然,p_human实际上指向的是Student类对象s,而Student重写了introduce方法,为什么p_human->introduce()调用的不能是”新版本“的introduce呢,它更适合Student对象啊。

在这种场景下,要想p_human->introduce()调用的是”新版本“的void Student::introduce(),那么introduce必须是虚函数。

虚函数和多态的含义

如果想在父类中定义一个成员函数留待子类中进行细化,我们必须在它前面加关键字virtual ,以便可以使用指针对指向相应的对象进行操作。

class Human
{
    public:
        void init(string n, int a, bool m, string i){
            name = n;
            age = a;
            is_male = m;
            id = i;
        }
        virtual void introduce();  // 虚函数
    private:
        string name;
        int age;
        bool is_male;
        string id;
};

void Human::introduce()
{
   cout << "大家好,我是" << name << endl;
    if(is_male){
         cout << "男性" << endl;
    }else{
         cout << "女性" << endl;
    }
    cout << age << "岁" << endl;
    if(id.length() == 0){            
        cout << "身份证号未知" << endl;
    }else{
        cout << "身份证号:" << id << endl;
    }  
}

class Student : public Human
{
    public:
        void init(string n, int a, bool m, string i, int g){
            Human::init(n, a, m, i);
            grade = g;
        }
        void introduce();
    private:
        int grade;  // 年级
};

void Student::introduce()
{
    Human::introduce();
    cout << "年级:" << grade << endl;
}

class Soldier : public Human
{
    public:
        void init(string n, int a, bool m, string i, string r){
            Human::init(n, a, m, i);
            rank = r;
        }
        void introduce();
    private:
        string rank;  // 军衔
};

void Soldier::introduce()
{
    Human::introduce();
    cout << "军衔:" << rank << endl;
}

int main()
{
    Human *p_human = NULL;

    Student s;
    s.init("周润发", true, 30, "12345", 1);
    Soldier s2;
    s2.init("刘伯承", true, 30, "23456", "元帅");

    p_human = &s;
    p_human->introduce();

    p_human = &s2;
    p_human->introduce();

    system("pause");

    return 0;
}

父类成员函数是虚函数,那么通过父类指针来调用,调用的版本由指针指向的对象来决定。这就体现了多态的含义。即一种接口,多种实现。

  • 一种调用形式:p_human->introduce()
  • 多种调用结果:实际被调用的可能是下面三个版本的introduce之一
    void Human::introduce()
    void Student::introduce()
    void Soldier::introduce()

大家可以去掉Human类中的virtual关键字,对比一下程序运行结果。

多态的应用

多态的作用

多态有什么作用呢?它帮助我们达到“软件复用”。

那在程序里,可能编写了很多处理Human类的代码,比如下面的函数,


...  // Human、Student、Soldier类的定义

void introduce_triple(Human *p)
{
    int i;
    for(i = 0; i < 3; i++){
        p->introduce();
    }
}

会让Human的进行3遍自我介绍。

那这个函数能否处理学生对象呢,即当p指向学生对象,p->introduce();是否可以调用学生类的introduce。在introduce函数是虚函数的情况下是可以的。


int main()
{
    Student s;
    s.init("周润发", true, 30, "12345", 1);
    Soldier s2;
    s2.init("刘伯承", true, 30, "23456", "元帅");

    introduce_triple(&s);

    introduce_triple(&s2);

    system("pause");

    return 0;    
}

举一个生活中的例子,TCL出了某款型号的彩电比如“栩栩如生”系列第一代产品,还有附带的遥控器可以来遥控电视。随后,后续又推出了该系列第二代、第三代产品。这时,大家可能会想,控制第一代的遥控器也可以控制它们就好了。如果可以,那遥控器就可以复用了。

什么时候父类成员函数加virtual关键字

什么时候需要将父类的成员函数修饰为虚函数,即加virtual关键字呢?就看这个函数是否是适应于子类的,

如果是成员函数适合子类的就不需要加virtual关键字。

如果子类可能会细化这个函数,那就需要在父类中加virtual关键字。

比如void Human::set_age(int age)int Human::get_age()这两个函数是用来设置和读取年龄的。显然,Human类的子类不需要重写这个函数。那么这两个函数就不需要设置为虚函数。

还支持引用类型

多态必须通过父类指针调用才可以起作用,或者引用也可以,如下面的代码


...  // Human、Student、Soldier类的定义

void introduce_triple(Human &p)  // 父类的引用
{  
    int i;
    for(i = 0; i < 3; i++){
        p.introduce();
    }
}

int main()
{
    Student s;
    s.init("周润发", true, 30, "12345", 1);
    Soldier s2;
    s2.init("刘伯承", true, 30, "23456", "元帅");

    introduce_triple(s);

    introduce_triple(s2);

    system("pause");

    return 0;    
}

父类类型本身没有多态效果

如果不是指针和引用,而是父类类型,那么最终调用的是父类的版本,没有多态的效果。如下面代码:


...  // Human、Student、Soldier类的定义

void introduce_triple(Human p)  // 父类类型本身
{ 
    int i;
    for(i = 0; i < 3; i++){
        p.introduce();
    }
}

int main()
{
    Student s;
    s.init("周润发", true, 30, "12345", 1);
    Soldier s2;
    s2.init("刘伯承", true, 30, "23456", "元帅");

    introduce_triple(s);

    introduce_triple(s2);

    system("pause");

    return 0;    
}

抽象基类和纯虚函数

shape类的例子

语法细节

虚函数特性是被继承的,子类重写虚函数,在类的定义里可加virtual关键字也可不加。当然加上更明显的说明此函数是虚函数。


...

class Student : public Human
{
    public:
        void init(string n, int a, bool m, string i, int g){
            Human::init(n, a, m, i);
            grade = g;
        }
        virtual void introduce(); // 正确,也可以不加virtual关键字
    private:
        int grade;  // 年级
};

类定义中指出是某个成员函数是虚函数,如果该函数的实现写在类定义外边,不能加virtual关键字了。

virtual void Student::introduce() // 错误,不能在这里加virtual关键字
{
    Human::introduce();
    cout << "年级:" << grade << endl;
}

其它参考

多态介绍可参考4.4 多态 (Polymorphism)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值