C++中virtual关键字解决了什么问题(二)

一、还是继承引发的问题
尽管子类继承了父类的所有属性和方法,但并不是所以方法均在子类中适用,所以,针对父类中的个别方法,子类中会重新实现。

class A
{
public:
    A(){cout << "Create A" << endl;}
    virtual ~A(){cout << "Destroy A" << endl;}
    void show(){cout << "A::show" << endl;}
};

class B:public A
{
public:
    B(){cout << "Create B" << endl;}
    ~B(){cout << "Destroy B" << endl;}
    void show(){cout << "B::show" << endl;} //与父类中的方法同名
};

int main(int argc, char* argv[])
{
    A* a = new B; //一个指向子类对象的父类指针
    a->show(); //意图执行子类的show方法
    delete a;
    return 0;
}

上面的代码里,子类将父类的show方法进行了“重写”,然而当程序运行起来时会发现,实际执行的show方法依旧是来自父类,并没有体现出多态性:

Create A
Create B
A::show  //执行了父类中的show方法
Destroy B
Destroy A

二、错误的重写,实际是隐藏
1.隐藏
上面的代码中子类的“重写”行为实际被叫做隐藏。那么为什么这么说呢?它隐藏了什么呢?
我们对上面的main函数稍加修改:

int main(int argc, char* argv[])
{
    B* b = new B; //修改指针为子类型
    b->show(); 
    delete b;
    return 0;
}

这次便如愿调用到了子类的show方法,因为在子类里把同名的父类方法隐藏了嘛:

Create A
Create B
B::show
Destroy B
Destroy A

这么简单的改动似乎并不能直观的表现出被隐藏的部分,这里我们借助一个容易与隐藏混淆的行为——重载,来加深对隐藏的理解。
构成重载操作的两个前提条件是:1.同一作用域;2.同(函数)名不同参(数列表)。这里很容易想当然的把子类中继承自父类的作用域当作是子类的作用域,进而将隐藏认为是重载,这是错误的。这里我们不妨将错就错看看有什么结果。

class A
{
public:
    A(){cout << "Create A" << endl;}
    virtual ~A(){cout << "Destroy A" << endl;}
    void show(int i){cout << "A::show i = " << i << endl;} //增加入参
};

class B:public A
{
public:
    B(){cout << "Create B" << endl;}
    ~B(){cout << "Destroy B" << endl;}
    void show(){cout << "B::show" << endl;}
};

int main(int argc, char* argv[])
{
    B* b = new B; 
    b->show(99);
    b->show();  //若是重载,则这两行show方法都能正常调用
    delete b;
    return 0;
}

来看看编译过程:

make
gcc main.cpp -Wall -Werror -m64 -g -c -o main.o
main.cpp: In function ‘int main(int, char**)’:
main.cpp:112:15: error: no matching function for call to ‘B::show(int)’
     b->show(99); //可以看到该调用没有匹配的函数,恰好可以说明父类中的show方法被隐藏
               ^
main.cpp:88:10: note: candidate: void B::show()
     void show(){cout << "B::show" << endl;}
          ^~~~
main.cpp:88:10: note:   candidate expects 0 arguments, 1 provided
Makefile:7: recipe for target 'main.o' failed
make: *** [main.o] Error 1

编译失败了,错误信息可以证明父类中的show方法被隐藏,而且还可以确认隐藏是发生在编译期静态绑定阶段。同样的,同名属性也会被隐藏,这里就不再做赘述了。
2.重写
相比起同名方法的隐藏,重写的直接区别就是多了virtual关键字,即重写的对象一定是虚函数。回到开头那段代码中,增加virtual关键字,实现真正的重写。

class A
{
public:
    A(){cout << "Create A" << endl;}
    virtual ~A(){cout << "Destroy A" << endl;}
    virtual void show(){cout << "A::show" << endl;}
};

class B:public A
{
public:
    B(){cout << "Create B" << endl;}
    ~B(){cout << "Destroy B" << endl;}
    void show() override {cout << "B::show" << endl;} //强烈建议使用override关键字
};

int main(int argc, char* argv[])
{
    A* a = new B; //一个指向子类对象的父类指针
    a->show(); //由于父类中声明了虚方法,此处就能用父类指针成功的调用子类重写后的方法
    delete a;
    return 0;
}

这里强烈建议在子类重写的函数中增加C++11新增的override关键字,既能像注释一样提高代码可读性,又能在编译时发现因手抖而导致重写方法变成了定义一个新方法,提高代码健壮性。
修改后的代码执行结果如下:

Create A
Create B
B::show
Destroy B
Destroy A

三、思维拓展
倘若我非要在子类里重载父类方法可以吗?当然可以。上代码:

class A
{
public:
    A(){cout << "Create A" << endl;}
    virtual ~A(){cout << "Destroy A" << endl;}
    void show(int i) {cout << "A::show i = " << i << endl;} //没有virtual关键字
};

class B:public A
{
public:
    B(){cout << "Create B" << endl;}
    ~B(){cout << "Destroy B" << endl;}
    using A::show;//引入父类作用域
    void show(){cout << "B::show" << endl;} //重载父类方法
};

int main(int argc, char* argv[])
{
    B* b = new B; 
    b->show(99);
    b->show(); //调用子类重载方法
    delete b;
    return 0;
}

我们知道,子类不能重载父类的方法是因为重载不能跨域。而上面的代码就是使用using关键字将父类域引入子类域中,满足重载作用域的条件限制,实现重载。
编译正常通过,程序执行结果如下:

Create A
Create B
A::show i = 99
B::show
Destroy B
Destroy A

未完待续...

posted on 2022-03-04 21:37  OrangeGLC  阅读(69)  评论(0)    收藏  举报

导航