C++和Java中的抽象方法和重写函数

3
在C++和Java中,或者是它们各自的规则中,对于重写抽象方法有哪些限制。必须匹配参数或返回类型吗?我通常看到抽象函数只实现了返回类型而没有参数,补充的部分由派生类来指定。具体是如何工作的呢?

你是指在C++和Java中……,我不认为C语言中可以有抽象方法。 - Peter Lawrey
6个回答

4

方法重写必须具有与其覆盖的父方法相同的方法签名,否则它不被视为重写。

Java

public abstract class AbstractTest {

    public abstract void test() throws Exception;
}

public class ConcreteTest extends AbstractTest {

    @Override
    public void test() throws Exception {

    }
}

正如您所看到的,ConcreteTest(它继承了AbstractTest)必须覆盖test()。它们具有相同的方法名称、返回类型和没有方法参数。子类可以省略从基类抛出的异常并抛出自己的异常。子类还可以添加其他(未)检查的异常。
正如Peter Lawrey所提到的,Java接口方法是隐式的抽象方法(请参见我在Java抽象接口上的SO问题)。
在这种情况下,关键是方法的可见性不能改变(因为它是一种分层可见性,即私有->受保护->公共)。不过以下内容是有效的:
public abstract class AbstractTest {

    protected abstract void test() throws Exception;
}

public class ConcreteTest extends AbstractTest {

    @Override
    public void test() throws Exception {

    }
}

父类有一个受保护的方法,子类可以重写相同的方法并只有两个可见性选择:protected或public。此外,假设您拥有:
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返回的是D而不是B。为什么?因为派生类遵循父类的相同签名,并且派生类的返回类型是父类返回类型的一个子类型。所以我可以这样写:
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++中,拥有一个以=0结尾的纯虚函数(pure virtual function)的类被称为抽象类(Abstract class)。子类(在C++中,使用冒号:来表示类继承关系)会覆盖(override)这个纯虚方法,但是不需要包含=0。子类覆盖后的函数签名与父类一致。
回到Java的例子,假设您有以下代码:
class B {

};

class D : B {

};

class Base {
public:
    virtual B* foo() = 0;
}

class Derived : Base {
public:
    D* foo() {
        return new D();
    }
}

这里使用与Java相同的推理。协变返回类型也适用于受保护和私有继承。更多关于协变返回类型的信息。


1
Java 部分至少有两个错误。(i) 覆盖方法的返回类型可以是超类方法返回类型的子类。(ii) throws 子句可以省略超类方法抛出的任何异常,或者可以声明抛出一个或多个超类方法抛出的异常的子类。 - Miserable Variable
@Hemal Panday,我不太明白你在(i)中的意思。 - Buhake Sindi
你应该 :) class B {} class D extends B{} class Base { public B foo() { return null;} } class Der extends Base { @Override public D foo() { return null;} } 思考一下为什么它能够工作。由于你的回答被接受,你应该修改它,以便其他人在未来得到正确的信息。 - Miserable Variable
你缺少了协变返回类型,请参考@Mahesh的答案来了解它。 - Alok Save
@Als 在 throws 子句中也缺少类似的限制条件。 - Miserable Variable
显示剩余7条评论

2
我不了解Java,但在C++中,您必须指定完全相同的参数类型。另一方面,返回类型是协变类型,这意味着如果在原始函数中返回对类型A的指针或引用,则覆盖函数可以返回对类型B的指针或引用,只要B是A或直接或间接地派生自它。
正如Als所指出的那样,函数必须声明为虚函数才能被覆盖。由于OP明确询问了抽象方法,这些方法都定义了virtual和=0,因此没有必要指出这一点。但是,我想额外强调的是,覆盖函数不需要声明为虚函数。正如引用的标准所说,与基本成员函数的签名(使用放宽规则的covariant类型)匹配的成员函数将是一个override,无论是否指定为virtual。也就是说,覆盖函数不需要声明为虚函数;另一方面,抽象成员函数必须这样做。

你可能误解了标准的含义。这很重要,因为你的回答没有提到应该提到的“虚拟”关键字。 - Alok Save
你是否错过了“虚拟成员函数”的提及?我不明白你怎么可能看不到“override”需要“virtual”的情况呢? - Alok Save
1
哦,我想我明白了,基类中的成员函数必须声明为虚函数。当然,如果不这样做,就没有什么可以重写的了。但是OP说他的方法是抽象的,所以它必须用virtual ... = 0来定义,所以我仍然认为你的评论只是对我的答案的干扰。 - K-ballo
OP提出了一个问题,而你没有给出确切正确的答案。指出这个错误让你感到冒犯,而你选择争论而不是修改答案使其更加正确,因此你应该得到-1分。 - Alok Save
我并不生气,OP问的是对于无法通过其他方式定义的重写抽象方法所施加的限制。基本定义中的虚拟键盘不仅仅是暗示,而且也不是问题所在。我发现你的评论让读者感到困惑,我将修改我的回答以反映这一点。谢谢。 - K-ballo
显示剩余4条评论

1

两种语言在覆盖方面的要求相似,但自然语义上存在差异。基本上,两者对调用代码(即参数)具有完全相同的约束,并提供相同或更严格的处理保证。这可能听起来有点模糊,但如果您记住这一点,它就很简单。

何时为覆盖

对于成员函数(方法)覆盖基类成员,两种语言都要求该函数是多态的(在C++中为virtual,在Java中不是final),具有相同的名称和相同数量和类型的参数。一些语言允许使用逆变参数类型,但Java和C++均不允许。

协变返回类型

Covariant 在这里的意思是返回类型的类型与成员函数实现的类型相同。也就是说,派生函数返回的类型必须是多态的,并且与基类中声明的类型相同或派生自该类型。Java 是一种引用语言,因此所有返回类型都可以表现出多态性,除了原始类型。C++ 是一种语言,只有引用指针是多态的。这意味着在 Java 中,返回类型必须完全匹配或是一个引用类型,并且派生自基类返回的类型。在 C++ 中,它必须是相同或派生类型的引用指针。正如在介绍中所述,原因是如果通过基类调用成员函数,则会得到一个与预期相匹配的对象。

异常规范

异常说明在C++中并不常见,但在Java中很常见。 在这两种语言中,即使覆盖的方法相同:派生类中的覆盖方法必须具有更严格的限制条件,以便抛出什么。 由于Java仅验证检查后的异常,因此在派生类型中允许未在基类中抛出的未检查异常。 另一方面,衍生函数不能添加在基类中不存在的新检查异常。 协变再次发挥作用,并且派生函数可以引发协变异常。 在C ++中,异常说明具有完全不同的含义,但是以相同的方式,派生类型中的规范必须比基类中的规范更加受限制,它还允许协变异常说明。

道理是相同的,如果您在通过对基本类型的引用调用时编写了try {} catch() {}块,并捕获在基本块中声明的所有异常,则对覆盖的调用将在相同块中捕获所有异常— —除非Java中的未检查异常。

访问修饰符

在Java中,派生方法的访问权限规定必须至少与基类相同或更加严格,也就是说,如果基类函数声明为protected,则派生函数不能为public,但可以是private。有趣的是,Java不允许你在基类中覆盖一个private函数。
在C++中,访问说明符对于覆盖并不起作用,并且您可以根据需要修改访问说明符,在派生类中使其更加严格或更加宽松。顺便说一句,在基类中您可以覆盖一个private成员(声明为virtual),这通常用于实现NVI模式(非虚拟接口),该模式必须通过在Java中实现protected方法来完成。
停止覆盖
Java允许您在任何级别上打破覆盖链,通过将成员函数标记为final或者将其设置为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
   } 
};

在这个例子中,由于继承是私有的,在派生类级别上,most_derived无法访问base子对象,从它的角度来看,它不是从base派生的(这就是为什么most_derived::f()内部编译失败的原因),但另一方面,通过实现一个带有签名void ()的函数,它提供了对base::f的覆盖。对most_derived对象调用g()将被分派到most_derived::f(),而对derived对象调用g()将被分派到base::f()

0

Java:

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++来说也是一样的,只是语法不同,即重写抽象方法的签名相同。

这是不正确的。标准规定了使用virtual关键字来表示重写。请参阅C++03标准10.3/2。 - Alok Save
@Als,我提到过一个人应该遵循语法方面的东西。 - Andrey Atapin

0

方法的签名(返回类型、参数类型和数量)在派生类中应该与基类完全匹配。否则,派生类也将变成抽象类。

示例:

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)’指定了冲突的返回类型 test.cc:2: 错误:覆盖‘virtual void foo::foobar(int)’

正如@Als所提到的,Covariant Return Type是一个例外,其中返回类型可以不同。不同意味着不同的类型应该与彼此兼容。在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;    
}

1
你是否忘记了协变返回类型 - Alok Save
@Als - 我完全忘记了。我会提到它的。谢谢。 - Mahesh

0

在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/

维基百科也有一个页面en.wikipedia.org/wiki/Method_overriding。 抽象方法可以有参数。没有任何限制。在许多情况下,传递参数可能没有意义。希望这可以帮到你 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接