Сегодня я сделал простой тест:

struct C{virtual void f()=0;};
void C::f(){printf("weird\n");}

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

Я пробовал и GCC, и VC, оба в порядке. Мне кажется, это должно быть частью стандарта C ++.

Но почему это не синтаксическая ошибка?

Причина, по которой я мог подумать, похожа на то, что в C # есть ключевые слова interface и abstract, interface не может иметь реализации, в то время как abstract может иметь некоторые реализации.

Не в этом ли причина моего замешательства, что C ++ должен поддерживать такой странный синтаксис?

4
Hind Forsum 8 Сен 2016 в 13:09

8 ответов

Лучший ответ

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

Классический пример:

class PersonBase
{
private:
    string name;
public:
    PersonBase(string nameIn) : name(nameIn) {} 

    virtual void printDetails() = 0
    {
        std::cout << "Person name " << name << endl;
    }
};

class Student : public PersonBase
{
private:
    int studentId;
public: 
    Student(string nameIn, int idIn) : PersonBase(nameIn), studentId(idIn) {  }
    virtual void printDetails()
    {
        PersonBase::printDetails(); // call base class function to prevent duplication
        std::cout << "StudentID " << studentId << endl;
    }
};
7
Gonen I 8 Сен 2016 в 10:46

По сути, лучшее из обоих миров (или худшее ...).

Производный класс необходим для реализации чистого виртуального метода, конструктор базового класса этого требует по какой-то причине. И базовый класс также предоставляет реализацию этого метода по умолчанию, которую можно использовать, если производный класс желает или требует.

Так что некоторый пример кода может выглядеть так:

class Base {
public:
  virtual int f() = 0;
};
int Base::f() {
  return 42;
}

class Derived : public Base {
public:
  int f() override {
    return Base::f() * 2;
  }
};

Итак, каков обычный вариант использования ...

Обычный вариант использования этого метода связан с деструктором - в основном разработчик базового класса хочет, чтобы это был абстрактный класс, но ни один из методов не имеет особого смысла как чистые виртуальные функции. Деструктор - возможный кандидат.

class Base {
public:
  ~Base() = 0;
};
Base::~Base() { /* destruction... */ }
2
Niall 9 Сен 2016 в 11:22

Обратите внимание, что вы не можете создать экземпляр объекта с помощью чистых виртуальных методов.

Попробуйте создать экземпляр:

C c;

С VC2015, как и ожидалось, возникает ошибка:

1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): error C2259: 'C': cannot instantiate abstract class 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: due to following members: 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(12): note: 'void C::f(void)': is abstract 
1>f:\dev\src\consoleapplication1\consoleapplication1.cpp(6): note: see declaration of 'C::f'

Чтобы ответить на ваш вопрос: механизмы только объявляют функцию чистой виртуальной, но все еще есть таблица виртуальных функций и базовый класс. Это позволит избежать создания экземпляра Baseclass (C), но не избежать его использования:

struct D : public C { virtual void f(); }; 
void D::f() { printf("Baseclass C::f(): "); C::f(); }
...
D d; 
d.f();
1
Bernhard Heinrich 8 Сен 2016 в 10:57

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

0
Daniele 8 Сен 2016 в 10:16

Чисто виртуальный означает «ребенок должен преобладать».

Так:

struct A{ virtual void foo(){}; };
struct B:A{ virtual void foo()=0; };
struct C:B{ virtual void foo(){}; };
struct D:C{ virtual void foo()=0; };
void D::foo(){};
struct E:D{ virtual void foo(){D::foo();}; };

У A есть виртуальный foo.

B делает это абстрактным. Перед созданием экземпляра производные типы должны реализовать его сейчас.

C реализует это.

D делает его абстрактным и добавляет реализацию.

E реализует это, вызывая реализацию D.

Экземпляры A, C и E могут быть созданы. B и D не могут.

Технику абстрактного с реализацией можно использовать для обеспечения частичной или неэффективной реализации, которую производные типы могут вызывать явно, когда хотят ее использовать, но не получают «по умолчанию», потому что это было бы нецелесообразно.

Другой интересный вариант использования - это когда родительский интерфейс находится в постоянном движении, а база кода tue велика. Имеет полностью функциональную реализацию. Дети, использующие значение по умолчанию, должны повторить подпись и явно переадресовать ее. Те, которые хотят переопределить, просто переопределят.

При изменении сигнатуры базового класса код не будет скомпилирован, если каждый дочерний элемент явно не вызовет значение по умолчанию или не переопределит его должным образом. До ключевого слова override это был единственный способ гарантировать, что вы случайно не создали новую виртуальную функцию вместо переопределения родительского типа, и он остается единственным способом, при котором политика применяется в родительском типе.

1
Yakk - Adam Nevraumont 8 Сен 2016 в 11:29

Чистая виртуальная функция должна быть переопределена в подклассах. Однако вы можете предоставить реализацию по умолчанию, которая будет работать для подклассов, но может быть не оптимальной.

Сконструированный вариант использования предназначен для абстрактных форм, например

class Shape {
public:
    virtual Shape() {}

    virtual bool contains(int x, int y) const = 0;
    virtual int width() const = 0;
    virtual int height() const = 0;
    virtual int area() const = 0;
}

int Shape::area() const {
    int a = 0;
    for (int x = 0; x < width(); ++x) {
        for (int y = 0; y < height(); ++y) {
            if (contains(x,y)) a++;
        }
    }
    return a;
}

Метод площади подойдет для любой формы, но он крайне неэффективен. Подклассам рекомендуется предоставить подходящую реализацию, но если ее нет, они все равно могут явно вызвать метод базового класса.

1
king_nak 8 Сен 2016 в 10:25

Другие упомянули согласованность языка с деструктором, поэтому я буду придерживаться точки зрения разработки программного обеспечения:

Это потому, что класс, который вы определяете, может иметь допустимую реализацию по умолчанию, но вызывать его рискованно / чрезмерно / как угодно. Если вы не определите его как чисто виртуальный, производные классы будут неявно наследовать эту реализацию. И может никогда не узнать до времени выполнения.

Если вы определяете его как чисто виртуальный, производный класс должен реализовывать эту функцию. И если все в порядке с риском / стоимостью / чем угодно, он может статически вызвать реализацию по умолчанию как Base::f();
Важно то, что это осознанное решение и явный призыв.

3
StoryTeller - Unslander Monica 8 Сен 2016 в 10:26

Деструктор должен быть определен, даже если он чисто виртуальный. Если вы не определите деструктор, компилятор его сгенерирует.

Изменить: вы не можете оставить деструктор, объявленный без определения, вызовет ошибку ссылки.

0
Zang MingJie 8 Сен 2016 в 10:13