0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內(nèi)不再提示

河套IT TALK——TALK 12:編程的技術|藝術|術術 下篇:對著代碼解讀編程的哲學

共熵服務中心 ? 來源:未知 ? 2022-12-16 19:35 ? 次閱讀



往期回顧

TALK 6:編程的技術|藝術|術術(上篇)骨灰級程序員的心路歷程

TALK 10:編程的技術|藝術|術術(中篇)編程的思想、藝術和哲學
前期回顧

前面兩篇里,骨灰級程序員梁峻墅給大家介紹了他的心路歷程,他談了程序員文化和武林文化的理解,將編程與孫子兵法對照,闡釋編程的藝術性表達以及哲學思考。本篇將不再務虛,而是直接上代碼,讓梁老師帶著你解讀牛逼代碼的高明之處。

一段黑客的代碼

務虛的事都講完了,現(xiàn)在得真的要講講務實的事了。前面講的那些是武功秘籍的目錄,而真正的武功秘籍在代碼里。實踐出真知,只有虛實結合,才能感同身受。

我找了一段Zero-Day(編者注:下文簡稱0day)組織幾乎每個程序都要用到的一段代碼作為示例。

0day,用過盜版軟件的朋友應該都很熟悉,它是全球最牛B的盜版組織,里面高手如云,都是Richard Stallman的追隨者。任何一個被他們盯上的大廠軟件,只要敢早上發(fā)布,中午的發(fā)布會招待宴還沒吃完,破解版就已經(jīng)在各大盜版網(wǎng)站上可以下載了,平均破解時間就是兩三個小時,承諾破解時間不超過24小時,所以叫0day,當天解決,童叟無欺。我們就來看看這些全球頂尖黑客是怎么寫代碼的。

我找的這段代碼的功能很簡單,就是一個基于文件的記錄日志類,其C++版本加上頭文件,總代碼行數(shù)不超過200行,而核心代碼不到100行,但就在這方寸之間,隱藏著十一個戰(zhàn)術思想,三個戰(zhàn)略思想,還有三個核彈級思想。就是個日志文件功能,如果是你設計,能有什么想法?而往往是簡單中蘊含的偉大,才能更加讓人震撼。現(xiàn)在咱們就按圖索驥,開始一段與頂尖高手同行的代碼探險之旅。

這段代碼是個標準的C++類,為方便演示,我使用的是其Windows平臺的版本,此類可以在所有Visual Studio的C++應用中使用,就兩個文件:LogFile.h和LogFile.cpp。

可以先總覽一下:

LogFile.h

// Log.h: interface for the CLog class.
//
//////////////////////////////////////////////////////////////////////


#if !defined(AFX_LOG_H__512AFEC0_D4E8_47F0_AB0C_4E29DAB9A9FC__INCLUDED_)
#define AFX_LOG_H__512AFEC0_D4E8_47F0_AB0C_4E29DAB9A9FC__INCLUDED_


#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000


#define _CRT_SECURE_NO_WARNINGS


#include
#include
#include
#include


#define MAX_LENGTH_CONTENT_PER_LINE 1024


class CLogFile {
public:
CLogFile(LPCTSTR pszPathName4User = _T(""));
virtual ~CLogFile();
bool Record(LPCTSTR pszFormat, ...);


bool SetPathName4Host(LPCTSTR pszPathName4Host);
bool SetPathName4User(LPCTSTR pszPathName4User);
bool SetFileName4Host(LPCTSTR pszFileName4Host);
bool SetFileName4User(LPCTSTR szFileName4User);
bool SetHeader(LPCTSTR szHeader);


LPCTSTR GetPathName4Host();
LPCTSTR GetPathName4User();
LPCTSTR GetFileName4Host();
LPCTSTR GetFileName4User();


LPCTSTR GetPathName();
LPCTSTR GetFileNameFullPath();


protected:
SYSTEMTIME m_tSystemTime;


TCHAR m_szPathName4Host[MAX_PATH + 1];
TCHAR m_szPathName4User[MAX_PATH + 1];
TCHAR m_szFileName4Host[MAX_PATH + 1];
TCHAR m_szFileName4User[MAX_PATH + 1];


TCHAR m_szPathName[MAX_PATH + 1];
TCHAR m_szFileNameFullPath[MAX_PATH + 1];


TCHAR m_szHeader[MAX_LENGTH_CONTENT_PER_LINE + 1];
TCHAR m_szLine[MAX_LENGTH_CONTENT_PER_LINE + 1];


bool IsPathOrFileExist(LPCTSTR pszPathOrFileName);
bool BuildFilePath();
bool BuildPathAndFilePath();
};


#endif // !defined(AFX_LOG_H__512AFEC0_D4E8_47F0_AB0C_4E29DAB9A9FC__INCLUDED_)
LogFile.cpp
// Log.cpp: implementation of the CLog class.
//
//////////////////////////////////////////////////////////////////////


#include "LogFile.h"


#define ZERO_MEMORY(p) memset(p, 0, sizeof(p))


CLogFile::CLogFile(LPCTSTR pszPathName4User) {
ZERO_MEMORY(m_szPathName4Host);
ZERO_MEMORY(m_szPathName4User);
ZERO_MEMORY(m_szFileName4Host);
ZERO_MEMORY(m_szFileName4User);
ZERO_MEMORY(m_szPathName);
ZERO_MEMORY(m_szFileNameFullPath);
ZERO_MEMORY(m_szLine);
ZERO_MEMORY(m_szHeader);
_tcscpy_s(m_szHeader, MAX_LENGTH_CONTENT_PER_LINE, _T("F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 F13 F14 F15"));


::GetModuleFileName(NULL, m_szFileNameFullPath, MAX_PATH);
_tsplitpath(m_szFileNameFullPath, m_szPathName4Host, m_szPathName, m_szFileName4Host, NULL);
_tcscat_s(m_szPathName4Host, MAX_PATH, m_szPathName);


if (pszPathName4User) {
if (*pszPathName4User) {
_tcscpy_s(m_szPathName4User, MAX_PATH, pszPathName4User);
}
else {
*m_szPathName4User = _T('.');
_tcscat_s(m_szPathName4User, MAX_PATH, m_szFileName4Host);
}
}


BuildPathAndFilePath();
}


CLogFile::~CLogFile() {
}


bool CLogFile::BuildFilePath() {
GetLocalTime(&m_tSystemTime);
int iReturn = _sntprintf(m_szFileNameFullPath, MAX_PATH, _T("%s%s.%s.%04d%02d%02d.%p.txt"),
m_szPathName, m_szFileName4Host, m_szFileName4User,
m_tSystemTime.wYear, m_tSystemTime.wMonth, m_tSystemTime.wDay, this);


return 0 < iReturn;
}


bool CLogFile::BuildPathAndFilePath() {
bool bReturn = 0 < _sntprintf(m_szPathName, MAX_PATH, _T("%s%s"), m_szPathName4Host, m_szPathName4User);
if (bReturn) {
bReturn = BuildFilePath();
}
return bReturn;
}


bool CLogFile::Record(LPCTSTR pszFormat, ...) {
int iReturn = 0;
FILE* pFile = NULL;
do {
if (!IsPathOrFileExist(m_szPathName)) {
break;
}
if (!BuildFilePath()) {
break;
}
bool bIsNew = !IsPathOrFileExist(m_szFileNameFullPath);
pFile = _tfopen(m_szFileNameFullPath, _T("a"));
if (!pFile) {
break;
}
if (bIsNew) {
iReturn = _ftprintf(pFile, _T("Time User %s "), m_szHeader);
if (0 >= iReturn) {
break;
}
}
va_list vlArgs;
va_start(vlArgs, pszFormat);
iReturn = _vsntprintf(m_szLine, MAX_LENGTH_CONTENT_PER_LINE, pszFormat, vlArgs);
va_end(vlArgs);
if (0 > iReturn) {
break;
}
iReturn = _ftprintf(pFile, _T("%02d:%02d:%02d.%03d %s %s "), m_tSystemTime.wHour,
m_tSystemTime.wMinute, m_tSystemTime.wSecond, m_tSystemTime.wMilliseconds,
m_szFileName4User, m_szLine);
} while (false);
if (pFile) {
fclose(pFile);
}
return 0 < iReturn;
}


bool CLogFile::IsPathOrFileExist(LPCTSTR pszPathOrFileName) {
return (0 == _taccess(pszPathOrFileName, 0));
}


bool CLogFile::SetPathName4Host(LPCTSTR pszPathName4Host) {
_tcsncpy(m_szPathName4Host, pszPathName4Host, MAX_PATH);
return BuildPathAndFilePath();
}


bool CLogFile::SetPathName4User(LPCTSTR pszPathName4User) {
_tcsncpy(m_szPathName4User, pszPathName4User, MAX_PATH);
return BuildPathAndFilePath();
}


bool CLogFile::SetFileName4Host(LPCTSTR pszFileName4Host) {
_tcsncpy(m_szFileName4Host, pszFileName4Host, MAX_PATH);
return BuildFilePath();
}


bool CLogFile::SetFileName4User(LPCTSTR szFileName4User) {
_tcsncpy(m_szFileName4User, szFileName4User, MAX_PATH);
return BuildFilePath();
}


bool CLogFile::SetHeader(LPCTSTR szHeader) {
_tcsncpy(m_szHeader, szHeader, MAX_PATH);
return true;
}


LPCTSTR CLogFile::GetPathName4Host() {
return m_szPathName4Host;
}


LPCTSTR CLogFile::GetPathName4User() {
return m_szPathName4User;
}


LPCTSTR CLogFile::GetFileName4Host() {
return m_szFileName4Host;
}


LPCTSTR CLogFile::GetFileName4User() {
return m_szFileName4User;
}


LPCTSTR CLogFile::GetPathName() {
return m_szPathName;
}


LPCTSTR CLogFile::GetFileNameFullPath() {
return m_szFileNameFullPath;
}
戰(zhàn)術思想

先從戰(zhàn)術思想談起,有十一個,容我逐一道來。

戰(zhàn)術思想一:全名命名規(guī)則

代碼的第一眼感覺,沒有注釋!這幫高手果然很清高啊,他們不會幫助你看懂,因為這個世界上總有人不配看懂。只能沉下心來,自力更生了。再仔細看代碼,會發(fā)現(xiàn)代碼中的變量名、方法名都很長。第一個戰(zhàn)術級思想浮出水面:全名命名規(guī)則。命名使用單詞全名,而很多程序員喜歡使用縮寫,而縮寫并不一定能與所有人達成共識,導致命名的意義大打折扣。良好的命名可以代替注釋,且效率更高。微軟的函數(shù)命名平均長度是13個字母,而0day代碼中的命名平均長度是16.8個字母,超過微軟水準將近30%??梢哉f,命名平均長度能夠作為代碼段位的參考之一。

戰(zhàn)術思想二:前綴命名規(guī)則

再仔細看每一個命名,第二個戰(zhàn)術級思想浮出水面:前綴命名規(guī)則。所有的變量名都有類型前綴,如字符串變量的前綴是“sz”,字符串指針變量的前綴是“psz” ,整型(int)變量的前綴是“i” ,布爾型(bool)變量的前綴是“b”,這些類型前綴雖然使用了縮寫,但這些縮寫都是C/C++程序員所共識的。還有,所有的類成員級變量在類型前綴前再加上“m_”前綴指示作用域,m是member的縮寫,其實還有一個“g_”前綴代表全局作用域,但全局變量只有在C代碼中很常見,而在C++代碼中幾乎從不使用。

戰(zhàn)術思想三:名詞前置命名規(guī)則

再再仔細看每一個命名,第三個戰(zhàn)術級思想浮出水面:名詞前置命名規(guī)則。例如類成員字符串變量“宿主路徑名”命名為m_szPathName4Host,“用戶路徑名”命名為m_szPathName4User,如果按人類正常思維應該是m_szHostPathName和m_szUserPathName才對,但他們卻名詞前置,形容詞動詞后置。其目的是為了給相關命名進行分類,最早是為了能在代碼統(tǒng)計工具的報告中,能把相關命名在α排序中排在一起,以便進行代碼分析;而在后來的現(xiàn)代IDE的代碼編輯器中,都有自動完成功能,根據(jù)輸入的部分字母自動提示可能的輸入,按名詞前置命名規(guī)則,提示內(nèi)容將把相關命名排在一起,便于程序員選擇。如鍵入“m_szP”,將提示出m_szPathName4Host和m_szPathName4User,方便程序員在使用相關變量或方法時提高效率。

戰(zhàn)術思想四:介詞縮寫命名規(guī)則

在上面提到的命名中,都有一個阿拉伯數(shù)字4,這是什么鬼?第四個戰(zhàn)術級思想浮出水面:介詞縮寫命名規(guī)則。用4的英文諧音代替介詞“for”,原命名應為m_szPathNameForHost,介詞作為前置命名分類與后置形容詞、動詞的分界線被大量使用,為節(jié)約鍵擊次數(shù)而在組織內(nèi)約定的縮寫。類似還有2,諧音英文的“to”,因為在程序中各種轉換也非常多,如BinToHex(二進制轉十六進制),可以縮寫為Bin2Hex。這可以理解為長命名思想與少鍵擊思想的辯證統(tǒng)一。

戰(zhàn)術思想五:對稱命名規(guī)則

看上面的成員變量和對應的設置方法名和獲取方法名,第五個戰(zhàn)術級思想浮出水面:對稱命名規(guī)則。如此整齊劃一的命名,不但能幫助閱讀者在沒有注釋的情況下快速理解各方法的意圖,還能讓使用者無需翻看源碼就能準確調(diào)用。

大家看看,一個小小的命名,已經(jīng)是殺機四伏,下足了功夫。十一個戰(zhàn)術思想接近一半,都是在談命名。就是因為命名是代碼的基石,它是多米諾骨牌效應里的第一塊骨牌,每塊磚不做好,將會影響整個大廈的安危。這些命名規(guī)則的終極目標都是為了用空間換時間。在你的每一次鍵擊中,每個思想可能只為你節(jié)約了0.1秒,但經(jīng)不住長年累月的積累,你的有效編程時間就是比別人多,還沒開始比賽,你就已經(jīng)勝過了。

戰(zhàn)術思想六:使用制表符縮進

代碼中還有一個不易察覺的細節(jié),其代碼縮進使用的是制表符(TAB鍵),第六個戰(zhàn)術級思想浮出水面:使用制表符縮進。關于縮進使用制表符還是空格,業(yè)界一直爭論不斷,且沒什么定論,主要原因就是覺得這是個小問題,無傷大雅,大家隨意,開心就好。但這些頂尖高手只用制表符,原因很暖心,僅僅是為了尊重同行!制表符最早出現(xiàn)是為了控制打印機在打印時的左邊距,當時定義為8個空格,可視化編程出現(xiàn)后才用于代碼縮進,但當時顯示器的分辨率是320*200,一行最多顯示80個字符,這8個空格實在是太長了,于是就在編輯器中定義為4個空格,但后來有人覺得2個才好,還有人覺得1個更好,最后干脆作為編輯器配置項,根據(jù)喜好自定義吧。所以使用制表符縮進的代碼在編輯器中的顯示樣式將會符合當前使用者的習慣,而使用空格縮進的代碼將可能會導致當前使用者不適。多么細致的人文關懷,面向人性編程,面向開發(fā)者編程,時刻謹記。

戰(zhàn)術思想七:調(diào)用必須有返回值

觀察代碼中的每一個方法,發(fā)現(xiàn)都有返回值,哪怕是返回固定值!

第七個戰(zhàn)術級思想浮出水面:調(diào)用必須有返回值。絕大多數(shù)編程語言都允許調(diào)用沒有返回值,但這幫頂級精英為什么在可以用這個規(guī)則的情況下還是不用呢?這就是接口的藝術,為了向下兼容,未雨綢繆,面向未來編程!因為誰也無法預測,隨著代碼的不斷迭代,這個方法的使用條件可能會發(fā)生變化,而有返回值的調(diào)用是可以兼容沒有返回值的調(diào)用的,這樣可保持接口的歷史一致性,進退自如。這樣的設計一旦在public調(diào)用中發(fā)揮過一次作用,可就不是節(jié)約0.1秒的事了。

戰(zhàn)術思想八:減少嵌套深度

上面是Record方法中的一段代碼,使用了一個do-while循環(huán)語句,但循環(huán)條件是個固定布爾值false,意味著這個循環(huán)永遠只會執(zhí)行一次,但為什么還要用循環(huán)語句呢?如果不用循環(huán)語句,正常的寫法應該是這樣的:

第八個戰(zhàn)術級思想浮出水面:減少嵌套深度。嵌套深度決定了人類大腦的思考深度,而思考深度則決定了消耗的能量和思考的難度。所以嵌套深度較低的代碼,讓人思考起來會比較輕松且不易出錯,而重度嵌套的代碼則更容易讓人疲倦且增加產(chǎn)生bug的幾率。

戰(zhàn)術思想九:辯證使用goto

這段代碼使用do-while循環(huán)語句的結構,并配合break語句來減少邏輯嵌套。第九個戰(zhàn)術級思想浮出水面:辯證使用goto。break語句的本質(zhì)是goto語句,只是受限而已。而goto語句在早期面向過程編程的時代,由于其高效的操作效率而被濫用,把代碼寫的像面條一樣,扯不清,理還亂,這導致了上世紀60年代的軟件危機,并最終引發(fā)了軟件工程革命。在面向?qū)ο缶幊痰臅r代,業(yè)界統(tǒng)一的共識是禁止使用goto。但goto語句的操作效率確實很高,所以善用break這種閹割版goto可以起到魚與熊掌兼得的效果。

戰(zhàn)術思想十:同一函數(shù)代碼不要跨屏

觀察Record方法的代碼行數(shù)達到36行,但業(yè)界一般的說法是每個函數(shù)的代碼行數(shù)不要超過30行,理由是人類的腦容量問題。但0day的判斷標準是,第十個戰(zhàn)術級思想浮出水面:同一函數(shù)代碼不要跨屏。只要任意函數(shù)的所有代碼在當前流行屏幕尺寸大小下能夠完全顯示即可。理由是只要整個代碼邏輯在人的靜態(tài)目視范圍之內(nèi),程序員的腦容量都夠用。除了靠減少代碼行數(shù)來防止縱向滾動屏幕,前面說的減少邏輯嵌套還能防止橫向滾動屏幕。代碼邏輯禁止跨屏規(guī)則能在很大程度上降低bug產(chǎn)生的幾率。

再觀察Record方法,發(fā)現(xiàn)一段有趣的代碼:

戰(zhàn)術思想十一:盡量使用順序代碼結構代替判斷代碼結構

BuildFilePath方法用于構造日志文件名,其中使用了系統(tǒng)日期作為文件名的一部分,目的就是把日志文件按天分隔,以防止文件過大。這意味著每次寫日志,都應判斷是否該更換文件名,但這種更換每天只發(fā)生一次。而這段代碼并沒有根據(jù)日期是否更改而構造文件名,而是每次都按當前日期構造文件名,這意味著文件名在一天內(nèi)的調(diào)用中都是重復構造相同的文件名,這不是做無用功嗎?第十一個戰(zhàn)術級思想浮出水面:盡量使用順序代碼結構代替判斷代碼結構。判斷語句是bug產(chǎn)生的源泉,盡量不要使用,哪怕代碼看上去有點愚蠢。不認同的人可以試試,使用判斷語句來修改這段代碼,讓其看起來更有效率。當你被源源不斷的bug改到懷疑人生時,你才能真切地體會到這個思想的精妙之處。對這個未知世界,心存敬畏,才能保你福如東海,壽比南山。

前面談到了十一個戰(zhàn)術思想,每一個戰(zhàn)術思想,可能看過來都不復雜,但偉大往往都藏在細節(jié)中。十一個戰(zhàn)術思想,處處都在體現(xiàn)著:以空間換時間,面向人性編程,面向開發(fā)者編程,面向未來編程,以及對這個未知世界心存敬畏。接下來我們談談戰(zhàn)略級思想。

戰(zhàn)略級思想

戰(zhàn)略級思想一:贈人玫瑰,手有余香

再觀察Record方法的定義,使用了非常罕見的不定長參數(shù),第一個戰(zhàn)略級思想橫空出世:贈人玫瑰,手有余香。作為一個日志文件類的主要方法,通常就是把傳入的字符串參數(shù),存儲到日志文件里就好了。為什么要使用一個非常冷門的技術?原因就是尊重傳統(tǒng),方便你的同行,讓調(diào)用者更干、更爽、更安心。如果參數(shù)是一個字符串,則意味著調(diào)用方必須在調(diào)用此方法前,拼裝好字符串:

使用不定長參數(shù),則可以這樣調(diào)用:

一行搞定!把方便留給別人,把困難留給自己,雷鋒精神時刻謹記。面向人性編程,面向開發(fā)者編程,面向開源編程。

戰(zhàn)略級思想二:簡單通用

通覽整體代碼,系統(tǒng)調(diào)用只使用過一次Windows API,其余均使用C運行時庫函數(shù),第二個戰(zhàn)略級思想橫空出世:簡單通用。作為一個工具類,會被廣泛使用,包括跨平臺應用。如果使用Windows API,此類要移植到Unix/Linux平臺上將付出巨大代價。而C運行時庫函數(shù)是語言標準而非平臺標準,在功能表現(xiàn)上所有平臺都是一致的,所以移植成本要低的多。而且代碼中還使用了C運行時庫函數(shù)的自適應字符集宏定義版本,使得此工具類無論編譯目標應用是MBCS字符集還是Unicode字符集都無需修改一行代碼!事實上,此工具類在組織內(nèi)不但有多平臺版本,甚至還有多語言版本,包括C#、java、VB等。受益于使用語言標準的設計思想,各語言、平臺版本的代碼一致性很高,產(chǎn)生bug的幾率很小,移植成本非常低。

現(xiàn)在我們正式開始通過瀏覽代碼來理解代碼邏輯,先看類構造函數(shù):

戰(zhàn)略級思想三:默認值的藝術

這個初始化還是相當復雜的,對關鍵類成員變量的默認值進行了規(guī)劃和設計,第三個戰(zhàn)略級思想橫空出世:默認值的藝術。為便于理解程序的設計思想,我寫了一個測試程序,使用不同的參數(shù)調(diào)用構造方法,然后調(diào)用對應的成員變量獲取方法,以查看成員變量的內(nèi)容:

從以上結果可以看出,構造函數(shù)通過傳遞不同的參數(shù),將成員變量初始化為不同使用理念的數(shù)據(jù)套。目的就是讓調(diào)用者在構造完類后,即可使用Record方法開始記錄日志,而無需任何配置!還是那句老話:把方便留給別人,把麻煩留給自己,雷鋒精神時刻謹記。

三個戰(zhàn)略級思想,以小搏大,已經(jīng)從簡單到人性上升到墨家的兼愛和利他主義,這其實就是面向開源的編程思想內(nèi)核。下面我再談三個核彈級思想。

核彈級思想

核彈級思想一:即時熱調(diào)試

繼續(xù)仔細研讀測試結果,可了解代碼初始化意圖:給構造函數(shù)傳參空指針(NULL),則日志文件路徑自動配置為當前可執(zhí)行文件路徑,緊接著調(diào)用Record方法即可產(chǎn)生日志文件,nice!如果給構造函數(shù)傳參非空字符串,如示例中是“l(fā)og”,則自動配置日志文件路徑為當前可執(zhí)行文件路徑后再附加“l(fā)og”路徑,enn…如果傳參是空字符串或不傳任何參數(shù)(這是默認情況,應該是該類建議的主要使用方式),則自動配置日志文件路徑為當前可執(zhí)行文件路徑后再附加帶前綴“.”的不包括擴展名的可執(zhí)行文件名,what?

代碼是看懂了,但為啥?難道要自動創(chuàng)建如此詭異的路徑?但在Record方法中,不但沒有找到創(chuàng)建路徑的方法,還看到了這樣一段代碼:

這段代碼的意思就是當日志文件路徑不存在時,將退出Record功能,什么也不干!這個類的作用不就是記錄日志嗎?居然在某些情況下還不應記錄?What the fuck!

第一顆核彈君臨天下:即時熱調(diào)試。像C/C++這種接近硬件底層的編譯型語言,預定義有兩種編譯應用的形態(tài):debug版本和release版本。debug版本用于在開發(fā)環(huán)境中調(diào)試,尤其是單步調(diào)試功能可以解決硬核的技術問題,而release版本用于正式發(fā)布,沒有調(diào)試功能。但代碼調(diào)試時,除了技術問題,還有更大量的業(yè)務邏輯問題需要調(diào)試。如果使用單步調(diào)試效率太低了,所以絕大多數(shù)C/C++程序員在debug版本中通過輸出日志調(diào)試業(yè)務邏輯。這些日志通過宏定義控制只在debug版本中編譯,而在release版本中忽略,因為正式發(fā)布的軟件不能在用戶方產(chǎn)生大量調(diào)試日志,否則日積月累會塞滿用戶的存儲空間。但是,誰也不能保證在debug版本中能調(diào)試完所有的業(yè)務邏輯問題,如果在用戶方部署的release版本出錯,大家束手無策。在互聯(lián)網(wǎng)發(fā)明以前,這個問題到也不太重要,因為即使在用戶方發(fā)現(xiàn)程序錯誤,程序員也沒辦法到達現(xiàn)場解決問題。但現(xiàn)在的互聯(lián)網(wǎng)技術可以支撐遠程登錄服務器或者個人計算機,賦予了技術支持人員可以在任何時間、任何地點、使用任何設備到達錯誤現(xiàn)場的能力,但老舊的編譯時debug和release機制,在新時代下也沒什么卵用。0day的精英們與時俱進,設計了這個動態(tài)debug和release機制:如果在當前可執(zhí)行文件的目錄下,存在一個特別指定的目錄,則程序進入debug狀態(tài),并在那個目錄下生成日志;否則程序保持release狀態(tài),不輸出日志。牛B的思想閃耀星空!把代碼的debug和release狀態(tài)確認由編譯時后移到運行時,這意味著當程序發(fā)生業(yè)務邏輯問題,程序員可直接登錄到現(xiàn)場,程序都不用重啟,直接建立指定目錄,即可知道當前程序正在干什么,找到問題后,再把目錄一刪,揮一揮衣袖,不帶走一片云彩!

這個頂級設計還有一些非常貼心的細節(jié)設計,第一是關于那個指定的目錄。默認是不包括擴展名的當前可執(zhí)行文件名,前面還有一個“.”。這是為了保持跨平臺操作的一致性,因為Unix/Linux平臺下的可執(zhí)行文件沒有擴展名,如果單純使用當前可執(zhí)行文件名,則因為重名而無法創(chuàng)建目錄,所以前面加個“.”來保證不重名,還順便成為隱藏目錄,因為Unix/Linux的文件系統(tǒng)定義以“.”開頭的目錄或文件具備隱藏屬性。雖然Windows平臺下不存在這些問題,但0day的絕大多數(shù)精英都是Windows平臺和Unix/Linux平臺雙料王牌,經(jīng)常需要在多平臺間切換工作,為保持操作一致性,只好委屈一下Windows平臺了。但也無需焦慮,這個指定目錄可以在初始化或運行時隨便修改。修改指定目錄還有一個使用技巧,比如在同一目錄下有A1,A2,A3,B1,B2共5個應用程序,其中A1,A2,A3是有鉤稽關系的第一組應用,B1,B2是有鉤稽關系的第二組應用,可以設計為建立A目錄,則在A目錄中同時產(chǎn)生A1,A2,A3的日志,建立B目錄,則在B目錄中同時產(chǎn)生B1,B2的日志,達到相關應用群日志自動分組的目的。

第二是關于日志文件名。整個文件名分為5個部分:第一部分是應用程序名,這個很容易理解,一看就知道這個日志是哪個應用產(chǎn)生的;第二部分是一個自定義的名字,這個作用比較硬核,咱門后面再講;第三部分是日志產(chǎn)生的日期,為了防止文件過大,每個應用程序每天只有一個日志文件;第四部分比較特殊,是運行時日志文件類實例的內(nèi)存地址,what?這能干啥用?使用實例的內(nèi)存地址意味著每次啟動這個類,文件名就會發(fā)生變化,可用于指示這個應用程序在這個日期下的不同啟動批次。第五部分是固定擴展名“txt”,指示系統(tǒng)可用文本編輯器打開此文件。整個設計考慮了使用上的方方面面,盡量讓使用者更方便、更舒適,愛心媽媽,呵護全家。面向人性編程,面向開發(fā)者編程,面向開源編程。

核彈級思想二:統(tǒng)計日志

再看向文件寫入內(nèi)容的代碼中使用制表符“ ”作為輸出內(nèi)容的分隔符,第二顆核彈石破天驚:統(tǒng)計日志。為了能理解這個設計,咱們先看看調(diào)用方是如何使用這個類的,典型調(diào)用像這樣:

意圖就是把需要輸出的狀態(tài)、數(shù)據(jù),如調(diào)用的方法名、錯誤描述等,組織成類似表格字段的方式分隔輸出。使用制表符可以保證用表格軟件打開日志文件或把文本復制到表格軟件里,效果是這樣的:

在第一個核彈的淫威下,再加上日志的記錄時間精確到毫秒的加持,程序員們徹底放開了,寫日志跟不要錢似的,瘋狂輸出,幾乎每個函數(shù)在返回前都會把當前處理結果輸出到日志里,面對這樣的海量日志,用眼睛找bug會看瞎的。所以創(chuàng)造性的利用表格的相關排序、分類匯總、透視圖等統(tǒng)計功能,快準狠地定位查找目標。比如示例那個日志,用透視圖看是這樣的:

或者是這樣的:

就說你想查啥?咋樣都行,就是拖拖拽拽的事。bug往哪里躲?它太難了…羽扇綸巾,談笑間,強擼灰飛煙滅。

核彈級思想三:調(diào)試多線程

還記得前面講到文件名的第二部分嗎?就是代碼里的成員變量m_szFileName4User,它是干什么用的?看遍代碼的上上下下,也看不出個所以然。我們對其賦值“robot”,看看出現(xiàn)啥情況:

日志文件名第二部分變成“robot”,日志文件中user列里面填充“robot”,仍然一頭霧水!第三顆潛射核彈韜跡隱智:調(diào)試多線程。多線程調(diào)試是程序員的噩夢,因為人類的大腦無法精確模擬計算機多線程的運行過程。所以多線程程序所產(chǎn)生的bug,尤其是無法必現(xiàn)的bug,常常讓人束手無策。在前面兩顆核彈的加持下,給解決這個問題帶來了希望。如果在日志文件類中加入同步機制,多個線程共享同一個日志文件類實例,則會導致多線程程序在調(diào)試狀態(tài)下被強制串行化為單線程程序,由于運行環(huán)境的變化很可能觸發(fā)不了那個多線程bug,所以每個線程必須單獨使用各自的日志文件。在分析時,將相關的所有線程日志全部拷貝到一個表格文件中,利用排序功能就能知道每個時刻,各個線程都正在干什么。這個user列就是用于區(qū)分這條記錄來自于哪個線程。多線程調(diào)試就這么被輕松地搞定了!說了那么多偉大,我都厭倦了:“老婆,快出來看上帝?!?/p>

核彈級思想是高手隱藏在編程中的不容易直接悟出來的彩蛋。但你一旦悟出來,一定會有醍醐灌頂?shù)臅晨炝芾臁?br>

寫在最后

我已把這段代碼上傳到gitee,訪問地址https://gitee.com/aeye/CTools/,歡迎大家共建,也可應用于項目中,給大家在代碼的海洋中探險時提供一把趁手的兵器。

最后,希望同學們也能夠創(chuàng)造出有思想,有靈魂,舉手投足之間都透露出優(yōu)雅的代碼:


<本文完>





原文標題:河套IT TALK——TALK 12:編程的技術|藝術|術術 下篇:對著代碼解讀編程的哲學

文章出處:【微信公眾號:開源技術服務中心】歡迎添加關注!文章轉載請注明出處。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 開源技術
    +關注

    關注

    0

    文章

    389

    瀏覽量

    7905
  • OpenHarmony
    +關注

    關注

    25

    文章

    3641

    瀏覽量

    16067

原文標題:河套IT TALK——TALK 12:編程的技術|藝術|術術 下篇:對著代碼解讀編程的哲學

文章出處:【微信號:開源技術服務中心,微信公眾號:共熵服務中心】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    廈門市開源芯片產(chǎn)業(yè)促進會R-Talk12期成功舉辦

    2024年11月8日,由中國開放指令生態(tài)(RISC-V)聯(lián)盟福建區(qū)域中心和廈門市開源芯片產(chǎn)業(yè)促進會(“開芯會”)聯(lián)合主辦的R-Talk12期活動在中科(廈門)數(shù)據(jù)智能研究院成功舉辦。本次活動邀請
    的頭像 發(fā)表于 11-09 01:10 ?144次閱讀
    廈門市開源芯片產(chǎn)業(yè)促進會R-<b class='flag-5'>Talk</b>第<b class='flag-5'>12</b>期成功舉辦

    凱茉銳電子 野攝像機與FCB-EV7520模組:點燃高清影像捕捉新火花

    在高清影像捕捉技術的不斷探索與追求中,野攝像機與索尼FCB-EV7520模組的結合,無疑為這一領域注入了新的活力與可能。
    的頭像 發(fā)表于 11-04 17:44 ?101次閱讀

    工業(yè)機器人的四種編程(示教編程、離線編程、自增強現(xiàn)實編程編程)剖析!

    和工作量,提高編程效率,實現(xiàn)編程的自適應性,從而提高生產(chǎn)效率,是機器人編程技術發(fā)展的終極追求。本文將就機器人編程技術的發(fā)展作一介紹,希望能給讀者帶來一些啟發(fā)。對工業(yè)
    的頭像 發(fā)表于 08-30 12:14 ?1464次閱讀
    工業(yè)機器人的四種<b class='flag-5'>編程</b>(示教<b class='flag-5'>編程</b>、離線<b class='flag-5'>編程</b>、自增強現(xiàn)實<b class='flag-5'>編程</b>主<b class='flag-5'>編程</b>)剖析!

    解讀 MEMS 可編程 LVCMOS 振蕩器 SiT1602 系列:精準頻率的創(chuàng)新之選

    解讀 MEMS 可編程 LVCMOS 振蕩器 SiT1602 系列:精準頻率的創(chuàng)新之選
    的頭像 發(fā)表于 08-09 15:39 ?282次閱讀
    <b class='flag-5'>解讀</b> MEMS 可<b class='flag-5'>編程</b> LVCMOS 振蕩器 SiT1602 系列:精準頻率的創(chuàng)新之選

    解讀 MEMS 可編程 LVCMOS 振蕩器 SiT8008 系列:精準與靈活的時脈之選

    解讀 MEMS 可編程 LVCMOS 振蕩器 SiT8008 系列:精準與靈活的時脈之選
    的頭像 發(fā)表于 08-09 10:29 ?264次閱讀
    <b class='flag-5'>解讀</b> MEMS 可<b class='flag-5'>編程</b> LVCMOS 振蕩器 SiT8008 系列:精準與靈活的時脈之選

    廈門市開源芯片產(chǎn)業(yè)促進會R-Talk第10期成功舉辦

    2024年6月27日,由中國開放指令生態(tài)(RISC-V)聯(lián)盟福建區(qū)域中心和廈門市開源芯片產(chǎn)業(yè)促進會(“開芯會”)聯(lián)合主辦,廈門市必易微電子科技有限公司協(xié)辦的R-Talk第10期活動在必易微公司成功
    的頭像 發(fā)表于 06-29 08:37 ?352次閱讀
    廈門市開源芯片產(chǎn)業(yè)促進會R-<b class='flag-5'>Talk</b>第10期成功舉辦

    PLC的編程方式及編程語言

    在工業(yè)自動化領域,PLC(Programmable Logic Controller,可編程邏輯控制器)因其強大的控制功能和靈活的編程方式而得到了廣泛應用。PLC的編程方式和編程語言是
    的頭像 發(fā)表于 06-27 14:08 ?560次閱讀

    流式細胞: OEM 激光引擎帶來諸多優(yōu)勢

    ? 基于微型元件的永久對準激光引擎降低了成本,簡化了組裝,可以為新一代流式細胞儀提供更好的變異系數(shù)。要想在任何地方使用激光,流式細胞,特別是多參數(shù)流式細胞是具挑戰(zhàn)性的儀器應用之一。 這是因為多個
    的頭像 發(fā)表于 06-03 06:26 ?1126次閱讀

    奇異摩爾攜手SEMiBAY Talk 邀您暢談互聯(lián)與計算

    2024年5月25日(本周六)19:30,由深圳市半導體與集成電路產(chǎn)業(yè)聯(lián)盟(SICA)主辦的 SEMiBAY Talk“Chiplet 與先進封裝技術和市場趨勢”將在線上舉行。奇異摩爾產(chǎn)品及解決方案
    的頭像 發(fā)表于 05-20 18:31 ?926次閱讀
    奇異摩爾攜手SEMiBAY <b class='flag-5'>Talk</b> 邀您暢談互聯(lián)與計算

    傳智教育聯(lián)合科大訊飛舉辦“AI開發(fā)者TALK”活動

    3月23日,由傳智教育與科大訊飛聯(lián)合組織的大模型實戰(zhàn)應用之“AI開發(fā)者 TALK·北京站”在海淀舉辦。本次活動圍繞“大模型應用”展開探討,旨在為廣大AI開發(fā)者提供一個交流、學習和展示的平臺。 活動
    的頭像 發(fā)表于 03-26 16:12 ?405次閱讀
    傳智教育聯(lián)合科大訊飛舉辦“AI開發(fā)者<b class='flag-5'>TALK</b>”活動

    用于流式細胞的新型紫外激光器

    現(xiàn)在,相干公司的 OBIS XT “智能”緊湊型紫外激光器系列,具有高達 150 mW 功率,全新 320 nm 波長。 在多參數(shù)流式細胞中,儀器和熒光調(diào)色板目前都進一步推向紫外波段,以增加參數(shù)
    的頭像 發(fā)表于 03-26 06:41 ?271次閱讀

    g73編程R怎么算

    編程是一門使用計算機語言來創(chuàng)建、編寫和修改代碼的技能。在編程過程中,計算機程序員通過使用各種編程語言來告訴計算機執(zhí)行特定的任務。其中,G73編程
    的頭像 發(fā)表于 02-14 15:57 ?686次閱讀

    在數(shù)控編程中,g代碼的作用是什么

    在數(shù)控編程中,G代碼是一種用于控制數(shù)控機床運動和功能的編程語言。它是數(shù)控加工過程中的重要組成部分,通過編寫G代碼,人們可以靈活地控制數(shù)控機床執(zhí)行各種精密和復雜的操作,從而實現(xiàn)高精度、高
    的頭像 發(fā)表于 02-14 15:53 ?1249次閱讀

    數(shù)控編程的g功能代碼是什么

    數(shù)控編程中,G代碼(也稱為指令代碼)是一種用于控制數(shù)控機床運動、輔助功能和工作過程的指令。在數(shù)控編程中,通過一系列的G代碼指令的組合和排列,
    的頭像 發(fā)表于 02-14 15:51 ?3521次閱讀

    Tech Talk解讀閃存原理與顆粒類型

    ,NAND閃存的存儲方式和堆疊技術也在持續(xù)演進。本文將圍繞閃存顆粒相關的概念以及發(fā)展趨勢做介紹。NAND閃存單元NAND閃存基于浮柵晶體管,通過其中所存儲的電荷量表示不同
    的頭像 發(fā)表于 02-05 18:01 ?872次閱讀
    Tech <b class='flag-5'>Talk</b>:<b class='flag-5'>解讀</b>閃存原理與顆粒類型