理解接口继承规则( http://www.cppblog.com/kesalin/)

重载(overload),重写(override),屏蔽(hide)

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

重写(override):派生类重写基类中同名同参数同返回值的函数(通常是虚函数,这是推荐的做法)。同样重写函数可以有不同的修饰符

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

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

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)。至此,聪明的你应该很容易明白为什么 d.f(&value);  无法通过编译了吧(VS编译器的提示信息很给力)。

 

三、函数继承规则

 

鉴于继承基类的函数有如此隐晦的概念需要弄懂,再加上virtual函数,public/protected/prvate继承等等,更是增加了理解一个类接口的难度,因此C++里面有很多针对类接口继承的惯用法:

1、优先使用组合而非继承。既然继承的代价如此之大,那么最好的就是不继承,当然不是说完全不用继承,只有在存在明确的IS-A关系时,继承的好处才会显示出来,而在其他情况下应该毫不犹豫的使用组合,而且要优先使用PIMP手法来使用组合。

 

2纯虚函数继承规则:声明纯虚函数的目的是让派生类来继承函数接口而非实现,使得纯虚函数就像Java中的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尽量不要屏蔽外围作用域的名字,屏蔽所带来的隐晦难以理解在前面已有描述。

如果没有选择必须重新定义或重写基类中的同名函数,那么你应该为每一个yu原本隐藏的名字引入一个using声明或使用转交函数(派生类定义同名同参函数,在该函数内部调用基类的同名同参函数)来使这些名字在派生类的作用域中可见。

 

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继承通常意味着根据某物实现出,此种情况下使用基类与派生类这样的术语并不太适合,因为他不满足liskov的替换规则,并且从基类继承而来的所有接口均为私有的,外部不可访问,private继承可用PIML手法取代。

文中已经两次提到 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、还有一种流派认为不应该公开出析构函数之外的虚函数接口,而应该公开一个非虚函数,在该非虚函数内protected/private的虚函数。这种做法是将接口合适被调用与接口如何被实现分离出来,已达到更好的效果,再设计模式上,这是一种侧路模式。通常在非虚函数内内联调用虚函数,所以在在效率上与直接调用虚函数相比不想上下。

 

posted @ 2014-11-02 13:32  liaotingpure  阅读(283)  评论(0编辑  收藏  举报