整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          真正的 C++ 殺手不是 Rust

          C++ 已經(jīng)死了 80%?”本文作者已經(jīng)使用 C++ 18 年了,他在體驗了數(shù)十門編程語言后,他指出,盡管 C++ 在過去幾十年中一直是程序員最常用的編程語言之一,但它存在一些問題,如不安全、效率低、浪費(fèi)程序員的精力等。因此,文章探討了一些可能會取代 C++ 的語言和技術(shù),包括 Spiral、Numba 和 ForwardCom 等,并分別對它們進(jìn)行了詳細(xì)的介紹。

          原文鏈接:https://wordsandbuttons.online/the_real_cpp_killers.html

          以下為譯文:

          我是 C++ 粉,已經(jīng)用 C++ 寫了 18 年代碼,而在這 18 年里,我一直在努力擺脫 C++。

          一切始于 2005 年末的一個三維空間模擬引擎。該引擎具備了當(dāng)時 C++ 所有的特性,三星指針、八層依賴關(guān)系,以及無處不在的 C 風(fēng)格的宏。還有一些匯編代碼片段,Stepanov 風(fēng)格的迭代器,以及 Alexandrescu 風(fēng)格的元編碼。總之是應(yīng)有盡有。那么,為什么呢?

          因為這款引擎前后歷時 8 年的時間,經(jīng)手了 5 個不同的團(tuán)隊。每個團(tuán)隊都把自己喜歡的時髦技術(shù)帶到了項目中,用時髦的包裝方式包裹舊代碼,但真正為引擎本身添加的價值卻很少。

          起初,我認(rèn)真地嘗試?yán)斫饷恳惶幮〖?xì)節(jié),但在碰了一鼻子灰之后,我放棄了。我還是老老實實完成任務(wù),改 bug 吧。不能說我的工作效率很高,只能說很勉強(qiáng),不至于被解雇。但后來我的老板問我:“你想把部分匯編代碼改成 GLSG 嗎?”雖然我并不了解GLSL是什么,但我覺得總不至于還不如 C++ 吧,于是我答應(yīng)了。結(jié)果確實不至于還不如 C++。

          后來,大部分的時間里我仍在用 C++ 寫代碼,但每當(dāng)有人問我:“你想不想嘗試一些非 C++ 的工作?”我就會說:“當(dāng)然!”然后我就會去做。我寫過 C89、MASM32、C#、PHP、Delphi、ActionScript、JavaScript、Erlang、Python、Haskell、D、Rust,以及令人聞風(fēng)喪膽的腳本語言 InstallShield。我甚至還寫過 VisualBasic、bash,以及幾種不能公開談?wù)摰膶S姓Z言。我甚至編寫過自己的語言,我寫了一個簡單的 Lisp 風(fēng)格解釋器,幫助游戲設(shè)計師自動加載資源,然后去度假了。回來后發(fā)現(xiàn)他們用這個解釋器編寫了整個游戲場景,所以在接下來的一段時間里我們必須支持這個解釋器。

          在過去的 17 年里,我一直在努力擺脫 C++,但每次嘗試過新技術(shù)后,總是會回到 C++。盡管如此,我仍然認(rèn)為使用 C++ 編寫程序是一個壞習(xí)慣。這門語言并不安全,效率也達(dá)不到人們的期望,而且程序員需要在與軟件制作毫無關(guān)系的工作上浪費(fèi)大量精力。你知道在 MSVC 中 uint16_t(50000) + uint16_t(50000) == -1794967296 嗎?你知道為什么嗎?你的看法與我不謀而合。

          我認(rèn)為,作為一名長期使用 C++ 的程序員,我有責(zé)任勸誡年輕一代程序員不要將 C++ 作為自己的專攻語言,就像有不良嗜好的人有責(zé)任勸誡不要重蹈覆轍。

          那么,為什么我無法放棄 C++ 呢?問題出在哪里?問題在于,所有的編程語言,尤其是那些所謂的“C++ 殺手”,真正帶來的優(yōu)勢都未能超越 C++。這些新語言大多會從一定程度上約束程序員。這本身沒什么問題,畢竟當(dāng)年晶體管密度每 18 個月翻一番,而程序員的數(shù)量每 5 年才翻一番,糟糕的程序員寫不出優(yōu)秀的代碼也并不是什么大問題。

          如今,我們生活在 21 世紀(jì)。經(jīng)驗豐富的程序員數(shù)量超過了歷史任何時期,而且我們更需要高效的軟件。

          上個世紀(jì),編寫軟件很簡單。你有一個想法,然后將其包裝成 UI,再作為桌面系統(tǒng)軟件產(chǎn)品出售就可以了。運(yùn)行速度太慢?沒人在乎!18 個月內(nèi),臺式機(jī)的速度就會翻倍。重要的是進(jìn)入市場,打開銷路,而且還沒有 bug。當(dāng)然,如果編譯器能防止程序員犯錯就更好了,因為 bug 不但不會產(chǎn)生收益,而且你還要付錢給程序員改 bug。

          而如今情況大不相同了。你有一個想法,然后將其包裝到 Docke 容器中,并在云中運(yùn)行。如今想獲取收入,你的軟件就必須為用戶解決問題。即使一款產(chǎn)品只做一件事,但只要做的正確,就能獲得報酬。你不必為了銷售新版本的產(chǎn)品而不斷擴(kuò)充功能。相反,如果你的代碼發(fā)揮不了真正的作用,買單的就是你自己。云賬單就能真實地反映出你的程序是否真的起作用。

          因此,在新的環(huán)境下,你需要的功能更少,但所有的功能都需要更出色的性能。

          在這個前提下你就會發(fā)現(xiàn),所有的“C++ 殺手”,甚至是我由衷喜歡和尊敬的 Rust、Julia 和 D,也沒有解決 21 世紀(jì)的問題。它們?nèi)匀煌A粼谏蟼€世紀(jì)。雖然這些語言可以幫助你編寫更多功能,而且 bug 更少,但當(dāng)你需要從租用的硬件中壓榨出最后一點 FLOPS 時,它們就沒有太大用處了。

          因此,這些語言只不過是比 C++ 更具競爭優(yōu)勢,或者說彼此之間可以競爭。但大多數(shù)編程語言,例如 Rust、Julia 和 Cland,甚至共享同一個后端。所有賽車手都坐在同一輛車上,何談?wù)l能贏得比賽呢?

          那么,究竟哪些技術(shù)比 C++ 或者傳統(tǒng)的預(yù)先編譯器更有優(yōu)勢呢?

          C++的頭號殺手:Spiral

          在討論 Spiral 之前,讓我先來考考你。你覺得以下哪個版本的代碼運(yùn)行速度更快?版本1:標(biāo)準(zhǔn)的 C++ 正弦函數(shù);版本2:由4個多項式模型組成的正弦函數(shù)?

          下一個問題。以下哪個版本的代碼運(yùn)行速度更快?版本1:使用短路邏輯運(yùn)算;版本2:將邏輯表達(dá)式轉(zhuǎn)換為算術(shù)表達(dá)式?

          第三個問題,以下哪個版本的三元組排序更快?版本1:帶有分支的交換排序;版本2:無分支的索引排序?

          如果你果斷地回答了以上所有問題,甚至沒有思考或上網(wǎng)搜索,那么只能說你被自己的直覺騙了。你沒有發(fā)現(xiàn)陷阱嗎?在沒有上下文的情況下,這些問題都沒有確定的答案。

          1. 如果使用 clang 11 和 -O2 -march=native 構(gòu)建,在英特爾Core i7-9700F 上運(yùn)行,多項式模型比標(biāo)準(zhǔn)正弦快 3 倍。但如果使用 NVCC 和 --use-fast-math 構(gòu)建,在GeForce GTX 1050 Ti Mobile 上運(yùn)行,標(biāo)準(zhǔn)正弦比多項式模型快10 倍。
          2. 在 i7 上,如果將短路邏輯替換為向量化算術(shù),可以將代碼的運(yùn)行速度提高一倍。但在 ARMv7 上,使用 clang 和-O2,標(biāo)準(zhǔn)邏輯比微優(yōu)化快 25%。
          3. 對于索引排序與交換排序,在英特爾上,索引排序比交換排序快 3 倍;而在 GeForce 上,交換排序比索引排序快 3 倍。

          因此,我們喜愛的微優(yōu)化都有可能將代碼的運(yùn)行提升3倍,也有可能導(dǎo)致速度下降90%。這完全取決于上下文。如果編譯器能為我們選擇最佳替代方案,那該多好,例如,當(dāng)我們切換構(gòu)建目標(biāo)時,索引排序會神奇地變成交換排序。但可惜編譯器做不到。

          1. 即使我們允許編譯器將正弦函數(shù)換成多項式模型,用犧牲精度的代價換取速度,它也不清楚我們的目標(biāo)精度。在 C++ 中,我們無法表達(dá):“此函數(shù)允許有誤差”。我們只有--use-fast-math之類的編譯器標(biāo)志,而且只在翻譯單元的范圍內(nèi)。
          2. 在第二個示例中,編譯器不知道我們的值僅限于 0 或 1,而且也不可能提出可以實施的優(yōu)化。雖然我們可以通過布爾類型來暗示,但這又是另一個問題了。
          3. 在第三個示例中,兩段代碼完全不同,編譯器無法將二者視為等效代碼。代碼描寫了太多細(xì)節(jié)。如果只有 std::sort,就可以給編譯器更多自由選擇算法的空間。但它不會選擇索引排序或交換排序,因為這兩種算法處理大型數(shù)組的效率都很低,而 std::sort 適合通用可迭代容器。

          此處就不得不提到 Spiral 了。該語言是卡內(nèi)基梅隆大學(xué)和蘇黎世聯(lián)邦理工學(xué)院的聯(lián)合項目。簡單來說,信號處理專家厭倦了每出現(xiàn)一種新硬件就需要手動重寫他們喜歡的算法,因此編寫了一個可自動完成這項工作的程序。該程序接受算法的高級描述和硬件架構(gòu)的詳細(xì)描述,并優(yōu)化代碼,直到在指定的硬件上實現(xiàn)最高效的算法。

          與 Fortran 等語言不同,Spiral 真正解決了數(shù)學(xué)意義上的優(yōu)化問題。它將運(yùn)行時定義為目標(biāo)函數(shù),并在受硬件架構(gòu)限制的可變因素空間內(nèi)尋找全局最優(yōu)實現(xiàn)。編譯器永遠(yuǎn)無法真正實現(xiàn)這種優(yōu)化。

          編譯器不會尋找真正的最優(yōu)解。它只不過是根據(jù)程序員所教的啟發(fā)式規(guī)則來優(yōu)化代碼。實質(zhì)上,編譯器并不是一個尋找最優(yōu)解的機(jī)器,更像一個匯編程序員。一個好的編譯器就像一個好的匯編程序員,僅此而已。

          Spiral是一個研究項目,范圍和預(yù)算都很有限。但最后展現(xiàn)的結(jié)果卻很驚人。在快速傅里葉變換中,他們的解決方案明顯優(yōu)于 MKL 和 FFTW 的實現(xiàn),他們的代碼速度約快了 2 倍,即使在英特爾上也是如此。

          為了突顯如此宏大的成就,需要說明一下,MKL 是英特爾自己的數(shù)學(xué)內(nèi)核庫(Math Kernel Library,簡稱MKL),因此他們非常了解如何充分利用自家的硬件。而WWTF(Fastest Fourier Transform in the West,西部最快傅里葉變換)是一種高度專業(yè)化的庫,由最了解該算法的人編寫。二者都是各自領(lǐng)域的冠軍,而 Spiral 的速度能夠達(dá)到二者兩倍,這實在太不可思議了。

          等到 Spiral 使用的優(yōu)化技術(shù)最終成熟并商業(yè)化,不僅僅是 C++,包括 Rust、Julia,甚至 Fortran 都將面臨前所未有的競爭壓力。既然能使用高級算法描述語言編寫2倍速的代碼,誰還會使用C++呢?

          C++ 殺手之二:Numba

          相信你很熟悉這門優(yōu)秀的編程語言。幾十年來,大多數(shù)程序員來說最熟悉的語言一直是 C。在 TIOBE 指數(shù)中,C語言一直名列第一,其他類似 C 的語言占據(jù)了前十名。然而,兩年前,一件前所未聞的事情發(fā)生了,C 語言第一名的地位不保。

          取而代之的語言是Python。90年代,沒有人看好Python,因為它不過是眾多腳本語言中的一個。

          有人會說:“Python很慢”,但這種說法很荒謬,就像說手風(fēng)琴或平底鍋很慢一樣,語言本身沒有快慢之分。就像手風(fēng)琴的速度取決于演奏者一樣,語言的快慢取決于編譯器的速度。

          可能還會有人說:“Python不是一種編譯語言”,這個說法也不嚴(yán)謹(jǐn)。Python 編譯器有很多,其中一個最被看好的編譯器也算是Python腳本。我來解釋一下。

          我曾經(jīng)有一個項目,是一個3D打印模擬,最初是用Python編寫的,后來“為了性能”改用C++重寫,后來又移植到 GPU 上,當(dāng)然這些都是在我進(jìn)入項目之前發(fā)生的事兒。后來,我花了幾個月的時間將構(gòu)建遷移到 Linux,優(yōu)化了 Tesla M60 的 GPU 代碼,因為這是當(dāng)時AWS中最便宜的GPU。之后,我又在 C++/CU 代碼中驗證了所有變更,以便與原來的Python代碼相結(jié)合。除了設(shè)計幾何算法之外,所有的工作都是由我完成的。

          在一切正常運(yùn)行后,Bremen 的一名兼職學(xué)生打電話給我問道:“聽說你很擅長使用多種技術(shù),能幫我在 GPU 上運(yùn)行一個算法嗎?”“當(dāng)然可以!”我給他講了CUDA、CMake、Linux 構(gòu)建、測試以及優(yōu)化等等,大約花了一個小時。他很有禮貌地聽完了我的介紹,最后說:“很有意思,但我想問一個非常具體的問題。我有一個函數(shù),我在函數(shù)的定義前面加了@cuda.jit,Python就無法編譯內(nèi)核了,還提示了一些關(guān)于數(shù)組的錯誤。你知道這里面有什么問題嗎?”

          我不知道。后來,他花了一天時間自己搞清楚了。原因是,Numba 無法處理原生的Python列表,只接受 NumPy 數(shù)組中的數(shù)據(jù)。他找到了問題所在,并在 GPU 上運(yùn)行了算法。使用的是Python。他沒有遇到我花費(fèi)了幾個月心思解決的任何“問題”。想在 Linux 上運(yùn)行代碼?沒問題,直接在Linux運(yùn)行即可。想針對目標(biāo)平臺優(yōu)化代碼?也不是問題。Numba 會替你優(yōu)化在平臺上運(yùn)行的代碼,因為它不會預(yù)先編譯代碼,而是在部署時按需編譯。

          很厲害,對不對?然而,對我來說并不是。我花費(fèi)了幾個月的時間,使用C++解決 Numba 中不會出現(xiàn)的問題,而那位Bremen的兼職學(xué)生完成相同的工作只花費(fèi)了幾天的時間。如果不是因為那是他第一次使用Numba,可能只需要幾個小時。說到底,Numba是什么?它是一種什么樣的魔法?

          沒有魔法。Python 的裝飾器將每一段代碼都轉(zhuǎn)換成了抽象語法樹,因此你可以隨意處理。Numba是一個 Python 庫,可使用任何后端、為任何支持的平臺編譯抽象語法樹。如果你想將Python 代碼編譯成以高度并行的方式在 CPU 核心上運(yùn)行,只需告訴 Numba 編譯即可。如果你希望在GPU上運(yùn)行代碼,同樣只需提出請求即可。

          Numba是一個Python編譯器,可以淘汰C++。然而,從理論上來說,Numba并沒有超越C++,因為二者使用的是同一個后端。Numba的GPU編程使用了CUDA,CPU編程使用了LLVM。實際上,由于它不需要針對每種新的架構(gòu)提前重建,因此能夠更好地適應(yīng)每種新硬件及其潛在的優(yōu)化。

          當(dāng)然,如果Numba能像Spiral那樣具有顯著的性能優(yōu)勢會更好。但Spiral更像是一個研究項目,最終可能會淘汰C++,但前提是足夠幸運(yùn)才行。Numba與Python的結(jié)合可以立即判C++死刑。如果可以使用Python編程,而且能擁有C++的性能,誰還會寫C++代碼呢?

          C++ 殺手之三:ForwardCom

          下面,我們再玩一個游戲。我給你三段代碼,你猜猜哪一段(也有可能是多段)是用匯編語言編寫的。

          第一段代碼:

          第二段代碼:

          第三段代碼:

          如果你猜到這三個例子都是匯編,那么恭喜你!

          第一個例子是用 MASM32 編寫的。這是一個帶有“if”和“while”的宏匯編器,用于編寫原生Windows 應(yīng)用程序。注意,不是以前有人這么寫,而是至今仍在采用這種寫法。微軟一直在積極維護(hù)Windows 與 Win32 API 的向后兼容性,因此所有以前編寫的 MASM32 程序都可以在現(xiàn)代 PC 上正常運(yùn)行。

          很諷刺的是,C 語言的發(fā)明是為了降低將 UNIX 從PDP-7 轉(zhuǎn)換成 PDP-11 的難度。C語言的設(shè)計初衷就是成為一種便攜式匯編語言,能夠在 70 年代硬件架構(gòu)的寒武紀(jì)爆發(fā)中生存下來。但在 21 世紀(jì),硬件架構(gòu)的演變?nèi)绱司徛以?20 年前用 MASM32 寫的程序如今仍然能完美運(yùn)行,但我不敢確定去年用 CMake 3.21 構(gòu)建的 C++ 應(yīng)用程序今時今日能否用 CMake 3.25 構(gòu)建。

          第二段代碼是 WebAssembly,這門技術(shù)甚至不是一個宏匯編器,沒有“if”和“while”,更像是人類可讀的瀏覽器機(jī)器碼。從概念上來說,可以是任何瀏覽器。

          WebAssembly代碼根本不依賴于硬件架構(gòu)。它提供的機(jī)器是抽象的、虛擬的、通用的,隨你怎么稱呼它。如果你能閱讀這段文字,說明你的物理機(jī)器上已經(jīng)有一個能運(yùn)行WebAssembly的硬件架構(gòu)了。

          最有趣的是第三段代碼。這是 ForwardCom:一款由著名的 C++ 和匯編優(yōu)化手冊作者 Agner Fog 提出的匯編器。與 Web Assembly 一樣,這不僅僅是一個匯編器,而且旨在實現(xiàn)向后以及向前兼容性的通用指令集。因此得名。ForwardCom 的全稱是an open forward-compatible instruction set architecture(一款開放式向前兼容指令集架構(gòu))。換句話說,它不僅是一個匯編器的提議,而且也是一份和平條約提議。

          我們知道最常見的計算機(jī)架構(gòu)系列 x64、ARM 和 RISC-V 都有不同的指令集。但沒有人知道為什么要保持這種狀態(tài)。所有現(xiàn)代處理器,除了最簡單的一些之外,運(yùn)行的都不是你提供的代碼,而是將你的輸入轉(zhuǎn)換為微碼。因此,不僅M1芯片提供英特爾的向后兼容層,每個處理器本質(zhì)上都為自己的早期版本提供了向后兼容層。

          那么,為什么架構(gòu)設(shè)計者未能就類似的向前兼容層達(dá)成統(tǒng)一意見呢?無外乎各個公司之間的競爭野心。但如果處理器制造商最終決定建立一個共同的指令集,而不是為每個競爭對手實現(xiàn)一個新的兼容層,F(xiàn)orwardCom就能夠讓匯編重回主流。這種向前兼容層可以治愈每個匯編程序員最大的心理創(chuàng)傷:“如今我為這個特定的架構(gòu)編寫一次性代碼,不出一年就會被淘汰?”

          有了向前兼容層,這些代碼就永遠(yuǎn)不會過時。這就是關(guān)鍵所在。

          此外,匯編編程還受到了另一種錯誤觀念的限制,人們普遍認(rèn)為匯編代碼太難寫,因此不實用。Fog 的提議也解決了這個問題。如果人們認(rèn)為寫匯編代碼太難,而寫 C 不難,那么我們就把匯編變成C語言。這不是問題。現(xiàn)代匯編語言沒有必要延續(xù)50年代祖宗的模樣。

          上面你看到的三個匯編示例都不像“傳統(tǒng)”的匯編,而且也不應(yīng)該還是老樣子。

          ForwardCom是一種匯編,可用于編寫永遠(yuǎn)不會過時的最佳代碼,并且不需要學(xué)習(xí)“傳統(tǒng)”的匯編。從現(xiàn)實的角度來看率,F(xiàn)orwardCom是未來的 C。不是 C++。

          C++ 什么時候終消亡?

          我們生活在一個后現(xiàn)代世界。與世長辭的不是技術(shù),而是人。就像拉丁語從未真正消失一樣,COBOL、Algol 68 和 Ada 也一樣,C++ 注定要永遠(yuǎn)介于生死參半的狀態(tài)。C++ 永遠(yuǎn)不會真正消失,它只會被更新更強(qiáng)大的新技術(shù)所取代。

          嚴(yán)格來說,不是“將來會被取代”,而是“正在被取代”。我的職業(yè)生涯源自 C++,而如今在使用 Python 寫代碼。我編寫方程式,SymPy 幫我求解,然后將解決方案轉(zhuǎn)換為 C++。然后,我將這段代碼粘貼到 C++ 庫中,甚至都無需調(diào)整格式,因為 clang-tidy 會自動完成。靜態(tài)分析器會檢查命名空間是否混亂,動態(tài)分析器會檢查內(nèi)存泄漏。CI/CD 負(fù)責(zé)跨平臺編譯。性能分析器讓我了解代碼實際的運(yùn)行情況,反匯編器可以解釋為什么。

          如果我用 C++ 之外的技術(shù)代替 C++,那么 80% 的工作不會有變化。對于我的大多數(shù)工作來說,C++ 根本無關(guān)緊要。這是否意味著,對于我來說,C++ 已經(jīng)死了 80%?

          icpick是一款非常實用的屏幕截圖工具,軟件為用戶提供了多種截取屏幕的方式,如全屏、活動窗口、窗口空間、滾動窗口、矩形區(qū)域、固定區(qū)域、任意形狀等等,用戶們可以根據(jù)自己的需求進(jìn)行選擇使用,滿足了不同用戶的使用需求,同時還提供了眾多的實用工具供用戶使用,比如取色器、放大鏡、標(biāo)尺、調(diào)色板、坐標(biāo)軸、量角器、白板等等,在截圖之后就可以直接使用這些實用工具制作你需要的素材圖片了,輕松就可以幫你制作出最優(yōu)質(zhì)的圖片,并且還可以對圖片進(jìn)行添加各種的效果,因此軟件內(nèi)置了一個強(qiáng)大的編輯器,用戶可以進(jìn)行添加文本、陰影、水印等一系列操作,非常的方便實用。總之使用picpick截圖軟件可以輕松幫你抓取到全屏幕或是局部的畫面,可以隨意進(jìn)行修改,而且操作也很簡單,是一款非常優(yōu)秀的截圖工具。picpick下載安裝-picpick截圖軟件下載 v5.1.9中文免費(fèi)版 - 多多軟件站

          軟件功能

          1、截獲任何截圖
          截獲屏幕截圖、活動窗口的截圖、桌面滾動窗口的截圖和任何特定區(qū)域的截圖等等。
          2、編輯你的圖片
          注釋并標(biāo)記您的圖片:您可以使用內(nèi)置圖片編輯器中的文本、箭頭、形狀和更多功能,并且該編輯器還帶有最新的Ribbon風(fēng)格菜單。
          3、增強(qiáng)效果
          為你的圖片添加各種效果:陰影、框架、水印、馬賽克、運(yùn)動模糊和亮度控制等等。
          4、分享到任何地方
          通過網(wǎng)絡(luò)、郵件、ftp、Dropbox、Google Drive、SkyDrive、Box、Evernote、Facebook、Twitter和其它更多方式來保存、分享或發(fā)送你的照片。
          5、平面設(shè)計附件
          各種平面設(shè)計附件包括顏色選擇器、顏色調(diào)色板、像素標(biāo)尺、量角器、瞄準(zhǔn)線、放大鏡和白板。
          6、自定義設(shè)置
          軟件帶有各種高級的設(shè)置,您可以自定義快捷鍵、文件命名、圖片質(zhì)量和許多其它的功能。

          picpick怎么修改快捷鍵?

          1、點擊左上角的文件,然后在彈出窗口中選擇程序選項;


          2、在打開的窗口左側(cè)有一項是快捷鍵,點擊打開快捷鍵窗口;


          3、在快捷鍵窗口中根據(jù)自己的喜好,分別設(shè)置需要的快捷鍵。設(shè)置完成后記得點擊底部的確定按鈕進(jìn)行保存。

          picpick怎么滾動截圖?

          1、首先打開軟件,然后點擊滾動窗口;


          2、這時候,可以看到窗口的外圍有個綠色的框框,不要亂動鼠標(biāo);


          3、用鼠標(biāo)點擊右側(cè)的滾動條,然后不要動鼠標(biāo),鍵盤和電腦;


          4、picpick自己截屏;


          5、當(dāng)滾動條滾動到最下方的時候,截屏結(jié)束,picpick自動保存圖片,打開圖片編輯界面;


          6、可以看一下,由于電腦自運(yùn)行比較慢的問題,有的地方截取效果不好,這需要自己修改。

          軟件特色

          1、友好的用戶界面,提供Windows 7的 Ribbon 樣式;
          2、擁有基本的編輯繪圖、形狀、指示箭頭、線條、文本等功能;
          3、同時還支持模糊,銳化,色調(diào),對比度,亮度,色彩平衡,像素化,旋轉(zhuǎn),翻轉(zhuǎn),邊框等效果;
          4、支持共享截圖至 FTP 、Web 、E-Mail、Facebook、Twitter 等社交網(wǎng)絡(luò);
          5、屏幕截圖:支持全屏、活動窗口、滾動窗口 、窗口控制、區(qū)域、固定區(qū)域、手繪、重復(fù)捕捉;
          6、Ribbon界面圖像編輯器:箭頭、線條等繪圖工具。模糊、銳化、像素化、旋轉(zhuǎn)、翻轉(zhuǎn),框架等特效;
          7、拾色器和調(diào)色板:支持RGB、HTML、C++、Delphi等代碼類型,Photoshop風(fēng)格轉(zhuǎn)換,保存顏色;
          8、屏幕放大鏡、量角器、屏幕坐標(biāo)計算功能,為你的演示文稿把屏幕當(dāng)作白板自由繪畫。

          PicPick截圖之后怎么批量保存圖片?

          1、打開PicPick,并使用PicPick截圖多張圖片。此時圖片處于未保存狀態(tài);


          2、然后我們點擊PicPick左上角的文件;


          3、接下來我們點擊如下圖所示的保存(或者另存為);


          4、然后我們看到并點擊“全部保存”;


          5、在彈出的保存窗口中,點擊三個點的圖標(biāo),找到你的圖片要保存的位置;


          6、之后點擊確定,即可完成批量保存。

          載大文件時,斷點續(xù)傳是很有必要的,特別是網(wǎng)速度慢且不穩(wěn)定的情況下,很難保證不出意外,一旦意外中斷,又要從頭下載,會很讓人抓狂。斷點續(xù)傳就能很好解決意外中斷情況,再次下載時不需要從頭下載,從上次中斷處繼續(xù)下載即可,這樣下載幾G或十幾G大小的一個文件都沒問題。本文介紹利用miniframe開源Web框架分別在lazarus、delphi下實現(xiàn)文件HTTP下載斷點續(xù)傳的功能。

          本文Demo還實現(xiàn)了批量下載文件,同步服務(wù)器上的文件到客戶端的功能。文件斷點續(xù)傳原理:分塊下載,下載后客戶端逐一合并,同時保存已下載的位置,當(dāng)意外中斷再次下載時從保存的位置開始下載即可。這其中還要保證,中斷后再次下載時服務(wù)器上相應(yīng)的文件如果更新了,還得重新下載,不然下載到的文件是錯了。說明:以下代碼lazarus或delphi環(huán)境下都能使用。全部源碼及Demo請到miniframe開源web框架下載: https://www.wyeditor.com/miniframe/或https://github.com/dajingshan/miniframe。

          服務(wù)器端代碼

          文件下載斷點續(xù)傳服務(wù)器端很簡單,只要提供客戶端要求下載的開始位置和指定大小的塊即可。

          以下是服務(wù)器獲取文件信息和下載一個文件一塊的代碼:

          <%@//Script頭、過程和函數(shù)定義
          program codes;
          %>
           
          <%!//聲明變量
          var
            i,lp: integer;
            FileName, RelativePath, FromPath, ErrStr: string;
            json: TminiJson;
            FS: TFileStream;
            
          function GetOneDirFileInfo(Json: TminiJson; Path: string): string;
          var
            Status: Integer;
            SearchRec: TSearchRec;
            json_sub: TminiJson;
          begin
            Path := PathWithSlash(Path);
            SearchRec := TSearchRec.Create;
            Status := FindFirst(Path + '*.*', faAnyFile, SearchRec);
            try
              while Status = 0 do
              begin 
                if SearchRec.Attr and faDirectory = faDirectory then
                begin
                  if (SearchRec.name <> '.') and (SearchRec.name <> '..') then
                    GetOneDirFileInfo(Json, Path + SearchRec.Name + '\');
                end else
                begin
                  FileName := Path + SearchRec.Name;
                  try
                    if FileExists(FileName) then
                    begin 
                      json_sub := Pub.GetJson;  
                      json_sub.SO; //初始化 或 json.Init;    
                      json_sub.S['filename'] := SearchRec.name;
                      json_sub.S['RelativePath'] := GetDeliBack(FileName, FromPath);
                      json_sub.S['FileTime'] := FileGetFileTimeA(FileName);
                      json_sub.I['size'] := SearchRec.Size;
                      json.A['list'] := json_sub;
                    end;
                  except
                    //print(ExceptionParam)
                  end;//}
                end; 
                Status := FindNext(SearchRec);
              end;
            finally
              FindClose(SearchRec);
              SearchRec.Free;
            end;//*) 
          end;
          %>
          <%
          begin
            FromPath := 'D:\code\delphi\sign\發(fā)行文件'; //下載源目錄
            
            json := Pub.GetJson; //這樣創(chuàng)建json對象不需要自己釋放,系統(tǒng)自動管理
            json.SO; //初始化 或 json.Init;
            
            // 驗證是否登錄代碼
            {if not Request.IsLogin('Logined') then
            begin 
              json.S['retcode'] := '300';
              json.S['retmsg'] := '你還沒有登錄(no logined)!'; 
              print(json.AsJson(true));
              exit; 
            end;//} 
            
            json.S['retcode'] := '200';
            json.S['retmsg'] := '成功!';
            if Request.V('opr') = '1' then
            begin //獲取服務(wù)上指定目錄的文件信息
              GetOneDirFileInfo(Json, FromPath);
            end else
            if Request.V('opr') = '2' then
            begin //下載指定文件給定大小的塊 
              FromPath := PathWithSlash(FromPath);   
              RelativePath := Request.V('fn');
              FileName := FromPath + RelativePath;
              Fs := Pub.GetFS(FileName, fmShareDenyWrite, ErrStr);
              if trim(ErrStr) <> '' then 
              begin
                json.S['retcode'] := '300';
                json.S['retmsg'] := ErrStr;
                print(json.AsJson(true));  
                exit;
              end;
              Fs.Position := StrToInt(Request.V('pos'));
              Response.ContentStream := TMemoryStream.Create; //注意不能用 Pub.GetMs,這是因為Pub.GetMs創(chuàng)建的對象在動態(tài)腳本運(yùn)行完就釋放了
              Response.ContentStream.CopyFrom(Fs, StrToInt(Request.V('size')));
              //返回流數(shù)據(jù)
              Response.ContentType := 'application/octet-stream';   
            end;
            print(json.AsJson(true));
          end;
          %>

          客戶端代碼

          客戶端收到塊后,進(jìn)行合并。全部塊下載完成后,還要把新下載的文件的文件修改為與服務(wù)器上的文件相同。以下是客戶端實現(xiàn)的主代碼:

          procedure TMainForm.UpgradeBlock_Run(var ThreadRetInfo: TThreadRetInfo);
          const
            BlockSize = 1024*1024; //1M
          var
            HTML, ToPath, RelativePath, FN, Tmp, TmpFileName, FailFiles, SuccFiles, Newfn, TmpToPath: string;
            Json, TmpJson: TminiJson;
            lp, I, Number, HadUpSize, AllSize, AllBlockCount, MySize, MyNumber: Int64;
            Flag: boolean;
            SL, SLDate, SLSize, SLTmp: TStringlist;
            MS: TMemoryStream;
            Fs: TFileStream;
            procedure HintMsg(Msg: string);
            begin
              FMyMsg := Msg; // '正在獲取文件列表。。。';
              ThreadRetInfo.Self.Synchronize(ThreadRetInfo.Self, MyUpdateface); //為什么不直接用匿名,因為laz不支持
            end;
          begin
            ToPath := 'D:\superhtml'; //如果是當(dāng)前程序更新  ExtractFilePath(ParamStr(0))
           
            ThreadRetInfo.Ok := false;
           
            HintMsg('正在獲取文件列表。。。');
            if not HttpPost('/接口/同步文件到客戶端.html?opr=1',
                '', ThreadRetInfo.ErrStr, ThreadRetInfo.HTML) then exit;
            if Pos('{', ThreadRetInfo.HTML) <> 1 then
            begin
              ThreadRetInfo.ErrStr :='請先檢查腳本源碼是否配置正確!';
              exit;
            end;
            ToPath := Pub.PathWithSlash(ToPath);
           
            Json := TminiJson.Create;
            SL := TStringlist.Create;
            SLDate := TStringlist.Create;
            SLSize := TStringlist.Create;
            SLTmp := TStringlist.Create;
            try
              Json.LoadFromString(ThreadRetInfo.HTML);
              if json.S['retcode'] = '200' then
              begin
                TmpJson := json.A['list'];
                for lp := 0 to TmpJson.length - 1 do
                begin
                  HintMsg(lp.ToString + '/' + TmpJson.length.ToString + '正在檢查文件:' + RelativePath);
                  RelativePath := TmpJson[lp].S['RelativePath'];
                  if trim(RelativePath) = '' then Continue;
                  Flag := FileExists(ToPath + RelativePath);
                  if Flag then
                  begin
                    if (PubFile.FileGetFileTimeA(ToPath + RelativePath) = TmpJson[lp].S['FileTime']) and
                       (PubFile.FileGetFileSize(ToPath + RelativePath) = TmpJson[lp].I['Size']) then
                    else
                      Flag := false;
                  end;
                  if not Flag then //此文件需要更新
                  begin
                    SL.Add(RelativePath);
                    SLDate.Add(TmpJson[lp].S['FileTime']);
                    SLSize.Add(TmpJson[lp].S['Size']);
                  end;
                end;
           
                //開始下載
                FailFiles := '';
                SuccFiles := '';
                HintMsg('需要更新的文件共有' + IntToStr(SL.Count) + '個。。。');
                for lp := 0 to SL.Count - 1 do
                begin
                  RelativePath := SL[lp];
                  if RelativePath[1] = '\' then RelativePath := Copy(RelativePath, 2, MaxInt);
                  FN := ToPath + RelativePath;
           
                  //先計算要分幾個包,以處理進(jìn)度
                  Number := 0;
                  HadUpSize := 0;
                  AllSize := StrToInt64(SLSize[lp]);
                  AllBlockCount := 0;
                  while true do
                  begin
                    AllBlockCount := AllBlockCount + 1;
                    if AllSize - HadUpSize >= BlockSize then
                       MySize := BlockSize
                    else
                       MySize := AllSize - HadUpSize;
                    HadUpSize := HadUpSize + MySize;
                    if HadUpSize >= AllSize then
                      break;
                  end;
           
                  //開始分塊下載
                  Number := 0;
                  HadUpSize := 0;
                  //AllSize := Fs.Size;
                  //TmpToPath := PubFile.FileGetTemporaryPath;
                  Newfn := '@_' + PubPWD.GetMd5(SLDate[lp] + SLSize[lp]) + ExtractFileName(FN);  //Pub.GetClientUniqueCode;
           
                  if FileExists(ToPath + Newfn) and (FileExists(FN)) then
                  begin
                    SLTmp.LoadFromFile(ToPath + Newfn);
                    MyNumber := StrToInt64(trim(SLTmp.Text));
                    Fs := TFileStream.Create(FN, fmOpenWrite);
                  end else
                  begin
                    MyNumber := 0;
                    Fs := TFileStream.Create(FN, fmCreate);
                  end;
                  try
                    while true do
                    begin
                      HintMsg('正在下載文件[' + Pub.GetDeliBack(RelativePath, '@@') + ']第[' + IntToStr(Number + 1) + '/' + IntToStr(AllBlockCount) + ']個包。。。');
           
                      if AllSize - HadUpSize >= BlockSize then
                         MySize := BlockSize
                      else
                         MySize := AllSize - HadUpSize;
                      Number := Number + 1;
                      if (MyNumber = 0) or (Number >= MyNumber) or (HadUpSize + MySize >= AllSize) then
                      begin
                        for I := 1 to 2 do //意外出錯重試一次
                        begin
                          if not HttpPost('/接口/同步文件到客戶端.html?opr=2fn=' + UrlEncode(RelativePath) +
                            'pos=' + UrlEncode(IntToStr(HadUpSize)) + 'size=' + UrlEncode(IntToStr(MySize)),
                            '', ThreadRetInfo.ErrStr, ThreadRetInfo.HTML, MS) then
                          begin
                            if I = 2 then
                            begin
                              ThreadRetInfo.ErrStr := Json.S['retmsg'];
                              exit;
                            end else
                              Continue;
                          end;
                          if Pos('{', ThreadRetInfo.HTML) < 1 then
                          begin
                            if I = 2 then
                            begin
                              ThreadRetInfo.ErrStr := Json.S['retmsg'];
                              exit;
                            end else
                              Continue;
                          end;
           
                          Json.LoadFromString(ThreadRetInfo.HTML);
                          if json.S['retcode'] <> '200' then
                          begin
                            if I = 2 then
                            begin
                              ThreadRetInfo.ErrStr := Json.S['retmsg'];
                              exit;
                            end else
                              Continue;
                          end;
                          break;
                        end;
           
                        if MS = nil then
                        begin
                          ThreadRetInfo.ErrStr := '沒能下載到文件[' + RelativePath + ']!' + json.S['retmsg'];
                          exit;
                        end else
                        begin
                          Fs.Position := HadUpSize;
                          MS.Position := 0;
                          Fs.CopyFrom(MS, MS.Size);
                          MS.Free;
                          MS := nil;
                          SLTmp.Text := Number.ToString;
                          try
                            SLTmp.SaveToFile(ToPath + Newfn);
                          except
                          end;
                        end;
                      end;
                      HadUpSize := HadUpSize + MySize;
           
                      if HadUpSize >= AllSize then
                      begin //全部下載完成
                        Fs.Free;
                        Fs := nil;
                        Sleep(10);
                        PubFile.FileChangeFileDate(Fn, SLDate[lp]);
                        DeleteFile(ToPath + Newfn);
                        SuccFiles := SuccFiles + #13#10 + RelativePath;
                        break;
                      end;
                    end;
                  finally
                    if Fs <> nil then
                      Fs.Free;
                  end;
                end;
                ThreadRetInfo.HTML := '';
                if trim(SuccFiles) <> '' then
                  ThreadRetInfo.HTML := '本次更新了以下文件:'#13#10 + SuccFiles;
                //if trim(FailFiles) <> '' then
                  //ThreadRetInfo.HTML := trim(ThreadRetInfo.HTML + #13#10'以下文件更新失敗:'#13#10 + FailFiles);
              end;
            finally
              SLTmp.Free;
              SLSize.Free;
              SL.Free;
              Json.Free;
              SLDate.Free;
            end;
            ThreadRetInfo.Ok := true;
          end;

          以下是Demo運(yùn)行界面:


          主站蜘蛛池模板: 在线电影一区二区| 成人精品一区二区三区校园激情| 国产精品伦子一区二区三区 | 亚洲综合无码一区二区痴汉| 日韩人妻精品一区二区三区视频| 一区二区三区亚洲| 成人国内精品久久久久一区| 无码av免费一区二区三区| 国产免费av一区二区三区| 中文字幕在线视频一区| 日韩人妻无码免费视频一区二区三区 | 动漫精品一区二区三区3d| 激情内射日本一区二区三区| 尤物精品视频一区二区三区| 另类一区二区三区| 国模少妇一区二区三区| 亚洲国模精品一区 | 国产一区二区三区亚洲综合| 中文精品一区二区三区四区| 亚洲人AV永久一区二区三区久久| 无码午夜人妻一区二区不卡视频 | 午夜无码一区二区三区在线观看| 亚洲国产成人久久一区二区三区| 日韩精品一区二区三区老鸭窝| 国产一区二区三区在线看| 久久国产视频一区| 亚欧色一区W666天堂| 久久精品黄AA片一区二区三区| 无码人妻精品一区二区三区99仓本| 久久精品国产一区二区电影| 亚洲一区二区视频在线观看 | 国产一区二区三区高清视频 | 精品无码中出一区二区| 午夜肉伦伦影院久久精品免费看国产一区二区三区 | 国产午夜精品一区二区三区漫画| 大伊香蕉精品一区视频在线| 中文字幕一区二区三区乱码| 蜜桃无码一区二区三区| 一区二区在线视频观看| 中文字幕精品亚洲无线码一区 | 狠狠综合久久AV一区二区三区 |