24-7 调用继承函数与覆盖行为
默认情况下,派生类会继承基类中定义的所有行为。在本节中,我们将更详细地探讨成员函数的选取机制,以及如何利用此机制在派生类中修改行为。
当对派生类对象调用成员函数时,编译器首先检查派生类中是否存在同名函数。若存在,则考虑所有同名重载函数,并通过函数重载解析过程确定最佳匹配项。若未找到,编译器将沿继承链向上遍历,依次对每个父类进行相同检查。
换言之,编译器将从至少包含一个同名函数的最派生类中选择最佳匹配函数。
调用基类函数
首先,让我们探讨当派生类没有匹配函数,但基类存在时会发生什么:
#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;
}
这将输出:

当调用基类基类.identify()时,编译器会检查基类Base中是否定义了名为identify()的函数。由于存在该函数,编译器继续检查是否匹配。匹配成功,因此调用该函数。
当调用派生类派生类.identify()时,编译器会检查派生类Derived中是否定义了名为identify()的函数。由于不存在该函数, 此时编译器会转至父类(本例中为基类),再次尝试查找。基类已定义identify()函数,故直接调用该函数。换言之,由于派生类未定义该函数,最终调用了基类::identify()。
这意味着当基类提供的行为已满足需求时,我们可直接沿用基类的行为。
重新定义行为
然而,如果我们在派生类中定义了Derived::identify()方法,那么该方法就会被优先使用。
这意味着我们可以通过在派生类中重新定义方法,让它们在派生类中表现出不同的行为!
例如,假设我们希望 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;
}
这将输出:

请注意,当你在派生类中重定义函数时,派生函数不会继承基类中同名函数的访问限定符。它将采用在派生类中定义时所使用的访问限定符。因此,基类中定义为 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(); // calls derived::print(), which is public
return 0;
}

扩展现有功能
有时我们并不想完全替换基类函数,而是希望在通过派生对象调用时为其添加额外功能。在上例中,请注意派生类::identify() 完全隐藏了基类::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(); // note call to Base::identify() here
}
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}
这将输出:

当执行 derived.identify() 时,它会解析为 Derived::identify()。打印 Derived::identify() 后,它会调用 Base::identify(),从而打印 Base::identify()。
这应该相当直观。为何需要使用作用域解析运算符 (:😃?如果我们像这样定义 Derived::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";
identify(); // no scope resolution results in self-call and infinite recursion
}
};
int main()
{
Base base {};
base.identify();
Derived derived {};
derived.identify();
return 0;
}


若未指定作用域解析限定符调用 identify() 函数,系统将默认采用当前类中的 identify() 实现,即 Derived::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";
// static_cast Derived to a Base object, so we call the right version of operator<<
out << static_cast<const Base&>(d);
return out;
}
};
int main()
{
Derived derived {};
std::cout << derived << '\n';
return 0;
}
由于派生类是基类的实例,我们可以将派生类对象静态转换为基类引用,从而调用使用基类的正确版本的<<运算符。
这将输出:

派生类中的重载解析
如本节开头所述,编译器将从具有至少一个同名函数的最派生类中选择最匹配的函数。
首先,让我们看一个简单的重载成员函数示例:
#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:
};
int main()
{
Derived d{};
d.print(5); // calls Base::print(int)
return 0;
}

对于调用 d.print(5),编译器在 Derived 中找不到名为 print() 的函数,因此它检查 Base,并在其中找到两个同名函数。它通过函数重载解析过程确定 Base::print(int) 比 Base::print(double) 更匹配。因此,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)"; } // this function added
};
int main()
{
Derived d{};
d.print(5); // calls Derived::print(double), not Base::print(int)
return 0;
}

对于调用 d.print(5),编译器在 Derived 中找到一个名为 print() 的函数,因此在确定解析为哪个函数时,它只会考虑 Derived 中的函数。该函数也是 Derived 中与本次函数调用匹配度最高的函数。因此,此调用将解析为 Derived::print(double)。
由于 Base::print(int) 的参数与整型参数 5 的匹配度优于 Derived::print(double),您可能预期此调用会解析为 Base::print(int)。但由于 d 是 Derived 实例,且 Derived 中至少存在一个 print() 函数,同时 Derived 比 Base 更具派生性,因此 Base 中的函数根本不会被纳入考虑范围。
那么若我们确实希望 d.print(5) 解析为 Base::print(int) 该怎么办?一种不太理想的方法是定义 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); } // works but not great, as we have to define
void print(double) { std::cout << "Derived::print(double)"; }
};
int main()
{
Derived d{};
d.print(5); // calls Derived::print(int), which calls Base::print(int)
return 0;
}

虽然这种方法可行,但并不理想,因为我们需要为每个需要传递到基类的重载函数在派生类中添加一个函数。这可能导致大量额外函数,它们本质上只是将调用转发给基类。
更好的方案是在派生类中使用using声明,使所有具有特定名称的基类函数在派生类内部可见:
#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; // make all Base::print() functions eligible for overload resolution
void print(double) { std::cout << "Derived::print(double)"; }
};
int main()
{
Derived d{};
d.print(5); // calls Base::print(int), which is the best matching function visible in Derived
return 0;
}

通过在派生类中放置使用声明 using Base::print;,我们告知编译器所有名为 print 的基类函数应在派生类中可见,这将使它们具备重载解析的资格。因此,系统会选择 Base::print(int) 而非 Derived::print(double)。

浙公网安备 33010602011771号