別再被坑了!C++ 重載 vs 重寫,這篇文章讓你秒懂區(qū)別
大家好,我是小康。
今天咱們聊個(gè)老生常談但又經(jīng)常被搞混的話題——函數(shù)重載和函數(shù)重寫。
說真的,每次面試的時(shí)候,面試官總愛問這個(gè)問題。我敢打賭,十個(gè)程序員有九個(gè)都在這兒栽過跟頭。要么就是概念搞混了,要么就是說得云里霧里的,讓面試官一臉懵逼。
今天我就用最簡(jiǎn)單粗暴的方式,把這倆貨給你講明白。保證你看完之后,再也不會(huì)傻傻分不清楚了!
一、先來個(gè)形象的比喻
想象一下,你家樓下有個(gè)包子鋪,老板姓張。
- 函數(shù)重載就像是:張老板會(huì)做包子,但他能做肉包子、菜包子、豆沙包子。雖然都叫"做包子",但根據(jù)你給的材料不同,他做出來的包子也不一樣。同一個(gè)人,同一個(gè)技能名字,但是根據(jù)輸入的不同,輸出也不同。
- 函數(shù)重寫就像是:張老板退休了,他兒子小張接手了包子鋪。小張也會(huì)"做包子",但他的做法跟他爸完全不一樣,可能更好吃,也可能更難吃。不同的人,同一個(gè)技能名字,但實(shí)現(xiàn)方式完全不同。
怎么樣,是不是一下子就明白了?
二、函數(shù)重載:同名不同參
1. 啥是函數(shù)重載?
簡(jiǎn)單說,就是同一個(gè)類里面,方法名字一樣,但參數(shù)不一樣。編譯器會(huì)根據(jù)你傳的參數(shù)來決定調(diào)用哪個(gè)方法。
就像你去餐廳點(diǎn)菜:
- "來份炒飯!"(無參數(shù))
- "來份炒飯,要辣的!"(一個(gè)參數(shù))
- "來份炒飯,要辣的,多放肉!"(兩個(gè)參數(shù))
服務(wù)員會(huì)根據(jù)你的要求做出不同的炒飯。
2. 代碼實(shí)戰(zhàn)
#include <iostream>
#include <string>
using namespace std;
class Cook {
public:
// 基礎(chǔ)版炒飯
void makeFriedRice() {
cout << "做了一份普通炒飯" << endl;
}
// 帶辣度的炒飯
void makeFriedRice(string spicy) {
cout << "做了一份" << spicy << "的炒飯" << endl;
}
// 帶辣度和配菜的炒飯
void makeFriedRice(string spicy, string ingredient) {
cout << "做了一份" << spicy << "的炒飯,加了" << ingredient << endl;
}
// 連數(shù)量都能指定
void makeFriedRice(int count, string spicy) {
cout << "做了" << count << "份" << spicy << "的炒飯" << endl;
}
// 還可以重載構(gòu)造函數(shù)
Cook() {
cout << "廚師準(zhǔn)備就緒!" << endl;
}
Cook(string name) {
cout << "廚師" << name << "準(zhǔn)備就緒!" << endl;
}
};
// 測(cè)試一下
int main() {
Cook chef("老王");
chef.makeFriedRice(); // 輸出:做了一份普通炒飯
chef.makeFriedRice("微辣"); // 輸出:做了一份微辣的炒飯
chef.makeFriedRice("中辣", "牛肉"); // 輸出:做了一份中辣的炒飯,加了牛肉
chef.makeFriedRice(3, "變態(tài)辣"); // 輸出:做了3份變態(tài)辣的炒飯
return0;
}
看到了嗎?同樣是makeFriedRice這個(gè)方法名,但根據(jù)你傳的參數(shù)不同,執(zhí)行的邏輯也不同。編譯器很聰明,它會(huì)自動(dòng)幫你選擇合適的方法。
3. 重載的規(guī)則(劃重點(diǎn)!)
- 方法名必須相同 - 這是基本要求
- 參數(shù)列表必須不同 - 要么數(shù)量不同,要么類型不同,要么順序不同
- 返回值類型可以相同也可以不同 - 但不能僅僅通過返回值類型來區(qū)分重載
- 在同一個(gè)作用域內(nèi)(同一個(gè)類)
記住:編譯器是通過參數(shù)來區(qū)分調(diào)用哪個(gè)函數(shù)的,跟返回值沒關(guān)系!
三、函數(shù)重寫:子承父業(yè),青出于藍(lán)
1. 啥是函數(shù)重寫?
函數(shù)重寫發(fā)生在繼承關(guān)系中。子類重新實(shí)現(xiàn)父類的方法,方法名、參數(shù)都一樣,但實(shí)現(xiàn)邏輯不同。
就像爸爸教你騎自行車的方法是"勇敢地騎上去",但你教你兒子的方法可能是"先學(xué)會(huì)平衡,再慢慢來"。同樣是"學(xué)騎車"這個(gè)方法,但實(shí)現(xiàn)方式完全不同。
2. 代碼實(shí)戰(zhàn)
#include <iostream>
#include <string>
using namespace std;
// 父類 - 老爸的教學(xué)方式
class OldTeacher {
public:
virtual void teachBikeRiding() { // virtual關(guān)鍵字是重點(diǎn)!
cout << "老爸的方法:別怕,直接騎上去,摔幾次就會(huì)了!" << endl;
}
virtual void teachSwimming() {
cout << "老爸的方法:扔到水里,不會(huì)游多喝水,自然就學(xué)會(huì)了!" << endl;
}
// 虛析構(gòu)函數(shù),養(yǎng)成好習(xí)慣
virtual ~OldTeacher() {
cout << "老爸累了,休息去了" << endl;
}
};
// 子類 - 新一代的教學(xué)方式
class ModernTeacher :public OldTeacher {
public:
void teachBikeRiding() override { // override關(guān)鍵字確保我們真的在重寫
cout << "現(xiàn)代方法:先練平衡,戴好護(hù)具,循序漸進(jìn),安全第一!" << endl;
}
void teachSwimming() override {
cout << "現(xiàn)代方法:從淺水區(qū)開始,學(xué)會(huì)漂浮,再學(xué)動(dòng)作,科學(xué)訓(xùn)練!" << endl;
}
// 子類還可以有自己獨(dú)有的方法
void teachOnline() {
cout << "現(xiàn)代獨(dú)有:在線視頻教學(xué)輔助指導(dǎo)" << endl;
}
~ModernTeacher() {
cout << "現(xiàn)代老師下班了" << endl;
}
};
// 測(cè)試一下
int main() {
OldTeacher dad;
ModernTeacher son;
cout << "爸爸的教學(xué)方法:" << endl;
dad.teachBikeRiding(); // 輸出:老爸的方法:別怕,直接騎上去,摔幾次就會(huì)了!
dad.teachSwimming(); // 輸出:老爸的方法:扔到水里,不會(huì)游多喝水,自然就學(xué)會(huì)了!
cout << "\n兒子的教學(xué)方法:" << endl;
son.teachBikeRiding(); // 輸出:現(xiàn)代方法:先練平衡,戴好護(hù)具,循序漸進(jìn),安全第一!
son.teachSwimming(); // 輸出:現(xiàn)代方法:從淺水區(qū)開始,學(xué)會(huì)漂浮,再學(xué)動(dòng)作,科學(xué)訓(xùn)練!
son.teachOnline(); // 輸出:現(xiàn)代獨(dú)有:在線視頻教學(xué)輔助指導(dǎo)
// 多態(tài)的魅力 - C++的精髓所在!
cout << "\n多態(tài)演示:" << endl;
OldTeacher* teacher = new ModernTeacher(); // 父類指針指向子類對(duì)象
teacher->teachBikeRiding(); // 輸出:現(xiàn)代方法:先練平衡,戴好護(hù)具,循序漸進(jìn),安全第一!
delete teacher; // 記得釋放內(nèi)存
return 0;
}
最后那個(gè)多態(tài)的例子特別有意思!雖然teacher的類型是OldTeacher,但它實(shí)際指向的是ModernTeacher對(duì)象,所以調(diào)用的是子類重寫后的方法。這就是面向?qū)ο缶幊痰镊攘λ冢?/p>
3. 重寫的規(guī)則(又是重點(diǎn)!)
- 必須有繼承關(guān)系 - 沒有父子關(guān)系就不叫重寫
- 父類方法必須是virtual - 這是C++特有的,沒有virtual就不是真正的重寫
- 方法名、參數(shù)列表、返回值類型都必須相同 - 一模一樣
- 子類建議使用 override 關(guān)鍵字 - (C++11推薦,不是必須但建議用)
- 基類析構(gòu)函數(shù)最好聲明為virtual - 避免內(nèi)存泄漏問題
四、來個(gè)終極對(duì)比
特征 | 函數(shù)重載(Overload) | 函數(shù)重寫(Override) |
發(fā)生位置 | 同一個(gè)類內(nèi) | 父子類之間 |
方法名 | 必須相同 | 必須相同 |
參數(shù)列表 | 必須不同 | 必須相同 |
返回值類型 | 可以不同 | 必須相同 |
決定時(shí)機(jī) | 編譯時(shí)決定 | 運(yùn)行時(shí)決定 |
關(guān)鍵詞 | 無特殊關(guān)鍵詞 | virtual + override |
目的 | 提供多種調(diào)用方式 | 改變父類行為 |
C++特色 | 支持操作符重載 | 需要virtual才能多態(tài) |
五、C++獨(dú)有的騷操作
1. 操作符重載
C++最牛逼的地方就是可以重載操作符!比如你可以讓兩個(gè)對(duì)象直接用+號(hào)相加:
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
// 重載+操作符
Point operator+(const Point& other) {
return Point(x + other.x, y + other.y);
}
// 重載<<操作符,方便輸出
friend ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}
};
int main() {
Point p1(1, 2);
Point p2(3, 4);
Point p3 = p1 + p2; // 直接用+號(hào)!
cout << p1 << " + " << p2 << " = " << p3 << endl;
// 輸出:(1, 2) + (3, 4) = (4, 6)
return 0;
}
2. 函數(shù)模板重載
C++還支持模板函數(shù)的重載:
#include <iostream>
using namespace std;
// 普通函數(shù)
int add(int a, int b) {
cout << "調(diào)用了int版本的add" << endl;
return a + b;
}
// 模板函數(shù)
template<typename T>
T add(T a, T b) {
cout << "調(diào)用了模板版本的add" << endl;
return a + b;
}
int main() {
cout << add(1, 2) << endl; // 調(diào)用普通函數(shù)
cout << add(1.5, 2.5) << endl; // 調(diào)用模板函數(shù)
cout << add<int>(1, 2) << endl; // 強(qiáng)制調(diào)用模板函數(shù)
return 0;
}
3. 重載的應(yīng)用
想想你平時(shí)用的cout:
#include <iostream>
using namespace std;
int main() {
cout << "字符串" << endl; // 輸出字符串
cout << 123 << endl; // 輸出整數(shù)
cout << 3.14 << endl; // 輸出浮點(diǎn)數(shù)
cout << true << endl; // 輸出布爾值
return 0;
}
這就是重載!同一個(gè)<<操作符,但可以接受不同類型的參數(shù)。
還有構(gòu)造函數(shù)重載:
class Student {
private:
string name;
int age;
public:
// 默認(rèn)構(gòu)造函數(shù)
Student() : name("未知"), age(0) {
cout << "創(chuàng)建了一個(gè)未知學(xué)生" << endl;
}
// 只有姓名的構(gòu)造函數(shù)
Student(string n) : name(n), age(0) {
cout << "創(chuàng)建了學(xué)生:" << name << endl;
}
// 完整信息的構(gòu)造函數(shù)
Student(string n, int a) : name(n), age(a) {
cout << "創(chuàng)建了學(xué)生:" << name << ",年齡:" << age << endl;
}
};
int main() {
Student s1; // 調(diào)用默認(rèn)構(gòu)造函數(shù)
Student s2("小明"); // 調(diào)用單參數(shù)構(gòu)造函數(shù)
Student s3("小紅", 18); // 調(diào)用雙參數(shù)構(gòu)造函數(shù)
return 0;
}
4. 重寫的應(yīng)用
比如做一個(gè)圖形繪制程序:
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() = 0; // 純虛函數(shù),子類必須實(shí)現(xiàn)
virtual double getArea() = 0;
virtual ~Shape() {} // 虛析構(gòu)函數(shù)
};
class Circle :public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override {
cout << "畫一個(gè)圓形 ?,半徑:" << radius << endl;
}
double getArea() override {
return3.14159 * radius * radius;
}
};
class Rectangle :public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
void draw() override {
cout << "畫一個(gè)矩形 ?,寬:" << width << ",高:" << height << endl;
}
double getArea() override {
return width * height;
}
};
class Triangle :public Shape {
private:
double base, height;
public:
Triangle(double b, double h) : base(b), height(h) {}
void draw() override {
cout << "畫一個(gè)三角形 ??,底:" << base << ",高:" << height << endl;
}
double getArea() override {
return 0.5 * base * height;
}
};
int main() {
Shape* shapes[] = {
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 8)
};
for (int i = 0; i < 3; i++) {
shapes[i]->draw();
cout << "面積:" << shapes[i]->getArea() << endl << endl;
delete shapes[i]; // 釋放內(nèi)存
}
return 0;
}
每個(gè)子類都重寫了draw和getArea方法,實(shí)現(xiàn)自己特有的繪制邏輯。
六、面試官最愛問的陷阱題
陷阱一:函數(shù)隱藏(最坑的那種!)
class Parent {
public:
void show() {
cout << "Parent的無參show" << endl;
}
void show(int x) {
cout << "Parent的帶參show: " << x << endl;
}
};
class Child :public Parent {
public:
void show() { // 注意:這里沒有virtual!
cout << "Child的show" << endl;
}
};
int main() {
Child c;
c.show(); // 正常調(diào)用Child的show
c.show(100); // 編譯錯(cuò)誤!為什么?
return 0;
}
答案:這既不是重載也不是重寫,而是函數(shù)隱藏!
Child類的show()把Parent類的所有show方法都隱藏了!即使Parent有show(int)版本,Child對(duì)象也看不見。
要解決這個(gè)問題,需要用using關(guān)鍵字:
class Child : public Parent {
public:
using Parent::show; // 把父類的show方法都"拉"過來
void show() {
cout << "Child的show" << endl;
}
};
陷阱二:非虛函數(shù)的偽重寫
class Base {
public:
void func() { // 注意:沒有virtual
cout << "Base::func" << endl;
}
};
class Derived :public Base {
public:
void func() { // 看起來像重寫,其實(shí)不是!
cout << "Derived::func" << endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->func(); // 輸出什么?
delete ptr;
return 0;
}
答案:輸出"Base::func"!
因?yàn)锽ase的func不是虛函數(shù),所以這不是重寫,只是函數(shù)隱藏。通過父類指針調(diào)用時(shí),永遠(yuǎn)調(diào)用的是父類版本。
陷阱三:const重載的陷阱
class Test {
public:
void print() {
cout << "非const版本" << endl;
}
void print() const { // 這是重載!
cout << "const版本" << endl;
}
};
int main() {
Test t1;
const Test t2;
t1.print(); // 調(diào)用哪個(gè)?
t2.print(); // 調(diào)用哪個(gè)?
return 0;
}
答案:
- t1調(diào)用非const版本
- t2調(diào)用const版本
這是C++特有的const重載,很多人不知道const也能構(gòu)成重載條件!
七、記憶口訣
最后給大家一個(gè)超好記的口訣:
重載看參數(shù),參數(shù)不同才叫重載,重寫看繼承,父子同名才叫重寫(父類要有virtual)
八、總結(jié)
好了,到這里應(yīng)該徹底搞明白了吧?
- 函數(shù)重載:同一個(gè)類里,方法名相同,參數(shù)不同,給用戶提供多種調(diào)用方式
- 函數(shù)重寫:父子類間,父類方法必須是virtual,子類用override重新實(shí)現(xiàn),方法簽名完全相同 ,子類改變父類的實(shí)現(xiàn)。
下次面試官再問這個(gè)問題,你就可以自信地回答了。不僅要說出區(qū)別,最好還能舉個(gè)生動(dòng)的例子,保證讓面試官印象深刻!
記住,編程不是死記硬背,而是要理解其中的道理。這兩個(gè)概念理解了,面向?qū)ο缶幊痰拇箝T就向你敞開了一半!