在 C++ 和 Java 或它们的相关规则中,对覆盖抽象方法的限制是什么。您必须匹配参数或返回类型。我通常会看到只使用返回类型而没有参数实现的抽象函数,是否由派生类来指定其余部分。它是如何工作的?
6 回答
方法覆盖必须具有与其覆盖的父方法相同的方法签名,否则不称为覆盖。
爪哇:
public abstract class AbstractTest {
public abstract void test() throws Exception;
}
public class ConcreteTest extends AbstractTest {
@Override
public void test() throws Exception {
}
}
如您所见,ConcreteTest
(其中 extends AbstractTest
)必须覆盖test()
. 它们具有相同的方法名称、返回类型并且没有方法参数。子类可以省略基类抛出的异常,抛出自己的异常。子类还可以添加额外的(未)检查异常。
正如Peter Lawrey 所提到的,java 接口方法是隐式抽象方法(请参阅我关于Java Abstract Interface的 SO 问题)。
这里的关键是在这种情况下方法可见性不能改变(因为它是分层可见性,即私有->保护->公共)。这是有效的:
public abstract class AbstractTest {
protected abstract void test() throws Exception;
}
public class ConcreteTest extends AbstractTest {
@Override
public void test() throws Exception {
}
}
(父类有一个受保护的方法,子类可以覆盖相同的方法,并且只有两种可见性选择:受保护或公共)。
另外,假设你有
public class B {
}
public class D extends B {
}
public abstract class Base {
public abstract B foo();
}
public class Derived extends Base {
@Override
public D foo() {
// TODO Auto-generated method stub
return new D();
}
}
您将看到Derived
返回 aD
而不是 a B
。这是为什么?这是因为派生类遵循与父类相同的签名,并且派生类subtype
的返回类型是父类的返回类型。
所以,我可以有这个:
Base pureBase = new Derived();
B b = pureBase.foo(); //which returns class D
if (b instanceof D) {
//sure, it is, do some other logic
}
在 C++ 中,您可以获得类似的效果,使用协变返回类型
C++
class AbstractTest {
public:
virtual void test() = 0;
};
class ConcreteTest : AbstractTest {
public:
void test() {
//Implementation here...
}
};
在 C++ 中,具有纯虚函数(以 a 结尾的虚函数=0
)的类称为抽象类。子类(在 C++ 中,类扩展由 分隔:
)覆盖纯虚方法(除非它不包含=0
)。它具有与父类相同的签名。
回到我们的 Java 示例,假设您有:
class B {
};
class D : B {
};
class Base {
public:
virtual B* foo() = 0;
}
class Derived : Base {
public:
D* foo() {
return new D();
}
}
相同的推理(如 java 中所解释的)在这里完成。协变返回类型也适用于受保护和私有继承。更多关于协变返回类型。
我不了解 Java,但在 C++ 中,您必须指定完全相同的参数类型。返回类型 - 另一方面 - 是协变类型,这意味着如果在原始函数中返回类型 A 的指针或引用,则覆盖函数可以返回类型 B 的指针或引用,只要 B要么是 A,要么直接或间接地从它派生。
正如 Als 所指出的,必须将函数声明为 virtual 才能被覆盖。由于 OP 明确询问了抽象方法,它们都定义了virtual
和=0
,因此无需指出这一点。但是,我想更清楚地说明重写函数不需要声明为虚拟的。正如引用的标准所说,匹配声明为 virtual 的基本成员函数的签名(具有宽松规则,如协变类型)的成员函数将被覆盖,无论它是否指定为 virtual。也就是说,重写函数不需要声明为虚拟的;另一方面,抽象成员函数必须是。
两种语言在覆盖自然语义差异的要求方面是相似的。基本上,两者都要求对调用代码(即参数)有完全相同的约束,并提供相同或更严格的处理保证。这在这里听起来可能有点模糊,但如果你牢记这一点,它就很简单。
什么时候覆盖
对于覆盖基类成员的成员函数(方法),两种语言都要求函数是多态的(virtual
在 C++ 中,而不是final
在 Java 中)具有相同的名称和相同数量的参数类型。一些语言允许逆变参数类型,但 Java 和 C++ 都不允许。
协变返回类型
这里的协变意味着返回类型的类型以与实现成员函数的类型相同的方式变化。也就是说,派生函数返回的类型必须是多态的,并且必须是相同的或派生自基类中声明的相同类型。Java 是一种参考语言,因此除了原始类型之外,所有返回类型都可以表现出多态性。C++ 是一种值语言,只有引用和指针是多态的。这意味着在 Java 中返回的类型必须完全匹配或者是引用类型,并且是从基返回的类型派生的。在 C++ 中,它必须是一个引用或指向相同或派生类型的指针。与介绍中一样,原因是如果您通过基类调用成员函数,您将拥有一个与您期望的对象相匹配的对象。
异常规范
异常规范在 C++ 中不是很常见,但在 Java 中却很常见。在这两种语言中,尽管覆盖的方法是相同的:派生类中的覆盖方法必须对可以抛出的内容有更严格的约束。这里语言表面的差异,因为 Java 仅验证已检查的异常,因此它将允许派生类型中未由基类抛出的未经检查的异常。另一方面,派生函数不能添加新的检查基类中不存在的异常,协变再次发挥作用,派生函数可以抛出协变异常。在 C++ 中,异常规范具有完全不同的含义,但同样,派生类型中的规范必须比基类型中的规范更受约束,并且它还允许协变异常规范。
基本原理是一样的,如果您try {} catch() {}
通过对基类型的引用在调用周围编写一个块,并且它捕获在基类型中声明的所有异常,则对覆盖的调用将使所有异常捕获在同一个块——Java 中可能存在未经检查的异常除外。
访问修饰符
在 Java 中,派生方法的访问规范必须至少与基函数的访问规范一样严格,也就是说,如果基函数声明指定protected
,则派生函数不能是public
,但另一方面可以是private
,有趣的是 Java 没有允许您覆盖private
基类中的函数。
在 C++ 中,访问说明符不会用于覆盖,您可以根据需要修改访问说明符,使其在派生类中或多或少受到限制。顺便说一句,您可以覆盖private
基类中的成员(即声明的virtual
),该成员通常用于实现 NVI 模式(非虚拟接口),必须通过protected
Java 中的方法来实现。
停止覆盖
final
Java 允许您通过将成员函数标记为或将其设为 来打破任何级别的覆盖链private
。在 C++(当前标准)中,您不能在任何时候破坏覆盖链,即使在最终覆盖器无法访问它所覆盖的成员函数的情况下也不行,这会产生奇怪的效果:
struct base {
virtual void f() {}
};
struct derived : private base {
void g() {
f();
}
};
struct most_derived : derived {
void f() { // overrides base::f!!!
//base::f(); // even if it does not have accesss to it
}
};
在那个例子中,因为继承在derived
级别上是私有的,most_derived
不能访问base
子对象,从它的角度来看,它不是派生自base
(为什么base::f()
会在内部编译失败的原因most_derived::f()
),而是通过实现一个函数通过签名void ()
,它为base::f
. g()
对对象的调用most_derived
将被调度到most_derived::f()
,而对derived
对象的调用将被调度到base::f()
。
爪哇:
abstract class MyAbstract {
abstract String sayHelloTo(String name);
}
final class SayEnglish extends MyAbstract {
@Override
public String sayHelloTo(String name) {
return "Hello, " + name + "!";
}
}
final class SayLatin extends MyAbstract {
@Override
public String sayHelloTo(String name) {
return "Lorem, " + name + "!";
}
}
考虑到语法差异的 C++ 也是如此,即重写抽象方法的签名相同。
您在 java 中的覆盖方法应该与您要覆盖的抽象方法具有相同的签名。此外,您不能限制访问超过父类。请参阅http://download.oracle.com/javase/tutorial/java/IandI/override.html
我假设你的意思是 C++。与 java 相同,覆盖方法签名应该匹配被覆盖的。见http://www.learncpp.com/cpp-tutorial/126-pure-virtual-functions-abstract-base-classes-and-interface-classes/
Wiki 也有一个页面 en.wikipedia.org/wiki/Method_overriding。抽象方法可以有参数。对此没有限制。在许多情况下,传递参数可能没有意义。希望这可以帮助 :)
方法的签名(返回类型、参数的类型和数量)在派生类中应该与基类的签名完全匹配。否则派生类也将变得抽象。
例子:
struct foo{
virtual void foobar( int myNum) = 0;
};
struct bar: foo{
int foobar(int myNum ){}
};
int main(){
foo *obj = new bar();
return 0;
}
test.cc:6:错误:为“virtual int bar::foobar(int)”指定的返回类型冲突<br> test.cc:2:错误:覆盖“virtual void foo::foobar(int)”</p >
正如@Als 提到的,协变返回类型是返回类型可以不同的异常。不同,我的意思是不同的类型应该是类型兼容的。C++ 中派生类类型的指针/引用与基类型的指针/引用类型兼容。
链接中的示例:
#include <iostream>
// Just create a class, and a subclass
class Foo {};
class Bar : public Foo {};
class Baz
{
public:
virtual Foo * create()
{
return new Foo();
}
};
class Quux : public Baz
{
public:
// Different return type, but it's allowed by the standard since Bar
// is derived from Foo
virtual Bar * create()
{
return new Bar();
}
};
int main()
{
Quux *tmp = new Quux();
Bar *bar = tmp->create();
return 0;
}