Да, я понимаю разницу между ними. Я хочу знать: зачем ПЕРЕПОЛНЯТЬ метод? Что хорошего в этом? В случае перегрузки: единственное преимущество в том, что вам не нужно думать о разных именах функций?
Переопределение и перегрузка в C++
Ответы (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 производного класса. В этом суть виртуальных функций. Без них вызывалась бы функция печати базы, а функция производного класса не переопределяла бы ее.
derived имеет перед собой ключевое слово virtual? Обязательно ли ставить virtual перед переопределением в производном классе?
- person Ziezi; 28.09.2015
virtual не имеет значения, но люди используют его, чтобы показать, что на самом деле это виртуальная функция, которую можно вызвать, например, через ссылку на базовый класс. Есть ловушка, если вы напишете virtual void prinf(...){ ...} (опечатка в имени), вы получите виртуальную функцию, но, конечно, она не связана с base::print, и в результате ошибка может быть незаметной. В C++11 квалификатор override был добавлен, чтобы явно указать переопределение: если вы напишете void prinf(...) override {...}, вы получите ошибку компиляции, пока не исправите опечатку.
- person greggo; 20.10.2016
virtual, так и override, например virtual void printf(...) override {...}
- person Herbert; 06.01.2017
Это внесет ясность в мысли.
Вы перегружаетезагружаете функции по трем причинам:
Чтобы предоставить две (или более) функции, которые выполняют похожие, тесно связанные вещи, различающиеся по типам и/или количеству принимаемых аргументов. Надуманный пример:
void Log(std::string msg); // logs a message to standard out void Log(std::string msg, std::ofstream); // logs a message to a fileПредоставить два (или более) способа выполнения одного и того же действия. Надуманный пример:
void Plot(Point pt); // plots a point at (pt.x, pt.y) void Plot(int x, int y); // plots a point at (x, y)Обеспечить возможность выполнения эквивалентного действия при наличии двух (или более) разных типов ввода. Надуманный пример:
wchar_t ToUnicode(char c); std::wstring ToUnicode(std::string s);
В некоторых случаях стоит возразить, что функция с другим именем лучше, чем перегруженная функция. В случае конструкторов перегрузка — единственный выбор.
Переопределение функции — это совершенно другое и служит совершенно другой цели. Переопределение функций — это то, как полиморфизм работает в C++. Вы переопределяете функцию, чтобы изменить поведение этой функции в производном классе. Таким образом, базовый класс обеспечивает интерфейс, а производный класс обеспечивает реализацию.
Переопределение полезно, когда вы наследуете от базового класса и хотите расширить или изменить его функциональность. Даже когда объект приводится как базовый класс, он вызывает вашу переопределенную функцию, а не базовую.
Перегрузка не обязательна, но иногда она делает жизнь проще или читабельнее. Возможно, это может усугубить ситуацию, но в этом случае его не следует использовать. Например, у вас могут быть две функции, выполняющие одну и ту же операцию, но воздействующие на разные вещи. Например, Divide(float, float) должно отличаться от Divide(int, int), но в основном это одна и та же операция. Не лучше ли запомнить одно имя метода «Divide», чем запоминать «DivideFloat», «DivideInt», «DivideIntByFloat» и так далее?
Люди уже определили как перегрузку, так и переопределение, поэтому я не буду вдаваться в подробности.
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# и т. д. Почему? потому что это более естественно.
И приведенные выше примеры — это только верхушка айсберга: при использовании шаблонов перегрузка становится очень и очень полезной, но это уже другая история.
Примером из учебника является класс Animal с методом speak(). Подкласс Dog переопределяет speak() на «лай», в то время как подкласс Cat переопределяет speak() на «мяу».
Одно из применений перегрузки — использование в шаблонах. В шаблонах вы пишете код, который можно использовать с разными типами данных, и вызываете его с разными типами. Если бы функции, которые принимают разные аргументы, должны были называться по-разному, код для разных типов данных в целом должен был бы быть другим, и шаблоны просто не работали бы.
Хотя вы, возможно, еще не пишете шаблоны, вы почти наверняка используете некоторые из них. Потоки — это шаблоны, как и векторы. Без перегрузки и, следовательно, без шаблонов вам нужно было бы вызывать потоки Unicode как-то иначе, чем потоки ASCII, и вам пришлось бы использовать массивы и указатели вместо векторов.