默认情况下,派生类会继承基类中定义的全部行为。本节将更详细地考察成员函数的选择机制,以及如何利用这一机制在派生类中改变行为。
当在派生类对象上调用某成员函数时,编译器首先查找该派生类中是否存在同名函数。若存在,则考虑所有同名重载函数,并通过函数重载解析过程确定最佳匹配;若不存在,则编译器沿继承链逐级向上,以相同方式检查每一个父类。
换言之,编译器将在“包含至少一个同名函数的最派生类”中选择最佳匹配函数。
调用基类函数
首先探讨当派生类无匹配函数而基类有匹配函数时的情形:
#include <iostream>
class Base
{
public:
Base() { }
void identify() const { std::cout << "Base::identify()\n"; }
};
class Derived: public Base
{
public:
Derived() { }
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
输出:
Base::identify()
Base::identify()
调用 base.identify()
时,编译器在 Base
中找到名为 identify
的函数,经匹配无误后予以调用。
调用 derived.identify()
时,编译器先在 Derived
类中查找同名函数,未找到;于是转向父类 Base
,找到 identify
函数并使用之。简言之,因 Derived::identify()
不存在,故使用 Base::identify()
。
这意味着,若基类提供的行为已满足需求,可直接复用基类行为。
重定义行为
若我们在 Derived
类中定义了 Derived::identify()
,则该版本将被优先选用。
因此,通过在派生类中重新定义同名函数,即可使派生类表现出不同的行为。
示例:希望 derived.identify()
打印 Derived::identify()
,只需在 Derived
类中添加同名函数:
#include <iostream>
class Base
{
public:
Base() { }
void identify() const { std::cout << "Base::identify()\n"; }
};
class Derived: public Base
{
public:
Derived() { }
void identify() const { std::cout << "Derived::identify()\n"; }
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
输出:
Base::identify()
Derived::identify()
注意:在派生类中重定义函数时,该函数不会继承基类同名函数的访问说明符,而是使用派生类中定义的访问说明符。因此,基类中为 private 的函数可在派生类中重定义为 public,反之亦然。
示例:
#include <iostream>
class Base
{
private:
void print() const
{
std::cout << "Base";
}
};
class Derived : public Base
{
public:
void print() const
{
std::cout << "Derived ";
}
};
int main()
{
Derived derived {};
derived.print(); // 调用 Derived::print(),其为 public
return 0;
}
在既有功能基础上进行扩展
有时我们并不想完全替换基类函数,而希望在调用派生类对象时为其增加额外功能。上述示例中,Derived::identify()
完全隐藏了 Base::identify()
。若这不是我们期望的行为,可让派生函数先调用同名基类函数(以复用代码),再补充新功能。
若要在派生函数中调用同名基类函数,只需在函数名前加基类作用域限定符即可:
#include <iostream>
class Base
{
public:
Base() { }
void identify() const { std::cout << "Base::identify()\n"; }
};
class Derived: public Base
{
public:
Derived() { }
void identify() const
{
std::cout << "Derived::identify()\n";
Base::identify(); // 显式调用 Base::identify()
}
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
输出:
Base::identify()
Derived::identify()
Base::identify()
执行 derived.identify()
时,实际调用 Derived::identify()
;打印 Derived::identify()
后,再调用 Base::identify()
打印 Base::identify()
。
为何必须使用作用域解析运算符 ::
?若写成:
void identify() const
{
std::cout << "Derived::identify()\n";
identify(); // 无作用域限定,导致自调用并无限递归
}
未加作用域限定符时,默认调用当前类的 identify()
,即 Derived::identify()
,从而导致无限递归。
当需要调用基类的友元函数(如 operator<<
)时,会略有技巧:基类友元并非基类成员,使用作用域限定符无效。解决方案是将派生类对象临时转换为基类引用,以便调用正确的函数版本。借助 static_cast
即可实现:
#include <iostream>
class Base
{
public:
Base() { }
friend std::ostream& operator<< (std::ostream& out, const Base&)
{
out << "In Base\n";
return out;
}
};
class Derived: public Base
{
public:
Derived() { }
friend std::ostream& operator<< (std::ostream& out, const Derived& d)
{
out << "In Derived\n";
// 将 Derived 对象 static_cast 为 Base 引用,以调用正确的 operator<<
out << static_cast<const Base&>(d);
return out;
}
};
int main()
{
Derived derived {};
std::cout << derived << '\n';
return 0;
}
由于 Derived 是一种 Base,可将 Derived 对象 static_cast
为 Base 引用,从而调用形参为 Base 的 operator<<
。
输出:
In Derived
In Base
派生类中的重载解析
如本节开头所述,编译器将在“包含至少一个同名函数的最派生类”中选择最佳匹配。
首先考察一个简单的重载示例:
#include <iostream>
class Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
};
int main()
{
Derived d {};
d.print(5); // 调用 Base::print(int)
return 0;
}
调用 d.print(5)
时,编译器在 Derived 中未发现 print
函数,于是检查 Base,发现两个同名函数,通过重载解析确定 Base::print(int)
为最佳匹配,故调用之。
接下来观察一个可能出乎意料的情况:
#include <iostream>
class Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
public:
void print(double) { std::cout << "Derived::print(double)"; }
};
int main()
{
Derived d {};
d.print(5); // 调用 Derived::print(double),而非 Base::print(int)
return 0;
}
调用 d.print(5)
时,编译器在 Derived 中找到名为 print
的函数,因此仅考虑 Derived 中的候选函数。该函数亦为最佳匹配,故调用 Derived::print(double)
。
尽管 Base::print(int)
的参数类型与实参 5
更匹配,但因 d
是 Derived 类型,且 Derived 中存在 print
函数,Derived 比 Base 更派生,Base 中的函数因而被忽略。
若确需 d.print(5)
解析为 Base::print(int)
,一种不甚理想的方法是在 Derived 中定义 Derived::print(int)
:
#include <iostream>
class Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
public:
void print(int n) { Base::print(n); } // 可行但欠佳,每需转发一个重载就得添加一个函数
void print(double) { std::cout << "Derived::print(double)"; }
};
int main()
{
Derived d {};
d.print(5); // 调用 Derived::print(int),其内部调用 Base::print(int)
return 0;
}
该方法可行,但每想透传一个重载就必须在 Derived 中新增一个转发函数,冗长且易出错。
更佳方案是使用 using 声明,将 Base 中同名函数全部引入 Derived:
#include <iostream>
class Base
{
public:
void print(int) { std::cout << "Base::print(int)\n"; }
void print(double) { std::cout << "Base::print(double)\n"; }
};
class Derived: public Base
{
public:
using Base::print; // 使所有 Base::print() 函数在 Derived 中可见
void print(double) { std::cout << "Derived::print(double)"; }
};
int main()
{
Derived d {};
d.print(5); // 调用 Base::print(int),因其是 Derived 中可见的最佳匹配
return 0;
}
通过在 Derived 中添加 using 声明 using Base::print;
,我们告知编译器:所有名为 print
的 Base 函数在 Derived 中均可见,从而参与重载解析。结果,Base::print(int)
被选中,而非 Derived::print(double)
。