【发布时间】:2009-01-09 18:22:45
【问题描述】:
是的,我确实了解它们之间的区别。我想知道的是:为什么要覆盖一个方法?这样做有什么好处? 在重载的情况下:唯一的好处是您不必考虑函数的不同名称?
【问题讨论】:
标签: c++ overriding overloading
是的,我确实了解它们之间的区别。我想知道的是:为什么要覆盖一个方法?这样做有什么好处? 在重载的情况下:唯一的好处是您不必考虑函数的不同名称?
【问题讨论】:
标签: c++ overriding overloading
重载通常意味着你在同一个作用域中有两个或多个同名的函数。调用时更好地匹配参数的函数获胜并被调用。需要注意的是,与调用虚函数相反,调用的函数是在编译时选择的。这完全取决于参数的静态类型。如果您有B 的重载和D 的重载,并且参数是对B 的引用,但它确实指向D 对象,则在C++ 中选择B 的重载。这称为静态调度,而不是动态调度。如果你想和另一个同名的函数做同样的事情,你就重载了,但是你想为另一个参数类型做同样的事情。示例:
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 也会被调用。这就是虚函数的意义所在。没有它们,将调用基类的打印函数,而派生类中的打印函数不会覆盖它。
【讨论】:
derived中的重写函数前面有关键字virtual?派生类中是否必须将virtual放在被覆盖的前面?
virtual 没有区别,但人们用它来表明它实际上是一个可以调用的虚函数,例如,通过对基类的引用。有一个陷阱,如果你写virtual void prinf(...){ ...}(名字中的错字)你会得到一个虚函数,但当然它没有连接到base::print,由此产生的错误可能很微妙。在 C++11 中,添加了 override 限定符以显式声明覆盖:如果您编写 void prinf(...) override {...},您将收到编译错误,直到您修复错字为止。
这将使想法更加清晰。
【讨论】:
您出于三个原因过度加载函数:
提供两个(或更多)函数,它们执行相似、密切相关的事情,由它接受的参数的类型和/或数量来区分。人为的例子:
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);
在一些 情况下,值得争论的是,一个不同名称的函数是比重载函数更好的选择。在构造函数的情况下,重载是唯一的选择。
覆盖覆盖一个函数是完全不同的,并且服务于一个完全不同的目的。函数覆盖是多态性在 C++ 中的工作方式。您重写一个函数以更改该函数在派生类中的行为。这样,基类提供接口,派生类提供实现。
【讨论】:
当您从基类继承并希望扩展或修改其功能时,覆盖非常有用。即使对象被转换为基类,它也会调用您的覆盖函数,而不是基类。
重载不是必需的,但有时它确实使生活更轻松或更易读。可以说它会使情况变得更糟,但那是不应该使用的时候。例如,您可以拥有两个执行相同操作但作用于不同类型事物的函数。例如Divide(float, float) 应该与Divide(int, int) 不同,但它们基本上是相同的操作。你不想记住一个方法名称“Divide”,而不是必须记住“DivideFloat”、“DivideInt”、“DivideIntByFloat”等等?
【讨论】:
人们已经定义了重载和覆盖,所以我不会详细说明。
ASAFE问:
[重载]的唯一优势是您没有想到函数的多个名称?
这已经是一个很大的优势了,不是吗?
让我们与已知的 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) ;
这意味着两件事:第一,您必须通过选择正确的函数来告诉编译器它将提供给函数的数据类型。第二,如果你想扩展它,你需要找到花哨的名字,你的函数的用户必须记住正确的花哨的名字。
而他/她想要的只是获得某个数值变量的绝对值...
一个动作意味着一个且只有一个函数名。
请注意,您不仅限于更改一个参数的类型。只要有意义,任何事情都可以改变。
让我们看看运营商:
// 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) */
}
所以即使在非对象语言中,也会使用这种重载的东西。
让我们看看对象基本方法的使用:它的构造函数:
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 流,并且您必须使用数组和指针而不是向量。
【讨论】: