はい、私はそれらの違いを理解しています。私が知りたいのは、なぜメソッドをオーバーライドするのかということです。それをすることの良いところは何ですか?過負荷の場合:唯一の利点は、関数の名前を変えて考える必要がないことです。
7 に答える
オーバーロードとは、通常、同じスコープ内に同じ名前を持つ 2 つ以上の関数があることを意味します。呼び出しが行われたときに引数をより適切に一致させる関数が勝ち、呼び出されます。仮想関数を呼び出すのとは対照的に、呼び出される関数はコンパイル時に選択されることに注意してください。それはすべて、引数の静的タイプに依存します。のオーバーロードと のオーバーロードがB
ありD
、引数が への参照B
であるが、実際にはD
オブジェクトを指している場合、 B
C++ ではオーバーロードの が選択されます。これは、動的ディスパッチではなく静的ディスパッチと呼ばれます. 同じ名前を持つ別の関数と同じことをしたいが、別の引数の型に対してそれをしたい場合は、オーバーロードします。例:
void print(Foo const& f) {
// print a foo
}
void print(Bar const& bar) {
// print a bar
}
どちらも引数を出力するため、オーバーロードされます。しかし、最初は foo を出力し、2 番目はバーを出力します。異なることを行う 2 つの関数がある場合、それらが同じ名前を持つとスタイルが悪いと見なされます。これは、関数を呼び出すときに実際に何が起こるかについて混乱を招く可能性があるためです。オーバーロードのもう 1 つの使用例は、関数に追加のパラメーターがあるが、制御を他の関数に転送するだけの場合です。
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 関数が常に呼び出されます。これは、ベースの print 関数をオーバーライドするためです。関数print
が仮想関数でない場合、派生関数は基本関数をオーバーライドせず、単に非表示にします。オーバーライドは、基本クラスを受け入れる関数と、それから派生したすべての関数がある場合に役立ちます。
void doit(base &b) {
// and sometimes, we want to print it
b.print();
}
ここで、コンパイラはコンパイル時に b が少なくともベースであることしか認識していませんが、派生クラスの print が呼び出されます。それが仮想関数のポイントです。それらがないと、ベースの print 関数が呼び出され、派生クラスの print 関数はそれをオーバーライドしません。
次の 3 つの理由で、関数をオーバーロードします。
受け入れる引数の型や数によって区別される、類似した密接に関連したことを実行する 2 つ (またはそれ以上) の関数を提供すること。考案された例:
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)
2 つ (またはそれ以上) の異なる入力タイプを指定して、同等のアクションを実行する機能を提供します。考案された例:
wchar_t ToUnicode(char c); std::wstring ToUnicode(std::string s);
場合によっては、オーバーロードされた関数よりも別の名前の関数の方が適切であると主張する価値があります。コンストラクターの場合、オーバーロードが唯一の選択肢です。
関数のオーバーライドはまったく異なり、まったく異なる目的を果たします。関数のオーバーライドは、C++ でポリモーフィズムがどのように機能するかです。関数をオーバーライドして、派生クラスでその関数の動作を変更します。このように、基底クラスはインターフェースを提供し、派生クラスは実装を提供します。
オーバーライドは、基本クラスから継承し、その機能を拡張または変更する場合に役立ちます。オブジェクトが基本クラスとしてキャストされている場合でも、オブジェクトは基本関数ではなく、オーバーライドされた関数を呼び出します。
オーバーロードは必要ありませんが、それは確かに人生を楽にしたり、読みやすくしたりします。おそらくそれはそれを悪化させる可能性がありますが、それはそれが使用されるべきではないときです。たとえば、同じ操作を実行するが、異なる種類のものに作用する2つの関数を持つことができます。たとえば、Divide(float, float)
とは異なるはずDivide(int, int)
ですが、基本的には同じ操作です。「DivideFloat」、「DivideInt」、「DivideIntByFloat」などを覚えておくよりも、1つのメソッド名「Divide」を覚えていませんか?
人々はすでにオーバーロードとオーバーライドの両方を定義しているので、詳しくは説明しません。
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 つのことを意味します。1 つは、適切な関数を選択して、関数に渡すデータの型をコンパイラに伝える必要があることです。2 つ目は、関数を拡張したい場合は、凝った名前を見つける必要があり、関数のユーザーは適切な凝った名前を覚えておく必要があります。
そして、彼/彼女が望んでいたのは、数値変数の絶対値を取得することだけでした...
1 つのアクションは、1 つだけの関数名を意味します。
1 つのパラメーターの型を変更することに限定されないことに注意してください。理にかなっている限り、何でも変更できます。
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# デリゲートなど、オーバーロードを既に使用しています。なぜでしょうか? そのほうが自然だから。
上記の例は氷山の一角にすぎません。テンプレートを使用すると、オーバーロードが非常に便利になりますが、これは別の話です。
教科書の例は、メソッド speak() を持つクラス Animal です。Dog サブクラスは speak() を「bark」にオーバーライドし、Cat サブクラスは speak() を「meow」にオーバーライドします。
オーバーロードの用途の 1 つは、テンプレートでの使用です。テンプレートでは、さまざまなデータ型で使用できるコードを記述し、さまざまな型でそれを呼び出します。異なる引数を取る関数に異なる名前を付ける必要がある場合、異なるデータ型のコードは一般的に異なる必要があり、テンプレートは機能しません。
あなたはまだテンプレートを書いていないかもしれませんが、ほぼ確実にそれらのいくつかを使用しています。ストリームはテンプレートであり、ベクターも同様です。オーバーロードがなければ、つまりテンプレートがなければ、Unicode ストリームを ASCII ストリームとは別のものと呼ぶ必要があり、ベクトルの代わりに配列とポインターを使用する必要があります。