C++中如何避免覆盖由继承而来的成员

简介: C++中如何避免覆盖由继承而来的成员

1.作用域覆盖


本篇文章主要讨论的是继承中的作用域覆盖问题,实质上还是作用域范围大小的问题。下面先回顾一下简单的变量作用域覆盖问题,在C/C++程序中下面类似的代码有很多。


int x;   // global变量
void someFunc(){
    double x;   // local变量
    cin >> x;
}


上面的程序中,cin其实读取的是局部变量x而非全局变量x,这是由于内层作用域中的同名变量x会覆盖外层作用域中的变量x。当编译器处于someFunc()函数的局部作用域内时,它就在局部作用域中查找变量x。如果它在此作用域中找到变量x,则不会去其他作用域中查找。值得注意的是:someFunc()函数中的变量x是double类型,而全局变量x则是int类型。记住:作用域覆盖仅与变量名称有关而与具体类型无关。


2.继承中的作用域覆盖问题


在C++继承中,位于子类中的成员函数内部且指向基类中的某物(成员变量、成员函数、typedef)时,编译器可以查找到我们在子类成员函数中所指向基类中的某物。这是由于子类继承了声明在基类中的所有东西。子类作用域被嵌套在基类作用域中。如下所示:


class Father{
private:
    int x;
public:
    virtual void mf1() = 0;   // 纯虚函数
    virtual void mf2();       // 虚函数
    void mf3();               // 普通成员函数
    // ...
};
class Son: public Father{
public:
    virtual void mf1();
    void mf4();
    // ...
};


上面的程序中,基类Father中包含了纯虚函数、虚函数、普通成员函数三种类别,这是为了强调上一节中所说作用域覆盖仅与名称有关而与其他无关。假设子类Son中的mf4()成员函数实现如下所示:


void Son::mf4(){
  // ...
  mf2();
  // ...
}


当编译器看到mf4()函数内部的mf2()时,它必须得想想这个mf2()它指向的是谁呢?编译器的做法:首先查找mf4()成员函数局部作用域看看有没有mf2(),然后查找mf4()成员函数的外部作用域,还是没有找到任务名称为mf2()的东西,再接着查找外部作用域即基类Father中的作用域,在这里找到了一个名为mf2()的东西,于是停止查找。假设基类Father中还是没找到mf2(),查找还会继续下去,查找包含基类Father的那个namespace命名空间,最后在全局作用域中查找。


考虑作用域覆盖背后隐藏的更复杂信息,对上述单继承程序进行修改。在基类Father中重载mf1()和mf3()函数,同时在子类Son中增加mf3()函数,如下所示:


class Father{
private:
    int x;
public:
    // 重载mf1()函数
    virtual void mf1() = 0;   // 纯虚函数
    virtual void mf1(int);
    virtual void mf2();       // 虚函数
    // 重载mf3()函数
    void mf3();               // 普通成员函数
    void mf3(double);
    // ...
};
class Son: public Father{
public:
    virtual void mf1();
    void mf3();
    void mf4();
    // ...
};


以作用域为基础的“同名成员名称覆盖规则”来看,Father中所有名为mf1()和mf3()的函数都被子类Son中的mf1()和mf3()覆盖了,上述程序中Father::mf1和Base::mf3不再被子类Son继承。


int main(){
    int x;
    Son s;
    // ...
    s.mf1();    // OK,调用Son::mf1
    s.mf1(x);   // 错误,因为Son::mf1覆盖了Father::mf1
    s.mf2();    // OK,调用Father::mf2
    s.mf3();    // OK,调用Son::mf3
    s.mf3(x);   // 错误,因为Son::mf3覆盖了Father::mf3
    return 0;
}

从上面的客户端程序中,我们可以看出即使基类Father和子类Son内的函数有不同的参数类型,而且不论函数是否是虚函数还是纯虚函数,作用域覆盖都是适用的。这样做的理由是:为了防止你在客户端程序内建立新的子类时顺带地继承父类中的重载函数。但是,当你使用公有继承但又不继承基类中的重载函数时,你已经违背了基类与子类之间的is-a关系。有些情况下你还必须得继承父类中的重载函数。此时,你该咋办呢?解决方法:使用using声明式,如下所示:


class Father{
private:
    int x;
public:
    // 重载mf1()函数
    virtual void mf1() = 0;   // 纯虚函数
    virtual void mf1(int);
    virtual void mf2();       // 虚函数
    // 重载mf3()函数
    void mf3();               // 普通成员函数
    void mf3(double);
    // ...
};
class Son: public Father{
public:
    using Father::mf1;    // 让Father类中的mf1和mf3所有东西在Son作用域内都可见
    using Father::mf3;
    virtual void mf1();
    void mf3();
    void mf4();
    // ...
};
int main(){
    int x;
    Son s;
    // ...
    s.mf1();    // OK,调用Son::mf1
    s.mf1(x);   // 现在OK啦,调用Father::mf1
    s.mf2();    // OK,调用Father::mf2
    s.mf3();    // OK,调用Son::mf3
    s.mf3(x);   // 现在OK啦,调用Father::mf3
    return 0;
}

有时候你并不想子类继承基类中所有的函数,在public继承下,这是绝对不被允许的,因为它违背了is-a关系。但是,在private继承下,前面的想法是可以的。例如:假设子类Son以private方式继承基类Father,而子类Son仅仅想继承基类Father中的无参数版本mf1()函数。此时,继续使用using声明方式已经不行啦,因为使用using声明式会令继承而来的所有的mf1()函数在子类Son中都可见。可以使用另一个解决方法简单的转交函数。如下所示:


class Father{
private:
    int x;
public:
    // 重载mf1()函数
    virtual void mf1() = 0;   // 纯虚函数
    virtual void mf1(int);
    virtual void mf2();       // 虚函数
    // 重载mf3()函数
    void mf3();               // 普通成员函数
    void mf3(double);
    // ...
};
// 注意:下面是私有继承方式
class Son: private Father{
public:
    // 转交函数实现子类仅继承基类中无参数版本的mf1()函数
    virtual void mf1(){
        Father::mf1();
    }
    void mf3();
    void mf4();
    // ...
};
int main(){
    int x;
    Son s;
    // ...
    s.mf1();    // OK,调用Son::mf1
    s.mf1(x);   // 错误,Father::mf1被覆盖了
    return 0;
}


3.总结


(1) 子类中的成员会覆盖基类中的同名称成员。在public继承方式下,每个人都不希望这样。
(2) 为了让被覆盖的同名成员可以重见天日,可以使用using声明式或转交函数。

相关文章
|
1月前
|
存储 编译器 C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(一)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
1月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
79 11
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
存储 编译器 C++
C++入门3——类与对象2-1(类的6个默认成员函数)
C++入门3——类与对象2-1(类的6个默认成员函数)
28 1
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解2
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
29 3
|
1月前
|
编译器 C++
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解1
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
44 3
|
1月前
|
C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(二)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
1月前
|
编译器 C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(三)
【C++】深入探索类和对象:初始化列表及其static成员与友元