騰訊一面面經(jīng):C++多態(tài)解決了什么問題?
在C++的編程世界里,多態(tài)性堪稱一項(xiàng)極為關(guān)鍵的特性,它在諸多實(shí)際場景中發(fā)揮著重要作用,也常常成為面試中的高頻考點(diǎn)。在騰訊一面的考場上,就聚焦于 C++ 多態(tài)如何解決編程中棘手的問題這一關(guān)鍵話題。
對于許多開發(fā)者而言,隨著項(xiàng)目規(guī)模不斷擴(kuò)張,代碼逐漸變得復(fù)雜,緊耦合問題隨之而來。所謂緊耦合,就好比一個齒輪組,各個齒輪緊密咬合,一個齒輪稍有變動,整個齒輪組都會受到影響。在代碼中,當(dāng)不同模塊或類之間相互依賴程度過高時,牽一發(fā)而動全身的情況屢見不鮮。比如,一個圖形繪制系統(tǒng)中,繪制不同圖形的類與主程序緊密相連,若要新增一種圖形,或者修改某一圖形的繪制邏輯,往往需要在多個相關(guān)類和函數(shù)中進(jìn)行修改,不僅工作量大,還容易引發(fā)新的錯誤,維護(hù)成本直線上升。
而 C++ 多態(tài)恰如一把 “利刃”,為斬?cái)嗑o耦合這團(tuán)亂麻提供了有效手段。它允許不同的對象對同一消息做出不同響應(yīng),通過虛函數(shù)和繼承機(jī)制,實(shí)現(xiàn)了接口的統(tǒng)一與行為的多樣。那么,多態(tài)具體是怎樣施展 “魔法”,在騰訊一面中又是如何被深入探討的呢?讓我們一同揭開其中的奧秘 。
一、多態(tài)初相識
在日常生活中,我們常常會遇到這樣一種現(xiàn)象:同樣的行為,在不同的對象上卻有著不同的表現(xiàn)。就好比 “開車” 這個行為,當(dāng)是賽車手駕駛賽車時,那速度與激情令人熱血沸騰;而當(dāng)是新手司機(jī)駕駛家用車時,可能就多了幾分謹(jǐn)慎與小心。在 C++ 編程的世界里,也有一個與之類似的概念,那就是多態(tài)。
從定義上來說,多態(tài)是指同一個行為具有多個不同表現(xiàn)形式或形態(tài)的能力 。在 C++ 中,多態(tài)主要是通過虛函數(shù)來實(shí)現(xiàn)的。簡單來說,當(dāng)一個基類的指針或引用指向不同的派生類對象時,調(diào)用同一個虛函數(shù),會呈現(xiàn)出不同的行為,這便是多態(tài)的魅力所在。比如動物類有一個 “叫” 的函數(shù),狗類和貓類繼承自動物類,并重寫了 “叫” 的函數(shù),當(dāng)用動物類的指針分別指向狗類和貓類的對象時,調(diào)用 “叫” 函數(shù),就會分別聽到狗叫和貓叫。
在 C++ 中,多態(tài)又可以細(xì)分為靜態(tài)多態(tài)和動態(tài)多態(tài)。靜態(tài)多態(tài)主要是通過函數(shù)重載和模板來實(shí)現(xiàn),它是在編譯期就確定了調(diào)用的函數(shù)版本;而動態(tài)多態(tài)則是基于虛函數(shù),在運(yùn)行時才根據(jù)對象的實(shí)際類型來決定調(diào)用哪個函數(shù),這也是我們后續(xù)重點(diǎn)探討的內(nèi)容。
二、多態(tài)如何解決代碼復(fù)用難題
在軟件開發(fā)中,代碼復(fù)用是提高開發(fā)效率、降低維護(hù)成本的關(guān)鍵。然而,在沒有多態(tài)的情況下,實(shí)現(xiàn)代碼復(fù)用往往面臨諸多挑戰(zhàn)。比如,我們要開發(fā)一個圖形繪制系統(tǒng),其中包含圓形、矩形和三角形等多種圖形。如果不使用多態(tài),那么為了繪制這些不同的圖形,我們可能需要編寫大量重復(fù)的代碼。
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;
}在這段代碼中,每個圖形類都有自己獨(dú)立的繪制函數(shù),當(dāng)我們需要繪制不同的圖形時,需要分別調(diào)用不同的函數(shù)。如果后續(xù)要添加新的圖形,比如梯形,就需要再次編寫新的繪制函數(shù),并且在使用時也需要額外添加調(diào)用邏輯,代碼的擴(kuò)展性和復(fù)用性都很差 。
而當(dāng)我們引入多態(tài)后,情況就大不相同了。我們可以定義一個基類,比如Shape,在其中聲明一個虛函數(shù)draw,然后讓各個圖形類繼承自Shape類,并重寫draw函數(shù)。
class Shape {
public:
virtual void draw() = 0; // 純虛函數(shù),使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;
}在這個改進(jìn)后的代碼中,drawShapes函數(shù)可以接受一個Shape類型的指針數(shù)組,無論數(shù)組中的元素是指向Circle、Rectangle還是Triangle對象,都可以通過調(diào)用draw函數(shù)來實(shí)現(xiàn)正確的繪制。這樣,當(dāng)我們需要添加新的圖形時,只需要創(chuàng)建一個新的派生類并重寫draw函數(shù),而drawShapes函數(shù)的代碼無需修改,大大提高了代碼的復(fù)用性和可擴(kuò)展性。
三、多態(tài)讓代碼擴(kuò)展更輕松
在軟件開發(fā)的過程中,我們常常面臨需求不斷變化和功能持續(xù)擴(kuò)展的挑戰(zhàn)。一個好的程序設(shè)計(jì)應(yīng)該能夠輕松應(yīng)對這些變化,而多態(tài)在其中扮演著至關(guān)重要的角色,它讓代碼的擴(kuò)展變得更加輕松。
以游戲開發(fā)為例,假設(shè)我們正在開發(fā)一款角色扮演游戲,游戲中有不同類型的角色,如戰(zhàn)士、法師和刺客 。每個角色都有自己獨(dú)特的攻擊方式和移動方式。
如果不使用多態(tài),我們可能會為每個角色編寫?yīng)毩⒌念?,每個類中包含各自的攻擊和移動方法。當(dāng)需要添加新的角色類型,比如牧師時,我們就需要在多個地方修改代碼。不僅要創(chuàng)建新的牧師類并編寫其獨(dú)特的技能方法,還可能需要在處理角色行為的邏輯中添加大量的條件判斷語句來處理牧師的行為。例如:
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() {
// 假設(shè)這里有一個變量表示角色類型
int characterType = 1; // 1代表戰(zhàn)士,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();
}
}可以看到,這種方式的代碼不僅冗長,而且維護(hù)起來非常困難。每添加一種新的角色類型,都需要在handleCharacterAction函數(shù)中添加大量的if - else判斷,這使得代碼的可讀性和可維護(hù)性都很差。
而利用多態(tài)的特性,我們可以定義一個基類Character,在其中聲明虛函數(shù)attack和move,然后讓戰(zhàn)士、法師和刺客等角色類繼承自Character類,并根據(jù)自身特點(diǎn)重寫這些虛函數(shù)。這樣,當(dāng)我們需要添加新的角色類型時,只需要創(chuàng)建一個新的派生類,重寫相應(yīng)的虛函數(shù),而不需要修改現(xiàn)有的核心代碼。例如:
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;
}在這個改進(jìn)后的代碼中,handleCharacterAction函數(shù)只需要接受一個Character類型的指針,無論傳入的是戰(zhàn)士、法師還是刺客的對象指針,都能正確地調(diào)用相應(yīng)的攻擊和移動方法。當(dāng)我們要添加牧師角色時,只需要創(chuàng)建一個Priest類繼承自Character類,并重寫attack和move方法,然后就可以直接在main函數(shù)中使用handleCharacterAction函數(shù)來處理牧師角色的行為,而無需修改handleCharacterAction函數(shù)的代碼。
四、多態(tài)對代碼維護(hù)的積極影響
除了在代碼復(fù)用和擴(kuò)展方面的顯著優(yōu)勢外,多態(tài)在代碼維護(hù)方面也有著不可忽視的積極影響。它能夠?qū)?fù)雜的條件邏輯轉(zhuǎn)化為更加清晰和簡潔的多態(tài)調(diào)用,從而極大地簡化代碼結(jié)構(gòu),提高代碼的可讀性和可維護(hù)性。
為了更直觀地理解這一點(diǎn),我們還是以游戲開發(fā)為例。在一個角色扮演游戲中,玩家可能會遇到各種各樣的怪物,每個怪物都有自己獨(dú)特的行為邏輯。比如,史萊姆可能會進(jìn)行簡單的跳躍攻擊,而狼人則會進(jìn)行撲咬攻擊,并且在受到攻擊時,不同的怪物也會有不同的反應(yīng)。
在沒有使用多態(tài)的情況下,我們可能會通過大量的條件判斷語句來處理不同怪物的行為。假設(shè)我們有一個函數(shù)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();
// 假設(shè)這里受到攻擊
slime.slimeReactToAttack();
}
else if (monsterType == 2) {
wolf.biteAttack();
// 假設(shè)這里受到攻擊
wolf.wolfReactToAttack();
}
}在這段代碼中,handleMonsterAction函數(shù)通過if - else語句來判斷怪物類型,并調(diào)用相應(yīng)的行為函數(shù)。隨著怪物種類的增加,這個函數(shù)會變得越來越龐大和復(fù)雜,充滿了各種重復(fù)的條件判斷邏輯。這不僅使得代碼的可讀性變差,而且在維護(hù)時,一旦需要修改某個怪物的行為或者添加新的怪物類型,都需要在這個函數(shù)中進(jìn)行大量的修改,很容易引入錯誤 。
而當(dāng)我們引入多態(tài)后,代碼就會變得簡潔明了。我們可以定義一個基類Monster,在其中聲明虛函數(shù)attack和reactToAttack,然后讓史萊姆類和狼人等怪物類繼承自Monster類,并根據(jù)自身特點(diǎn)重寫這些虛函數(shù)。
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();
// 假設(shè)這里受到攻擊
monster->reactToAttack();
}在這個改進(jìn)后的代碼中,handleMonsterAction函數(shù)只需要接受一個Monster類型的指針,無論傳入的是史萊姆還是狼人的對象指針,都可以通過調(diào)用attack和reactToAttack虛函數(shù)來實(shí)現(xiàn)正確的行為。這樣,代碼的結(jié)構(gòu)更加清晰,可讀性大大提高。當(dāng)需要添加新的怪物類型時,只需要創(chuàng)建一個新的派生類,重寫相應(yīng)的虛函數(shù),而handleMonsterAction函數(shù)的代碼無需修改,極大地降低了代碼維護(hù)的難度。
五、多態(tài)在設(shè)計(jì)模式中的應(yīng)用
多態(tài)作為面向?qū)ο缶幊痰暮诵奶匦灾?,在各種設(shè)計(jì)模式中發(fā)揮著舉足輕重的作用。它為設(shè)計(jì)模式提供了更加靈活和強(qiáng)大的解決方案,使得軟件系統(tǒng)的結(jié)構(gòu)更加清晰、可維護(hù)性更強(qiáng)。下面我們就來探討一下多態(tài)在策略模式和工廠方法模式中的具體應(yīng)用 。
(1)策略模式中的多態(tài)應(yīng)用
策略模式是一種行為型設(shè)計(jì)模式,它定義了一系列算法,并將每個算法封裝起來,使它們可以相互替換。策略模式的核心在于將算法的選擇和使用與算法的具體實(shí)現(xiàn)分離開來,而多態(tài)正是實(shí)現(xiàn)這一分離的關(guān)鍵。
以一個簡單的計(jì)算器程序?yàn)槔?,我們可以使用策略模式和多態(tài)來實(shí)現(xiàn)不同的運(yùn)算邏輯。首先,定義一個抽象的運(yùn)算策略接口,其中包含一個用于執(zhí)行運(yùn)算的方法:
class OperationStrategy {
public:
virtual double execute(double num1, double num2) = 0;
};然后,分別創(chuàng)建具體的運(yùn)算策略類,如加法策略類、減法策略類、乘法策略類和除法策略類,它們都繼承自O(shè)perationStrategy接口,并實(shí)現(xiàn)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;
}
};接下來,定義一個計(jì)算器類,它持有一個OperationStrategy指針,并通過該指針調(diào)用具體的運(yùn)算策略:
class Calculator {
private:
OperationStrategy* strategy;
public:
Calculator(OperationStrategy* s) : strategy(s) {}
double calculate(double num1, double num2) {
return strategy->execute(num1, num2);
}
};在客戶端代碼中,我們可以根據(jù)需要選擇不同的運(yùn)算策略,并將其傳遞給計(jì)算器對象,從而實(shí)現(xiàn)不同的運(yùn)算:
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;
// 釋放內(nèi)存
delete addStrategy;
delete subtractStrategy;
delete multiplyStrategy;
delete divideStrategy;
return 0;
}在這個例子中,多態(tài)使得我們可以在運(yùn)行時動態(tài)地選擇不同的運(yùn)算策略,而無需修改計(jì)算器類的代碼。如果后續(xù)需要添加新的運(yùn)算,比如求冪運(yùn)算,只需要創(chuàng)建一個新的策略類并實(shí)現(xiàn)execute方法,然后在客戶端代碼中使用新的策略類即可,極大地提高了系統(tǒng)的靈活性和可擴(kuò)展性 。
(2)工廠方法模式中的多態(tài)應(yīng)用
工廠方法模式是一種創(chuàng)建型設(shè)計(jì)模式,它定義了一個用于創(chuàng)建對象的接口,但由子類決定實(shí)例化哪個類。工廠方法模式將對象的創(chuàng)建和使用分離,使得代碼更加靈活和可維護(hù),而多態(tài)在其中起到了至關(guān)重要的作用。
假設(shè)我們正在開發(fā)一個游戲,游戲中有不同類型的角色,如戰(zhàn)士、法師和刺客。我們可以使用工廠方法模式和多態(tài)來創(chuàng)建這些角色。首先,定義一個抽象的角色類,作為所有具體角色類的基類:
class Character {
public:
virtual void display() = 0;
};然后,分別創(chuàng)建具體的角色類,如戰(zhàn)士類、法師類和刺客類,它們都繼承自Character類,并實(shí)現(xiàn)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;
}
};接下來,定義一個抽象的角色工廠類,其中包含一個純虛的工廠方法,用于創(chuàng)建角色對象:
class CharacterFactory {
public:
virtual Character* createCharacter() = 0;
};然后,創(chuàng)建具體的角色工廠類,如戰(zhàn)士工廠類、法師工廠類和刺客工廠類,它們都繼承自CharacterFactory類,并實(shí)現(xiàn)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();
}
};在客戶端代碼中,我們可以通過具體的角色工廠類來創(chuàng)建不同類型的角色對象:
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();
// 釋放內(nèi)存
delete warrior;
delete mage;
delete assassin;
delete warriorFactory;
delete mageFactory;
delete assassinFactory;
return 0;
}在這個例子中,多態(tài)使得我們可以通過抽象的CharacterFactory類來創(chuàng)建不同類型的角色對象,而無需在客戶端代碼中直接實(shí)例化具體的角色類。當(dāng)需要添加新的角色類型時,只需要創(chuàng)建一個新的具體角色類和對應(yīng)的角色工廠類,而客戶端代碼幾乎不需要修改,提高了代碼的可維護(hù)性和可擴(kuò)展性。




























