Почему наследование ведет себя по-разному в Java и C++ с суперклассами, вызывающими (или не) подклассами?

Я написал - как бы то ни было - точно такой же пример наследования как на Java, так и на C++. Я очень удивлен, увидев различные результаты этих программ. Позвольте мне поделиться как фрагментами кода, так и соответствующими выводами.


C++ Код:

class A
{
public:
    A() {}
    void sleep() {
        cout << "A.Sleep" << endl;
        eat();
    }
    void eat() {cout << "A.Eat" << endl;}
};

class B: public A
{
public:
    B() {}
    void sleep() {
        A::sleep();
        cout << "B.Sleep " <<endl;
        this->eat();
    }
    void eat() {
        cout << "B.Eat" << endl;
        run();
    }
    void run() {
        A::sleep();
        cout << "B.run" << endl;
    }
};

int main()
{
    B *b = new B();
    b->sleep();
}

Выход:

A.Sleep
A.Eat
B.Sleep
B.Eat
A.Sleep
A.Eat
B.run

executed successfully...

Код Java:

class A
{
    A() {}
    void sleep() {
        System.out.println("A.Sleep");
        this.eat();
    }
    void eat() { System.out.println("A.Eat");}
};

class B extends A
{
    B() {}
    @Override
    void sleep() {
        super.sleep();
        System.out.println("B.Sleep");
        this.eat();
    }
    @Override
    void eat() {
        System.out.println("B.Eat");
        run();
    }
    void run() {
        super.sleep();
        System.out.println("B.Run");
    }
}

public class Test {
    public static void main(String[] args) {
        B b = new B();
        b.sleep();
    }
}

Выход:

A.Sleep
B.Eat
A.Sleep
B.Eat
A.Sleep
......
......
......
(Exception in thread "main" java.lang.StackOverflowError)

Я не знаю, почему эти два примера наследования ведут себя по-разному. Не должно ли оно работать аналогичным образом? Мне очень интересно узнать... что объясняет этот сценарий?

Ответ 1

В примере C++ вы скрываете базовые методы, но вы не переопределяете их. Таким образом, они на самом деле разные методы, которые имеют одно и то же имя. Если вы звоните

A* a = new B();
a->sleep();

он на самом деле напечатает "A.Sleep". Если вы хотите переопределить метод, вам нужно объявить его virtual в базовом классе (автоматически сделать его виртуальным во всех подклассах тоже). Вы можете больше узнать о функции, скрывающейся против переопределения в C++ в этом сообщении.

В вашем примере Java вы фактически переопределяете методы, поэтому они являются одним и тем же методом. Один заменил старый. Вы можете думать об этом так: все функции Java тайно обозначаются как virtual, то есть они могут быть переопределены. Если вы хотите, чтобы метод не был переопределяемым на Java, вы должны объявить его final.

Ответ 2

Примечание: будьте осторожны, каждый язык - это собственный способ мышления. Существует много способов интерпретировать/реализовать OO. Даже если C++ и Java выглядят одинаково, они далеки от аналогичных.

На обоих языках компилятор проверяет во время компиляции, если вы можете вызвать метод, изучив класс (и тот, который унаследован от текущего и т.д.) Для метода правильной подписи и видимости. Что отличает разные вещи от того, как действительно вызывается звонок.

C++:

В случае не виртуальных методов вызванный метод полностью определяется во время компиляции. Вот почему, даже если объект имеет класс B, когда он выполняет A::sleep вызов eat разрешен как вызов A::eat (eat не виртуально, а компилятор вызывает A::eat потому что вы находитесь в уровень A). В B::sleep() вызов this->eat() разрешен как вызов B.eat() потому что в этом месте this тип B Вы не можете спуститься в иерархию наследования (вызов, чтобы eat в классе A, никогда не будет называть метод eat в классе ниже).

Имейте в виду, что в случае виртуальных методов все по-другому (оно больше похоже на случай Java, будучи другим).

Java:

В Java указанный метод определяется во время выполнения и является тем, который наиболее связан с экземпляром объекта. Поэтому, когда в A.sleep вызов для eat будет вызовом, связанным с типом текущего объекта, это означает тип B (поскольку текущий объект имеет тип B), тогда B.eat.

Затем у вас переполнение стека, потому что, когда вы играете с объектом типа B вызов B.sleep() вызовет A.sleep(), который вызовет B.eat(), который, в свою очередь, вызовет B.run() который вызовет A.sleep() и т.д. в бесконечном цикле.