【C++入門筆記】覆蓋方法和過載方法
前言
通過前面的學習,我們已經知道了如何通過建立一個新的子類來重用現有的程式碼(繼承)。
但是如果我們需要在基類裡提供一個通用的函式,但在它的某個子類裡需要修改這個方法的實現,在C++裡,覆蓋(overriding)就可以做到。
覆蓋方法
C++可以讓我們很容易實現這種既有共同特徵又需要在不同的類裡面有不同實現的方法。
我們需要做的就是在類裡重新宣告這個方法,然後再改寫一下它的實現程式碼(就想它是一個增加的方法那樣)就行了。
比如說修改一下以前的例題:為我們的Animal新增eat()方法,並在Pig和Turtle中覆蓋。
#include <iostream> #include <string> class Animal { public: Animal(std::string theName); void eat(); void sleep(); void drool(); protected: std::string name; }; class Pig : public Animal { public: Pig(std::string theName); void climb(); void eat(); // new! }; class Turtle : public Animal { public: Turtle(std::string theName); void swim(); void eat(); // new! }; Animal::Animal(std::string theName) { name = theName; } void Animal::eat() { std::cout << "I'm eating!" << std::endl; } void Animal::sleep() { std::cout << "I'm sleeping!Don't disturb me!\n" << std::endl; } void Animal::drool() { std::cout << "我是公的,看到母的我會流口水,我正在流口水。。。\n" << std::endl; } Pig::Pig(std::string theName) : Animal(theName) { } void Pig::climb() { std::cout << "我是一個只漂亮的小母豬豬,我會上樹,我正在爬樹,噓。。。\n" << std::endl; } void Pig::eat() { Animal::eat(); std::cout << name << "正在吃魚!\n\n"; // new! } Turtle::Turtle(std::string theName) : Animal(theName) { } void Turtle::swim() { std::cout << "我是一隻小甲魚,當母豬想抓我的時候,我就游到海里。。哈哈。。\n" << std::endl; } void Turtle::eat() { Animal::eat(); std::cout << name << "正在吃東坡肉!\n\n"; // new! } int main() { Pig pig("小豬豬"); Turtle turtle("小甲魚"); // std::cout << "這隻豬的名字是: " << pig.name << std::endl; // 錯誤 // std::cout << "每隻烏龜都有個偉大的名字: " << turtle.name << std::endl; // 錯誤 pig.eat(); turtle.eat(); pig.climb(); turtle.swim(); return 0; }
I'm eating!
小豬豬正在吃魚!I'm eating!
小甲魚正在吃東坡肉!我是一個只漂亮的小母豬豬,我會上樹,我正在爬樹,噓。。。
我是一隻小甲魚,當母豬想抓我的時候,我就游到海里。。哈哈。。
過載
簡化程式設計工作和提高程式碼可讀性的另一種方法就是對方法進行過載。
過載機制允許你可以定義多個同名的方法(函式),只是它們的輸入引數必須不同。(因為編譯器是依靠不同的輸入引數來區分不同的方法)
過載並不是一個真正的面向物件特徵,它只是可以簡化程式設計工作的捷徑,而簡化程式設計工作正是C++的全部追求。
下面對eat()進行過載。
#include <iostream> #include <string> class Animal { public: Animal(std::string theName); void eat(); void eat(int eatCount); void sleep(); void drool(); protected: std::string name; }; class Pig : public Animal { public: Pig(std::string theName); void climb(); }; class Turtle : public Animal { public: Turtle(std::string theName); void swim(); }; Animal::Animal(std::string theName) { name = theName; } void Animal::eat() { std::cout << "I'm eating!" << std::endl; } void Animal::eat(int eatCount) { std::cout << "我吃了" << eatCount << "碗餛飩!\n\n"; } void Animal::sleep() { std::cout << "I'm sleeping!Don't disturb me!\n" << std::endl; } void Animal::drool() { std::cout << "我是公的,看到母的我會流口水,我正在流口水。。。\n" << std::endl; } Pig::Pig(std::string theName) : Animal(theName) { } void Pig::climb() { std::cout << "我是一個只漂亮的小母豬豬,我會上樹,我正在爬樹,噓。。。\n" << std::endl; } Turtle::Turtle(std::string theName) : Animal(theName) { } void Turtle::swim() { std::cout << "我是一隻小甲魚,當母豬想抓我的時候,我就游到海里。。哈哈。。\n" << std::endl; } int main() { Pig pig("小豬豬"); Turtle turtle("小甲魚"); // std::cout << "這隻豬的名字是: " << pig.name << std::endl; // 錯誤 // std::cout << "每隻烏龜都有個偉大的名字: " << turtle.name << std::endl; // 錯誤 pig.eat(); turtle.eat(); pig.eat(15); pig.climb(); turtle.swim(); return 0; }
I'm eating!
I'm eating!
我吃了15碗餛飩!我是一個只漂亮的小母豬豬,我會上樹,我正在爬樹,噓。。。
我是一隻小甲魚,當母豬想抓我的時候,我就游到海里。。哈哈。。
Pay Attention
對方法進行覆蓋時(注意區分覆蓋和過載)一定要看仔細,因為只要宣告的輸入引數和返回值與原來的不一樣,那你編寫的就將是一個過載方法而不是覆蓋方法。
對從基類繼承來的方法不能進行過載!!
例如
#include <iostream>
#include <string>
class Animal
{
public:
Animal(std::string theName);
void eat();
void sleep();
void drool();
protected:
std::string name;
};
class Pig : public Animal
{
public:
Pig(std::string theName);
void climb();
void eat(int eatCount);
};
class Turtle : public Animal
{
public:
Turtle(std::string theName);
void swim();
};
Animal::Animal(std::string theName)
{
name = theName;
}
void Animal::eat()
{
std::cout << "I'm eatting!" << std::endl;
}
void Animal::sleep()
{
std::cout << "I'm sleeping!Don't disturb me!\n" << std::endl;
}
void Animal::drool()
{
std::cout << "我是公的,看到母的我會流口水,我正在流口水。。。\n" << std::endl;
}
Pig::Pig(std::string theName) : Animal(theName)
{
}
void Pig::climb()
{
std::cout << "我是一個只漂亮的小母豬豬,我會上樹,我正在爬樹,噓。。。\n" << std::endl;
}
void Pig::eat(int eatCount)
{
std::cout << "我吃了" << eatCount << "碗餛飩!\n\n";
}
Turtle::Turtle(std::string theName) : Animal(theName)
{
}
void Turtle::swim()
{
std::cout << "我是一隻小甲魚,當母豬想抓我的時候,我就游到海里。。哈哈。。\n" << std::endl;
}
int main()
{
Pig pig("小豬豬");
Turtle turtle("小甲魚");
// std::cout << "這隻豬的名字是: " << pig.name << std::endl; // 錯誤
// std::cout << "每隻烏龜都有個偉大的名字: " << turtle.name << std::endl; // 錯誤
pig.eat();
turtle.eat();
pig.eat(15);
pig.climb();
turtle.swim();
return 0;
}
error C2660: “Pig::eat”: 函式不接受 0 個引數
note: 參見“Pig::eat”的宣告
因為 pig.eat()過載失敗,因為它被有引數的pig.eat(15)宣告的一個新的方法給覆蓋掉了。所以只能在同一個類裡進行過載,繼承後的不能過載。
C++成員函式的過載、覆蓋、隱藏區別
成員函式被過載的特徵:
- 相同的範圍(在同一個類中);
- 函式名字相同;
- 引數不同;
- virtual 關鍵字可有可無
覆蓋是指派生類函式覆蓋基類函式,特徵是:
- 不同的範圍(分別位於派生類和基類中);
- 函式的名字相同;
- 引數相同;
- 基類函式必須有virtual關鍵字。
以下示例中,函式Base::f(int)與Base::f(float)相互過載,而Base::g(void)被Derived::g(void)覆蓋。
#include <iostream>
using namespace std;
class Base
{
public:
void f(int x)
{
cout << "Base::f(int) " << x << endl;
}
void f(float x)
{
cout << "Base::f(float) " << x << endl;
}
virtual void g(void)
{
cout << "Base::g(void)" << endl;
}
};
class Derived : public Base
{
public:
virtual void g(void)
{
cout << "Derived::g(void)" << endl;
}
};
int main(void)
{
Derived d;
Base *pb = &d;
pb->f(42); // Base::f(int) 42
pb->f(3.14f); // Base::f(float) 3.14
pb->g(); // Derived::g(void)
return 0;
}
Base::f(int) 42
Base::f(float) 3.14
Derived::g(void)
令人疑惑的隱藏規則
本來僅僅區分過載和覆蓋並不算困難,但是C++的隱藏規則使問題複雜性陡然上升。
這裡“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下:
- 如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。(過載是要求在同一個類中同名、引數不同)
- 如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)。(覆蓋是要求不同的範圍,函式名字和引數相同,但必須有virtual關鍵字)
以下示例中:
(1)函式Derived::f(float) 覆蓋了 Base::f(float)。
(2)函式Derived::g(int) 隱藏了 Base::g(float),而不是過載。
(3)函式Derived::h(float) 隱藏了 Base::h(float),而不是覆蓋。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void f(float x)
{
cout << "Base::f(float) " << x << endl;
}
void g(float x)
{
cout << "Base::g(float) " << x << endl;
}
void h(float x)
{
cout << "Base::h(float) " << x << endl;
}
};
class Derived : public Base
{
public:
virtual void f(float x)
{
cout << "Derived::f(float) " << x << endl;
}
void g(int x)
{
cout << "Derived::g(int) " << x << endl;
}
void h(float x)
{
cout << "Derived::h(float) " << x << endl;
}
};
“隱藏”的發生可謂神出鬼沒,常常產生令人迷惑的結果。
如以下示例中,bp 和 dp 指向同一地址,按理說執行結果應該是相同的,可事實並非這樣。
int main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
pb -> f(3.14f); // Derived::f(float) 3.14
pd -> f(3.14f); // Derived::f(float) 3.14
pb -> g(3.14f); // Base::g(float) 3.14
pd- > g(3.14f); // Derived::g(int) 3 (surprise!)
pb -> h(3.14f); // Base::h(float) 3.14 (surprise!)
pd -> h(3.14f); // Derived::h(float) 3.14
return 0;
}
Derived::f(float) 3.14
Derived::f(float) 3.14
Base::g(float) 3.14
Derived::g(int) 3
Base::h(float) 3.14
Derived::h(float) 3.14
擺脫隱藏
隱藏規則引起了不少麻煩。以下示例中,語句pd->f(10)的本意是想呼叫函式 Base::f(int),但是 Base::f(int) 不幸被 Derived::f(char *str) 隱藏了。由於數字10不能被隱式地轉化為字串,所以在編譯時出錯。
class Base
{
public:
void f(int x);
};
class Derived : public Base
{
public:
void f(char *str);
};
void Test(void)
{
Derived *pd = new Derived;
pd -> f(10); // error
}
error C2664: “void Derived::f(char *)”: 無法將引數 1 從“int”轉換為“char *”
從整型強制轉換為指標型別要求 reinterpret_cast、C 樣式強制轉換或函式樣式強制轉換
總結
上述內容是我在FishC的視訊中以及瀏覽部落格做的筆記,感謝魚C大佬的講解,以下附上視訊地址以及部落格地址。以後關於覆蓋、過載以及隱藏有了自己的見解也會陸續加入其中。
只要不是過載和覆蓋其他的都是隱藏,過載很好理解,覆蓋就是多了個virtual,其他的一切情況都是隱藏,當然,編譯出錯情況例外。
參考地址
《C++快速入門--小甲魚》https://www.bilibili.com/video/av7595819/?p=16
C++成員函式的過載、覆蓋、隱藏區別 https://fishc.com.cn/blog-9-1122.html