模型與性能實(shí)戰(zhàn))
1. 項(xiàng)目概述從內(nèi)存管理的“泥潭”到智能指針的“救贖”在C的世界里摸爬滾打十幾年我見過太多因?yàn)閮?nèi)存管理不當(dāng)而引發(fā)的“血案”。從早期的new/delete手動管理到后來的auto_ptr一個充滿設(shè)計(jì)缺陷的嘗試再到如今現(xiàn)代CC11及以后提供的unique_ptr、shared_ptr和weak_ptr內(nèi)存管理這門“手藝”已經(jīng)發(fā)生了翻天覆地的變化。今天我們不談那些陳年舊事就聚焦于兩個最常用、也最容易用錯的“利器”unique_ptr和shared_ptr。這個標(biāo)題看似簡單但背后涉及的是資源所有權(quán)的哲學(xué)、對象生命周期的掌控以及如何避免內(nèi)存泄漏、懸空指針和多線程下的數(shù)據(jù)競爭等核心問題。很多開發(fā)者甚至一些有經(jīng)驗(yàn)的同行也只是停留在“會用”的層面對于“何時用”、“為何用”以及“如何正確用”缺乏深刻理解。這篇文章就是想把我在實(shí)際項(xiàng)目開發(fā)、代碼審查和性能調(diào)優(yōu)中積累的經(jīng)驗(yàn)和踩過的坑系統(tǒng)地梳理出來讓你不僅能寫出安全的代碼更能寫出高效、意圖清晰的代碼。2. 核心概念與所有權(quán)模型解析2.1 所有權(quán)的本質(zhì)誰負(fù)責(zé)“生殺大權(quán)”在討論智能指針之前我們必須先理解“所有權(quán)”這個概念。在C中一個動態(tài)分配的對象在堆上必須有一個明確的“所有者”來負(fù)責(zé)它的生命周期——即何時創(chuàng)建更重要的是何時銷毀。傳統(tǒng)的手動new/delete將所有權(quán)和責(zé)任完全交給了程序員這就像把一輛沒有剎車和方向盤的跑車交給一個新手出事故是大概率事件。智能指針的核心價值就是將這個所有權(quán)語義封裝在對象內(nèi)部利用RAIIResource Acquisition Is Initialization機(jī)制將資源的生命周期與對象的生命周期綁定。當(dāng)智能指針對象離開其作用域時其析構(gòu)函數(shù)會自動釋放所管理的資源。unique_ptr和shared_ptr代表了兩種最根本的所有權(quán)模型。獨(dú)占所有權(quán)Exclusive Ownership這是unique_ptr的哲學(xué)。一個資源在任何時刻有且僅有一個明確的所有者。所有者對資源擁有完全的控制權(quán)包括決定其何時銷毀。當(dāng)所有者unique_ptr被移動move時所有權(quán)也隨之轉(zhuǎn)移原所有者變?yōu)榭?。這模擬了現(xiàn)實(shí)世界中許多資源的獨(dú)占性比如文件句柄、互斥鎖、或者一個復(fù)雜引擎的核心組件。使用unique_ptr你的代碼意圖非常清晰“這個東西歸我管也只有我能管?!惫蚕硭袡?quán)Shared Ownership這是shared_ptr的哲學(xué)。一個資源可以被多個“所有者”共同持有。系統(tǒng)會通過引用計(jì)數(shù)來追蹤有多少個shared_ptr指向同一個資源。只有當(dāng)最后一個指向該資源的shared_ptr被銷毀或重置時資源才會被釋放。這適用于那些生命周期不明確、需要被多個上下文共享的對象比如緩存中的數(shù)據(jù)、UI組件樹中的子節(jié)點(diǎn)、或者觀察者模式中的被觀察者。注意所有權(quán)模型的選擇是設(shè)計(jì)問題而非技術(shù)細(xì)節(jié)。選錯了模型后續(xù)會帶來一系列復(fù)雜性和性能問題。一個簡單的判斷方法是如果你能清晰地回答“這個對象應(yīng)該由誰負(fù)責(zé)銷毀”并且答案是唯一的那么unique_ptr通常是更好的選擇。2.2 unique_ptr輕量、高效的所有權(quán)載體std::unique_ptr是一個獨(dú)享所有權(quán)的智能指針。它不能被復(fù)制只能被移動。這意味著在任何時間點(diǎn)只有一個unique_ptr實(shí)例擁有對某個對象的所有權(quán)。當(dāng)這個unique_ptr被銷毀例如離開作用域它所擁有的對象也會被自動銷毀。核心特性與內(nèi)部機(jī)制零開銷抽象在典型的實(shí)現(xiàn)中unique_ptr的大小等同于一個原生指針并且大多數(shù)操作如解引用* 訪問成員-沒有任何運(yùn)行時開銷。它的高效性源于其簡單的所有權(quán)模型。自定義刪除器unique_ptr允許你指定一個自定義刪除器Deleter這是一個在釋放資源時被調(diào)用的函數(shù)或函數(shù)對象。這對于管理非new分配的資源如malloc,fopen,SDL_CreateWindow至關(guān)重要。刪除器是unique_ptr類型的一部分這允許編譯器進(jìn)行深度優(yōu)化。// 使用 lambda 表達(dá)式作為自定義刪除器 auto FileDeleter [](FILE* fp) { if(fp) fclose(fp); }; std::unique_ptrFILE, decltype(FileDeleter) filePtr(fopen(data.txt, r), FileDeleter); // 管理數(shù)組不推薦優(yōu)先使用std::vector或std::array std::unique_ptrint[] arrayPtr(new int[10]);與STL容器的完美配合由于unique_ptr是可移動的它可以安全地存儲在std::vector、std::map等STL容器中用于管理容器內(nèi)動態(tài)分配的元素從而避免容器析構(gòu)時的內(nèi)存泄漏。使用場景與最佳實(shí)踐工廠函數(shù)返回值工廠函數(shù)返回一個unique_ptr明確地將資源的所有權(quán)轉(zhuǎn)移給調(diào)用者。std::unique_ptrWidget createWidget() { return std::make_uniqueWidget(/* args */); } auto myWidget createWidget(); // 所有權(quán)轉(zhuǎn)移至myWidget作為類的成員變量當(dāng)某個類獨(dú)占某個資源時使用unique_ptr作為成員。這明確了類的資源管理責(zé)任并保證了在類對象析構(gòu)時資源會被正確釋放。在函數(shù)參數(shù)中傳遞所有權(quán)如果函數(shù)需要取得某個資源的所有權(quán)即消費(fèi)這個資源使用unique_ptr作為參數(shù)并按值傳遞通過移動。void processResource(std::unique_ptrResource res) { // 函數(shù)內(nèi)部擁有res的所有權(quán) } auto res std::make_uniqueResource(); processResource(std::move(res)); // 轉(zhuǎn)移所有權(quán)此后res為空2.3 shared_ptr共享所有權(quán)的利器與潛在陷阱std::shared_ptr通過引用計(jì)數(shù)實(shí)現(xiàn)了共享所有權(quán)。多個shared_ptr可以指向同一個對象系統(tǒng)會維護(hù)一個控制塊control block其中包含引用計(jì)數(shù)和弱引用計(jì)數(shù)等元數(shù)據(jù)。核心特性與內(nèi)部機(jī)制引用計(jì)數(shù)每次拷貝構(gòu)造或拷貝賦值一個shared_ptr其所指對象的引用計(jì)數(shù)加1。每次一個shared_ptr被銷毀離開作用域或被重置引用計(jì)數(shù)減1。當(dāng)引用計(jì)數(shù)降為0時管理對象被銷毀內(nèi)存被釋放??刂茐K開銷shared_ptr的大小通常是兩個原生指針一個指向?qū)ο笠粋€指向控制塊。控制塊是動態(tài)分配的這帶來了額外的內(nèi)存開銷和間接訪問的成本。線程安全shared_ptr的引用計(jì)數(shù)操作是原子的atomic因此從多個線程拷貝/銷毀指向同一對象的shared_ptr是線程安全的。但這不意味著其所指向的對象本身是線程安全的你仍然需要額外的同步機(jī)制如互斥鎖來保護(hù)對象內(nèi)部的數(shù)據(jù)。循環(huán)引用問題這是shared_ptr最著名的陷阱。如果兩個或多個對象通過shared_ptr互相引用形成一個環(huán)那么它們的引用計(jì)數(shù)永遠(yuǎn)無法降到0導(dǎo)致內(nèi)存泄漏。struct Node { std::shared_ptrNode next; // std::shared_ptrNode prev; // 如果也是shared_ptr則與next形成循環(huán)引用 std::weak_ptrNode prev; // 正確的做法將其中一個改為weak_ptr };使用場景與最佳實(shí)踐共享緩存數(shù)據(jù)多個模塊或線程需要讀取同一份緩存數(shù)據(jù)其生命周期由所有使用者共同決定。觀察者模式多個觀察者shared_ptrObserver訂閱一個主題shared_ptrSubject主題持有觀察者的shared_ptr以通知它們。這里需要仔細(xì)設(shè)計(jì)生命周期通常主題持有的是weak_ptrObserver以避免觀察者無法被釋放。圖形界面組件樹父節(jié)點(diǎn)持有子節(jié)點(diǎn)的shared_ptr同時子節(jié)點(diǎn)可能需要反向引用父節(jié)點(diǎn)此時應(yīng)使用weak_ptr或原生指針。優(yōu)先使用std::make_sharedstd::make_sharedWidget(args...)在單次內(nèi)存分配中同時分配對象和控制塊這比先new Widget再構(gòu)造shared_ptr更高效且能避免潛在的異常安全問題。3. unique_ptr與shared_ptr的深度對比與選型指南3.1 性能開銷對比選擇哪種智能指針性能是一個重要的考量因素。下面的表格從幾個關(guān)鍵維度進(jìn)行了對比特性維度std::unique_ptrTstd::shared_ptrT分析與建議內(nèi)存開銷通常為一個指針大小例如64位系統(tǒng)上為8字節(jié)。無額外控制塊。通常為兩個指針大小例如16字節(jié)。外加一個動態(tài)分配的控制塊包含引用計(jì)數(shù)、弱引用計(jì)數(shù)、刪除器等。unique_ptr在內(nèi)存占用上有絕對優(yōu)勢尤其當(dāng)需要創(chuàng)建大量對象時。時間開銷創(chuàng)建/拷貝構(gòu)造和移動開銷極低等同于原生指針操作。不支持拷貝。構(gòu)造尤其是make_shared涉及控制塊分配和初始化??截愋枰硬僮餍薷囊糜?jì)數(shù)這在多線程環(huán)境下雖安全但有開銷。對于頻繁拷貝或傳遞的場景shared_ptr的原子操作可能成為性能瓶頸。unique_ptr的移動操作成本極低。時間開銷訪問解引用*,-是直接的內(nèi)存訪問零開銷。解引用需要先通過指針訪問對象與unique_ptr相同。但因其更大的尺寸和可能更差的局部性對緩存可能不那么友好。兩者訪問開銷在理論上相同但unique_ptr因結(jié)構(gòu)簡單在極端優(yōu)化場景下可能更優(yōu)。適用場景明確、單一的所有權(quán)關(guān)系。工廠模式、獨(dú)占資源、PImpl慣用法、作為移動語義的載體。共享所有權(quán)生命周期由多個上下文共同管理。緩存、觀察者、共享數(shù)據(jù)結(jié)構(gòu)節(jié)點(diǎn)。默認(rèn)首選unique_ptr。僅在所有權(quán)必須共享時才考慮shared_ptr。實(shí)操心得在性能敏感的系統(tǒng)如游戲引擎、高頻交易系統(tǒng)中我們會對容器內(nèi)成千上萬個實(shí)體使用unique_ptr來管理。只有在確有必要時比如一個全局的資源管理器才會引入shared_ptr。曾經(jīng)在一個服務(wù)模塊中將某個核心數(shù)據(jù)結(jié)構(gòu)的成員從shared_ptr改為unique_ptr并結(jié)合移動語義在壓力測試下帶來了近15%的吞吐量提升主要就是減少了原子操作的爭用。3.2 所有權(quán)語義與代碼清晰度智能指針的選擇極大地影響了代碼的可讀性和設(shè)計(jì)意圖的表達(dá)。unique_ptr傳達(dá)清晰意圖當(dāng)你看到一個函數(shù)接收unique_ptr參數(shù)時你立刻知道這個函數(shù)將接管資源的所有權(quán)。當(dāng)你看到一個類擁有unique_ptr成員時你知道這個類獨(dú)占該資源。這種明確性減少了代碼閱讀者的心智負(fù)擔(dān)也使得資源生命周期更容易推理。class Renderer { std::unique_ptrGraphicsDevice device_; // Renderer獨(dú)占這個設(shè)備 public: explicit Renderer(std::unique_ptrGraphicsDevice device) : device_(std::move(device)) {} // 構(gòu)造函數(shù)接管所有權(quán) };shared_ptr可能模糊邊界過度使用shared_ptr會導(dǎo)致“所有權(quán)無處不在又仿佛 nowhere”的局面。很難確定最后一個引用在哪里被釋放這使得調(diào)試內(nèi)存泄漏和生命周期問題變得復(fù)雜。它容易誘導(dǎo)開發(fā)者進(jìn)行“懶惰設(shè)計(jì)”——當(dāng)不確定所有權(quán)歸屬時就扔一個shared_ptr了事。選型決策流程圖問這個資源是否天然只有一個所有者如果是unique_ptr。問如果不止一個這些所有者之間的關(guān)系是否明確能否確定一個主所有者其他用觀察者如裸指針或引用或弱引用weak_ptr如果能優(yōu)先考慮unique_ptrweak_ptr/裸指針的方案。問資源生命周期是否真的由多個完全獨(dú)立的、無法協(xié)調(diào)的上下文決定只有當(dāng)這個問題的答案是肯定的時才應(yīng)該使用shared_ptr。注意在模塊或組件接口中使用shared_ptr作為參數(shù)或返回值相當(dāng)于將共享所有權(quán)的決定權(quán)強(qiáng)加給了所有調(diào)用者。這限制了實(shí)現(xiàn)的靈活性。更好的做法是在接口中使用unique_ptr或裸指針/引用傳遞所有權(quán)或觀察權(quán)在模塊內(nèi)部根據(jù)需要使用shared_ptr進(jìn)行管理。3.3 與weak_ptr的協(xié)同打破循環(huán)引用的鑰匙std::weak_ptr總是與shared_ptr相伴而生。它指向一個由shared_ptr管理的對象但不增加其引用計(jì)數(shù)。你可以將weak_ptr視為對共享資源的一個“臨時觀察許可”它不能直接訪問資源必須通過調(diào)用lock()方法嘗試提升promote為一個shared_ptr。如果此時原始對象還存在引用計(jì)數(shù)0則提升成功返回一個有效的shared_ptr同時增加引用計(jì)數(shù)否則返回一個空的shared_ptr。核心用途打破循環(huán)引用如前文Node例子所示在雙向鏈表、樹形結(jié)構(gòu)或任何可能形成引用環(huán)的場景中將其中一個方向的引用改為weak_ptr。緩存緩存中持有對象的weak_ptr。當(dāng)需要訪問緩存對象時嘗試lock()。如果對象還在被其他部分使用則直接使用如果對象已被釋放則重新加載并創(chuàng)建新的shared_ptr放入緩存。這實(shí)現(xiàn)了緩存的自動清理。避免懸掛指針相比于直接存儲原生指針或引用weak_ptr提供了一種安全的方式來訪問可能已被釋放的共享對象。lock()操作是原子的提供了線程安全檢查。使用模式示例class ExpensiveObject { /* ... */ }; class Cache { std::unordered_mapint, std::weak_ptrExpensiveObject cache_; std::mutex mutex_; public: std::shared_ptrExpensiveObject get(int key) { std::lock_guardstd::mutex lock(mutex_); auto it cache_.find(key); if (it ! cache_.end()) { // 嘗試從weak_ptr提升 if (auto sp it-second.lock()) { return sp; // 緩存命中且對象仍存活 } else { // 對象已被釋放從緩存中移除無效條目 cache_.erase(it); } } // 緩存未命中或失效創(chuàng)建新對象 auto obj std::make_sharedExpensiveObject(key); cache_[key] obj; // 存儲weak_ptr return obj; } };4. 高級用法、陷阱與性能優(yōu)化實(shí)錄4.1 自定義刪除器與復(fù)雜資源管理智能指針的強(qiáng)大之處在于它能管理任何類型的資源只要你提供正確的刪除器。unique_ptr的自定義刪除器刪除器是類型的一部分。這允許編譯器進(jìn)行內(nèi)聯(lián)等優(yōu)化但也會使持有不同刪除器的unique_ptr成為不同類型。// 管理動態(tài)數(shù)組C17后std::unique_ptrT[]有特化更推薦 struct ArrayDeleter { void operator()(int* p) const { delete[] p; } }; std::unique_ptrint, ArrayDeleter arr(new int[100]); // 管理SDL窗口 struct SDLWindowDeleter { void operator()(SDL_Window* w) const { if(w) SDL_DestroyWindow(w); } }; using SDLWindowPtr std::unique_ptrSDL_Window, SDLWindowDeleter;shared_ptr的自定義刪除器刪除器存儲在控制塊中不是類型的一部分。因此擁有不同刪除器的shared_ptr仍然是同一類型可以相互賦值。這提供了更大的靈活性但可能犧牲一些優(yōu)化機(jī)會。// 使用shared_ptr管理內(nèi)存映射文件 void* mappedData mmap(...); std::shared_ptrvoid sp(mappedData, [](void* p) { if(p) munmap(p, ...); }); // 管理數(shù)據(jù)庫連接 std::shared_ptrsqlite3 dbConn(nullptr, [](sqlite3* conn) { if(conn) sqlite3_close(conn); }); if(sqlite3_open(db.sqlite, rawPtr) SQLITE_OK) { dbConn.reset(rawPtr); // 重置shared_ptr使用自定義刪除器 }實(shí)操心得對于unique_ptr如果刪除器是無狀態(tài)的如函數(shù)指針、無捕獲的lambda其大小不會增加。對于有狀態(tài)的刪除器如帶捕獲的lambdaunique_ptr可能需要額外空間存儲它。而shared_ptr的刪除器始終存儲在控制塊中。在管理非內(nèi)存資源時務(wù)必確保刪除器正確處理了資源可能為nullptr的情況。4.2 多線程環(huán)境下的安全使用unique_ptr一個unique_ptr實(shí)例本身不是線程安全的。同時從多個線程移動、重置或訪問同一個unique_ptr對象會導(dǎo)致數(shù)據(jù)競爭。但是多個線程可以安全地訪問各自擁有的、指向不同對象的unique_ptr。如果需要在線程間傳遞所有權(quán)必須通過鎖或其他同步機(jī)制來保護(hù)unique_ptr的移動操作。shared_ptr引用計(jì)數(shù)操作是原子的、線程安全的。這是指控制塊內(nèi)的引用計(jì)數(shù)增減操作本身。多個線程同時讀寫同一個shared_ptr實(shí)例例如同時進(jìn)行拷貝賦值是不安全的需要外部同步。指向的對象本身不是線程安全的。shared_ptr只保證了引用計(jì)數(shù)的安全不保證*sp或sp-member的訪問安全。你必須使用互斥鎖等機(jī)制來保護(hù)被管理對象的內(nèi)部狀態(tài)。weak_ptr::lock()是線程安全的。它原子地檢查引用計(jì)數(shù)并可能提升。一個常見的多線程陷阱// 錯誤示例看似安全的“雙重檢查鎖定”在shared_ptr下可能失效 std::shared_ptrConfig globalConfig; std::mutex configMutex; std::shared_ptrConfig getConfig() { if (!globalConfig) { // 第一次檢查無鎖線程不安全 std::lock_guardstd::mutex lock(configMutex); if (!globalConfig) { globalConfig std::make_sharedConfig(...); } } return globalConfig; // 返回拷貝引用計(jì)數(shù)安全增加 }問題在于第一次無鎖的if (!globalConfig)檢查可能讀到另一個線程正在構(gòu)造globalConfig過程中的中間狀態(tài)比如對象已分配但構(gòu)造函數(shù)未完成。正確的做法是始終使用鎖保護(hù)對globalConfig變量的讀寫或者使用std::call_once、局部靜態(tài)變量C11以后線程安全等機(jī)制。4.3 性能優(yōu)化與常見陷阱排查1. 避免從this指針創(chuàng)建shared_ptr如果類本身被shared_ptr管理在成員函數(shù)內(nèi)直接shared_ptrT(this)會創(chuàng)建一個新的、獨(dú)立的控制塊導(dǎo)致同一對象被多個控制塊管理最終會被重復(fù)釋放雙殺。解決方案是讓類繼承自std::enable_shared_from_thisT然后使用shared_from_this()成員函數(shù)來獲取當(dāng)前對象的shared_ptr。class Widget : public std::enable_shared_from_thisWidget { public: void process() { // auto badPtr std::shared_ptrWidget(this); // 災(zāi)難 auto goodPtr shared_from_this(); // 正確返回與已有控制塊關(guān)聯(lián)的shared_ptr // ... 將goodPtr傳遞給其他需要共享所有權(quán)的地方 } }; // 注意必須在對象已經(jīng)被一個shared_ptr管理之后才能調(diào)用shared_from_this()。 auto w std::make_sharedWidget(); w-process(); // 此時內(nèi)部調(diào)用shared_from_this()是安全的2.shared_ptr的構(gòu)造開銷與make_shared的優(yōu)勢std::make_shared通常比直接使用new然后構(gòu)造shared_ptr更優(yōu)單次分配make_shared將對象和控制塊分配在單塊連續(xù)內(nèi)存中提高了內(nèi)存局部性減少了內(nèi)存分配器調(diào)用次數(shù)。異常安全考慮函數(shù)調(diào)用foo(std::shared_ptrT(new T), std::shared_ptrU(new U))編譯器可能以任意順序求值new T、new U、構(gòu)造shared_ptr。如果new T成功但new U拋出異常那么T對象已分配但未被任何shared_ptr管理導(dǎo)致泄漏。make_shared將分配和構(gòu)造包裝成一個原子操作避免了這個問題。潛在缺點(diǎn)由于對象和控制塊內(nèi)存綁定即使所有shared_ptr都被銷毀只要還有weak_ptr存在弱引用計(jì)數(shù)0這塊內(nèi)存包含對象和控制塊就不能被整體釋放因?yàn)榭刂茐K需要存活以供weak_ptr查詢。對象占用的內(nèi)存會延遲釋放。3. 循環(huán)引用檢測與調(diào)試循環(huán)引用是shared_ptr最隱蔽的泄漏源。排查方法包括代碼審查仔細(xì)檢查所有shared_ptr成員變量特別是存在雙向關(guān)聯(lián)的類如父子節(jié)點(diǎn)、觀察者與被觀察者。將其中非所有權(quán)的引用改為weak_ptr或裸指針。使用工具Valgrind、AddressSanitizer等內(nèi)存檢測工具可以幫助發(fā)現(xiàn)泄漏但定位循環(huán)引用點(diǎn)可能仍需分析。弱引用分析如果懷疑某個對象泄漏可以在其析構(gòu)函數(shù)中添加日志或者使用自定義刪除器來記錄銷毀事件。如果析構(gòu)函數(shù)從未被調(diào)用但程序邏輯上已不再需要該對象則很可能存在循環(huán)引用。結(jié)構(gòu)化設(shè)計(jì)從根本上避免循環(huán)引用。明確資源的所有權(quán)層級使用“單向擁有”原則。在必須存在反向引用時嚴(yán)格使用weak_ptr。4. 不要濫用shared_ptr作為函數(shù)參數(shù)如果函數(shù)只需要訪問對象而不需要共享所有權(quán)或延長其生命周期那么應(yīng)該傳遞裸指針T*或引用T或者傳遞const版本。傳遞shared_ptr會不必要地增加引用計(jì)數(shù)原子操作影響性能并模糊了接口的意圖。// 不好的接口暗示函數(shù)可能需要共享所有權(quán)或延長生命周期 void process(const std::shared_ptrData data); // 好的接口明確表示函數(shù)只是觀察/使用數(shù)據(jù)不管理其生命周期 void process(const Data* data); void process(const Data data); // 如果確定對象一定存在且需要可修改用 Data* // 如果確定對象一定存在且只讀用 const Data 或 const Data*在我經(jīng)歷的一個大型分布式系統(tǒng)中初期大量接口使用了shared_ptr作為參數(shù)導(dǎo)致性能分析時發(fā)現(xiàn)引用計(jì)數(shù)的原子操作占據(jù)了相當(dāng)可觀的CPU時間。后來我們進(jìn)行了一輪大規(guī)模重構(gòu)將只讀接口改為傳遞const 將需要可選對象的接口改為傳遞裸指針系統(tǒng)整體延遲下降了約5%。這個教訓(xùn)讓我深刻意識到即使是一個小小的指針傳遞選擇在宏觀尺度上也會產(chǎn)生顯著影響。