您的位置:首页 > 编程语言 > C语言/C++

C++接口继承规则

2013-12-10 11:21 176 查看
1、重载:在相同作用域内,函数名称相同,参数或常量性不同的相关函数称为重载。
重载函数之间的区分主要在参数和常量性的不同上,若仅仅是返回值或修饰符 virtual,public/protected/private的不同不被视为重载函数(无法通过编译)。
不同参数是指参数的个数或类型不同,而类型不同是指各类型之间不能进行隐身类型转换或不多于一次的用户自定义类型转换。当调用发生时,编译器在进行重载决议时根据调用所提供的参数来选择最佳匹配的函数。

2、重写:派生类重写基类中同名同参数同返回值的函数(通常是虚函数,这是推荐的做法)。

同样重写的函数可以有不同的修饰符virtual,public/protected/private。

3、屏蔽:一个内部作用域(派生类,嵌套类或名字空间)内提供一个同名但不同参数或不同常量性的函数,使得外围作用域的同名函数在内部作用域不可见,编译器在进行名字查找时将在内部作用域找到该名字从而停止去外围作用域查找,因而屏蔽外围作用域的同名函数。

注意:编译器在决定哪一个函数应该被调用时,依次要做三件事:名字查找,重载决议,访问性检查。

4、示例:

class Base  
{  
public:  
    virtual void f() { cout << "Base::f()" << endl; }  
    void f(int) { cout << "Base::f(int)" << endl; }  
    virtual void f(int) const { cout << "Base::f(int) const" << endl; }  
    virtual void f(int *) { cout << "Base::f(int *)" << endl; }  
};  
  
class Derived : public Base  
{  
public:  
    virtual void f() { cout << "Derived::f()" << endl; }  
    virtual void f(char) { cout << "Derived::f(char)" << endl; }  
};  
const Base b;  
b.f(10);  
  
Derived d;  
int value = 10;  
  
d.f();  
d.f('A');  
d.f(10);  
//d.f(&value);//编译报错


上面代码,Base 中的一系列名为 f 的函数在同一作用域内,且同名不同参或不同常量性,故为重载函数;而 Derived 中的 f() 则是重写了基类同名同参的 f();而 Derived 中的 f(char) 则屏蔽了 Base 中所有的同名函数。所以上面代码的执行结果是:

Base::f(int) const
Derived::f()
Derived::f(char)
Derived::f(char)
对 d.f(10);这两个调用,看似基类 Base 中有更好的匹配,但实际上由于编译器在进行名字查找时,首先在 Derived 类作用域中进行查找,找到 f(char) 就停止去基类作用域中查找,因而基类的所有同名函数没有机会进入重载决议,因而被屏蔽了。因此编译器将 10 隐式转型为 char 调用 Derived 中的 f(char)。

5、函数继承规则:
鉴于继承基类的函数有如此隐晦的概念需要弄懂,再加上 virtual 函数,public/protected/private 继承等等,更是增加了理解一个类接口的难度(因为你不仅要看类自身的接口,还有向上追溯所有基类的接口,以及是以何种方式继承基类的接口等等)。因此,C++里面有很多针对类接口继承的惯用法:
1)优先使用组合而非继承。
既然继承代价如此之大,那么最好的就是不继承。当然不是说完全不用继承,只有在存在明确的“IS-A”关系时,继承的好处才会显现出来(可以用多态-但要遵循 Liskov 替换原则);
而其他情况下(”HAS-A”或“Is-implemented-in-terms-of”)应毫不犹豫地使用组合,而且要优先使用 PIMPL(Point to implementation) 手法来使用组合。
2)纯虚函数继承规则。
声明纯虚函数的目的是让派生类来继承函数接口而非实现,使得纯虚函数就像Java或C#中的 interface 一样。
唯一的例外就是需要纯析构函数提供实现(避免资源泄漏)。
3)非纯虚函数继承规则。
声明非纯虚函数的目的是让派生类继承函数接口及默认实现。
但这是一种欠佳的做法,因为默认实现能让新加入的没有重写该实现的派生类通过编译并运行,而默认实现有可能并不适用于新加入的派生类,对此编译器并不会提供任何信息(警告都没一个)。
为了应对这一潜在的陷阱,诞生了另一个规则:”纯虚函数的声明提供接口,纯虚函数的实现提供默认实现;派生类必须重写该接口,但在实现时可以调用基类的默认实现。如下代码所示:

class Base
{
public:
    virtual void f() = 0;
};

void Base::f()
{
    cout << "Base::f() default implement." << endl;
}

class DerivedA : public Base
{
public:
    virtual void f()
    {
        Base::f();
    }
};

class DerivedB : public Base
{
public:
    virtual void f()
    {
        cout << "DerivedB::f() override." << endl;
    }
};
4)非虚函数继承规则。
永远也不要重写基类中的非虚函数。非虚函数的目的就是为了让派生类继承基类的强制性实现,它并不希望被派生类改写。

5)尽量不要屏蔽外围作用域(包括继承而来的)名字。
如果没得选择(我还真没想到有什么场景会出现这种情况,通常换个名字都是可行的)必须重新定义或重写基类中同名函数,那么你应该为每一个原本会被隐藏的名字引入一个 using 声明或使用转交函数(派生类定义同名同参函数,在该函数内部调用基类的同名同参函数)来使这些名字在派生类的作用域中可见。(Effective C++ 条款33)。

该规则应用如下:

class Base
{
public:
    virtual void f() { cout << "Base::f()" << endl; }
    void f(int) { cout << "Base::f(int)" << endl; }
    virtual void f(int) const { cout << "Base::f(int) const" << endl; }
    virtual void f(int *) { cout << "Base::f(int *)" << endl; }
};

class Derived : public Base
{
public:
    using Base::f;
    virtual void f() { cout << "Derived::f()" << endl; }
    //virtual void f(char) { cout << "Derived::f(char)" << endl; }
};
const Base b;
b.f(10);

Derived d;
int value = 10;

d.f();
d.f('A');
d.f(10);
d.f(&value);

运行得到的结果为:
Base::f(int) const
Derived::f()
Base::f(int)
Base::f(int)
Base::f(int *)
在这里,因为使用了 using Base::f; ,因此基类中的所有名字 f 对子类来说都是可见的,所有 d.f(&value); 等均可通过编译运行了。再次提醒:这是一种非常不好的做法。

6)基类的析构函数应当为虚函数,以避免资源泄漏。
假设有如下情况,带非虚析构函数的基类指针 pb 指向一个派生类对象 d,而派生类在其析构函数中释放了一些资源,如果我们 delete pb; 那么派生类对象的析构函数就不会被调用,从而导致资源泄漏发生。因此,应该声明基类的析构函数为虚函数。
7)避免 private 继承 。
private 继承通常意味着根据某物实现出(Is-implemented-in-terms-of),此种情况下使用基类与派生类这样的术语并不太合适,因为它不满足 Liskov 替换原则,并且从基类继承而来的所有接口均为私有的,外部不可访问。
private 继承可用 PIMPL 手法取代。

文中已经两次提到 PIMPL 利器,在这里就 private 继承先给出一个示例,以后再详述 PIMPL 的好处。
原先使用 private 继承:

class SomeClass
{
public:
    void DoSomething(){}
};

class OtherClass : private SomeClass
{
private:
    void DoSomething(){}
};

使用 PIMPL 手法替代:
class SomeClass
{
public:
    void DoSomething(){}
};

class OtherClass
{
public:
    OtherClass();
    ~OtherClass();
void DoSomething();
private:
    SomeClass * pImpl;
};

OtherClass::OtherClass()
{
    pImpl = new SomeClass();
}

OtherClass::~OtherClass()
{
    delete pImpl;
}

void OtherClass::DoSomething()
{
    pImpl->DoSomething();
}

8)不要改写继承而来的缺省参数值。

因为虚函数是动态绑定的,而缺省参数值却是静态绑定的,这样你在进行多态调用时:函数是由动态类型决定的,而其缺省参数却是由静态类型决定的,违反直觉。

class Base
{
public:
    // 前面的示例为了简化代码没有遵循虚析构函数规则,在这里说明下
    virtual ~Base() {}; 
    virtual void f(int defaultValue = 10)
    {
        cout << "Base::f() value = " << defaultValue << endl;
    }
};

class Derived : public Base
{
public:
    virtual void f(int defaultValue = 20)
    {
        cout << "Derived::f() value = " << defaultValue << endl;
    }
};
这段代码的输出为:
Derived::f() value = 10
调用的是动态类型 d -派生类 Derived的函数接口,但缺省参数值却是由静态类型 pb-基类 Base 的函数接口决定的,这等隐晦的细节很可能会浪费你一下午来调试,所以还是早点预防为好。

9)还有一种流派认为不应公开(public)除虚析构函数之外的虚函数接口,而应公开一个非虚函数,在该非虚函数内 protected/private 的虚函数。
这种做法是将接口何时被调用(非虚函数)与接口如何被实现(虚函数)分离开来,以达到更好的隔离效果。
在设计模式上,这是一种策略模式。通常在非虚函数内内联调用(直接在头文件函数申明处实现就能达到此效果)虚函数,所以在效率上与直接调用虚函数相比不相上下。

class Base
{
public:
    virtual ~Base() {}
    
    void DoSomething()
    {
        StepOne();
        StepTwo();
    }
private:
    virtual void StepOne() = 0;
    virtual void StepTwo() = 0;
};

class Derived : public Base
{
private:
    virtual void StepOne()
    {
        cout << "Derived StepOne: do something." << endl;
    }
    virtual void StepTwo()
    {
        cout << "Derived StepTwo: do something." << endl;
    }
};
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: