Переопределение и перегрузка в C++

Да, я понимаю разницу между ними. Что я хочу знать: зачем ПЕРЕПРЕРЫВАТЬ метод? Что в этом хорошего? В случае перегрузки: единственное преимущество в том, что вам не нужно думать разными именами для функций?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
33
0
69 818
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

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

В перегрузке нет необходимости, но она, несомненно, иногда упрощает жизнь или делает ее более читаемой. Возможно, это может усугубить ситуацию, но тогда его не следует использовать. Например, у вас могут быть две функции, которые выполняют одну и ту же операцию, но действуют на разные вещи. Например, Divide(float, float) должен отличаться от Divide(int, int), но в основном это одна и та же операция. Разве вы не лучше запомните одно имя метода, «Divide», чем запомните «DivideFloat», «DivideInt», «DivideIntByFloat» и т. д.?

Пример из учебника - это класс Animal с методом speak (). Подкласс Dog переопределяет Speak () на «лай», а подкласс Cat переопределяет speak () на «мяукать».

Ответ принят как подходящий

Перегрузка обычно означает, что у вас есть две или более функций в одной области действия с одинаковым именем. Функция, которая лучше соответствует аргументам при вызове, выигрывает и вызывается. Важно отметить, что, в отличие от вызова виртуальной функции, вызываемая функция выбирается во время компиляции. Все зависит от статического типа аргумента. Если у вас есть перегрузка для B и одна для D, и аргумент является ссылкой на B, но на самом деле указывает на объект D, то перегрузка для B выбрана в C++. Это называется статическая отправка в отличие от динамическая отправка. Вы перегружаете, если хотите сделать то же самое, что и другая функция с тем же именем, но хотите сделать это для другого типа аргумента. Пример:

void print(Foo const& f) {
    // print a foo
}

void print(Bar const& bar) {
    // print a bar
}

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

void print(Foo & f, PrintAttributes b) { 
    /* ... */ 
}

void print(Foo & f, std::string const& header, bool printBold) {
    print(f, PrintAttributes(header, printBold));
}

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

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

struct base {
    virtual void print() { cout << "base!"; }
}

struct derived: base {
    virtual void print() { cout << "derived!"; }
}

Теперь, если у вас есть объект и вы вызываете функцию-член print, всегда вызывается функция печати производной функции, поскольку она переопределяет одну из базовых. Если бы функция print не была виртуальной, тогда производная функция не переопределяла бы базовую функцию, а просто Спрятать ее. Переопределение может быть полезно, если у вас есть функция, которая принимает базовый класс и все, что от него происходит:

void doit(base &b) {
    // and sometimes, we want to print it
    b.print();
}

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

да, они соревнуются :) не нашел лучшего слова для этого. :: изменил это, потому что это действительно звучало странно: p

Johannes Schaub - litb 09.01.2009 22:00

Почему перед переопределенной функцией в классе derived стоит ключевое слово virtual? Обязательно ли ставить virtual перед переопределением в производном классе?

Ziezi 28.09.2015 17:20

А для тех, кому интересно, что такое разница между переопределением метода и скрытием метода: stackoverflow.com/questions/19736281/…

Flow 13.05.2016 14:07

@Ziezi нет, в этом примере virtual не имеет значения, но люди используют его, чтобы прояснить, что на самом деле это виртуальная функция, которая может быть вызвана, например, через ссылку на базовый класс. Есть ловушка, если вы напишете virtual void prinf(...){ ...} (опечатка в имени), вы получите виртуальную функцию, но, конечно, она не связана с base::print, и возникающая в результате ошибка может быть незаметной. В C++ 11 был добавлен квалификатор override, чтобы явно указать переопределение: если вы напишете void prinf(...) override {...}, вы получите ошибку компиляции, пока не исправите опечатку.

greggo 20.10.2016 19:51

@greggo Спасибо за разъяснения.

Ziezi 20.10.2016 20:48

@greggo Как насчет использования как virtual, так и override, например virtual void printf(...) override {...}

Herbert 06.01.2017 01:25

@Herbert совершенно законен, но когда разрешено «переопределение», метод уже виртуален по контексту, так что это вопрос предпочтений.

greggo 07.01.2017 01:22

Вы перекрываете функции нагрузка по трем причинам:

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

    void Log(std::string msg); // logs a message to standard out
    void Log(std::string msg, std::ofstream); // logs a message to a file
    
  2. Чтобы предоставить два (или более) способа выполнения одного и того же действия. Надуманный пример:

    void Plot(Point pt); // plots a point at (pt.x, pt.y)
    void Plot(int x, int y); // plots a point at (x, y)
    
  3. Чтобы предоставить возможность выполнять эквивалентное действие при наличии двух (или более) разных типов ввода. Надуманный пример:

    wchar_t      ToUnicode(char c);
    std::wstring ToUnicode(std::string s);
    

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


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

+1 за столь удачное использование слова. Теперь я не могу забыть, что значит надуманное.

Kingkong Jnr 13.08.2012 02:44

Одним из основных преимуществ перегрузки в C++ является перегрузка оператор, которую разрешает механизм перегрузки. Я бы назвал это четвертой (или вообще первой) причиной.

bloody 20.12.2020 16:01

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

Хотя вы, возможно, еще не пишете шаблоны, вы почти наверняка используете некоторые из них. Потоки - это шаблоны, как и векторы. Без перегрузки и, следовательно, без шаблонов вам нужно будет вызывать потоки Unicode чем-то отличным от потоков ASCII, и вам придется использовать массивы и указатели вместо векторов.

Люди уже определили как перегрузку, так и переопределение, поэтому я не буду вдаваться в подробности.

ASAFE asked:

the only advantage [to overloading] is you haven't think in several names to functions?

1. Не нужно думать несколькими именами.

И это уже огромное преимущество, не правда ли?

Сравним с известными функциями C API и их вымышленными вариантами C++:

/* C */
double fabs(double d) ;
int abs(int i) ;

// C++ fictional variants
long double abs(long double d) ;
double abs(double d) ;
float abs(float f) ;
long abs(long i) ;
int abs(int i) ;

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

И все, что он / Она хотел, это иметь абсолютное значение некоторой числовой переменной ...

Одно действие означает одно и только одно имя функции.

Обратите внимание, что вы не ограничены изменением типа одного параметра. Все может измениться, если в этом есть смысл.

2. Для операторов обязательно

Посмотрим на операторов:

// C++
Integer operator + (const Integer & lhs, const Integer & rhs) ;
Real operator + (const Real & lhs, const Real & rhs) ;
Matrix operator + (const Matrix & lhs, const Matrix & rhs) ;
Complex operator + (const Complex & lhs, const Complex & rhs) ;

void doSomething()
{
   Integer i0 = 5, i1 = 10 ;
   Integer i2 = i0 + i1 ; // i2 == 15

   Real r0 = 5.5, r1 = 10.3 ;
   Real r2 = r0 + r1 ; // r2 = 15.8

   Matrix m0(1, 2, 3, 4), m1(10, 20, 30, 40) ;
   Matrix m2 = m0 + m1 ; // m2 == (11, 22, 33, 44)

   Complex c0(1, 5), c1(10, 50) ;
   Complex c2 = c0 + c1 ; // c2 == (11, 55)
}

В приведенном выше примере вы хочу, чтобы избежать использования чего-либо еще, кроме оператора +.

Обратите внимание, что C имеет неявную перегрузку оператора для встроенных типов (включая сложный тип C99):

/* C */
void doSomething(void)
{
   char c = 32 ;
   short s = 54 ;
   c + s ; /* == C++ operator + (char, short) */
   c + c ; /* == C++ operator + (char, char) */
}

Так что даже в необъектных языках используется эта перегрузка.

3. Для объектов обязательно

Давайте посмотрим на использование основных методов объекта: Его конструкторы:

class MyString
{
   public :
      MyString(char character) ;
      MyString(int number) ;
      MyString(const char * c_style_string) ;
      MyString(const MyString * mySring) ;
      // etc.
} ;

Некоторые могут рассматривать это как перегрузку функции, но на самом деле это больше похоже на перегрузку оператора:

void doSomething()
{
   MyString a('h') ;                  // a == "h" ;
   MyString b(25) ;                   // b == "25" ;
   MyString c("Hello World") ;        // c == "Hello World" ;
   MyString d(c) ;                    // d == "Hello World" ;
}

Вывод: перегрузка - это круто

В C, когда вы даете имя функции, параметры неявно являются частью сигнатуры при вызове. Если у вас есть "double fabs (double d)", тогда, хотя сигнатура fabs для компилятора - недекорированные "fabs", это означает, что ты должен знать, что он принимает только double.

В C++ имя функции не означает, что ее подпись является принудительной. Его сигнатура при вызове - это его имя и его параметры. Таким образом, если вы напишете abs (-24), компилятор будет знать, какую перегрузку abs он должен вызвать, и вы при ее написании сочтете это более естественным: вам нужно абсолютное значение -24.

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

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

Это добавит ясности мыслям.

Хороший рисунок! Можете ли вы объяснить вторую картинку для переопределения? Для аналогии лук - это «класс», стрелка - это функция (и). Первое изображение означает, что класс определяет 3 функции (стрельба в разные стороны); Второй значит?

milesma 01.08.2018 02:58

Другие вопросы по теме