C++ 覆盖 重载 隐藏 浅析

来源:互联网 发布:自动光学检测仪编程 编辑:程序博客网 时间:2024/06/05 09:40

【摘要】

本文首先阐释覆盖和重载的基本定义以及它与异常类型、异常数目还有返回值等相关概念间的关系;然后,介绍多态与虚函数等相关概念,并给出代码示例,以比较说明基类指向子类对象地址的指针与子类指向强制转换为子类的基类对象地址的指针在虚函数与一般成员函数(也是隐藏机制的函数)上,输出情况的异同;最后,阐明隐藏等相关概念,给出代码示例,以比较说明指向子类对象地址的基类指针和子类指针在虚函数、隐藏函数、一般成员函数上输出的异同。

【文末,给出本文最关键的一句话,指针的数据类型是实函数的类型,指针指向的对象的数据类型,是虚函数的数据类型。】

【正文】

重载的定义

在同一可访问区内被声名的几个具有不同参数列表的(参数的类型、个数、顺序不同)同名函数,程序会根据不同的参数列表来确定具体调用哪个函数,这种机制叫重载,重载不关心函数的返回值类型。但是,只有返回值相同的同名函数,才有可能构成重载关系。

重载的相关概念

1、在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int, float), 但是不能为fun(int, int));

2、不能通过访问权限、返回类型、抛出的异常进行重载;

3、方法的异常类型和数目不会对重载造成影响;

覆盖的定义

覆盖是指派生类中存在重新定义的函数,其函数名、参数列、返回值类型必须同父类中的相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体(花括号中的部分)不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖。

覆盖的相关概念

1、覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;

2、覆盖的方法的返回值必须和被覆盖的方法的返回一致;

3、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;

4、被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。

多态的概念

面向对象程序设计中一个重要概念是多态性。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。可以把一组对象放到一个数组中,然后调用它们的方法,在这种场合下,多态性的作用就体现出来了,这些对象不必是相同类型的对象。当然,如果它们都继承自某个类,你可以把这些派生类,都放到一个数组中。如果这些对象都有同名方法,就可以调用每个对象的同名方法。同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。多态性通过派生类重载基类中的虚函数来实现。

● 编译时的多态性

编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作。

● 运行时的多态性

运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。运行时的多态性通过虚成员实现。

编译时的多态性为我们提供了运行速度快的特点,而运行时的多态性则带来了高度灵活和抽象的特点。

举个简单的例子:   

void test(CBase *pBase)

{

pBase->VirtualFun();

}

这段程序编译的时刻并不知道运行时刻要调用那个子类的函数,所以,编译的时刻并不会选择跳转到那个函数去!

如果不是虚函数,那么跳转的伪汇编代码应该是call VirtuallFun!

如果是虚函数,就变成了call    pBase->虚函数表里的一个变量,不同的子类在这个变量含有不同的函数地址,这就是所谓的运行时刻。但事实上pBase->虚函数表里的一个变量也是在编译时刻就产生的,它是固定的。所以,运行时刻还是编译时刻,事实上也并不严密,重要的还是理解它的实质!虚函数只是一个函数指针表,具体调用哪个类的相关函数,要看运行时,对象指针或引用所指的真实类型,由于一个基类的指针或引用可以指向不同的派生类,所以,当用基类指针或引用调用虚函数时,结果是由运行时对象的类型决定的。

【给出本文最关键的一句话,指针的数据类型是实函数的类型,指针指向的对象的数据类型,是虚函数的数据类型。】

【多态代码用例】

#include<iostream>using namespace std;class A{public:    void foo()    {        printf("1\n");    }    virtual void fun()    {        printf("2\n");    }};class B : public A{public:    void foo()    {        printf("3\n");    }    void fun()    {        printf("4\n");    }};int main(void){    A a;    B b;    A *p = &a;    p->foo();    p->fun();    p = &b;    p->foo();    p->fun();    return 0;}
【解析】

第一个p->foo()和p->fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是1、2。
第二个输出结果就是1、4。p->foo()和p->fuu()则是基类指针指向子类对象,正是体现多态的用法。p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此,此时指向的就只能是基类的foo()函数的代码了,输出的结果是1。而p->fun()指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此,根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4。
笔试的题目中还有一个另类测试方法。即

B *ptr = (B *)&a;  ptr->foo();  ptr->fun();

问这两调用的输出结果。这是一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是3,2。
  并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现。而ptr->fun()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。

考察类函数指针的类型转换与取值变换

【总结一句话】

Class_B Val_b;

Class_A Val_a = (Class_A*)Val_b;

此刻,Val_a的实函数时Class_A的类型,虚函数是Class_B的类型!!!

覆盖与重载输入输出参数的比较

重载的特征有:

1) 相同的范围(在同一个类中);

2) 函数名字相同;

3) 参数不同;

4) virtual关键字可有可无。

覆盖的特征有:

1) 不同的范围(分别位于派生类与基类);

2) 函数名字相同;

3) 参数相同;

4) 基类函数必须有virtual关键字。

隐藏的定义

隐藏是指派生类的函数屏蔽了与其同名的基类函数。

隐藏的相关概念

(1)如果派生类的函数与基类的函数同名,但是,参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是,基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

代码示例

# include <iostream.h>class Base{public:virtual void f(float x){ cout << "Base::f(float) " << x << endl; }void g(float x){ cout << "Base::g(float) " << x << endl; }void h(float x){ cout << "Base::h(float) " << x << endl; }};  class Derived : public Base{public:virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }void g(int x){ cout << "Derived::g(int) " << x << endl; }void h(float x){ cout << "Derived::h(float) " << x << endl; }}; 
示例程序中:

(1)函数Derived::f(float)覆盖了Base::f(float)。

(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。

(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。

void main(void){Derived d;Base *pb = &d;Derived *pd = &d;// Good : behavior depends solely on type of the objectpb->f(3.14f); // Derived::f(float) 3.14pd->f(3.14f); //Derived::f(float) 3.14// Bad : behavior depends on type of the pointerpb->g(3.14f); // Base::g(float) 3.14pd->g(3.14f); // Derived::g(int) 3        (surprise!)// Bad : behavior depends on type of the pointerpb->h(3.14f); // Base::h(float) 3.14      (surprise!)pd->h(3.14f); // Derived::h(float) 3.14} 

  • 解析:

f( )函数属于覆盖,而g( )与h( )属于隐藏。从上面的运行结果,我们可以注意到在覆盖中,用基类指针和派生类指针调用函数f( )时,系统都是执行的派生类函数f( ),而非基类的f( ),这样实际上就是完成的“接口”功能。而在隐藏方式中,用基类指针和派生类指针调用函数g( )时,系统会进行区分,基类指针调用时,系统执行基类的g( ),而派生类指针调用时,系统“隐藏”了基类的g( ),执行派生类的g( ),这也就是“隐藏”的由来。

  • 从示例看来,隐藏规则似乎很愚蠢。但是隐藏规则至少有两个存在的理由:
    • 写语句pd->g(char)的人可能真的想调用Derived::g(int)函数,只是他误将参数写错了。有了隐藏规则,编译器就可以明确指出错误,这未必不是好事。否则,编译器会静悄悄地将错就错,程序员将很难发现这个错误,留下祸根。
    • 假如类Derived有多个基类(多重继承),有时搞不清楚哪些基类定义了函数g。如果没有隐藏规则,那么pd->g('O')可能会调用一个出乎意料的基类函数g。尽管隐藏规则看起来不怎么有道理,但它的确能消除这些意外。

【最后的话】

指针的类型是实函数的类型,指针所指向对象的类型是虚函数的类型 !!!

1 0
原创粉丝点击