实际开发中,隐藏通常很可怕,所以要禁止出现隐藏
重载的特征:
相同的范围(在同一个类中)
函数名字相同
参数不同
virtual 关键字可有可无
返回值可相同也可不同
代码如下:
class Base
{
public:
void Test()
{
cout << "Base::Test()" << endl;
}
virtual void Test(int nParam)
{
cout << "Base::Test(int nParam)" << endl;
}
};
// overload
{
Base b;
b.Test();
b.Test(10);
}
输出:
Base::Test()
Base::Test(int nParam)
重写的特征:
在派生类中覆盖基类中的同名函数,要求基类函数必须是虚函数
父类指针和引用指向子类的实例时,通过父类指针或引用可以调用子类的函数(多态)
通常在子类重写函数的后面增加override关键字,用来告诉编译器这是重写函数,让编译器来帮助检查是否正确重写,如果没有正确重写则编译报错,这类错误要在 编译期暴露出来,如果没有override关键字,编译又没有报错,在运行期有可能出现隐藏带来的未知行为
代码如下:
class Drived1 : public Base
{
public:
void Test(int nParam)
{
cout << "Drived1::Test(int nParam)" << endl;
}
};
// 当Drived1子类指针指向Drived1时,此时调用基类的无参Test函数,编译期就不通过
// 显示Drived1没有无参Test函数,正验证重写的第5特征
{
// overwrite
Drived1* pDrived1 = new Drived1;
pDrived1->Test();
pDrived1->Test(20);
delete pDrived1;
}
// 若基类Base指针指向子类Drived1时,此时调用基类的无参Test函数,没有编译错误
{
// overwrite
Base* pBase = new Drived1;
pBase->Test();
pBase->Test(20);
delete pBase;
}
输出:
Base::Test()
Drived1::Test(int nParam)
隐藏的特征:
不同范围(基类、子类)
当子类指针指向子类本身时,如果子类的函数与基类的函数同名,参数不同, 此时,不论有无virtual关键字,基类的函数将被隐藏
当基类指针指向子类本身时,如果子类的函数与基类的函数同名,参数相同,基类函数没有virtual关键字, 此时通过基类指针调用子类函数时,其实是调用基类函数
代码如下:
先看看隐藏的第2特征:
class Drived1 : public Base
{
public:
void Test(int nParam1,short nParam2)
{
cout << "Drived1::Test(int nParam1,short nParam2)" << endl;
}
};
// 当Drived1子类指针指向Drived1时,此时调用基类的无参Test和有参函数,编译期就不通过
{
// hide
Drived1* pDrived1 = new Drived1;
pDrived1->Test();
pDrived1->Test(20);
delete pDrived1;
}
隐藏的第3特征:
class Base
{
public:
void Test()
{
cout << "Base::Test()" << endl;
}
void Test(int nParam)
{
cout << "overload Base::Test(int nParam)" << endl;
}
};
class Drived1 : public Base
{
public:
void Test(int nParam)
{
cout << "Drived1::Test(int nParam)" << endl;
}
};
{
Base* pBase = new Drived1;
pBase->Test();
pBase->Test(20);
delete pBase;
}
输出:
Base::Test()
Base::Test(int nParam)
很明显调用基类函数
故事还要从作用域说起,下面代码:
int x;
void Func()
{
double x;
std::cin >> x;
}
当编译器遇见函数Func 中的std::cin » x语句时,此时x应该是函数Func内的x还是外层的int x,这里涉及到名称隐藏name-hiding rules,以上段代码为例此时编译器按照如下 顺序查找x:
在Func()函数内查找
如果第一步没有找到,则向外层查找,即外层的int x
函数Func()内的x会隐藏global作用域内的x
当有继承时会怎么样?例如:
class Base
{
public:
virtual void MemberFunc1();
}
class Derived : public Base
{
public:
void DerivedFunc()
{
MemberFunc1();
}
}
当编译器遇见子类函数DerivedFunc内部的函数调用MemberFunc1(),此时必须决议出MemberFunc1是什么,此时按照和上面例子相似的顺序查找MemberFunc1:
在DerivedFunc()函数内查找
如果第一步没有找到,再向外层查找,即Derived作用域内
如果第二步没有找到,则再向外层查找,即Base作用域内,DerivedFunc()则是在Base作用域内
如果第三步没有找到,则再向外层查找,即Base所在的namespace作用域
如果第四步没有找到,则再向外层查找,即global作用域
上述两个例子只是粗略的列出查找顺序,不够精确但是方向是对的,一层一层的作用域查找
如果再加上重载呢,虚函数的重载,非虚函数的重载,子类重写虚函数,子类重写非虚函数,例如下面的例子:
class Base
{
public:
virtual void MemberFunc1();
// MemberFunc1的重载
virtual void MemberFunc1(int param);
public:
void MemberFunc2();
void MemberFunc2(int param);
};
class Derived : public Base
{
public:
// MemberFunc1的重写
virtual void MemberFunc1();
public:
// MemberFunc2的重写
void MemberFunc2();
};
Derived d;
int param = 0;
d.MemberFunc1();
// 错误,Derived::MemberFunc1隐藏了Base::MemberFunc1
d.MemberFunc1(param);
d.MemberFunc2();
// 错误,Derived::MemberFunc2隐藏了Base::MemberFunc2
d.MemberFunc2(param)
子类Derived只重写了基类Base的MemberFunc1函数和MemberFunc2函数中的个别函数,在编译期,d.MemberFunc1(param)这段代码和d.MemberFunc2(param)就抛出错误,因为子类Derived的函数 MemberFunc1和函数MemberFunc2遮掩了基类Base的同名函数,所以从名称隐藏规则来看子类Derived不再继承基类Base的MemberFunc1函数和MemberFunc2函数, 从类型上讲函数MemberFunc1()和函数MemberFunc1(int param)是不同类型的,但是名称隐藏规则不关心类型是否相同
那么如果非要使用被遮掩的函数呢?可以使用using声明式,像下面这样:
class Derived : public Base
{
public:
using Base::MemberFunc1;
// MemberFunc1的重写
virtual void MemberFunc1();
public:
using Base::MemberFunc2;
// MemberFunc2的重写
void MemberFunc2();
};
Derived d;
int param = 0;
d.MemberFunc1();
d.MemberFunc1(param);
d.MemberFunc2();
d.MemberFunc2(param)
此时编译通过,另外,子类Derived重写了MemberFunc2函数,这种行为在实际的开发规范里要明确禁止的,这可能触发隐藏并且导致非预期的行为,参考上面的隐藏特征
以上面例子来说using 声明式的一个缺点是它会将基类的同名函数全部暴露出来,但是有时候子类又希望只使用其中几个基类函数,并不希望全部暴露出去,此时就出现了矛盾, 矛盾点在于基类Base的虚函数是public,而子类又是public继承,子类又不希望将基类的虚函数全部暴露出去,这就违背了public继承就代表is-a的事实,那么有下面几种方案可选
维持is-a的关系
维持is-a的关系那么需要调整虚函数的属性,可以改为protected,而这种前提是你必须同时是基类和子类的设计者
class Base
{
protected:
virtual void MemberFunc1();
// MemberFunc1的重载
virtual void MemberFunc1(int param);
public:
void MemberFunc2();
void MemberFunc2(int param);
};
// 伪代码
class Derived : public Base
{
public:
void Func()
{
MemberFunc1();
}
}
不维持is-a的关系并且通过继承来实现
不维持is-a的关系并且通过继承来实现,这种情况通常是子类的设计者没有办法修改基类的虚函数的public属性,子类的设计者只能看到头文件, 可以使用private继承,例如:
class Base
{
public:
virtual void MemberFunc1();
// MemberFunc1的重载
virtual void MemberFunc1(int param);
public:
void MemberFunc2();
void MemberFunc2(int param);
}
// 伪代码
class Derived : private Base
{
public:
virtual void MemberFunc1() override
{
int param = 10;
Base::MemberFunc1(param);
}
}
那么子类Derived只是将虚函数MemberFunc1()暴露给外面
不维持is-a的关系通过包含关系
这种情况和上面类似,包装类的设计者没有办法修改基类的虚函数的public属性,包装类的设计者只能看到头文件,例如:
class Base
{
public:
virtual void MemberFunc1();
// MemberFunc1的重载
virtual void MemberFunc1(int param);
public:
void MemberFunc2();
void MemberFunc2(int param);
}
class Widget
{
public:
void WidgetFunc()
{
base.MemberFunc1();
}
private:
Base base;
}
将基类作为包装类的private成员变量