C++建造者模式進化論

還在為 C++ 對象那?長得令人發指?的構造函數參數列表抓狂嗎?🤯 是不是經常在?int hp, int mp, int strength, int faith...?這樣的參數“連連看”中迷失自我,一不小心就把法力值傳給了血量,或者力量值填到了信仰欄?😱 代碼調用丑陋不堪,維護起來更是步步驚心!這簡直是每個 C++ 程序員都可能遭遇的?“參數地獄”!🔥

這種痛,你我都懂!😫 但如果告訴你,有一種優雅的設計模式,能徹底終結這場噩夢呢?

??噔噔噔噔!建造者模式 (Builder Pattern) 閃亮登場!??

它就像一位專業的建筑師,能幫你?條理清晰、安全可靠地?構建出最復雜的對象,告別混亂的參數列表,讓你的代碼瞬間清爽健壯!💪 更重要的是,它還是面試官考察你 C++?設計思維?和?現代特性掌握程度?的“照妖鏡”!

想知道這位“建筑師”是如何施展魔法,輕松搞定“參數地獄”的嗎?🤔 想了解它是如何在現代 C++ (從 C++98 的蹣跚學步,到 C++11/17/23 的華麗變身) 中不斷進化,變得越來越強大的嗎?🚀

別急,這趟從“石器時代”到“未來已來”的建造者模式探索之旅,不僅能幫你?徹底擺脫構造函數的痛苦,更能讓你在面試中?自信秀翻全場,為 Offer 加碼!🎁 準備好了嗎?我們出發!👇

一、😭 “參數地獄”與“構造函數噩夢”:不是我慘,是代碼真的慘!😫

面試官:“同學,你知道啥時候需要用建造者模式嗎?” 🤔

如果你只會說“對象屬性多的時候”,那格局就小了!😉 要想拿高分,得先講清楚不用它,到底有多慘!這才能體現出建造者模式的“救世主”光環嘛!?

咱們還是回到那個創建游戲角色的例子。假設我們要創建一個威風凜凜的圣騎士?Paladin。一個合格的圣騎士,得有名字、種族、初始血量、初始藍量、力量、信仰值、初始攜帶的神圣法術書、可能還有一把新手錘……屬性真是不少。

方案一:硬漢構造函數 (The "All-in-One" Tough Guy)

最直觀的想法?把所有參數懟到一個構造函數里!就像這樣:

// ?? 警告:前方高能參數密集區!請佩戴護目鏡!👓
class?Paladin?{
public:Paladin(std::string?name, Race race,?int?hp,?int?mp,?int?strength,?int?faith,std::string?holyBook,?std::string?startingHammer,?bool?hasShield,?int?level) {// ... 一大堆賦值 ...std::cout?<<?"創建圣騎士:"?<< name <<?",種族:"?<< race <<?",血量:"?<< hp <<?"...\n";}// ... 其他 ...
private:std::string?name_;Race race_;int?hp_;int?mp_;int?strength_;int?faith_;std::string?holyBook_;std::string?startingHammer_;bool?hasShield_;int?level_;
};// 召喚圣騎士!但是... 咒語太長記不住啊!🤯
Paladin?uther("烏瑟爾", ? ? ? ? ?// 名字 (string)Race::Human, ? ? ??// 種族 (enum)200, ? ? ? ? ? ? ??// 血量? (int)150, ? ? ? ? ? ? ??// 藍量? (int) 還是反了?😱18, ? ? ? ? ? ? ? ?// 力量? (int)25, ? ? ? ? ? ? ? ?// 信仰? (int) 哪個是哪個??😵?💫"白銀之手圣契", ? ?// 圣書 (string)"新兵的戰錘", ? ? ?// 錘子 (string)true, ? ? ? ? ? ? ?// 有盾牌? (bool)1? ? ? ? ? ? ? ? ??// 等級? (int) 啊啊啊救命!😭
);

看看這有多“坑爹”:

  1. 參數連連看,越看越糊涂:一堆?intstringbool?擠在一起,調用的時候,你得像玩連連看一樣,小心翼翼地把值和參數對應上。血量和藍量傳反了?力量和信仰弄混了?編譯器可不報錯!它覺得類型對就行。結果呢?你可能得到一個藍比血厚、智力型肌肉猛男圣騎士!🤣 這 bug 藏得深,查起來想撞墻!

  2. 可選參數?加量不加價?難!:萬一“初始錘子”不是必需的呢?或者“盾牌”是可選的?難道讓調用者傳個空字符串?""?或者?false?嗎?這既不優雅,也容易讓人困惑:傳?""?是表示“沒有錘子”還是“錘子名字就叫空字符串”?🤔

方案二:構造函數“套娃”大法 (Telescoping Constructors)

有人說:“可選參數?簡單!搞函數重載啊!” 于是,代碼變成了這樣:

class?Paladin?{
public:// 只有必需的Paladin(std::string?name, Race race,?int?hp,?int?mp,?int?strength,?int?faith) {?/*...*/?}// 帶圣書的Paladin(std::string?name, Race race,?int?hp,?int?mp,?int?strength,?int?faith,?std::string?book) {?/*...*/?}// 帶錘子的 (不帶書)Paladin(std::string?name, Race race,?int?hp,?int?mp,?int?strength,?int?faith,?std::string?hammer,?bool?placeholder) {?/*... 為了區分重載加了個沒用的bool? 好蠢...*/?}// 帶書又帶錘子的Paladin(std::string?name, Race race,?int?hp,?int?mp,?int?strength,?int?faith,?std::string?book,?std::string?hammer) {?/*...*/?}// 帶書帶錘子還有盾的...// 帶書帶錘子有盾還有等級的...// ... (此處省略 N 個構造函數) ... 🤯🤯🤯
};

這種“套娃”的壞處顯而易見:

  1. 構造函數數量爆炸:每增加一個可選參數,構造函數的數量就可能翻倍!幾個可選參數下來,構造函數的數量能讓你寫到懷疑人生。維護?簡直是噩夢!😱

  2. 代碼重復或復雜委托:要么每個構造函數里都有一堆重復的賦值代碼,要么你就得搞復雜的構造函數互相調用(委托構造),一不小心就邏輯混亂。

  3. 還是可能搞混:就算有多個構造函數,如果參數類型相似(比如都是?string),你還是可能調錯版本,把錘子當成圣書傳進去!

方案三:“隨心所欲”Setter大法 (JavaBeans Style)

還有一種思路,常見于某些語言(嗯,說的就是你,JavaBeans!):先用一個簡單的(甚至無參)構造函數創建個“半成品”對象,然后像貼便利貼一樣,用一大堆?setXXX?方法往上加屬性:

class?Paladin?{
public:Paladin() {?std::cout?<<?"創建了一個空的圣騎士架子...\n"; }?// 無參構造void?setName(std::string?name)?{ name_ =?std::move(name); }void?setRace(Race race)?{ race_ = race; }void?setHp(int?hp)?{ hp_ = hp; }void?setFaith(int?faith)?{ faith_ = faith; }void?setStartingHammer(std::string?hammer)?{ startingHammer_ =?std::move(hammer); }// ... 一大堆 setters ...bool?isReady()?const?{?// 可能還需要一個方法來檢查是否“組裝”完成return?!name_.empty() && race_ != Race::Unknown && hp_ >?0?&& faith_ >?0;}
private:// ... 成員變量 ...
};// 使用 setter "組裝" 圣騎士
Paladin arthas;?// 創建了一個“空殼”圣騎士,它現在是無效狀態!🚨
arthas.setName("阿爾薩斯");
arthas.setRace(Race::Human);
arthas.setHp(180);
// ... 中間忘了調用 setFaith(...) ... 😱
arthas.setStartingHammer("新手錘");
// ... 繼續調用其他 setters ...if?(arthas.isReady()) {?// 使用前還得檢查一下?好麻煩!std::cout?<<?"圣騎士 "?<< arthas.getName() <<?" 準備就緒!(但可能缺了點啥...)\n";
}?else?{std::cout?<<?"糟糕,圣騎士還沒組裝好!缺胳膊少腿!\n";?// 比如忘了設置信仰值
}

Setter 大法的弊端,直擊要害:

  1. 對象狀態不一致性:在調用完所有必需的?setXXX?方法之前,arthas?對象一直處于一個無效的、不完整的狀態!這就像一輛沒裝輪子、沒裝引擎的汽車,隨時可能出問題。如果你在中間某個環節忘了調用某個重要的 setter(比如?setFaith),這個圣騎士就是個“殘次品”,后續使用可能導致各種奇怪的 bug!💣

  2. 線程安全問題:如果這個對象要在多線程環境中使用,這些 setter 方法會讓對象變得**易變 (Mutable)**,你需要非常小心地處理加鎖,否則多個線程同時“組裝”一個圣騎士,場面會非常混亂。

  3. 繁瑣且易錯:你需要手動調用一長串 setter,很容易遺漏。而且,代碼也顯得冗長。

總結一下“三大慘狀”:

  • 巨型構造函數:難讀、難用、易錯、對可選參數不友好。

  • 構造函數套娃:數量爆炸、維護困難、依然可能傳錯參數。

  • Setter 連環call:對象狀態不一致、線程不安全隱患、代碼繁瑣易遺漏。

看到沒?當對象創建變得復雜時,這些“傳統藝能”都顯得力不從心,甚至會變成 bug 的溫床、加班的元兇!😭 這時候,我們就迫切需要一種更優雅、更安全、更靈活的解決方案。于是,建造者模式,它來了!它帶著光環來了!?

二、🏗? 建造者模式閃亮登場:披薩店的智慧,專治各種不服!🍕

想象一下,你走進一家高檔披薩店(不是路邊攤哦 😏),想定制一個屬于你的完美披薩。服務員(我們的建造者 Builder)絕不會劈頭蓋臉問你十幾個問題然后讓你一次性回答。他會怎么做?

服務員:“您好!今天想來點什么披薩?” (明確目標:產品 Product?-?Pizza) 你:“我要個12寸的。”

服務員:“好的,12寸。您想要什么**面團 (Dough)**?” (調用?setDough("...")?-?清晰指定步驟) 你:“意式薄底。”

服務員:“收到,薄底。**醬料 (Sauce)**呢?” (調用?setSauce("...")?-?又一步) 你:“經典番茄醬。”

服務員:“好的。需要加什么**配料 (Toppings)**嗎?” (調用?addTopping("...")?-?處理可選部分) 你:“嗯...加雙份芝士,再來點意大利辣腸和蘑菇。” (可以多次調用?addTopping)

服務員:“沒問題!雙份芝士、辣腸、蘑菇。” (Builder 內部默默記下你的選擇)

等你確認完所有細節,服務員才轉身去后廚,把所有信息匯總,然后“嘭”地一聲(或者優雅地),一個完整、符合你要求的披薩就出爐了!(調用?build()?-?最終構建)

這個過程,就是建造者模式的核心思想!我們再來看看,它到底牛在哪里,怎么就完美 KO 了前面那“三大慘狀”:

  1. 告別“參數連連看”,擁抱“指名道姓”!(解決:可讀性差、易傳錯)

    • 對比:Paladin("烏瑟爾", Race::Human, 200, 150, 18, 25, ...)?vs?PaladinBuilder("烏瑟爾", Race::Human).setHp(200).setMp(150).setStrength(18).setFaith(25)...

    • 優勢:看到?setHp(200),傻子都知道這是在設置血量!方法名自帶說明書,代碼可讀性瞬間 MAX!再也不用擔心把?hp?和?mp?傳反了。參數是“具名的”,錯誤無處遁形!😎

  2. 可選參數?“想加就加”,輕松自如!(解決:可選參數處理難、構造函數爆炸)

    • 對比:為了“錘子可選”寫一堆重載構造函數 vs?builder.setStartingHammer("雷霆之怒,逐風者的祝福之錘")?(想加就加這句,不想要就不加,builder 依然能工作!)

    • 優勢:可選屬性對應可選的?setXXX?方法。需要哪個就調用哪個,不需要就忽略。代碼簡潔,擴展性強。想給圣騎士加個“光環效果”的新屬性?只需要在?Paladin?類里加個成員,再給?PaladinBuilder?加個?setAuraEffect(...)?方法就行了,完全不影響之前的代碼!維護起來不要太爽!🥳

  3. 核心大招:“一步到位”拿成品,杜絕“半成品裸奔”!(解決:對象狀態不一致 &?揭秘與 Setter 的本質區別)

    • Setter 連環 Call:你面前已經有了一個“圣騎士空殼” (Paladin arthas;),它從誕生那一刻起就暴露在外,但可能缺這少那,是個無效的半成品。你調用的?setXXX?是直接在這個空殼上敲敲打打、增增補補。在你手動完成所有必需步驟前,這個“空殼”隨時可能被別人(或你自己忘了)錯誤地使用,或者就一直處于“殘廢”狀態。你得自己保證最后它是好的,或者每次用前都檢查?isReady()。風險極高!😱

    • 建造者模式:你調用的?builder.setXXX(),修改的不是最終的圣騎士,而是?builder?這個“訂單”本身的狀態!?那個最終的、威風凜凜的圣騎士,在?build()?被調用前,要么根本還沒被創建,要么被?Builder?安全地“藏”在內部,外部根本碰不到!builder?就像那個一絲不茍的服務員,仔細記錄你的每一個要求。

    • 等等!這里是關鍵!?有同學可能會說:“builder.setHp(200).setMp(150)...?和之前那個?arthas.setHp(180); arthas.setMp(150); ...?不都叫?set?嗎?看著好像啊?”

    • 錯!大錯特錯!它們貌似孿生,實則一個是“預購定制”,一個是“地攤自組”!區別在于:

    • 優勢建造者模式把“不一致性”牢牢鎖在了內部!?外部用戶永遠不會接觸到一個“正在組裝中”的、狀態不確定的對象。只有當你最終確認訂單,喊出?build(),并且 Builder 內部的“質檢環節”(比如檢查必需參數是否都已設置)通過,它才會原子性地一次性地創建并交付一個保證完整、狀態有效的最終產品!這就像工廠流水線的最后一道封裝工序,合格才出廠,絕不讓次品流出!這對于保證對象的不變性 (Immutability) 和多線程環境下的安全至關重要!👍

  4. “專業的事交給專業的人”:關注點分離!

    • 優勢Paladin?類只需要關心一個圣騎士應該有什么?(屬性和核心能力)。而?PaladinBuilder?則專注于如何一步步構建一個圣騎士。兩者職責分明,代碼更清晰,修改起來也更方便。改構建邏輯?動?Builder?就行。改圣騎士本身的能力?動?Paladin?類。互不干擾,豈不美哉?😌

簡單來說,建造者模式的優勢就是:

  • 可讀性好:鏈式調用 + 方法名,代碼像說話一樣自然。

  • 靈活性高:輕松應對多參數,尤其是大量可選參數。

  • 控制力強:保證最終創建的對象是完整有效的。

  • 易于維護和擴展:職責分離,修改方便。

所以,當面試官問你為啥要用建造者模式時,你就可以自信地回答:為了避免構造函數參數地獄、構造函數爆炸、以及對象狀態不一致這些天坑,建造者模式提供了一種清晰、靈活、安全的方式來構建復雜對象,讓代碼更健壯、更易讀、更易維護!這不僅僅是技巧,更是良好設計的體現!💯

鋪墊了這么多,是不是已經迫不及待想看看在 C++ 里,這位“披薩師傅”到底長啥樣了?別急,下一節,我們就從“石器時代”的 C++ 實現開始,一步步看它是如何進化的!🛠?

三、🦖 “石器時代”的建造者:裸指針與手動擋的憂傷 (C++98/03)

歡迎來到 C++ 的“遠古時代”!那時候,沒有智能指針幫忙管理內存,也沒有移動語義來提升效率。一切都得靠程序員自己小心翼翼地操作,就像開著一輛需要手動換擋、沒有剎車助力的老爺車。我們來看看用當時的“技術”實現建造者模式是啥樣的:

第一步:定義我們的產品 - 披薩 🍕

// --- 產品:披薩 ---
class?Pizza?{
public:// 公開的設置方法,讓 Builder 可以修改狀態void?setDough(const?std::string& dough)?{ dough_ = dough; }void?setSauce(const?std::string& sauce)?{ sauce_ = sauce; }void?addTopping(const?std::string& topping)?{ toppings_.push_back(topping); }void?describe()?const?{?/* ... (和之前一樣,輸出披薩詳情) ... */?}private:std::string?dough_;std::string?sauce_;std::vector<std::string> toppings_;// 構造函數通常設為私有或保護,防止外面直接亂造// 只有好朋友 PizzaBuilder 才能創建我!🤝friendclass?PizzaBuilder;Pizza() =?default;?// 提供一個默認構造函數供 Builder 調用
};

這里的?Pizza?類本身比較簡單,就是定義了披薩該有的屬性(面團、醬料、配料)和一些操作。關鍵點在于:它的構造函數是?private?的(或者?protected),并且聲明了?PizzaBuilder?是它的?friend(朋友)。這就像是在說:“我自己不隨便出門(不能直接創建),只有我的好朋友‘建造者’才能帶我出來玩(創建我)!” 這樣做是為了強制大家必須通過建造者來創建披薩,保證流程統一。

第二步:創建“披薩師傅” - 建造者登場

#include?<iostream>
#include?<string>
#include?<vector>
#include?<stdexcept> // 為了后面模擬異常// --- 建造者 ---
class?PizzaBuilder?{
private:// 😱 啊哦!一個指向 Pizza 的裸指針!危險的氣息...Pizza* pizza_;public:// 構造函數:準備開工!但這里直接 new 了一個 Pizza...PizzaBuilder() {std::cout?<<?"PizzaBuilder: 準備開始做一個新披薩!(分配內存...)\n";// 💣 手動用 new 在堆上分配內存!// 這就意味著,我們必須在未來的某個地方手動 delete 它!pizza_ =?new?Pizza();}// (稍后看析構函數...)

PizzaBuilder?來了!它心里藏著一個?Pizza*?指針?pizza_,用來指向它正在“揉捏”的那個披薩對象。關鍵風險點就在構造函數里:pizza_ = new Pizza();。這行代碼在堆內存中創建了一個?Pizza?對象。在 C++98/03 時代,new?出來的東西,必須手動?delete,否則它就會永遠留在內存里,直到程序結束(或者系統資源耗盡),這就是內存泄漏!這就像借了錢,必須記得還一樣,忘記還錢(忘記?delete)后果很嚴重!💸

第三步:一步步“定制”披薩 - Setter 方法

? ??// ... PizzaBuilder 內部 ...// 為了鏈式調用,Setter 方法返回 *this 的引用PizzaBuilder&?setDough(const?std::string& dough)?{std::cout?<<?" ?設置面團: "?<< dough <<?std::endl;// 模擬構建過程中可能發生的意外if?(dough ==?"爆炸性面團") {throwstd::runtime_error("面團不穩定,炸了!💥");?// 拋出異常!}pizza_->setDough(dough);?// 操作裸指針指向的對象return?*this;?// 返回自身引用,這樣就可以 .setSauce()... 繼續點下去}PizzaBuilder&?setSauce(const?std::string& sauce)?{std::cout?<<?" ?設置醬料: "?<< sauce <<?std::endl;pizza_->setSauce(sauce);return?*this;}PizzaBuilder&?addTopping(const?std::string& topping)?{std::cout?<<?" ?添加配料: "?<< topping <<?std::endl;pizza_->addTopping(topping);return?*this;}

這些?setXXX?方法負責具體的構建步驟。它們通過?pizza_->?來操作那個裸指針指向的?Pizza?對象。為了實現?builder.setDough(...).setSauce(...).addTopping(...)?這種流暢的鏈式調用,每個 setter 方法最后都?return *this;,返回對?PizzaBuilder?對象自身的引用。注意?setDough?里我們模擬了一個可能拋出異常的情況,這在后面分析異常安全性時很重要。

第四步:“披薩好了,出爐!” - build() 方法與所有權轉移

? ??// ... PizzaBuilder 內部 ...// 構建完成,把成果(披薩的指針)交出去Pizza*?build()?{std::cout?<<?"PizzaBuilder: 披薩做好了!給你!(指針給你,你自己看著辦哦~)\n";// 臨時保存一下指針Pizza* result = pizza_;// 關鍵一步:Builder 放棄對這個 Pizza 的所有權!// 它把自己的 pizza_ 設為 nullptr,表示“這個披薩 art?k (不再) 歸我管了”// 這是為了防止 Builder 在后續(比如析構時)意外地把已經交給別人的披薩給 delete 掉pizza_ =?nullptr;// ?? 返回裸指針!燙手的山芋!// 調用者(拿到這個指針的人)現在是這個 Pizza 對象的新主人,// 必須承擔起未來 delete 它的責任!return?result;}

build()?方法是建造過程的終點。它把內部一直持有的?pizza_?指針返回給調用者。但這里有個非常微妙且關鍵的操作:pizza_ = nullptr;。這是 Builder 在“放手”,告訴大家:“這個披薩的所有權我已經轉交出去了,以后它的生殺大權(何時?delete)就歸拿到指針的那個人了!”?但這種口頭約定非常脆弱!?調用者萬一忘了?delete?怎么辦?或者?build?之后這個 Builder 對象被復用(雖然這里的設計不鼓勵復用,但萬一呢?),它內部的?pizza_?已經是?nullptr?了,再操作就會崩潰!💥

第五步:Builder 的“遺言” - 析構函數

? ??// ... PizzaBuilder 內部 ...// Builder 對象生命周期結束時調用~PizzaBuilder() {std::cout?<<?"PizzaBuilder: 我要被銷毀了...\n";// 注意!這里通常 *不應該* 寫 delete pizza_;// 為什么?因為正常情況下,build() 已經把所有權轉移給調用者了。// 如果這里還 delete,就會導致一個披薩被 delete 兩次(Double Delete),程序崩潰!💀// 但是!如果 build() 沒被調用(比如中途異常了),// 而析構函數又不 delete,那 new 出來的 Pizza 就沒人管了,泄漏!💧// 這就是 C++98/03 手動管理資源的“兩難困境”!// delete pizza_; // 所以這里通常是注釋掉或者根本不寫}
};?// PizzaBuilder 類結束

析構函數?~PizzaBuilder?在?PizzaBuilder?對象生命周期結束時被調用。這里的注釋解釋了為什么它通常不能?delete pizza_:因為正常情況下所有權已經通過?build()?轉移了。但這也直接導致了異常安全問題:如果在?build()?調用之前發生異常(比如?setDough?拋異常),PizzaBuilder?對象會被銷毀,析構函數被調用,但它不會?delete?那個已經?new?出來的?Pizza?對象!這個?Pizza?對象就成了內存中的“孤魂野鬼”,永遠無法被回收。這就是典型的異常不安全導致的內存泄漏。

第六步:客戶怎么用?(成功與失敗的演示)

// --- 客戶端代碼 ---
int?main()?{Pizza* myPizza =?nullptr;?// 準備一個裸指針來接收披薩try?{std::cout?<<?"--- 正常流程 ---\n";PizzaBuilder builder;?// 1. 創建建造者 (內部 new Pizza)// 2. 鏈式調用設置屬性myPizza = builder.setDough("薄脆型").setSauce("番茄醬").addTopping("芝士").addTopping("蘑菇").build();?// 3. 獲取披薩指針 (builder 內部 pizza_ 變 null)std::cout?<<?"\n我的披薩詳情:\n";myPizza->describe();?// 4. 使用披薩// ... 模擬異常流程 ...// std::cout << "\n--- 異常流程模擬 ---\n";// PizzaBuilder explodingBuilder;// myPizza = explodingBuilder.setDough("爆炸性面團").build(); // 這會拋異常}?catch?(conststd::exception& e) {std::cerr?<<?"\n--- 捕獲到異常 ---\n";std::cerr?<<?"錯誤信息: "?<< e.what() <<?std::endl;// 如果異常發生在 build() 之前,builder 對象會被銷毀,// 但它析構函數通常不清理 pizza_ 指向的內存(怕 double delete)。// 所以,那個 new 出來的 Pizza 對象就泄漏了!😱💧std::cerr?<<?"糟糕,異常導致內存泄漏了!剛才那個披薩沒人管了!\n";// 此時 myPizza 仍然是 nullptr 或者指向一個無效地址(取決于異常點)}// 最最最重要的一步:手動釋放內存!std::cout?<<?"\n--- 清理工作 ---\n";std::cout?<<?"吃完披薩,記得自己收拾(delete)...\n";// 只有在沒有異常,myPizza 成功指向披薩對象時,才需要 delete// 如果 myPizza 是 nullptr (比如異常了),delete nullptr 是安全的,啥也不做。delete?myPizza;?// 🙏 千萬別忘了!否則就是內存泄漏!std::cout?<<?"\n程序結束。\n";return0;
}

客戶端代碼展示了兩種情況:

  • 正常流程:創建 Builder -> 鏈式調用 ->?build()?獲取指針 -> 使用 -> **最后必須手動?delete**!

  • 異常流程:如果在?setXXX?中拋了異常,catch?塊會被執行。我們清楚地看到,這種情況下會發生內存泄漏,因為析構函數通常不負責清理。

總結一下“石器時代”的痛點(現在是不是更清晰了?):

  1. 手動內存管理地獄new?和?delete?必須配對,責任全在程序員。忘了?delete?就泄漏,delete?多了就崩潰。心累!💔

  2. 脆弱的異常安全:一旦在構建過程中發生異常,很容易導致資源(new?出來的?Pizza)泄漏。程序不夠健壯。💧

  3. 混亂的所有權轉移:靠裸指針和口頭約定來轉移“誰負責?delete”的責任,非常容易出錯。

  4. Builder 復用困難build()?后內部指針變?nullptr,難以安全復用同一個 Builder 實例。

  5. 缺少強制約束:沒有機制保證必需的步驟(如?setDough)一定被調用。

這種寫法,就像在雷區里跳舞,步步驚心!😂 這也正是為什么 C++11 及之后的新特性如此重要,它們就是來填這些“上古巨坑”的!下一站,我們就去看看 C++11 的英雄們是如何帶來曙光的!??

四、🛡? “青銅時代”:智能指針騎士登場 (C++11 內存安全)

告別了“石器時代”的手動擋和提心吊膽,C++11 帶來了劃時代的進步!其中最耀眼的明星之一就是智能指針,比如?std::unique_ptr?和?std::shared_ptr。它們運用了一種叫做?RAII (Resource Acquisition Is Initialization)?的強大技術(名字很長,但意思很簡單:資源在對象創建時獲取,在對象銷毀時自動釋放)。這直接讓內存管理從“手動擋”升級到了“自動擋”!媽媽再也不用擔心我忘記?delete?啦!🎉

我們先用?std::unique_ptr?來改造披薩建造者。unique_ptr?的核心是獨占所有權,就像一把鑰匙只能開一把鎖,一個?unique_ptr?在同一時間只能指向一個對象。這和建造者模式的需求完美契合:Builder 辛辛苦苦造好一個獨一無二的披薩,然后把這把“鑰匙” (unique_ptr) 完全交給你,它自己就不再保管了。

第一步:產品?Pizza?類的小調整

#include?<iostream>
#include?<string>
#include?<vector>
#include?<memory> // 智能指針的頭文件!必須包含!
#include?<stdexcept>// --- 產品:披薩 ---
class?Pizza?{
public:// 構造函數可以公開了!因為我們將用 std::make_unique 來安全地創建它// 不再依賴 Builder 作為 friend 來調用私有構造函數Pizza() {?std::cout?<<?"[Pizza 對象被創建]\n"; }~Pizza() {?std::cout?<<?"[Pizza 對象被銷毀]\n"; }?// 加個析構函數,方便觀察// set/describe 方法和之前一樣void?setDough(const?std::string& dough)?{ dough_ = dough; }void?setSauce(const?std::string& sauce)?{ sauce_ = sauce; }void?addTopping(const?std::string& topping)?{ toppings_.push_back(topping); }void?describe()?const?{?/* ... (輸出詳情) ... */?}private:std::string?dough_;std::string?sauce_;std::vector<std::string> toppings_;
};

主要變化是?Pizza?的構造函數現在是?public?的了。為什么?因為 C++11 推薦使用?std::make_unique<T>(...)?來創建由?unique_ptr?管理的對象,它比直接?new T(...)?更安全(尤其是在異常處理方面),而?make_unique?需要能訪問到類的構造函數。我們還加了個析構函數打印信息,方便后面看智能指針啥時候幫我們自動釋放內存。

第二步:建造者內部的“升級換代”

// --- 建造者 ---
class?PizzaBuilder?{
private:// ? 告別裸指針!擁抱 std::unique_ptr!// 它現在負責管理 Pizza 對象的生命周期。std::unique_ptr<Pizza> pizza_;public:// 構造函數:使用 std::make_unique 創建 PizzaPizzaBuilder() {std::cout?<<?"PizzaBuilder (智能版): 開工!(創建 unique_ptr 管理 Pizza)\n";// 使用 std::make_unique<Pizza>() 創建對象,并讓 pizza_ 指向它。// 這一步就完成了資源獲取 (RAII 的 'RA')// 而且 make_unique 本身是異常安全的。pizza_ =?std::make_unique<Pizza>();}

看到沒?Pizza* pizza_;?被替換成了?std::unique_ptr<Pizza> pizza_;。這是質的飛躍!現在,pizza_?這個智能指針對象擁有它所指向的?Pizza?對象。在?PizzaBuilder?的構造函數里,我們用了?std::make_unique<Pizza>(),這是 C++11/14 推薦的方式,它安全地在堆上創建了一個?Pizza?對象,并把管理權交給了?pizza_

第三步:高枕無憂的析構函數

? ??// ... PizzaBuilder 內部 ...// 析構函數:啥也不用干!真正的“全自動”!👍~PizzaBuilder() {std::cout?<<?"PizzaBuilder (智能版): 收工!(內部的 unique_ptr 會自動清理內存,我躺平了~)\n";// 當 PizzaBuilder 對象被銷毀時,它的成員變量 pizza_ (unique_ptr) 也會被銷毀。// unique_ptr 在自己被銷毀時,會自動調用 delete 刪除它所管理的 Pizza 對象!// 這就是 RAII 的魔力!資源生命周期和對象生命周期綁定!}

對比一下“石器時代”那個糾結的析構函數,這里簡直是天堂!我們啥都不用寫!因為 RAII 機制保證了:當?PizzaBuilder?對象生命結束時,它的成員?pizza_?(一個?unique_ptr?對象) 也會被銷毀。而?unique_ptr?在它自己的析構函數里,會自動?delete?它所指向的那個?Pizza?對象!完美閉環,無需操心!

第四步:更安全的 Setter 和異常處理

? ??// ... PizzaBuilder 內部 ...// Setter 方法基本不變,但異常處理更安全了PizzaBuilder&?setDough(const?std::string& dough)?{std::cout?<<?" ?設置面團: "?<< dough <<?std::endl;// 加個檢查確保 pizza_ 還指向有效對象 (雖然 build() 后會變 null)if?(!pizza_) {?throwstd::logic_error("披薩已經被 build 走了,不能再設置了!"); }if?(dough ==?"爆炸性面團") {?// 模擬構建中的異常std::cout?<<?" ?(模擬異常拋出...)\n";// 如果這里拋異常,會發生什么?// 1. 函數調用棧展開 (Stack Unwinding)。// 2. 如果 PizzaBuilder 對象是在棧上創建的,它會被銷毀。// 3. 其成員 pizza_ (unique_ptr) 隨之銷毀。// 4. unique_ptr 的析構函數自動 delete 掉它管理的 Pizza 對象!?// 內存不會泄漏!異常安全!throwstd::runtime_error("面團不穩定,又炸了!💥");}pizza_->setDough(dough);?// 通過智能指針訪問對象成員return?*this;}// 其他 setSauce, addTopping 類似...PizzaBuilder&?setSauce(const?std::string& sauce)?{if?(!pizza_)?throwstd::logic_error("披薩已經被 build 走了!");std::cout?<<?" ?設置醬料: "?<< sauce <<?std::endl;pizza_->setSauce(sauce);return?*this;}PizzaBuilder&?addTopping(const?std::string& topping)?{if?(!pizza_)?throwstd::logic_error("披薩已經被 build 走了!");std::cout?<<?" ?添加配料: "?<< topping <<?std::endl;pizza_->addTopping(topping);return?*this;}

Setter 方法的邏輯沒大變,還是通過?pizza_->?來操作。但請注意異常處理部分:如果?setDough?拋出異常,由于?pizza_?是?unique_ptr,RAII 機制會確保在棧回溯過程中,pizza_?被正確銷毀,進而它管理的?Pizza?對象也被?delete內存泄漏的風險被徹底消除了!?這就是智能指針帶來的巨大異常安全性提升!我們還加了個?!pizza_?的檢查,防止在?build()?之后誤操作。

第五步:優雅的所有權轉移 -?build()?與?std::move

? ??// ... PizzaBuilder 內部 ...// 構建完成,返回產品的所有權 (通過 unique_ptr)std::unique_ptr<Pizza>?build()?{if?(!pizza_) {?// 再次檢查,防止重復 buildthrowstd::logic_error("不能重復構建,或者披薩對象已失效!");}std::cout?<<?"PizzaBuilder (智能版): 披薩做好了!所有權(unique_ptr)轉移給你!\n";// 關鍵:使用 std::move() 來轉移所有權!// std::move 告訴編譯器:“我確定要把 pizza_ 的所有權轉走,// 允許你調用 unique_ptr 的移動構造函數/移動賦值運算符”。// 調用后,builder 內部的 pizza_ 會變成空指針 (nullptr),// 而返回的那個新的 unique_ptr 則接管了 Pizza 對象的所有權。// 這個過程非常高效,沒有實際的內存拷貝。returnstd::move(pizza_);?// ? 安全、清晰、高效地轉移所有權!}
};?// PizzaBuilder 類結束

build()?方法現在返回?std::unique_ptr<Pizza>。最核心的變化是?return std::move(pizza_);std::move?是 C++11 的另一個利器,它用于轉移所有權。在這里,它把?pizza_?這個?unique_ptr?所擁有的對?Pizza?對象的所有權,“移動”給了將要返回的那個?unique_ptr。移動之后,builder?內部的?pizza_?就變成了空指針,它不再擁有那個?Pizza?了。這個所有權轉移的過程既安全(不會有兩個?unique_ptr?同時指向一個對象)又高效(只是指針操作,沒有深拷貝)。

第六步:客戶端代碼的解放

// --- 客戶端代碼 ---
int?main()?{// 聲明 unique_ptr 來接收披薩,初始為 nullptrstd::unique_ptr<Pizza> myPizza =?nullptr;try?{PizzaBuilder builder;?// 創建建造者 (內部自動管理 Pizza 的 unique_ptr)// 鏈式調用設置屬性,和以前一樣流暢myPizza = builder.setDough("全麥").setSauce("香蒜辣椒醬").addTopping("烤雞肉").addTopping("菠菜")// .setDough("爆炸性面團") // 試試在這里拋異常?內存也不會漏!.build();?// 獲取 unique_ptr (builder 內部 pizza_ 變 null)std::cout?<<?"\n我的披薩詳情:\n";// 使用智能指針就像使用普通指針一樣 (通過 -> 或 *)myPizza->describe();// 嘗試再次 build? (會觸發 builder 內部的 !pizza_ 檢查,拋異常)// auto anotherPizza = builder.build();}?catch?(conststd::exception& e) {std::cerr?<<?"\n--- 捕獲到異常 ---\n";std::cerr?<<?"錯誤信息: "?<< e.what() <<?std::endl;// 即使異常發生在 build() 之前,builder 銷毀時,// 其成員 pizza_ (unique_ptr) 也會自動 delete 它管理的 Pizza 對象。std::cerr?<<?"放心,就算異常了,智能指針也會負責善后,不會內存泄漏!😎\n";// 此時 myPizza 仍然是 nullptr}std::cout?<<?"\n--- 清理工作 ---\n";std::cout?<<?"吃完披薩...\n";// 看這里!看這里!👇// 不需要手動調用 delete myPizza 了!!!🥳🥳🥳// 當 myPizza 這個 unique_ptr 離開作用域 (main 函數結束) 時,// 它會自動檢查自己是否指向一個對象,如果是,就自動 delete 掉!// RAII 萬歲!智能指針萬歲!std::cout?<<?"程序即將結束,myPizza (unique_ptr) 將自動釋放管理的 Pizza 對象...\n";return0;?// myPizza 在這里離開作用域,自動調用它指向的 Pizza 的析構函數
}

客戶端代碼最大的變化是什么?沒有?delete myPizza;?了!?🎉 當?main?函數結束,myPizza?這個?unique_ptr?對象離開作用域時,它的析構函數會被自動調用,然后它會負責把它管理的?Pizza?對象給?delete?掉。這就是 RAII 的終極體現:資源的生命周期和管理它的對象的生命周期完全綁定,再也不用擔心忘記釋放資源了!同時,異常處理部分也印證了,即使出錯,內存也能被安全回收。

智能指針帶來的巨大改進【總結】:

  1. 自動內存管理unique_ptr?接管了?new?和?delete?的職責,利用 RAII 機制確保資源在適當的時候被釋放。告別手動?delete?的噩夢!🎉

  2. 明確的所有權unique_ptr?的“獨占”語義和?std::move?的配合,讓所有權的轉移清晰、安全且高效。誰擁有資源,一目了然。

  3. 大大提升異常安全性:無論程序是正常結束還是中途因異常退出,RAII 都能保證?unique_ptr?管理的資源被正確釋放,幾乎消除了內存泄漏的風險。👍

用上了智能指針,我們的建造者模式代碼終于擺脫了“石器時代”的粗糙和危險,邁入了更安全、更現代化的“青銅時代”!

但是,還有改進空間嗎?

  • 必需參數問題:我們還是可以?PizzaBuilder().build()?創建一個空的披薩。如果面團和醬料是必需的呢?智能指針沒解決這個問題。🤔

  • 建造者復用性build()?之后 Builder 內部的?pizza_?就空了,還是不能方便地復用同一個 Builder 實例來造下一個披薩。

看來,進化之路還未結束!下一站,我們將探索如何結合 C++11 的其他特性(如構造函數約束和值語義)來解決這些遺留問題!🚀

五、? “白銀時代”:強制配料與值語義 (更健壯的接口)

想象一下,我們披薩店對品質的要求更高了!規定了:沒面團、沒醬料的,那不叫披薩,頂多算個烤餅!必須強制顧客先選這兩樣!同時,我們希望顧客拿到披薩后,這披薩就是他自己的了,跟我們店員(Builder)徹底沒關系,店員也不能再對這個已售出的披薩指手畫腳。

怎么實現呢?三大策略聯手出擊:

  1. 構造函數強制必需參數:把必需的東西(面團、醬料)直接放到?PizzaBuilder?的構造函數里。想創建 Builder?先把這兩樣告訴我!否則編譯器直接罷工!🚫 這是在編譯期就鎖死錯誤的強力手段!

  2. 值語義 (Value Semantics):從“手持圖紙”到“手握實物”的轉變!

    • **極簡的生命周期管理 (RAII 絕配!)**:

    • 減少間接層,可能提高數據局部性

    • 更清晰的狀態封裝

    • **與移動語義珠聯璧合,實現高效?build()**:

    • 回想指針時代:我們總得操心堆上那個?Pizza?對象的生死。手動?new/delete?累心易錯,unique_ptr?雖好,但也涉及堆分配和智能指針的規則。

    • 值語義下:被包含的?pizza_?對象的生命周期,和包含它的?PizzaBuilder?完全自動綁定PizzaBuilder?創建,pizza_?就跟著創建;PizzaBuilder?銷毀,pizza_?也隨之自動銷毀。不再需要任何手動的?new/delete,甚至連智能指針都不需要(在 Builder 內部管理產品這塊兒)!?C++ 對象模型和 RAII 把一切安排得明明白白,代碼更簡單,心智負擔更輕!?

    • 指針訪問總要繞一下 (pizza_->),而且 Builder 和堆上的 Pizza 對象內存位置通常是分開的。

    • 值語義下是直接成員訪問 (pizza_.),pizza_?就存儲在?PizzaBuilder?對象的內存里。這可能帶來更好的緩存局部性(CPU 處理挨著的數據更快),雖然往往是微優化,但更重要的是心智模型的簡化——數據就在這兒,不隔著一層。

    • PizzaBuilder?對象現在?本身就包含?了正在構建對象的狀態。它是一個更內聚、自給自足的單元,既持有配置信息,也持有實際的對象數據。感覺更整體, Builder 不僅是“監工”,它?體現?了構建過程和當前結果。

    • 之前?build()?移動的是?unique_ptr。現在,build()?可以直接通過?std::move?移動整個被包含的?Pizza?對象!這會觸發?Pizza?的移動構造函數,把資源(比如?vector?里的配料)高效地“搬”走,而不是拷貝。感覺更像是把熱乎的披薩直接遞給你,而不是只給個取餐牌。🚚

    • 核心思想:讓?PizzaBuilder?不再持有指向?Pizza?的指針(無論是裸指針?Pizza*?還是智能指針?unique_ptr<Pizza>),而是直接持有?Pizza?對象本身!就像店員手里不再是訂單或圖紙,而是拿著一個真實的、正在制作中的披薩胚子。
      class?PizzaBuilder?{
      private:Pizza pizza_;?// 看!直接包含 Pizza 對象!// ...
      };
      
    • 為什么要這樣做?好處多多!

  3. **移動語義 (Move Semantics)**:當?build()?時,不再傳遞指針或拷貝對象,而是把 Builder 手里那個(通過值語義持有的)Pizza?對象高效地“搬”給調用者,避免不必要的性能開銷。

我們來看代碼如何實現這些策略的協同:

第一步:產品?Pizza?的配合 - 擁抱移動

#include?<iostream>
#include?<string>
#include?<vector>
#include?<utility> // 為了 std::move// --- 產品:披薩 ---
class?Pizza?{
public:// 為了能讓 Builder 高效地把 Pizza 對象 "搬" 走,// 我們需要提供移動構造函數 (Move Constructor)Pizza() {?std::cout?<<?"[Pizza 對象默認構造]\n"; }~Pizza() {?std::cout?<<?"[Pizza 對象銷毀]\n"; }// 當從一個臨時的 Pizza 對象創建新 Pizza 時調用 (比如 build 返回時)Pizza(Pizza&& other)?noexcept// noexcept 很重要,表示移動不會拋異常: dough_(std::move(other.dough_)),?// 把對方的資源 "偷" 過來sauce_(std::move(other.sauce_)),toppings_(std::move(other.toppings_)) {std::cout?<<?"[Pizza 移動構造]: 嘿咻!我被搬家啦!🚚 舊的變空殼了。\n";// 注意:移動后,other 對象的狀態通常是有效的,但內容已被移走}// (可選) 移動賦值運算符,如果需要的話Pizza&?operator=(Pizza&& other)?noexcept?{?/* ... (類似移動構造) ... */return?*this; }// (推薦) 禁止拷貝構造和拷貝賦值!// 披薩做好了就是獨一份,不應該被隨便復制粘貼!// 這也強制我們必須使用移動語義。Pizza(const?Pizza&) =?delete;Pizza&?operator=(const?Pizza&) =?delete;// set/describe 方法不變...void?setDough(std::string?dough)?{ dough_ =?std::move(dough); }?// 內部也用 move 提高效率void?setSauce(std::string?sauce)?{ sauce_ =?std::move(sauce); }void?addTopping(std::string?topping)?{ toppings_.push_back(std::move(topping)); }void?describe()?const?{?/* ... (輸出詳情) ... */?}private:std::string?dough_;std::string?sauce_;std::vector<std::string> toppings_;?// 這個 vector 是主要的資源所在地
};

這里的?Pizza?類為了配合“被移動”,做了幾個重要改動:

  • **提供了移動構造函數?Pizza(Pizza&& other)**:移動語義的核心。高效地“竊取”源對象的資源,避免昂貴的拷貝。🚀

  • **禁止了拷貝構造和拷貝賦值 (= delete)**:好習慣,強制使用移動。

第二步:建造者的重大變革 - 值語義與構造函數約束

// --- 建造者 (持有 Pizza 對象本身) ---
class?PizzaBuilder?{
private:// ? 核心變化:不再是指針!直接包含一個 Pizza 對象作為成員變量!Pizza pizza_;// (可選) 加個標記,防止 build() 被調用多次bool?built_ =?false;public:// ? 構造函數強制接收必需參數!編譯期把關!PizzaBuilder(std::string?dough,?std::string?sauce)// pizza_ 在此被默認構造{std::cout?<<?"PizzaBuilder (值語義版): 開工!面團 '"?<< dough<<?"' 和醬料 '"?<< sauce <<?"' 已就位。\n";// 直接設置內部 pizza_ 對象的屬性pizza_.setDough(std::move(dough));pizza_.setSauce(std::move(sauce));// 無需 new/delete/智能指針,生命周期由 PizzaBuilder 自動管理!}// 析構函數:依然啥也不用干!內部 pizza_ 自動銷毀。~PizzaBuilder() {std::cout?<<?"PizzaBuilder (值語義版): 收工!(內部 Pizza 對象 pizza_ 會自動隨我銷毀)\n";}

PizzaBuilder?的變化是革命性的:

  • **Pizza pizza_;**:值語義的核心體現。Builder 和它構建的對象生命周期緊密綁定。

  • **PizzaBuilder(std::string dough, std::string sauce)**:構造函數強制必需參數,編譯期保證。?

  • 自動資源管理:完全無需手動或通過智能指針管理?pizza_?的生命周期。

第三步:可選參數的設置和狀態檢查

? ??// ... PizzaBuilder 內部 ...// 設置可選參數:添加配料PizzaBuilder&?addTopping(std::string?topping)?{if?(built_) {throw?std::logic_error("披薩已經做好了,不能再加料了!");}std::cout?<<?" ?添加配料: "?<< topping <<?std::endl;pizza_.addTopping(std::move(topping));?// 直接操作內部 pizza_return?*this;}

可選參數設置直接作用于內部的?pizza_?對象。增加了?built_?檢查,提高健壯性。

第四步:終極交付 -?build() &&?與移動語義

? ??// ... PizzaBuilder 內部 ...// 構建完成,通過移動語義返回 Pizza 對象本身// '&&' 限定符是關鍵,增強了安全性Pizza?build()?&&?{if?(built_) {throwstd::logic_error("不能重復構建!");}std::cout?<<?"PizzaBuilder (值語義版): 披薩做好了!整個給你!(移動過去...)\n";built_ =?true;// std::move(pizza_) 觸發 Pizza 的移動構造函數,高效轉移資源returnstd::move(pizza_);}// (可選的 & 限定版本,通常涉及拷貝,較少用)// Pizza build() & { ... }
};?// PizzaBuilder 類結束

build()?方法是這一版的核心亮點:

  • **Pizza build() &&**:返回?Pizza?對象,&&?限定符防止對左值 Builder 誤操作。

  • **return std::move(pizza_);**:高效地將內部?pizza_?對象的內容“搬”到返回值中,利用移動語義避免拷貝。

第五步:客戶端代碼的進化

// --- 客戶端代碼 ---
int?main()?{try?{// ? 必需參數構造時提供,編譯期檢查!// PizzaBuilder(...) 返回臨時對象(右值),可直接調用 build() &&Pizza myDeliciousPizza = PizzaBuilder("意式薄底",?"經典瑪格麗特醬").addTopping("水牛芝士").addTopping("羅勒葉").build();?// 調用 Pizza build() &&std::cout?<<?"\n我的披薩詳情:\n";myDeliciousPizza.describe();?// 得到的是獨立的 Pizza 對象// 嘗試無參創建 Builder? 編譯失敗!?// 嘗試對左值 Builder 直接 build()? 編譯失敗!? (需用 std::move)}?catch?(conststd::exception& e) {std::cerr?<<?"\n捕獲到異常: "?<< e.what() <<?std::endl;}std::cout?<<?"\n程序結束。\n";// myDeliciousPizza 生命周期由作用域自動管理,無需 delete!return0;
}

客戶端代碼變得非常安全和簡潔,必需參數在編譯期得到保證,內存管理完全自動化。

“白銀時代”的亮點【總結】:

  1. **強制必需參數 (編譯期)**:構造函數把關,健壯性 ++!?

  2. 值語義,狀態清晰,管理簡單:Builder 直接持有對象,生命周期自動綁定,無需指針操心!🧠

  3. 移動語義優化性能build()?時高效“搬運”對象,避免拷貝。🚀

  4. &&?限定符增強安全性:防止誤用,意圖更明確。🔒

  5. 完全自動內存管理:告別?delete?和模式內部的智能指針!🧘

這種結合了構造函數約束、值語義和移動語義的建造者模式,是現代 C++ 中非常實用和推薦的方案,它在多個維度上都取得了顯著的進步。

但故事還沒完!還能不能更靈活、更強大?“黃金時代”在向我們招手!🌟

六、🌟 “黃金時代”:現代C++特性加持 (C++11/14/17...)

進入 C++11 及以后的時代,我們有了更多強大的武器來武裝我們的建造者模式,讓它變得更靈活、更安全、更簡潔!

1. 內嵌建造者與友元訪問

將?Builder?作為產品類 (Pizza) 的內嵌類 (Pizza::Builder) 是一種常見的現代實踐。這使得?Builder?的邏輯與?Pizza?更緊密地結合在一起,并且可以方便地訪問?Pizza?的私有成員(例如,通過將?Builder?聲明為?Pizza?的友元)。

// 產品類:披薩
class?Pizza?{
public:// 核心:聲明內嵌 Builder 為友元friendclass?Builder;// ... (公開接口,如 describe(), 拷貝/移動控制等) ...void?describe()?const?{?/* ... */?}Pizza(const?Pizza&) =?delete;?/* ... */private:// 核心:構造函數私有化,強制使用 BuilderPizza() =?default;// ... (私有成員變量: dough_, sauce_, toppings_, validated_) ...std::string?dough_;// ...// 核心:提供內部方法供 Builder 調用void?setDoughInternal(std::string?dough)?{?/* ... */?}void?setSauceInternal(std::string?sauce)?{?/* ... */?}// ... (其他內部方法: addToppingInternal, markValidated, validatePizza) ...
};// --- 內嵌建造者 ---
// 核心:Builder 定義為 Pizza 的內嵌類
class?Pizza::Builder {
private:Pizza pizza_;?// Builder 持有正在構建的 Pizza 對象// ... (其他 Builder 狀態,如 built_) ...public:// Builder 構造函數Builder(std::string?dough,?std::string?sauce) {// 核心:通過友元權限調用 Pizza 的內部方法pizza_.setDoughInternal(std::move(dough));pizza_.setSauceInternal(std::move(sauce));}// ... (Builder 的鏈式調用方法,如 addTopping) ...// ... (Builder 的 build 方法) ...
};

講解: 這里,Pizza::Builder?被聲明為?Pizza?的?friend class,允許?Builder?調用?Pizza?的私有方法如?setDoughInternal。同時,Pizza?的默認構造函數被設為?private,確保只能通過?Builder?來創建?Pizza?對象,增強了封裝性。

2. 移動語義 (&&) 與防止重復構建

build()?方法通常是建造過程的最后一步,它應該轉移(移動)內部構建好的產品對象的所有權給調用者,而不是拷貝。同時,為了防止一個?Builder?實例被用來構建多次,或在構建后被繼續修改,我們可以使用?&&?限定符和狀態標志。

// (續 Pizza::Builder 類)// 構建方法,使用 && 限定符,表示只能對即將銷毀的右值 Builder 調用// [[nodiscard]] (C++17) 提示編譯器檢查返回值是否被使用[[nodiscard]]Pizza?build()?&&?{?// 注意這里的 '&&' 限定符if?(built_)?throw?std::logic_error("不能重復構建!");std::cout?<<?"Pizza::Builder: 直接構建完成!(移動...)\n";built_ =?true;?// 標記為已構建return?std::move(pizza_);?// 使用 std::move 轉移所有權}
// }; // Builder 類定義結束 (暫時注釋掉,下面繼續添加方法)

講解

  • build() &&: 末尾的?&&(右值引用限定符)意味著?build()?方法只能被右值?Builder?對象調用。這通常發生在?Builder?對象即將被銷毀時,例如?Pizza p = Pizza::Builder(...).build();?中的臨時?Builder?對象。這強制了?build()?通常是鏈式調用的最后一步,并且自然地配合了移動語義。

  • return std::move(pizza_);: 高效地將內部?pizza_?對象“移動”給調用者,避免了不必要的拷貝。

  • built_?標志和檢查:確保一旦?build()?被調用,該?Builder?實例不能再用于添加配料或再次構建。

  • [[nodiscard]]?(C++17): 如果調用者調用了?build()?但沒有使用其返回值(比如?Pizza::Builder(...).build();?單獨一行),編譯器會發出警告,有助于防止忘記接收構建結果。

3. C++17 特性:std::optional,?if?初始化, 結構化綁定

C++17 提供了更優雅的方式來處理可能失敗的構建操作(例如,驗證不通過)。std::optional?可以清晰地表示“可能有值,也可能沒有”,而?if?初始化語句和結構化綁定可以簡化處理帶有狀態和原因的驗證結果。

// (續 Pizza::Builder 類)// 核心:使用 C++17 特性進行構建和驗證[[nodiscard]]?// 提示調用者不要忽略返回值std::optional<Pizza>?buildWithValidation()?&&?{?// 返回 optional,表示可能成功或失敗// ... (檢查是否已構建等前置邏輯) ...if?(built_) {?/* ... 拋出異常 ... */?}std::cout?<<?"Pizza::Builder: 準備構建并驗證...\n";// 核心:C++17 的 if 初始化語句 + 結構化綁定// 1. 調用 pizza_.validatePizza(),它返回 std::pair<bool, std::string>// 2. auto [isValid, reason] = ... 將 pair 的兩個元素解包到新變量 isValid 和 reason// 3. ... ; !isValid 是 if 的條件判斷部分// isValid 和 reason 的作用域僅限于此 if 語句if?(auto?[isValid, reason] = pizza_.validatePizza(); !isValid) {std::cerr?<<?" ?構建失敗!原因: "?<< reason <<?std::endl;// ... (標記已構建等) ...returnstd::nullopt;?// 核心:驗證失敗,返回空的 optional}// ... (驗證成功后的邏輯,如標記 pizza_ 為 validated_) ...pizza_.markValidated();built_ =?true;std::cout?<<?" ?驗證通過!披薩制作完成!(移動...)\n";// 核心:驗證成功,移動 pizza_ 到 optional 中并返回returnstd::move(pizza_);}
};?// Pizza::Builder 類定義結束// --- 客戶端代碼 (main 函數中) ---
int?main()?{// ... (之前的構建示例 p1) ...// 核心:調用帶驗證的構建方法std::optional<Pizza> p2_opt = Pizza::Builder("奇怪的面團",?"蒜蓉醬")/* ... 添加配料 ... */.buildWithValidation();// 核心:處理 std::optional 的結果if?(p2_opt) {?// 檢查 optional 是否有值 (構建是否成功)std::cout?<<?"\n披薩 P2 (驗證成功) 詳情:\n";p2_opt->describe();?// 通過 -> 或 * 安全訪問 Pizza 對象}?else?{std::cout?<<?"\n披薩 P2 構建失敗!無法獲得披薩對象。\n";}std::cout?<<?"--------------------\n";// ... (其他構建示例 p3_opt 和異常處理 try-catch) ...return0;
}

核心講解

  • std::optional<Pizza>?返回類型:替代了可能返回空指針或拋異常的方式,明確表示構建操作的結果是“要么有一個?Pizza?對象,要么什么都沒有”。調用者必須顯式檢查?optional?是否包含值(如用?if (p2_opt)?或?.has_value())才能安全訪問,提高了代碼的健壯性。

  • **if (auto [isValid, reason] = pizza_.validatePizza(); !isValid)**:這是 C++17 的亮點。

    • **結構化綁定 (auto [...])**:簡潔地將?validatePizza()?返回的?std::pair?的兩個成員解包到?isValid?和?reason?變量中。

    • **if?初始化語句 (... ; ...)**:將變量的聲明和初始化(auto [...] = ...)與條件判斷(!isValid)結合在?if?語句內部,使得?isValid?和?reason?的作用域被限制在這個?if?塊內,代碼更整潔、變量作用域最小化。

  • **return std::nullopt;?/?return std::move(pizza_);**:根據驗證結果,分別返回表示失敗的空?optional?或包含成功構建的?Pizza?對象的?optional(通過移動優化)。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/900923.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/900923.shtml
英文地址,請注明出處:http://en.pswp.cn/news/900923.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在Ubuntu內網環境中為Gogs配置HTTPS訪問(通過Apache反向代理使用IP地址)

一、準備工作 確保已安裝Gogs并運行在HTTP模式(默認端口3000) 確認服務器內網IP地址(如192.168.1.100) 二、安裝Apache和必要模塊 sudo apt update sudo apt install apache2 -y sudo a2enmod ssl proxy proxy_http rewrite headers 三、創建SSL證書 1. 創建證書存儲目錄…

數據中臺、BI業務訪談(二):組織架構梳理的坑

這是數據中臺、BI業務訪談系列的第二篇文章&#xff0c;在上一篇文章中&#xff0c;我重點介紹了在給企業的業務部門、高層管理做業務訪談之前我們要做好行業、業務知識的功課。做好這些功課之后&#xff0c;就到了實際的訪談環節了。 業務訪談關鍵點 那么在具體業務訪談的時…

spark集群,Stand alone,Hadoop集群有關啟動問題

你的問題是因為 start-all.sh 是 Hadoop 的啟動腳本&#xff08;用于啟動 HDFS 和 YARN&#xff09;&#xff0c;而不是 Spark 的啟動腳本。而你已經通過 start-cluster.sh 啟動了 Hadoop 相關服務&#xff08;HDFS/YARN&#xff09;&#xff0c;再次執行 start-all.sh 會導致服…

Kotlin 通用請求接口設計:靈活處理多樣化參數

在 Kotlin 中設計一個通用的 ControlParams 類來處理不同的控制參數&#xff0c;有幾種常見的方法&#xff1a;方案1&#xff1a;使用密封類&#xff08;Sealed Class&#xff09; sealed class ControlParamsdata class LightControlParams(val brightness: Int,val color: S…

aspark 配置2

編寫Hadoop集群啟停腳本 1.建立新文件&#xff0c;編寫腳本程序 在hadoop101中操作&#xff0c;在/root/bin下新建文件&#xff1a;myhadoop&#xff0c;輸入如下內容&#xff1a; 2.分發執行權限 保存后退出&#xff0c;然后賦予腳本執行權限 [roothadoop101 ~]$ chmod x /r…

Webstorm 使用搜不到node_modules下的JS內容 TS項目按Ctrl無法跳轉到函數實現

將node_modules標記為不排除&#xff0c;此時要把內存改大&#xff0c;不然webstorm中途建立索引時&#xff0c;會因為內存不足&#xff0c;導致索引中途停止&#xff0c;造成后續搜索不出來 更改使用內存設置 內存調為4096 若出現搜不出來js內容時&#xff0c;請直接重啟下該項…

vue-element-plus-admin的安裝

文檔鏈接&#xff1a;開始 | vue-element-plus-admin 之前嘗試按照官方文檔來安裝&#xff0c;運行npm run dev命令卻不能正常打開訪問瀏覽器&#xff0c;換一個方式 首先在目錄下打開命令窗口 1、克隆項目 從 GitHub 獲取代碼 # clone 代碼 git clone https://github.com…

【windows10】基于SSH反向隧道公網ip端口實現遠程桌面

【windows10】基于SSH反向隧道公網ip端口實現遠程桌面 1.背景2.SSH反向隧道3.遠程連接電腦 1.背景 ?Windows 10遠程桌面協議的簡稱是RDP&#xff08;Remote Desktop Protocol&#xff09;?。 RDP是一種網絡協議&#xff0c;允許用戶遠程訪問和操作另一臺計算機。 遠程桌面功…

軟考系統架構設計師之大數據與人工智能筆記

一、大數據架構設計 1. 核心概念與挑戰 大數據特征&#xff1a;體量大&#xff08;Volume&#xff09;、多樣性&#xff08;Variety&#xff09;、高速性&#xff08;Velocity&#xff09;、價值密度低&#xff08;Value&#xff09;。傳統數據庫問題&#xff1a;數據過載、性…

【數據結構 · 初階】- 單鏈表

目錄 一.相關指針知識點 二.鏈表 1.為什么學了順序表還要學鏈表 2.優點 三.實現 1.鏈表的打印 —— 理解鏈表結構 (2) 物理結構圖 2.鏈表的尾插 —— 入門 錯誤寫法&#xff1a;tail ! NULL 總結&#xff1a; 正確代碼物理圖解&#xff1a; (2) 尾插整體代碼 (思考…

按鍵消抖(用狀態機實現)

基于狀態機的設計代碼 module key_filter(clk,rst,key,key_p_flag,key_r_flag,key_state);input clk,rst;input key;output reg key_p_flag;output reg key_r_flag;output reg key_state;reg [1:0]r_key; //后面用來判斷什么時候pedge&#xff0c;什么時候nedgealways…

大數據(7.2)Kafka萬億級數據洪流下的架構優化實戰:從參數調優到集群治理

目錄 一、海量數據場景下的性能之殤1.1 互聯網企業的數據增長曲線1.2 典型性能瓶頸分析 二、生產者端極致優化2.1 批量發送黃金法則2.1.1 分區選擇算法對比 2.2 序列化性能突破 三、消費者端并發藝術3.1 多線程消費模式演進3.1.1 消費組Rebalance優化 3.2 位移管理高階技巧 四、…

MyBatis深度解析與實戰指南:細節完整,從入門到精通

MyBatis深度解析與實戰指南&#xff1a;細節完整&#xff0c;從入門到精通 整理這份筆記&#xff0c;是因為學習 MyBatis 時發現很多教程要么只講基礎 CRUD&#xff0c;要么直接跳到 Spring 整合&#xff0c;對 MyBatis 核心特性講解不全面&#xff0c;基礎部分也不夠完整。實…

【科學技術部政務服務平臺-用戶注冊/登錄安全分析報告】

前言 由于網站注冊入口容易被黑客攻擊&#xff0c;存在如下安全問題&#xff1a; 暴力破解密碼&#xff0c;造成用戶信息泄露短信盜刷的安全問題&#xff0c;影響業務及導致用戶投訴帶來經濟損失&#xff0c;尤其是后付費客戶&#xff0c;風險巨大&#xff0c;造成虧損無底洞…

【Audio開發三】音頻audio中幀frameSize ,周期大小periodsize,緩沖區buffer原理詳解以及代碼流程分析

一、基礎概述 在分析獲取最小幀數前&#xff0c;我們先來了解幾個相關的概念。 1&#xff0c;幀 幀&#xff08;frame&#xff09;&#xff1a;表示一個完整的聲音單元&#xff0c;所謂的聲音單元是指一個采樣樣本。如果是雙聲道&#xff0c;那么一個完整的聲音單元就是 2 個樣…

K8S學習之基礎七十五:istio實現灰度發布

istio實現灰度發布 上傳鏡像到harbor 創建兩個版本的pod vi deployment-v1.yaml apiVersion: apps/v1 kind: Deployment metadata:name: appv1labels:app: v1 spec:replicas: 1selector:matchLabels:app: v1apply: canarytemplate:metadata:labels:app: v1apply: canaryspec…

C++藍橋杯填空題(攻克版)

片頭 嗨~小伙伴們&#xff0c;咱們繼續攻克填空題&#xff0c;先把5分拿到手~ 第1題 數位遞增的數 這道題&#xff0c;需要我們計算在整數 1 至 n 中有多少個數位遞增的數。 什么是數位遞增的數呢&#xff1f;一個正整數如果任何一個數位不大于右邊相鄰的數位。比如&#xf…

【Python】數據結構

【Python】數據結構&#xff1a; Series&#xff1a;1、通過列表創建Series類對象2、顯示地給數據指定標簽索引3、通過字典創建Series類對象4、獲取索引5、獲取數據 DataFrame&#xff1a;1、通過數組創建一個DataFrame類對象2、指定列索引3、指定行索引4、獲取列的數據5、查看…

Android XML布局與Compose組件對照手冊

下面我將詳細列出傳統 XML 布局中的組件與 Compose 組件的對應關系&#xff0c;幫助您更好地進行遷移或混合開發。 基礎布局對應 XML 布局Compose 組件說明LinearLayout (vertical)Column垂直排列子項LinearLayout (horizontal)Row水平排列子項FrameLayoutBox層疊子項Relativ…

云原生運維在 2025 年的發展藍圖

隨著云計算技術的不斷發展和普及&#xff0c;云原生已經成為了現代應用開發和運維的主流趨勢。云原生運維是指在云原生環境下&#xff0c;對應用進行部署、監控、管理和優化的過程。在 2025 年&#xff0c;云原生運維將迎來更加廣闊的發展前景&#xff0c;同時也將面臨著一系列…