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

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


person Hai    schedule 09.01.2009    source источник


Ответы (7)


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

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

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

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

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 производного класса. В этом суть виртуальных функций. Без них вызывалась бы функция печати базы, а функция производного класса не переопределяла бы ее.

person Johannes Schaub - litb    schedule 09.01.2009
comment
ага, конкурируют :) лучшего слова не нашел. :: изменил его, потому что это действительно звучало странно :p - person Johannes Schaub - litb; 09.01.2009
comment
Почему переопределенная функция в классе derived имеет перед собой ключевое слово virtual? Обязательно ли ставить virtual перед переопределением в производном классе? - person Ziezi; 28.09.2015
comment
А для тех, кому интересно, в чем разница между переопределением метода и сокрытием метода: stackoverflow.com/questions/19736281/ - person Flow; 13.05.2016
comment
@Ziezi нет, в этом примере virtual не имеет значения, но люди используют его, чтобы показать, что на самом деле это виртуальная функция, которую можно вызвать, например, через ссылку на базовый класс. Есть ловушка, если вы напишете virtual void prinf(...){ ...} (опечатка в имени), вы получите виртуальную функцию, но, конечно, она не связана с base::print, и в результате ошибка может быть незаметной. В C++11 квалификатор override был добавлен, чтобы явно указать переопределение: если вы напишете void prinf(...) override {...}, вы получите ошибку компиляции, пока не исправите опечатку. - person greggo; 20.10.2016
comment
@greggo Спасибо за разъяснения. - person Ziezi; 20.10.2016
comment
@greggo Как насчет использования как virtual, так и override, например virtual void printf(...) override {...} - person Herbert; 06.01.2017
comment
@Herbert совершенно законно, но когда разрешено «переопределение», метод уже является виртуальным по контексту, так что это вопрос предпочтений. - person greggo; 07.01.2017

Это внесет ясность в мысли.введите здесь описание изображения

person subhash kumar singh    schedule 10.06.2014
comment
Хороший рисунок! Можете ли вы объяснить 2-е изображение для Overriding? Для аналогии лук — это класс, стрела — функция (функции). Первое изображение означает, что класс определяет 3 функции (стрельба в разных направлениях); Второй значит? - person milesma; 01.08.2018

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

  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);
    

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


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

person P Daddy    schedule 09.01.2009
comment
+1 за использование слова, придуманного так удачно. Теперь я не могу забыть, что значит надуманный. - person Kingkong Jnr; 13.08.2012
comment
Одним из основных преимуществ перегрузки в C++ является перегрузка operator, которую обеспечивает механизм перегрузки. Я бы назвал это четвертой (или даже первой) причиной. - person bloody; 20.12.2020

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

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

person lc.    schedule 09.01.2009

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

ASAFE спросил:

единственное преимущество [перед перегрузкой] в том, что вы не думаете о нескольких именах функций?

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", это означает, что вы должны знать, что он принимает только двойные числа.

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

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

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

person paercebal    schedule 09.01.2009

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

person jdigital    schedule 09.01.2009

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

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

person David Thornley    schedule 09.01.2009