《C++20設計模式》學習筆記---原型模式

C++20設計模式

  • 第 4 章 原型模式
    • 4.1 對象構建
    • 4.2 普通拷貝
    • 4.3 通過拷貝構造函數進行拷貝
    • 4.4 “虛”構造函數
    • 4.5 序列化
    • 4.6 原型工廠
    • 4.7 總結
    • 4.8 代碼

第 4 章 原型模式

考慮一下我們日常使用的東西,比如汽車或手機。它們并不是從零開始設計的,相反,制造商會選擇一個現有的設計方案對其作適當的改進,使其外觀區別于以往的設計,然后淘汰老式的方案,開始銷售新產品。這是普遍存在的場景,在軟件世界中,我們也會遇到類似的情形:有時,相比從零開始創建對象(此時工廠和構造器可以發揮作用),我們更希望使用預先構建好的對象或拷貝或基于此做一些自定義設計。
由此,我們產生了一種想法,即原型模式:一個原型是指一個模型對象,我們對其進行拷貝、自定義拷貝,然后使用它們。原型模式的挑戰實際上是拷貝部分,其他一切都很簡單。

4.1 對象構建

大多數對象通過構造函數進行構建。但是如果已經有一個完整配置的對象,為ieshme不簡單的拷貝該對象而非要重新創建一個相同的對象呢?如果必須使用構造器模式來簡化逐段構建對象的過程,那么理解原型模式尤其重要。
我們先看一個簡單但可以直接說明對象拷貝的示例:

Contact john{"John Doe", Address{"123 East Dr" , "Londo",  10}};
Contact jane{"Jane Doe", Address{"123 East Dr" , "Londo",  11}};

john和jane工作在同一棟建筑大樓的不同辦公室。可能有許多人也在123 East Dr工作,在構建對象時我們想避免重復對該地址信息做初始化。怎么做呢?
原型模式與對象拷貝相關。當然,我們沒有通用的方法來拷貝對象,但是可以選擇一些可選的對象拷貝方法。

4.2 普通拷貝

如果曾在拷貝一個值和一個其所有成員都是通過值的方式來存儲的對象,那么拷貝毫無問題。例如,在之前的示例中,如果Contact和Address定義為:

    class Address{public:std::string street;std::string city;int suite;};class Contact{public:std::string name;Address address;};

那么在使用賦值運算符進行拷貝時,絕對不會有問題(string類型拷貝為深拷貝):

    void testOrdinaryCopy() {// here is the prototypeContact worker{"", {"123 East Dr", "London", 0}};// make a copy pf prototype and customize itContact john = worker;john.name = "John Doe";john.address.suite = 10;}

但是,在實際應用中,這種按值存儲和拷貝的方式較少見。在許多場景中,通常將內部的Address對象作為指針或者引用,例如:

    class Contact{public:std::string name;Address* address;~Contact() {delete address;}};

現在有一個很棘手的問題,因為代碼Contact jane = john將會拷貝地址指針,所以john和jane以及其他每一個原型拷貝都會共享同一個地址,這絕對不是我們想要的。


4.3 通過拷貝構造函數進行拷貝

避免拷貝指針的最簡單的方法時確保對象的所有組成部分(如上面的實例中的Contact和Address)都完整定義了拷貝構造函數。例如如果使用原始指針保存地址,即:

    class Contact{public:std::string name;Address* address;~Contact() {delete address;}};

那么,我們需要定義一個拷貝構造函數。在本示例中,實際上有兩種方法可以做到這一點。迎頭而來的方法看起來像下面這種:

        Contact(const Contact& other):name(other.name)/*, address(new Address(*other.address)*/) {address = new Address{other.address->street,other.address->city,other.address->suite}

不幸的是,這種方法并不通用。這種方法在上面的示例中當然沒有問題(前提是Address提供了一個初始化其所有成員的構造函數)。但是如果Address的street的成員是由街道名稱、門牌號和一些附加信息組成的,那該怎么版?那時,我們又會遇到同樣的拷貝問題。
一種明智的做法是,為Address定義拷貝構造函數。在本示例中,Address的拷貝構造函數相當簡單(C++ string類型數據實現為深拷貝致使該拷貝構造函數非常簡單):

        Address(std::string street, std::string city, int suite):street(street), city(city), suite(suite) {}

現在我們可以重寫Contact的構造函數中可以重用拷貝構造函數,即:

        Contact(const Contact& other):name(other.name), address(new Address(*other.address)) {}

請注意,ReSharper代碼生成器在生成拷貝構造函數和移動構造函數的同時,也會生成拷貝賦值函數。在本實例中,拷貝賦值函數定義為:

        Contact operator=(const Contact& other) {if (this == &other) {return *this;}name = other.name;address = other.address;return *this;}

【注】上述的拷貝賦值函數存在一定的問題,當我們調用到賦值函數時,并沒有為address重新指定新的Address地址。會存在多個對象指向一塊Address地址的問題,這個可能不是我們所想見到的。
完成這些函數定義后,我們可以像之前一樣構造對象的原型,然后重用它:

    void testCopyConstructor() {Contact worker{"",new Address{"123 East Dr", "London", 0}};Contact john = worker;john.name = "john";john.address->suite = 10;}

【注】在上述的測試代碼中,雖然使用了 “=”,但是并不會發生異常,這和我們上一個注釋說的就有點矛盾了,是什么原因導致的呢?

當對象賦值給另一個對象時,C++會根據情況調用拷貝構造函數或者拷貝賦值函數。如果在賦值操作時對象已經被初始化過,那么會調用拷貝賦值函數。但如果在賦值操作時對象尚未初始化,即對象已經存在,那么會調用拷貝構造函數。這是因為賦值操作需要先創建對象,然后再將值賦給已經存在的對象。因此,這時會調用拷貝構造函數來初始化新對象。

所以,這里雖然使用了 “=”, 但是其調用的是拷貝構造函數,并不會調用拷貝賦值,因此,不會存在問題,我們不妨把測試代碼改寫如下:

    void testCopyConstructor() {Contact worker{"",new Address{"123 East Dr", "London", 0}};Contact john;john = worker;john.name = "john";john.address->suite = 10;}

然后猜想下會發生什么異常呢?


使用當前這種通過拷貝構造函數進行拷貝的方法是有效。使用這種方法唯一不足而且難以解決的問題是,我們為此需要付出額外的工作,以實現拷貝構造函數,移動構造函數,拷貝賦值函數等。誠然,類似于ReSharper代碼生成器一類的工具可以為大多數場景快速生成代碼,但會產生很多警告。例如,我們編寫如下的嗲嗎,并且忘記了提供Address類的拷貝賦值函數的實現,會發生什么:

Contact john = worker;

是的, 程序仍然會通過編譯。如果提供了拷貝構造函數會更好一些,因為如果在沒有定義構造函數的情況下嘗試調用構造函數,程序將會出錯,然而賦值運算符 “=” 是普遍存在的。即使你沒有為賦值運算符提供特殊的定義和實現。
還有一個問題:假設使用類似二級指針的東西(例如 void **)或unique_str呢?即使它們各有獨特之處,但此時像ReSharper和Clion這樣的工具也不可能生成正確的代碼,所以使用工具為這些類型快速生成代碼也許不是一個好主意。


4.4 “虛”構造函數

拷貝構造函數使用之處相當有限,并且存在的一個問題是,為了對變量的深度拷貝。我們需要知道變量具體是那種類型。假設ExtendedAddress類繼承自Addressl類:

    class ExtendedAddress : public Address {public:std::string country;std::string postcode;ExtendedAddress(const std::string& street, const std::string& city, const int suite, const std::string& country,const std::string& postcode):Address(street, city, suite), country(country) {}ExtendedAddress* clone()override {return new ExtendedAddress(street, city, suite, country, postcode);}};

若我們要拷貝一個存在多態性質的變量:

ExtendedAddress ea = ...;
Address& a = ea;
// dow do you deep-copy 'a' ?

這樣的做法存在問題,因為我們并不知道變量a的最終派生類型時是么。由于最終派生類引發的問題,以及拷貝構造函數不能是虛函數。因此我們需要采用其他方法來創建對象的拷貝。
首先,我們以Address對象為例,引入一個虛函數clone(),然后,我們嘗試:

        virtual Address clone() {return Address{street, city, suite};}

不幸的是,這并不能解決繼承場景下的問題。請記住,對于派生對象,我們想返回的是ExtendedAddress類型。但上述代碼展示的接口將返回類型固定為Address。我們需要是指針形式的多態,因此再次嘗試:

        virtual Address* clone() {return new Address{street, city, suite};}

現在,我們可以在派生類中做同樣的事情,只不過要提供對應的返回類型:

        ExtendedAddress* clone()override {return new ExtendedAddress(street, city, suite, country, postcode);}

現在,我們可以安全放心的調用clone()函數,而不必擔心對象由于繼承體系被切割:

    void testVirtualConstructor() {std::cout << __FUNCTION__ <<"() begin.\n\n";ExtendedAddress ea{"123 East Dr", "London", 0, "UK", "SW101EG"};Address& a = ea; //upcastauto cloned = a.clone();printf("\n\nea: %s\n", typeid(ea).name()); // ExtendedAddressprintf("\n\na: %s\n", typeid(a).name()); // ExtendedAddressprintf("\n\ncloned: %s\n", typeid(cloned).name()); // Address*std::cout << __FUNCTION__ <<"() end.\n\n";}

現在,變量cloned的確是一個指向深度拷貝ExtendedAddress對象的指針了。當然,這個指針的類型是Address*,所以,如果我們需要額外的成員,則可以通過dynamic_cast進行轉換或者調用某些虛函數。
如果處于某些原因,我們想要使用拷貝構造函數,則clone()接口可以簡化為

        ExtendedAddress* clone()override {return new ExtendedAddress(*this);}

之后,所有的工作都可以在拷貝構造函數中完成。
使用clone()方法的不足之處是,編譯器并不會檢查整個繼承體系每個類中實現的clone()方法(并且也沒有強行進行檢查的方法)。例如,如果忘記在ExtendedAddress類中實現clone()方法,示例代納同樣可以通過編譯并且正常運行,但當調用clone()方法是, clone()將構造一個Address而不是ExtendedAddress。

4.5 序列化

其他編程語言的設計者也遇到同樣的問題,即必須對整個對象顯式定義拷貝操作,并很快意識到類需要“普通可序列化”—默認情況下,類應該可以直接寫入字符串和流,而不必使用任何額外的注釋(最多可能是一個或兩個屬性)來指定類或其成員。
這與我們正在討論的問題有關系嗎?當然有,如果可以將類對象序列化到文件或內存中,則可以再將其反序列化,并保留包括其所依賴的對象在內的所有信息。這樣,我們就不需要在通過顯式定義拷貝操作這種方式做處理獲得一個在某個對象基礎上的新對象。
遺憾的是,與其他語言不同的是,當提到序列化時,C++不提供免費的午餐。我們不能將復雜的對象序列化為文件。為什么不能?在其他編程語言中,編譯的二進制文件不僅包括可執行代碼,還包括大量的元數據,而序列化是通過一種反射的特性來實現的,目前這個在C++中是不支持的。
如果我們想要序列化,那么就像顯式拷貝操作一樣,我們需要自己實現它。幸運的是,我們可以使用名為Boost.Serialization的現成的庫來解決序列化的問題,而不用費勁的處理和思考序列化std::string的方法。
【注】由于暫時不使用Boost庫,序列化就看到這塊了,后面有需要在補充…


4.6 原型工廠

如果我們預定義了要拷貝的對象,那么我們會將它們保存在哪里?全局變量中嗎?或許吧!事實上,假設我們公司有主辦公室和備用辦公室,我們可以這樣聲明全局變量:

Contact main{"", new Address{"123 East Dr", "London", 0}};
Contact aux{"", new Address{"123B East Dr", "London", 0}};

我們可以將這些預定義的對象放在 Contact.h文件中, 任何使用Contact類的人都可以獲取這些全局變量并進行拷貝。但更明智的方法是使用某種專用的類來存儲原型,并基于所謂的原型,根據需要產生自定義拷貝。這將給我們帶來更多的靈活性。例如,我們可以定義工具函數,產生適當初始化的unique_ptr:

class EmployeeFactory {static Contact main;static Contact aux;static std::unique_ptr<Contact> NewEmployee(std::string name,int suite, Contact& proto) {auto result = std::make_unique<Contact>(proto); //這里會調用拷貝構造result->name = name;result->address->suite = suite;return result;}public:static std::unique_ptr<Contact> NewMainOfficeEmployee(std::string name , int suite) {return NewEmployee(name, suite, main);}static std::unique_ptr<Contact> NewAuxMainOfficeEmployee(std::string name, int suite) {return NewEmployee(name, suite, aux);}};

現在可以按如下方式使用:

    void testPrototypeFactory() {auto john = EmployeeFactory::NewMainOfficeEmployee("John Doe", 123);auto jane = EmployeeFactory::NewAuxMainOfficeEmployee("Jane Doe", 125);}

為什么要使用工廠呢?考慮這樣一種場景:我們從某個原型拷貝得到一個對象,但忘記自定義該對象的某些屬性,此時該對象的某些本該有具體參數值的參數將為0或者空字符串。如果使用之前討論的工廠,我們可以將所有非完全初始化的構造函數聲明為私有的,并且將EmployeeFactory聲明為friend class。現在,客戶將不再得到為完整構建的Contact對象。

4.7 總結

原型模式體現了對對象進行深度拷貝的概念,因此,不必每次都進行完全初始化,而是可以獲取一個預定義的對象,拷貝它,稍微修改它,然后獨立于原始的對象使用它。
在C++中,有兩種方式實現原型模式的方法,它們都需要手動操作:

  • 編寫正確拷貝原始對象的代碼,也就是執行深度拷貝的代碼。這項工作可以在拷貝構造函數 / 拷貝賦值運算符或者單獨的成員函數中完成。
  • 編寫支持序列化 / 反序列化的代碼,使用序列化 / 反序列化機制,在完成序列化后立即進行反序列化,由此完成復制。該方法會引入額外的開銷,是否使用這種方法取決于具體使用場景下的拷貝頻率。與使用拷貝構造函數相比,這種方法的唯一優點是可以不受限制地使用序列化功能。

不論選擇那種方法,有些工作是必須完成的。如果決定采取上述兩種方法的一種。則可采用一些代碼生成工具(比如,類似于ReShareper和CLion的集成開發環境)來輔助。
最后,別忘了,如果對所有數據采用按值存儲的方式,實際上并不會有問題,只需要operator=就夠了。

4.8 代碼

本章學習代碼

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

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

相關文章

超過 50% 的內部攻擊使用特權提升漏洞

特權提升漏洞是企業內部人員在網絡上進行未經授權的活動時最常見的漏洞&#xff0c;無論是出于惡意目的還是以危險的方式下載有風險的工具。 Crowdstrike 根據 2021 年 1 月至 2023 年 4 月期間收集的數據發布的一份報告顯示&#xff0c;內部威脅正在上升&#xff0c;而利用權…

基于SSM的劇本殺預約系統的設計與實現

末尾獲取源碼 開發語言&#xff1a;Java Java開發工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 數據庫&#xff1a;MySQL5.7和Navicat管理工具結合 服務器&#xff1a;Tomcat8.5 開發軟件&#xff1a;IDEA / Eclipse 是否Maven項目&#xff1a;是 目錄…

【第三屆】:“玄鐵杯”RISC-V應用創新大賽(基于yolov5和OpenCv算法 — 智能警戒哨兵)

文章目錄 前言 一、智能警戒哨兵是什么&#xff1f; 二、方案流程圖 三、硬件方案 四、軟件方案 五、演示視頻鏈接 總結 前言 最近參加了第三屆“玄鐵杯”RISC-V應用創新大賽&#xff0c;我的創意題目是基于 yolov5和OpenCv算法 — 智能警戒哨兵 先介紹一下比賽&#xf…

docker容器配置MySQL與遠程連接設置(純步驟)

以下為ubuntu20.04環境&#xff0c;默認已安裝docker&#xff0c;沒安裝的網上隨便找個教程就好了 拉去mysql鏡像 docker pull mysql這樣是默認拉取最新的版本latest 這樣是指定版本拉取 docker pull mysql:5.7查看已安裝的mysql鏡像 docker images通過鏡像生成容器 docke…

大數據HCIE成神之路之數據預處理(1)——缺失值處理

缺失值處理 1.1 刪除1.1.1 實驗任務1.1.1.1 實驗背景1.1.1.2 實驗目標1.1.1.3 實驗數據解析 1.1.2 實驗思路1.1.3 實驗操作步驟1.1.4 結果驗證 1.2 填充1.2.1 實驗任務1.2.1.1 實驗背景1.2.1.2 實驗目標1.2.1.3 實驗數據解析 1.2.2 實驗思路1.2.3 實驗操作步驟1.2.4 結果驗證 1…

【STM32】ADC模數轉換器

1 ADC簡介 ADC&#xff08;Analog-Digital Converter&#xff09;模擬-數字轉換器 ADC可以將引腳上連續變化的模擬電壓轉換為內存中存儲的數字變量&#xff0c;建立模擬電路到數字電路的橋梁 STM32是數字電路&#xff0c;只有高低電平&#xff0c;沒有幾V電壓的概念&#xff…

安裝 DevEco Studio 后不能用本地 Node.js 打開

安裝 DevEco Studio 后第一次打開時&#xff0c;不能用本地 Node.js 打開 答&#xff1a;因為本地 Node.js 文件夾名字中有空格 Node.js路徑只能包含字母、數字、“。”、“_”、“-”、“:”和“V” 解決方法&#xff1a; 1.修改文件夾名稱 2.重新下載 注意&#xff1a;找一…

Qt 通過命令行編譯程序

前言 從服務器拉代碼到編譯成可執行文件一個腳本解決問題。使用的項目文件見上一個文章 Qt生成動態鏈接庫并使用動態鏈接庫 腳本代碼 為了方便易懂這是一個很簡單的Qt編譯腳本 call E:\vs2015\VC\vcvarsall.bat x86 rmdir /s /q my-project git clone gitgitee.com:wenbai1…

【CF245H】Queries for Number of Palindromes(字符串區間dp)

Queries for Number of Palindromes - 洛谷 # Queries for Number of Palindromes ## 題面翻譯 題目描述 給你一個字符串s由小寫字母組成&#xff0c;有q組詢問&#xff0c;每組詢問給你兩個數&#xff0c;l和r&#xff0c;問在字符串區間l到r的字串中&#xff0c;包含多少…

1-3算法基礎-標準模板庫STL

1.pair pair用于存儲兩個不同類型的值&#xff08;元素&#xff09;作為一個單元。它通常用于將兩個值捆綁在一起&#xff0c;以便一起傳遞或返回。 #include <iostream> #include <utility> using namespace std; int main() {pair<int, string> person m…

TailwindCSS 多主題色配置

TailwindCSS 多主題色配置 現在大多數網站都支持主題色變換&#xff0c;比如切換深色模式。那么我們該如何進行主題色配置呢&#xff1f; tailwind dark tailwind 包含一個 dark變體&#xff0c;當啟用深色模式時&#xff0c;可以為網站設置不同樣式 <div class"bg-whi…

ThingWorx 9.2 Windows安裝

參考官方文檔安裝配置 1 PostgreSQL 13.X 2 Java, Apache Tomcat, and ThingWorx PTC Help Center 參考這里安裝 數據庫 C:\ThingworxPostgresqlStorage 設置為任何人可以full control 數據庫初始化 pgadmin4 創建用戶twadmin并記錄口令password Admin Userpostgres Thin…

漏刻有時百度地圖API實戰開發(9)Echarts使用bmap.js實現軌跡動畫效果

Bmap.js是Echarts和百度地圖相結合開發的一款JavaScript API&#xff0c;它可以幫助用戶在web應用中獲取包括地圖中心點、地圖縮放級別、地圖當前視野范圍、地圖上標注點等在內的地圖信息&#xff0c;并且支持在地圖上添加控件&#xff0c;提供包括智能路線規劃、智能導航(駕車…

C# WPF上位機開發(通訊協議的編寫)

【 聲明&#xff1a;版權所有&#xff0c;歡迎轉載&#xff0c;請勿用于商業用途。 聯系信箱&#xff1a;feixiaoxing 163.com】 作為上位機&#xff0c;它很重要的一個部分就是需要和外面的設備進行數據溝通的。很多時候&#xff0c;也就是在這個溝通的過程當中&#xff0c;上…

PyQt下使用OpenCV實現人臉檢測與識別

背景&#xff1a; 一 數字圖像處理與識別警務應用模型 基于前期所學知識&#xff0c;與公安實踐相結合&#xff0c;綜合設計數字圖像處理與識別警務應用模型,從下列4個研究課題中選擇2個進行實驗實現&#xff1a;圖像增強與復原、人臉檢測與識別、虹膜內外圓檢測與分割、車牌…

Html轉PDF,前端JS實現Html頁面導出PDF(html2canvas+jspdf)

Html轉PDF&#xff0c;前端JS實現Html頁面導出PDF&#xff08;html2canvasjspdf&#xff09; 文章目錄 Html轉PDF&#xff0c;前端JS實現Html頁面導出PDF&#xff08;html2canvasjspdf&#xff09;一、背景介紹二、疑問三、所使用技術html2canvasjspdf 四、展示開始1、效果展示…

C語言----文件操作(一)

一&#xff1a;C語言中文件的概念 對于文件想必大家都很熟悉&#xff0c;無論在windows上還是Linux中&#xff0c;我們用文件去存儲資料&#xff0c;記錄筆記&#xff0c;常見的如txt文件&#xff0c;word文檔&#xff0c;log文件等。那么&#xff0c;在C語言中文件是什么樣的存…

threadpool github線程池學習

參考項目 https://github.com/progschj/ThreadPool 源碼分析 // 常規頭文件保護宏, 避免重復 include #ifndef THREAD_POOL_H #define THREAD_POOL_H// 線程池, 存儲線程對象; #include <vector>// 任務隊列, 雙向都可操作隊列, queue 不能刪除首個元素 #include <…

微信小程序制作-背單詞的小程序制作

微信小程序–背單詞的 好久沒有發過文章了&#xff0c;但是不代表著我不去學習了嘍&#xff0c;以下是我最近做的東西&#xff0c;前端的UI由朋友設計的&#xff0c;目前這個是前端使用的是微信小程序后端是Python的一個輕量型框架&#xff0c;FastApi&#xff0c;嗯&#xff…

MyBatis 四大核心組件之 Executor 源碼解析

&#x1f680; 作者主頁&#xff1a; 有來技術 &#x1f525; 開源項目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 倉庫主頁&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 歡迎點贊…