Страницы

Поиск по вопросам

Показаны сообщения с ярлыком виртуальная-функция. Показать все сообщения
Показаны сообщения с ярлыком виртуальная-функция. Показать все сообщения

среда, 29 января 2020 г.

Использование виртуальных функций

#cpp #virtual #виртуальная_функция


Может ли кто-нибудь пояснить какая от них практическая польза? Дело в том, что я
понимаю механизм их работы, но я не понимаю для чего они нужны и где их можно использовать. 

Рассмотрим на примере:

class Animal
{
public:
    Animal():itsAge(1) { cout << "Animal constructor...\n"; }
    virtual ~Animal() { cout << "Animal destructor...\n"; }
    virtual void Speak() const { cout << "Animal speak!\n"; }
protected:
    int itsAge;
};

class Dog : public Animal
{
public:
    Dog() { cout << "Dog constructor...\n"; }
    virtual ~Dog() { cout << "Dog destructor...\n"; }
    void Speak() const { cout << "Woof!\n"; }
    void WagTail() { cout << "Wagging Tail...\n"; }
}

int main()
{
    Animal *pDog = new Dog;
    pDog->Speak();
    return 0;
}


РЕЗУЛЬТАТ:


Animal constructor...
Dog constructor...
Woof!


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

Но во-первых, с помощью указателя класса Animal *pDog, всё равно не получишь доступ
к методу WagTail() (махать хвостом), поскольку он не был определен в классе Animal.
 А во-вторых, используя данный механизм придется расплатиться определенными издержками,
связанными с созданием v-таблицы (каждый элемент которой занимает ресурсы оперативной
памяти).

К тому же я не понимаю, зачем передавать указатель на объект производного класса,
когда ожидается указатель на объект базового класса? 

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

Dog *pDog = new Dog;


вместо:

Animal *pDog = new Dog;


Где "профит"?
    


Ответы

Ответ 1



Рассмотрите классический пример с графическими формами. Вы можете определить класс Shape, который содержит общие методы для всех геометрических фигур, которые вы собираетесь использовать в своем приложении. Этот класс определяет общий интерфейс для всех геометрических фигур. И, допустим, у вас есть форма, на которой вы хотите разместить геометрические фигуры. Форма заранее не знает, какие геометрические фигуры ей придется в себя включать. Она относится к геометрическим фигурам, как к некоторым абстрактным объектам, которые наделены некоторыми методами, которые форма может использовать, чтобы вывести эти фигуры на консоль. Так как к форме можно добавить любое количество геометрических фигур разного вида, то возникает вопрос, а как их в форме хранить? Если нет общего абстрактного представления этих фигур, то их нельзя будет хранить в форме, так как нужен один определенный тип для объектов, чтобы их всех можно было бы хранить в каком-нибудь одном контейнере и не заботиться о том, что фигуры на самом деле различны. Это легко сделать, если наследовать все фигуры от одного класса, как в данном случае от класса Shape, и в этом классе определить виртуальные методы, с которыми форма может работать не зависимо от того, с каким конкретным объектом форма имеет дело. Ниже приведена простая демонстрационная программа, которая реализует описанные идеи. Есть один класс Form , который хранит все геометрические фигуры (в данном случае это объекты классов LeftTriangle, RightTriangle и Rectangle) в стандартном контейнере std::vector, и который имеет метод display, позволяющий вывести все формы на консоль, делегируя каждой фигуре процесс вывода самой себя. // Shape.cpp: определяет точку входа для консольного приложения. // // #include "stdafx.h" #include #include #include #include struct Point { int x; int y; }; class Shape { protected: Point upper_left; char pixel = '*'; public: explicit Shape(Point p = { 0, 0 }) : upper_left(p) { } virtual ~Shape() = default; char set_pixel(char pixel) { char old_pixel = this->pixel; this->pixel = pixel; return old_pixel; } virtual std::ostream & draw(std::ostream &os = std::cout) const = 0; Point move(int dx = 0, int dy = 0) { Point old_upper_left = this->upper_left; this->upper_left.x += dx; this->upper_left.y += dy; if (this->upper_left.x < 0) this->upper_left.x = 0; if (this->upper_left.y < 0) this->upper_left.y = 0; return old_upper_left; } }; class Triangle : public Shape { protected: unsigned int height; public: explicit Triangle(unsigned int height = 1) : height(height) { } }; class LeftTriangle : public Triangle { public: explicit LeftTriangle(unsigned int height = 1) : Triangle(height) { } std::ostream & draw(std::ostream &os = std::cout) const override { for (int i = 0; i < upper_left.y; i++) os << '\n'; for (unsigned int i = 0; i < height; i++) { os << std::setw( upper_left.x ) << std::setfill( ' ' ) << "" << std::setw(i + 2) << std::setfill(pixel) << '\n'; } return os; } }; class RightTriangle : public Triangle { public: explicit RightTriangle( unsigned int height = 1) : Triangle( height) { } std::ostream & draw(std::ostream &os = std::cout) const override { for (int i = 0; i < upper_left.y; i++) os << '\n'; for (unsigned int i = height; i != 0; i-- ) { os << std::setw(upper_left.x + i - 1 ) << std::setfill( ' ' ) << "" << std::setw( height - i + 2 ) << std::setfill( pixel ) << '\n'; } return os; } }; class Rectangle : public Shape { protected: unsigned int height; unsigned int width; public: explicit Rectangle( unsigned int height = 1, unsigned int width = 1 ) : height(height), width( width ) { } std::ostream & draw(std::ostream &os = std::cout) const override { for (int i = 0; i < upper_left.y; i++) os << '\n'; for (unsigned int i = 0; i < height; i++) { os << std::setw(upper_left.x ) << std::setfill( ' ' ) << "" << std::setw( width + 1 ) << std::setfill(pixel) << '\n'; } return os; } }; class Form { public: Form() = default; void add( Shape * &&shape ) { shapes.push_back(std::unique_ptr( shape )); } std::ostream & display(std::ostream &os = std::cout) const { const int Step = 10; int dx = 0; for (auto &p : shapes) { p->move(dx); p->draw(os) << std::endl; dx += Step; } return os; } private: std::vector> shapes; }; int main() { Form form; form.add(new RightTriangle(5)); form.add(new LeftTriangle(5)); form.add(new Rectangle(5, 5)); form.display(); return 0; } Вывод программы на консоль * ** *** **** ***** * ** *** **** ***** ***** ***** ***** ***** ***** Виртуальные методы определяют общий интерфейс для всех производных классов, позволяя им самим определять реализацию данного интерфейса. Чтобы можно было обращаться к объектам производных классов, как к однотипным объектам, наделенных общими свойствами, их надо привести к какому-то общему типу. Таким общем типом может быть один из общих базовых классов этих объектов. Тем самым достигается полиморфизм, то есть объекты, выглядящие как объекты одного типа, имеют множество форм поведения и представления. Конечно каждый производный класс может дополнительно определять свои члены данных и методы. Но в таком случае это то, что различает их от объектов других производных классов. Например, вы можете сказать, что каждая женщина и каждый мужчина, это человек. Но вы не можете сказать, например, что каждый человек - это женщина, или каждый человек - это мужчина. Если рассматривать женщин и мужчин как людей, то вы можете обращаться к ним независимо от пола, посылая им, как говорят в ООП, различные сообщения. Например, если вы - кондуктор в автобусе, то вы можете потребовать предъявить проездной билет. Для вас женщины и мужчины в автобусе - это пассажиры, и они должны иметь общие свойства такие, как наличие проездного билета. Для этого вы должны рассматривать мужчин и женщин как объектов некоторого общего типа, в данном случае, как пассажиров. Тем не менее мужчины и женщины как объекты своего индивидуального класса различаются. Например, женщины могут рожать, а мужчины не могут (если только мужчин - это не женщина, формально сменившая пол по документам).

Ответ 2



Я не буду расписывать преимущества для всяких собачек или геометрических фигур, я выскажу одно банально звучащее соображение, но которое для моего понимания в свое время многого значило. Когда говорят, что наследование облегчает повторное использование кода, то - о каком коде речь? О том, что производный класс использует код из базового класса? Отнюдь. Такие вещи можно делать простыми вызовами функций. Наследование дает возможность по-новому использовать уже написанный (а то и скомпилированный в виде динамических библиотек) код. Какая-нибудь f(Base*); используется заново без каких-либо изменений, работая с кодом, который и близко не был написан, а может, и даже не проектировался, когда была написана и скомпилирована эта f() - просто это код виртуальной функции в производном от Base класса. Да, это в определенной степени аналог передачи в функции указателей на другие функции, но только в очень определенной степени. А, кроме того, вопрос "зачем нужна передача в функции других функций?", надеюсь, не вызывает у вас недоумения "где же профит?"

Ответ 3



Как известно, С++ является компилируемым статически типизированным языком, что означает разрешимость типов во время компиляции, так вот механизм виртуальных методов расширяет эту возможность для случая, когда программист хочет именно в run time определить тип объекта, применяя, например, dynamic_cast который бы в случае с НЕ полиморным классом не сработал бы, а выдал ошибку компиляции. Где "профит"? допустим есть такой код struct SomeThing { virtual void someGenericOperation() { cout << "base generic operation" } ~SomeThing(); }; struct SomeThingConcrete : SomeThing { void someConcreteOperation() {} void someGenericOperation() { cout << "override generic op" } // override }; SomeThing* p_smth = ...; И пускай у нас такая ситуация в которой мы хотим проверить, что p_smth указывает на тип SomeThingConcrete чтобы была возможность вызывать методы специфические для SomeThingConcrete тоесть - someConcreteOperation(). проверка осуществляется следующим образом SomeThingConcrete* p_concrete = dynamic_cast(p_smth); if (p_concrete != NULL) { p_concrete->someConcreteOperation(); } else { // p_concrete does not point to SomeThingInterface } Тоесть если downcast к типу SomeThing сработал (условие p_concrete != NULL выполнилось), значит, наше предположение потвердилось и p_smth действительно указывает на SomeThingConcrete, в противном случае p_smth указывает на какой - то другой производный от SomeThing класс. Кроме этого, не будь базовый класс полиморфный, получилась бы лапша лишнего кода, например, если передать в функцию указатель на базовый void f(SomeThing*), то из-за свойства статического связывания пришлось бы перегружать функцию f для SomeThingConcrete и для каждого нового наследника от SomeThing - чтобы вызывалась именно переопределённая в производном версия унаследованного метода. Динамическое связывание, применяя встроенный механизм вызова виртуальных методов избавило бы от подобных перегрузок вовсе - тоесть void f(SomeThing* p_smth) // f is a single polymorphic function { p_smth->someGenericOperation(); { int main() { SomeThing* p_smth = new SomeThing(); SomeThing* p_smthConcrete = new SomeThingConcrete(); f(p_smth); // base generic operation f(p_smthConcrete); // override generic operation }

пятница, 10 января 2020 г.

Вызов дочернего метода через базовый класс

#cpp #ооп #виртуальная_функция


Есть базовый класс Shapes, в нем есть два абстрактных метода P (Периметр) и S (Площадь).
Также есть дочерний класс Circle, в котором реализованы методы P и S с помощью полиморфизма. 

Вопрос: как вызвать метод P и S из класса Circle через класс Shapes? 

Это же называется динамическое связывание?

Shapes.h

class Shpes
{
private:    
public:
    Shpes();
    virtual double P() = 0;
    virtual double S() = 0;
};

Shpes::Shpes()
{

}


Circle.h 

#include "Shpes.h"

class Circle :
    public Shpes
{
private: 
    double r;
    void SetCheck(double R);

public: 
    Circle(double R = 0);
    virtual double P();
    virtual double S();
    void Print();
};


Circle.cpp

#include "Circle.h"
#include 
#include 

using namespace std;

void Circle::SetCheck(double R)
{
    R < 0 ? r = abs(R) : r = R; 
}

Circle::Circle(double R)
{
    SetCheck(R);
}

double Circle::P()
{   
    return 2 * 3.12 * r;
}

double Circle::S()
{   
    return 3.14 * r * r;
}

void Circle::Print()
{
    cout << "При радиусе круга = " << r <

Ответ 1



Это можно сделать через указатель или ссылку на базовый класс, инициализированными объектом производного класса. Например, Circle c( 10 ); Shape *ps = &c; ps->R(); ps->S(); Shape &rs = c; rs.R(); rs.S(); Имейте в виду, что деструктор в базовом классе также следует объявить как виртуальный. Например, virtual ~Shape() = default; или virtual ~Shape(){} ОБратите внимание на расхождение double Circle::P() { return 2 * 3.12 * r; ^^^^^ } double Circle::S() { return 3.14 * r * r; ^^^^ } Чтобы не допускать такого вида ошибки, лучше используемым константам назначать имена. Вы могли бы определить соответствующий член в вашем классе (при условии, что компилятор поддерживает данную конструкцию), например, class Circle : public Shpes { private: double r; void SetCheck(double R); constexpr static double PI = 3.14; public: Circle(double R = 0); virtual double P(); virtual double S(); void Print(); }; И записать определения функций следующим образом double Circle::P() { return 2 * PI * r; ^^^^^ } double Circle::S() { return PI * r * r; ^^^^ } Если компилятор не поддерживает такое объявление члена класса, то можно просто написать class Circle : public Shpes { private: double r; void SetCheck(double R); const static double PI; public: Circle(double R = 0); virtual double P(); virtual double S(); void Print(); }; И затем определить эту статическую переменную double Circle::PI = 3.14;

Ответ 2



Очень просто: Shpes *figure = new Circle( 1 ); cout << figure->P() << endl;

понедельник, 30 декабря 2019 г.

Дефолтные параметры и полиморфизм

#cpp #ооп #классы #виртуальная_функция


Почему выводит A A, а не A B?

struct A {
    virtual void foo(char x = 'A'){
        std::cout << x << ' ';
    };
};

struct B : A {
    void foo(char x = 'B') override {
        A::foo(x);
    }
};

int main() {
    A* a = new A{};
    A* b = new B{};

    a->foo();
    b->foo();
}

    


Ответы

Ответ 1



Потому что значения аргументов по умолчанию обрабатываются при компиляции, а виртуальная диспетчеризация - во время выполнения. Что видит компилятор? вызов foo() для типа A - и понимает, что нужно подставить значение аргумента по умолчанию (для типа A это A). При вызове вызывается B::foo() с аргументом, полученным при компиляции - а именно, A. Вот представьте, что в каком-то файле будет какой-то опосредованный потомок с еще каким-то аргументом по умолчанию - как вообще компилятор сможет его найти? Это что, компилятор в такой ситуации должен хранить кроме таблицы виртуальных функций еще и таблицу значений аргументов по умолчанию (причем для тех потомков, которых во время компиляции еще и в проекте нет)?

Ответ 2



Всё просто на самом деле. В этом месте b->foo(); компилятор должен проверить и построить легальный код для вызова. Он прекрасно видит, что есть один вариант foo, который можно использовать таким образом (вызов без указания параметра), и ему нужно здесь передать в функцию аргумент: b->foo(параметр-заданный-по-умолчанию);. Внимание, у нас этап компиляции, и компилятору неизвестно объект какого типа реально окажется в памяти. Поэтому сначала кратенько напомню, что в C++ имеются статический и динамический типы. Динамический тип - это тип того объекта, который непосредственно будет создан и лежать в памяти, т.е. в нашем случае это объект типа B. Статический тип - это тип выражения, который определяется в результате анализа программы без учета выполнения семантики. В нашем случае в выражении b->foo(); b - это указатель на объект типа A. То, что в памяти при b->foo(); окажется объект типа B будет известно только во время выполнения, в общем случае, во время компиляции компилятору неизвестно какой реально тип имеет объект, на который указывает b, но код вызова b->foo(); он построить должен уже сейчас, так что у него нет другого выбора, кроме как взять параметр по-умолчанию, исходя из статического типа, и поставить его в аргументы, т.е. параметр будет взят из A::foo, и будет построен код аналогичный b->foo('A');. Именно об этом говорит пункт 11.3.6/10 (N4659) стандарта C++: 11.3.6 Default arguments A virtual function call (13.3) uses the default arguments in the declaration of the virtual function determined by the static type of the pointer or reference denoting the object. An overriding function in a derived class does not acquire default arguments from the function it overrides.

понедельник, 2 декабря 2019 г.

Отличия переопределения метода от перекрытия

#c_sharp #классы #функции #виртуальная_функция


Чем отличается перекрытие от переопределения метода?
    


Ответы

Ответ 1



Перекрытие (в ваших терминах) означает, что метод в производном классе скрывает метод с той же самой сигнатурой в базовом классе. Поэтому в C# рекомендуется для методов производных классов, которые перекрывают методы базовых классов, использовать ключевое слово new. Если вы забудете это сделать, то компилятор вас предупредит, что вы скрываете одноименную функцию с той же сигнатурой в базовом классе. Например, class Base { public void Hello() { Console.WriteLine( "I'm the base class" ); } } class Derived : Base { public new void Hello() { Console.WriteLine( "I'm the derived class" ); } } В этом случае для ссылок на базовый класс Base всегда будет вызываться метод Hello базового класса, а для ссылок на производный класс будет вызываться метод Hello производного класса. Например, Base b = new Base(); b.Hello(); // I'm the base class Derived d = new Derived(); d.Hello(); // I'm the derived class b = d; b.Hello(); // I'm the base class Переопределение применяется к виртуальным функциям. В производном классе виртуальная функция переопределяется. Для этого в базовом классе функция должна быть определена с ключевым словом virtual (или abstract), а в производном классе для ее переопределения необходимо указывать другое ключевое слово override. Например, class Base { public virtual void Hello() { Console.WriteLine( "I'm the base class" ); } } class Derived : Base { public override void Hello() { Console.WriteLine( "I'm the derived class" ); } } Различие с предыдущим примером состоит в том, что если ссылка базового класса указывает на объект производного класса, то в этом случае будет вызвана переопределенная функция производного класса. Base b = new Base(); b.Hello(); // I'm the base class Derived d = new Derived(); d.Hello(); // I'm the derived class b = d; b.Hello(); `// I'm the derived class` То есть различие состоит в этих двух предложениях. В первом случае для перекрытия b = d; b.Hello(); // I'm the base class ^^^^^^^^^^^^^^^^^^ а во втором случае при переопределении имеем b = d; b.Hello(); `// I'm the derived class` ^^^^^^^^^^^^^^^^^^^^^^^^ То есть в первом случае мы в каждом новом производном классе в иерархии классов объявляем новую функцию, которая скрывает функцию с той же сигнатурой в базовых классах. А во втором случае функция с тем же именем заново не объявляется, а переопределяется уже объявленная функция базового класса. Это позволяет динамически вызывать нужное определение одной и той же функции в зависимости от объекта, с которым функция вызывается.

Ответ 2



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

суббота, 6 июля 2019 г.

Виртуальный метод при композиции

Объект класса с двумя методами getOuter и getInner оборачивается другим классом, имеющим такой же интерфейс, но используется не наследование а композиция, т. е. объект вкладывается внутрь другого и они должны иметь независимые поля.
Известно, что из метода getOuter ровно один раз вызывается метод getInner. Надо сделать так, чтобы вызывался getInner не оборачиваемого объекта, а оборачивающего. В качестве прокси используется класс Wrapper, от которого наследуются обрачивающие классы. Что надо в него дописать, чтобы работало соответствующим образом?
Желаемый вывод после дописывания врэппера:
outer c b a (<[Bb][Aa]>)
class A { constructor() { this.x = 'A'; this.a = 'a'; } getOuter() { console.log('outer'); return `(${this.getInner()})`; } getInner() { console.log('a'); return this.x + this.a; } } var a = new A class Wrapper { constructor(original) { this.original = original } getOuter() { return this.original.getOuter(); } getInner() { return this.original.getInner(); } } class B extends Wrapper { constructor(original) { super(original); this.x = 'B'; this.b = 'b'; } getInner() { console.log('b'); return `[${this.x}${this.b}][${super.getInner()}]`; } } var b = new B(a); class C extends Wrapper { constructor(original) { super(original); this.x = 'C'; this.c = 'c'; } getInner() { console.log('c'); return `<${this.x}${this.c}><${super.getInner()}>`; } } var c = new C(b); console.log(c.getOuter());


Ответ

Ура, кажется получилось!
Надо в getOuter в оборачиваемый инстанс сохранить забинженный метод getInner текущего класса, в когда тот вызовет super.getInner, то эту ссылку затереть и продолжить вызывать по цепочке:
class A { constructor() { this.x = 'A'; this.a = 'a'; } getOuter() { console.log('outer'); return `(${this.getInner()})`; } getInner() { console.log('a'); return this.x + this.a; } } var a = new A class Wrapper { constructor(original) { this.original = original } getOuter() { this.original.getInner = this.getInner.bind(this); return this.original.getOuter(); } getInner() { delete this.original.getInner; return this.original.getInner(); } } class B extends Wrapper { constructor(original) { super(original); this.x = 'B'; this.b = 'b'; } getInner() { console.log('b'); return `[${this.x}${this.b}][${super.getInner()}]`; } } var b = new B(a); class C extends Wrapper { constructor(original) { super(original); this.x = 'C'; this.c = 'c'; } getInner() { console.log('c'); return `<${this.x}${this.c}><${super.getInner()}>`; } } var c = new C(b); console.log(c.getOuter());

среда, 20 февраля 2019 г.

Вызов дочернего метода через базовый класс

Есть базовый класс Shapes, в нем есть два абстрактных метода P (Периметр) и S (Площадь). Также есть дочерний класс Circle, в котором реализованы методы P и S с помощью полиморфизма.
Вопрос: как вызвать метод P и S из класса Circle через класс Shapes?
Это же называется динамическое связывание?
Shapes.h
class Shpes { private: public: Shpes(); virtual double P() = 0; virtual double S() = 0; };
Shpes::Shpes() {
}
Circle.h
#include "Shpes.h"
class Circle : public Shpes { private: double r; void SetCheck(double R);
public: Circle(double R = 0); virtual double P(); virtual double S(); void Print(); };
Circle.cpp
#include "Circle.h" #include #include
using namespace std;
void Circle::SetCheck(double R) { R < 0 ? r = abs(R) : r = R; }
Circle::Circle(double R) { SetCheck(R); }
double Circle::P() { return 2 * 3.12 * r; }
double Circle::S() { return 3.14 * r * r; }
void Circle::Print() { cout << "При радиусе круга = " << r < Это можно сделать через указатель или ссылку на базовый класс, инициализированными объектом производного класса.
Например,
Circle c( 10 );
Shape *ps = &c;
ps->R(); ps->S();
Shape &rs = c;
rs.R(); rs.S();
Имейте в виду, что деструктор в базовом классе также следует объявить как виртуальный. Например,
virtual ~Shape() = default;
или
virtual ~Shape(){}
ОБратите внимание на расхождение
double Circle::P() { return 2 * 3.12 * r; ^^^^^ }
double Circle::S() { return 3.14 * r * r; ^^^^ }
Чтобы не допускать такого вида ошибки, лучше используемым константам назначать имена. Вы могли бы определить соответствующий член в вашем классе (при условии, что компилятор поддерживает данную конструкцию), например,
class Circle : public Shpes { private: double r; void SetCheck(double R); constexpr static double PI = 3.14;
public: Circle(double R = 0); virtual double P(); virtual double S(); void Print(); };
И записать определения функций следующим образом
double Circle::P() { return 2 * PI * r; ^^^^^ }
double Circle::S() { return PI * r * r; ^^^^ }
Если компилятор не поддерживает такое объявление члена класса, то можно просто написать
class Circle : public Shpes { private: double r; void SetCheck(double R); const static double PI;
public: Circle(double R = 0); virtual double P(); virtual double S(); void Print(); };
И затем определить эту статическую переменную
double Circle::PI = 3.14;

среда, 10 октября 2018 г.

Отличия переопределения метода от перекрытия

Чем отличается перекрытие от переопределения метода?


Ответ

Перекрытие (в ваших терминах) означает, что метод в производном классе скрывает метод с той же самой сигнатурой в базовом классе.
Поэтому в C# рекомендуется для методов производных классов, которые перекрывают методы базовых классов, использовать ключевое слово new. Если вы забудете это сделать, то компилятор вас предупредит, что вы скрываете одноименную функцию с той же сигнатурой в базовом классе.
Например,
class Base { public void Hello() { Console.WriteLine( "I'm the base class" ); } }
class Derived : Base { public new void Hello() { Console.WriteLine( "I'm the derived class" ); } }
В этом случае для ссылок на базовый класс Base всегда будет вызываться метод Hello базового класса, а для ссылок на производный класс будет вызываться метод Hello производного класса.
Например,
Base b = new Base(); b.Hello(); // I'm the base class
Derived d = new Derived(); d.Hello(); // I'm the derived class
b = d; b.Hello(); // I'm the base class
Переопределение применяется к виртуальным функциям. В производном классе виртуальная функция переопределяется. Для этого в базовом классе функция должна быть определена с ключевым словом virtual (или abstract), а в производном классе для ее переопределения необходимо указывать другое ключевое слово override
Например,
class Base { public virtual void Hello() { Console.WriteLine( "I'm the base class" ); } }
class Derived : Base { public override void Hello() { Console.WriteLine( "I'm the derived class" ); } }
Различие с предыдущим примером состоит в том, что если ссылка базового класса указывает на объект производного класса, то в этом случае будет вызвана переопределенная функция производного класса.
Base b = new Base(); b.Hello(); // I'm the base class
Derived d = new Derived(); d.Hello(); // I'm the derived class
b = d; b.Hello(); `// I'm the derived class`
То есть различие состоит в этих двух предложениях. В первом случае для перекрытия
b = d; b.Hello(); // I'm the base class ^^^^^^^^^^^^^^^^^^
а во втором случае при переопределении имеем
b = d; b.Hello(); `// I'm the derived class` ^^^^^^^^^^^^^^^^^^^^^^^^
То есть в первом случае мы в каждом новом производном классе в иерархии классов объявляем новую функцию, которая скрывает функцию с той же сигнатурой в базовых классах. А во втором случае функция с тем же именем заново не объявляется, а переопределяется уже объявленная функция базового класса.
Это позволяет динамически вызывать нужное определение одной и той же функции в зависимости от объекта, с которым функция вызывается.