重载重写隐藏

实际开发中,隐藏通常很可怕,所以要禁止出现隐藏

重载


重载的特征:

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

  2. 函数名字相同

  3. 参数不同

  4. virtual 关键字可有可无

  5. 返回值可相同也可不同

代码如下:

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)

重写


重写的特征:

在派生类中覆盖基类中的同名函数,要求基类函数必须是虚函数

  1. 不同范围(基类、子类)
  2. 函数名、参数列表、返回值相同
  3. 基类函数有virtual
  4. 子类函数virtual可有可无
  5. 当子类指针指向子类本身时,若子类重写函数是一个重载版本,那么基类的其他同名重载函数将在子类中隐藏

父类指针和引用指向子类的实例时,通过父类指针或引用可以调用子类的函数(多态)

通常在子类重写函数的后面增加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)

隐藏


隐藏的特征:

  1. 不同范围(基类、子类)

  2. 当子类指针指向子类本身时,如果子类的函数与基类的函数同名,参数不同, 此时,不论有无virtual关键字,基类的函数将被隐藏

  3. 当基类指针指向子类本身时,如果子类的函数与基类的函数同名,参数相同,基类函数没有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:

  1. 在Func()函数内查找

  2. 如果第一步没有找到,则向外层查找,即外层的int x

函数Func()内的x会隐藏global作用域内的x

当有继承时会怎么样?例如:

class Base
{
public:
	virtual void MemberFunc1();
}

class Derived : public Base
{
public:
	void DerivedFunc()
	{
		MemberFunc1();
	}
}

当编译器遇见子类函数DerivedFunc内部的函数调用MemberFunc1(),此时必须决议出MemberFunc1是什么,此时按照和上面例子相似的顺序查找MemberFunc1:

  1. 在DerivedFunc()函数内查找

  2. 如果第一步没有找到,再向外层查找,即Derived作用域内

  3. 如果第二步没有找到,则再向外层查找,即Base作用域内,DerivedFunc()则是在Base作用域内

  4. 如果第三步没有找到,则再向外层查找,即Base所在的namespace作用域

  5. 如果第四步没有找到,则再向外层查找,即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成员变量