騰訊一面面經:C++多態解決了什么問題?
在C++的編程世界里,多態性堪稱一項極為關鍵的特性,它在諸多實際場景中發揮著重要作用,也常常成為面試中的高頻考點。在騰訊一面的考場上,就聚焦于 C++ 多態如何解決編程中棘手的問題這一關鍵話題。
對于許多開發者而言,隨著項目規模不斷擴張,代碼逐漸變得復雜,緊耦合問題隨之而來。所謂緊耦合,就好比一個齒輪組,各個齒輪緊密咬合,一個齒輪稍有變動,整個齒輪組都會受到影響。在代碼中,當不同模塊或類之間相互依賴程度過高時,牽一發而動全身的情況屢見不鮮。比如,一個圖形繪制系統中,繪制不同圖形的類與主程序緊密相連,若要新增一種圖形,或者修改某一圖形的繪制邏輯,往往需要在多個相關類和函數中進行修改,不僅工作量大,還容易引發新的錯誤,維護成本直線上升。
而 C++ 多態恰如一把 “利刃”,為斬斷緊耦合這團亂麻提供了有效手段。它允許不同的對象對同一消息做出不同響應,通過虛函數和繼承機制,實現了接口的統一與行為的多樣。那么,多態具體是怎樣施展 “魔法”,在騰訊一面中又是如何被深入探討的呢?讓我們一同揭開其中的奧秘 。
一、多態初相識
在日常生活中,我們常常會遇到這樣一種現象:同樣的行為,在不同的對象上卻有著不同的表現。就好比 “開車” 這個行為,當是賽車手駕駛賽車時,那速度與激情令人熱血沸騰;而當是新手司機駕駛家用車時,可能就多了幾分謹慎與小心。在 C++ 編程的世界里,也有一個與之類似的概念,那就是多態。
從定義上來說,多態是指同一個行為具有多個不同表現形式或形態的能力 。在 C++ 中,多態主要是通過虛函數來實現的。簡單來說,當一個基類的指針或引用指向不同的派生類對象時,調用同一個虛函數,會呈現出不同的行為,這便是多態的魅力所在。比如動物類有一個 “叫” 的函數,狗類和貓類繼承自動物類,并重寫了 “叫” 的函數,當用動物類的指針分別指向狗類和貓類的對象時,調用 “叫” 函數,就會分別聽到狗叫和貓叫。
在 C++ 中,多態又可以細分為靜態多態和動態多態。靜態多態主要是通過函數重載和模板來實現,它是在編譯期就確定了調用的函數版本;而動態多態則是基于虛函數,在運行時才根據對象的實際類型來決定調用哪個函數,這也是我們后續重點探討的內容。
二、多態如何解決代碼復用難題
在軟件開發中,代碼復用是提高開發效率、降低維護成本的關鍵。然而,在沒有多態的情況下,實現代碼復用往往面臨諸多挑戰。比如,我們要開發一個圖形繪制系統,其中包含圓形、矩形和三角形等多種圖形。如果不使用多態,那么為了繪制這些不同的圖形,我們可能需要編寫大量重復的代碼。
class Circle {
public:
void drawCircle() {
// 繪制圓形的具體代碼
std::cout << "Drawing a circle" << std::endl;
}
};
class Rectangle {
public:
void drawRectangle() {
// 繪制矩形的具體代碼
std::cout << "Drawing a rectangle" << std::endl;
}
};
class Triangle {
public:
void drawTriangle() {
// 繪制三角形的具體代碼
std::cout << "Drawing a triangle" << std::endl;
}
};
int main() {
Circle circle;
Rectangle rectangle;
Triangle triangle;
circle.drawCircle();
rectangle.drawRectangle();
triangle.drawTriangle();
return 0;
}
在這段代碼中,每個圖形類都有自己獨立的繪制函數,當我們需要繪制不同的圖形時,需要分別調用不同的函數。如果后續要添加新的圖形,比如梯形,就需要再次編寫新的繪制函數,并且在使用時也需要額外添加調用邏輯,代碼的擴展性和復用性都很差 。
而當我們引入多態后,情況就大不相同了。我們可以定義一個基類,比如Shape,在其中聲明一個虛函數draw,然后讓各個圖形類繼承自Shape類,并重寫draw函數。
class Shape {
public:
virtual void draw() = 0; // 純虛函數,使Shape成為抽象類
};
class Circle : public Shape {
public:
void draw() override {
// 繪制圓形的具體代碼
std::cout << "Drawing a circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
// 繪制矩形的具體代碼
std::cout << "Drawing a rectangle" << std::endl;
}
};
class Triangle : public Shape {
public:
void draw() override {
// 繪制三角形的具體代碼
std::cout << "Drawing a triangle" << std::endl;
}
};
void drawShapes(Shape* shapes[], int count) {
for (int i = 0; i < count; ++i) {
shapes[i]->draw();
}
}
int main() {
Circle circle;
Rectangle rectangle;
Triangle triangle;
Shape* shapes[] = {&circle, &rectangle, &triangle};
int count = sizeof(shapes) / sizeof(shapes[0]);
drawShapes(shapes, count);
return 0;
}
在這個改進后的代碼中,drawShapes函數可以接受一個Shape類型的指針數組,無論數組中的元素是指向Circle、Rectangle還是Triangle對象,都可以通過調用draw函數來實現正確的繪制。這樣,當我們需要添加新的圖形時,只需要創建一個新的派生類并重寫draw函數,而drawShapes函數的代碼無需修改,大大提高了代碼的復用性和可擴展性。
三、多態讓代碼擴展更輕松
在軟件開發的過程中,我們常常面臨需求不斷變化和功能持續擴展的挑戰。一個好的程序設計應該能夠輕松應對這些變化,而多態在其中扮演著至關重要的角色,它讓代碼的擴展變得更加輕松。
以游戲開發為例,假設我們正在開發一款角色扮演游戲,游戲中有不同類型的角色,如戰士、法師和刺客 。每個角色都有自己獨特的攻擊方式和移動方式。
如果不使用多態,我們可能會為每個角色編寫獨立的類,每個類中包含各自的攻擊和移動方法。當需要添加新的角色類型,比如牧師時,我們就需要在多個地方修改代碼。不僅要創建新的牧師類并編寫其獨特的技能方法,還可能需要在處理角色行為的邏輯中添加大量的條件判斷語句來處理牧師的行為。例如:
class Warrior {
public:
void attackWarrior() {
std::cout << "Warrior attacks with a sword" << std::endl;
}
void moveWarrior() {
std::cout << "Warrior moves quickly" << std::endl;
}
};
class Mage {
public:
void attackMage() {
std::cout << "Mage casts a spell" << std::endl;
}
void moveMage() {
std::cout << "Mage moves slowly" << std::endl;
}
};
class Assassin {
public:
void attackAssassin() {
std::cout << "Assassin attacks with a dagger" << std::endl;
}
void moveAssassin() {
std::cout << "Assassin moves stealthily" << std::endl;
}
};
void handleCharacterAction() {
// 假設這里有一個變量表示角色類型
int characterType = 1; // 1代表戰士,2代表法師,3代表刺客
Warrior warrior;
Mage mage;
Assassin assassin;
if (characterType == 1) {
warrior.attackWarrior();
warrior.moveWarrior();
}
else if (characterType == 2) {
mage.attackMage();
mage.moveMage();
}
else if (characterType == 3) {
assassin.attackAssassin();
assassin.moveAssassin();
}
}
可以看到,這種方式的代碼不僅冗長,而且維護起來非常困難。每添加一種新的角色類型,都需要在handleCharacterAction函數中添加大量的if - else判斷,這使得代碼的可讀性和可維護性都很差。
而利用多態的特性,我們可以定義一個基類Character,在其中聲明虛函數attack和move,然后讓戰士、法師和刺客等角色類繼承自Character類,并根據自身特點重寫這些虛函數。這樣,當我們需要添加新的角色類型時,只需要創建一個新的派生類,重寫相應的虛函數,而不需要修改現有的核心代碼。例如:
class Character {
public:
virtual void attack() = 0;
virtual void move() = 0;
};
class Warrior : public Character {
public:
void attack() override {
std::cout << "Warrior attacks with a sword" << std::endl;
}
void move() override {
std::cout << "Warrior moves quickly" << std::endl;
}
};
class Mage : public Character {
public:
void attack() override {
std::cout << "Mage casts a spell" << std::endl;
}
void move() override {
std::cout << "Mage moves slowly" << std::endl;
}
};
class Assassin : public Character {
public:
void attack() override {
std::cout << "Assassin attacks with a dagger" << std::endl;
}
void move() override {
std::cout << "Assassin moves stealthily" << std::endl;
}
};
void handleCharacterAction(Character* character) {
character->attack();
character->move();
}
int main() {
Warrior warrior;
Mage mage;
Assassin assassin;
handleCharacterAction(&warrior);
handleCharacterAction(&mage);
handleCharacterAction(&assassin);
return 0;
}
在這個改進后的代碼中,handleCharacterAction函數只需要接受一個Character類型的指針,無論傳入的是戰士、法師還是刺客的對象指針,都能正確地調用相應的攻擊和移動方法。當我們要添加牧師角色時,只需要創建一個Priest類繼承自Character類,并重寫attack和move方法,然后就可以直接在main函數中使用handleCharacterAction函數來處理牧師角色的行為,而無需修改handleCharacterAction函數的代碼。
四、多態對代碼維護的積極影響
除了在代碼復用和擴展方面的顯著優勢外,多態在代碼維護方面也有著不可忽視的積極影響。它能夠將復雜的條件邏輯轉化為更加清晰和簡潔的多態調用,從而極大地簡化代碼結構,提高代碼的可讀性和可維護性。
為了更直觀地理解這一點,我們還是以游戲開發為例。在一個角色扮演游戲中,玩家可能會遇到各種各樣的怪物,每個怪物都有自己獨特的行為邏輯。比如,史萊姆可能會進行簡單的跳躍攻擊,而狼人則會進行撲咬攻擊,并且在受到攻擊時,不同的怪物也會有不同的反應。
在沒有使用多態的情況下,我們可能會通過大量的條件判斷語句來處理不同怪物的行為。假設我們有一個函數handleMonsterAction用于處理怪物的行為,代碼可能如下:
class Slime {
public:
void jumpAttack() {
std::cout << "Slime jumps and attacks" << std::endl;
}
void slimeReactToAttack() {
std::cout << "Slime wobbles when attacked" << std::endl;
}
};
class Wolf {
public:
void biteAttack() {
std::cout << "Wolf bites and attacks" << std::endl;
}
void wolfReactToAttack() {
std::cout << "Wolf growls when attacked" << std::endl;
}
};
void handleMonsterAction(int monsterType) {
Slime slime;
Wolf wolf;
if (monsterType == 1) {
slime.jumpAttack();
// 假設這里受到攻擊
slime.slimeReactToAttack();
}
else if (monsterType == 2) {
wolf.biteAttack();
// 假設這里受到攻擊
wolf.wolfReactToAttack();
}
}
在這段代碼中,handleMonsterAction函數通過if - else語句來判斷怪物類型,并調用相應的行為函數。隨著怪物種類的增加,這個函數會變得越來越龐大和復雜,充滿了各種重復的條件判斷邏輯。這不僅使得代碼的可讀性變差,而且在維護時,一旦需要修改某個怪物的行為或者添加新的怪物類型,都需要在這個函數中進行大量的修改,很容易引入錯誤 。
而當我們引入多態后,代碼就會變得簡潔明了。我們可以定義一個基類Monster,在其中聲明虛函數attack和reactToAttack,然后讓史萊姆類和狼人等怪物類繼承自Monster類,并根據自身特點重寫這些虛函數。
class Monster {
public:
virtual void attack() = 0;
virtual void reactToAttack() = 0;
};
class Slime : public Monster {
public:
void attack() override {
std::cout << "Slime jumps and attacks" << std::endl;
}
void reactToAttack() override {
std::cout << "Slime wobbles when attacked" << std::endl;
}
};
class Wolf : public Monster {
public:
void attack() override {
std::cout << "Wolf bites and attacks" << std::endl;
}
void reactToAttack() override {
std::cout << "Wolf growls when attacked" << std::endl;
}
};
void handleMonsterAction(Monster* monster) {
monster->attack();
// 假設這里受到攻擊
monster->reactToAttack();
}
在這個改進后的代碼中,handleMonsterAction函數只需要接受一個Monster類型的指針,無論傳入的是史萊姆還是狼人的對象指針,都可以通過調用attack和reactToAttack虛函數來實現正確的行為。這樣,代碼的結構更加清晰,可讀性大大提高。當需要添加新的怪物類型時,只需要創建一個新的派生類,重寫相應的虛函數,而handleMonsterAction函數的代碼無需修改,極大地降低了代碼維護的難度。
五、多態在設計模式中的應用
多態作為面向對象編程的核心特性之一,在各種設計模式中發揮著舉足輕重的作用。它為設計模式提供了更加靈活和強大的解決方案,使得軟件系統的結構更加清晰、可維護性更強。下面我們就來探討一下多態在策略模式和工廠方法模式中的具體應用 。
(1)策略模式中的多態應用
策略模式是一種行為型設計模式,它定義了一系列算法,并將每個算法封裝起來,使它們可以相互替換。策略模式的核心在于將算法的選擇和使用與算法的具體實現分離開來,而多態正是實現這一分離的關鍵。
以一個簡單的計算器程序為例,我們可以使用策略模式和多態來實現不同的運算邏輯。首先,定義一個抽象的運算策略接口,其中包含一個用于執行運算的方法:
class OperationStrategy {
public:
virtual double execute(double num1, double num2) = 0;
};
然后,分別創建具體的運算策略類,如加法策略類、減法策略類、乘法策略類和除法策略類,它們都繼承自OperationStrategy接口,并實現execute方法:
class AddStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
return num1 + num2;
}
};
class SubtractStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
return num1 - num2;
}
};
class MultiplyStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
return num1 * num2;
}
};
class DivideStrategy : public OperationStrategy {
public:
double execute(double num1, double num2) override {
if (num2 != 0) {
return num1 / num2;
}
// 這里可以拋出異常或者返回一個特殊值表示錯誤
return 0;
}
};
接下來,定義一個計算器類,它持有一個OperationStrategy指針,并通過該指針調用具體的運算策略:
class Calculator {
private:
OperationStrategy* strategy;
public:
Calculator(OperationStrategy* s) : strategy(s) {}
double calculate(double num1, double num2) {
return strategy->execute(num1, num2);
}
};
在客戶端代碼中,我們可以根據需要選擇不同的運算策略,并將其傳遞給計算器對象,從而實現不同的運算:
int main() {
OperationStrategy* addStrategy = new AddStrategy();
Calculator addCalculator(addStrategy);
double result1 = addCalculator.calculate(5, 3);
std::cout << "5 + 3 = " << result1 << std::endl;
OperationStrategy* subtractStrategy = new SubtractStrategy();
Calculator subtractCalculator(subtractStrategy);
double result2 = subtractCalculator.calculate(5, 3);
std::cout << "5 - 3 = " << result2 << std::endl;
OperationStrategy* multiplyStrategy = new MultiplyStrategy();
Calculator multiplyCalculator(multiplyStrategy);
double result3 = multiplyCalculator.calculate(5, 3);
std::cout << "5 * 3 = " << result3 << std::endl;
OperationStrategy* divideStrategy = new DivideStrategy();
Calculator divideCalculator(divideStrategy);
double result4 = divideCalculator.calculate(5, 3);
std::cout << "5 / 3 = " << result4 << std::endl;
// 釋放內存
delete addStrategy;
delete subtractStrategy;
delete multiplyStrategy;
delete divideStrategy;
return 0;
}
在這個例子中,多態使得我們可以在運行時動態地選擇不同的運算策略,而無需修改計算器類的代碼。如果后續需要添加新的運算,比如求冪運算,只需要創建一個新的策略類并實現execute方法,然后在客戶端代碼中使用新的策略類即可,極大地提高了系統的靈活性和可擴展性 。
(2)工廠方法模式中的多態應用
工廠方法模式是一種創建型設計模式,它定義了一個用于創建對象的接口,但由子類決定實例化哪個類。工廠方法模式將對象的創建和使用分離,使得代碼更加靈活和可維護,而多態在其中起到了至關重要的作用。
假設我們正在開發一個游戲,游戲中有不同類型的角色,如戰士、法師和刺客。我們可以使用工廠方法模式和多態來創建這些角色。首先,定義一個抽象的角色類,作為所有具體角色類的基類:
class Character {
public:
virtual void display() = 0;
};
然后,分別創建具體的角色類,如戰士類、法師類和刺客類,它們都繼承自Character類,并實現display方法:
class Warrior : public Character {
public:
void display() override {
std::cout << "This is a warrior" << std::endl;
}
};
class Mage : public Character {
public:
void display() override {
std::cout << "This is a mage" << std::endl;
}
};
class Assassin : public Character {
public:
void display() override {
std::cout << "This is an assassin" << std::endl;
}
};
接下來,定義一個抽象的角色工廠類,其中包含一個純虛的工廠方法,用于創建角色對象:
class CharacterFactory {
public:
virtual Character* createCharacter() = 0;
};
然后,創建具體的角色工廠類,如戰士工廠類、法師工廠類和刺客工廠類,它們都繼承自CharacterFactory類,并實現createCharacter方法:
class WarriorFactory : public CharacterFactory {
public:
Character* createCharacter() override {
return new Warrior();
}
};
class MageFactory : public CharacterFactory {
public:
Character* createCharacter() override {
return new Mage();
}
};
class AssassinFactory : public CharacterFactory {
public:
Character* createCharacter() override {
return new Assassin();
}
};
在客戶端代碼中,我們可以通過具體的角色工廠類來創建不同類型的角色對象:
int main() {
CharacterFactory* warriorFactory = new WarriorFactory();
Character* warrior = warriorFactory->createCharacter();
warrior->display();
CharacterFactory* mageFactory = new MageFactory();
Character* mage = mageFactory->createCharacter();
mage->display();
CharacterFactory* assassinFactory = new AssassinFactory();
Character* assassin = assassinFactory->createCharacter();
assassin->display();
// 釋放內存
delete warrior;
delete mage;
delete assassin;
delete warriorFactory;
delete mageFactory;
delete assassinFactory;
return 0;
}
在這個例子中,多態使得我們可以通過抽象的CharacterFactory類來創建不同類型的角色對象,而無需在客戶端代碼中直接實例化具體的角色類。當需要添加新的角色類型時,只需要創建一個新的具體角色類和對應的角色工廠類,而客戶端代碼幾乎不需要修改,提高了代碼的可維護性和可擴展性。