是的,我确实理解它们之间的区别。我想知道的是:为什么要覆盖一个方法?这样做有什么好处?在重载的情况下:唯一的优点是您不必为函数考虑不同的名称?
7 回答
重载通常意味着您在同一范围内有两个或多个具有相同名称的函数。调用时更好地匹配参数的函数获胜并被调用。需要注意的是,与调用虚函数相反,调用的函数是在编译时选择的。这完全取决于参数的静态类型。如果你有一个 forB
和一个 for的重载D
,并且参数是对 的引用B
,但它确实指向一个D
对象,那么B
在 C++ 中选择了 for 的重载。这称为静态调度,而不是动态调度. 如果你想和另一个同名的函数做同样的事情,你就重载了,但是你想为另一个参数类型做同样的事情。例子:
void print(Foo const& f) {
// print a foo
}
void print(Bar const& bar) {
// print a bar
}
他们都打印了他们的论点,所以他们被重载了。但是第一个打印一个 foo,第二个打印一个 bar。如果你有两个函数做不同的事情,当它们具有相同的名称时,它被认为是不好的样式,因为这可能会导致混淆调用函数时实际会发生什么。重载的另一个用例是当您为函数提供附加参数时,它们只是将控制权转发给其他函数:
void print(Foo & f, PrintAttributes b) {
/* ... */
}
void print(Foo & f, std::string const& header, bool printBold) {
print(f, PrintAttributes(header, printBold));
}
如果经常使用重载所采用的选项,这对调用者来说可能很方便。
覆盖是完全不同的东西。它不与超载竞争。这意味着如果您在基类中有虚函数,则可以在派生类中编写具有相同签名的函数。派生类中的函数会覆盖基类的函数。样本:
struct base {
virtual void print() { cout << "base!"; }
}
struct derived: base {
virtual void print() { cout << "derived!"; }
}
现在,如果您有一个对象并调用print
成员函数,则始终调用派生的打印函数,因为它覆盖了基函数之一。如果函数print
不是虚拟的,那么派生的函数不会覆盖基函数,而只会隐藏它。如果您有一个接受基类的函数以及从它派生的每个函数,则覆盖可能很有用:
void doit(base &b) {
// and sometimes, we want to print it
b.print();
}
现在,即使在编译时编译器只知道 b 至少是基类,派生类的 print 也会被调用。这就是虚函数的意义所在。没有它们,将调用基类的打印函数,而派生类中的打印函数不会覆盖它。
这将使想法更加清晰。
您因三个原因过载功能:
提供两个(或更多)执行相似、密切相关的事情的函数,由它接受的参数的类型和/或数量来区分。人为的例子:
void Log(std::string msg); // logs a message to standard out void Log(std::string msg, std::ofstream); // logs a message to a file
提供两种(或更多)方式来执行相同的操作。人为的例子:
void Plot(Point pt); // plots a point at (pt.x, pt.y) void Plot(int x, int y); // plots a point at (x, y)
提供在给定两种(或更多)不同输入类型的情况下执行等效操作的能力。人为的例子:
wchar_t ToUnicode(char c); std::wstring ToUnicode(std::string s);
在某些情况下,值得争论的是,与重载函数相比,不同名称的函数是更好的选择。在构造函数的情况下,重载是唯一的选择。
Override一个函数是完全不同的,并且服务于一个完全不同的目的。函数覆盖是多态性在 C++ 中的工作方式。您重写一个函数以更改该函数在派生类中的行为。这样,基类提供接口,派生类提供实现。
当您从基类继承并希望扩展或修改其功能时,覆盖很有用。即使对象被转换为基类,它也会调用您的覆盖函数,而不是基类。
重载不是必需的,但有时它确实使生活更轻松或更易读。可以说它会使情况变得更糟,但那是不应该使用的时候。例如,您可以拥有两个执行相同操作但作用于不同类型事物的函数。例如Divide(float, float)
应该不同于Divide(int, int)
,但它们基本上是相同的操作。难道你不想记住一个方法名称“Divide”,而不是必须记住“DivideFloat”、“DivideInt”、“DivideIntByFloat”等等?
人们已经定义了重载和覆盖,所以我不会详细说明。
ASAFE问:
[重载]的唯一优势是您没有想到函数的多个名称?
1.您不必考虑多个名称
这已经是一个巨大的优势,不是吗?
让我们与已知的 C API 函数及其虚构的 C++ 变体进行比较:
/* C */
double fabs(double d) ;
int abs(int i) ;
// C++ fictional variants
long double abs(long double d) ;
double abs(double d) ;
float abs(float f) ;
long abs(long i) ;
int abs(int i) ;
这意味着两件事:第一,您必须通过选择正确的函数来告诉编译器它将提供给函数的数据类型。第二,如果你想扩展它,你需要找到花哨的名字,你的函数的用户必须记住正确的花哨的名字。
而他/她想要的只是获得某个数值变量的绝对值......
一个动作意味着一个且只有一个函数名。
请注意,您不限于更改一个参数的类型。只要有意义,任何事情都可以改变。
2.对于运营商来说,是强制性的
让我们看看运营商:
// C++
Integer operator + (const Integer & lhs, const Integer & rhs) ;
Real operator + (const Real & lhs, const Real & rhs) ;
Matrix operator + (const Matrix & lhs, const Matrix & rhs) ;
Complex operator + (const Complex & lhs, const Complex & rhs) ;
void doSomething()
{
Integer i0 = 5, i1 = 10 ;
Integer i2 = i0 + i1 ; // i2 == 15
Real r0 = 5.5, r1 = 10.3 ;
Real r2 = r0 + r1 ; // r2 = 15.8
Matrix m0(1, 2, 3, 4), m1(10, 20, 30, 40) ;
Matrix m2 = m0 + m1 ; // m2 == (11, 22, 33, 44)
Complex c0(1, 5), c1(10, 50) ;
Complex c2 = c0 + c1 ; // c2 == (11, 55)
}
在上面的示例中,您确实希望避免使用除 + 运算符之外的任何其他内容。
请注意,C 对内置类型(包括 C99 复杂类型)具有隐式运算符重载:
/* C */
void doSomething(void)
{
char c = 32 ;
short s = 54 ;
c + s ; /* == C++ operator + (char, short) */
c + c ; /* == C++ operator + (char, char) */
}
所以即使在非对象语言中,也会用到这个重载的东西。
3.对于对象,是强制性的
让我们看看对象基本方法的使用: 它的构造函数:
class MyString
{
public :
MyString(char character) ;
MyString(int number) ;
MyString(const char * c_style_string) ;
MyString(const MyString * mySring) ;
// etc.
} ;
有些人可能认为这类似于函数重载,但实际上,它更类似于运算符重载:
void doSomething()
{
MyString a('h') ; // a == "h" ;
MyString b(25) ; // b == "25" ;
MyString c("Hello World") ; // c == "Hello World" ;
MyString d(c) ; // d == "Hello World" ;
}
结论:重载很酷
在 C 中,当您给出函数的名称时,参数隐含地是调用签名的一部分。如果你有“double fabs(double d)”,那么编译器的 fabs 签名是未修饰的“fabs”,这意味着你必须知道它只需要双精度数。
在 C++ 中,函数的名称并不意味着它的签名是强制的。它在调用时的签名是它的名称和它的参数。因此,如果您编写 abs(-24),编译器将知道它必须调用什么 abs 重载,并且您在编写它时会发现它更自然:您需要 -24 的绝对值。
无论如何,任何使用运算符在任何语言中进行某种程度编码的人都已经使用过重载,无论是 C 还是 Basic 数字运算符、Java 字符串连接、C# 委托等。为什么?因为它更自然。
而上面展示的例子只是冰山一角:使用模板时,重载变得非常有用,但这是另一回事。
教科书的例子是类 Animal 和方法 speak()。Dog 子类将 speak() 覆盖为“吠叫”,而 Cat 子类将 speak() 覆盖为“喵”。
重载的一种用途是在模板中使用。在模板中,您编写可用于不同数据类型的代码,并以不同类型调用它。如果采用不同参数的函数必须以不同的方式命名,则不同数据类型的代码通常必须不同,模板就无法工作。
虽然您可能还没有编写模板,但您几乎可以肯定会使用其中的一些模板。流是模板,向量也是。如果没有重载,因此没有模板,您需要调用与 ASCII 流不同的 Unicode 流,并且您必须使用数组和指针而不是向量。