靜態檢查是比較好的一種自動檢查代碼工具,可以發現一些隱藏問題,當然更多是讓你的代碼更加規范,更加在可控范圍內。
以下是我整理的錯誤,也是自己對C++進一步的思考
- 構造函數顯式調用問題
提示:
Class 'CLBTimeSpan' has a constructor with 1 argument that is not explicit. Such constructors should in general be explicit for type safety reasons. Using the explicit keyword in the constructor means some mistakes when using the class can be avoided.
翻譯:
類'CLBTimeSpan'有一個帶有1個參數的構造函數,它不是顯式的。 出於類型安全的原因,這些構造函數通常應該是明確的。 在構造函數中使用explicit關鍵字意味着可以避免在使用類時出現一些錯誤。
具體源碼:
原因分析:
1. 構造只有一個參數時,容易被隱式調用,這樣容易導致初始化錯誤。
例如:
CLBTimeSpan dtSpan = 0; //隱式調用其構造函數,默認調用
2. 要使用“explicit” 關鍵字進行約束
改進要點:
改成:explicit CLBTimeSpan(const time_t &t);
改成這樣之后
CLBTimeSpan dtSpan = 0; //編譯錯誤,不能隱式調用其構造函數
CLBTimeSpan dtSpan(0); //顯式調用成功
擴展閱讀:
C++中,一個參數的構造函數(或者除了第一個參數外其余參數都有默認值的多參構造函數),承擔了兩個角色。1 是個構造器 ,2 是個默認且隱含的類型轉換操作符。
所以,有時候在我們寫下如 AAA a = XXX,這樣的代碼,且恰好XXX的類型正好是AAA單參數構造器的參數類型,這時候編譯器就自動調用這個構造器,創建一個AAA的對象。
這樣看起來好象很酷,很方便。但在某些情況下,卻違背了我們(程序員)的本意。這時候就要在這個構造器前面加上explicit修飾,指定這個構造器只能被明確的調用/使用,不能作為類型轉換操作符被隱含的使用。
- 構造函數直接賦值
提示:
When an object of a class is created, the constructors of all member variables are called consecutively in the order the variables are declared, even if you don't explicitly write them to the initialization list. You could avoid assigning 'm_timeSpan' a value by passing the value to the constructor in the initialization list.
翻譯:
創建類的對象時,即使您沒有將它們顯式寫入初始化列表,也會按聲明變量的順序連續調用所有成員變量的構造函數。 您可以通過將值傳遞給初始化列表中的構造函數來避免為'm_timeSpan'賦值。
對應的源碼:
原因分析:
這個性能改進項,主要意思就是通過構造函數對成員變量進行初始化的,建議使用直接初始化,不用調用=進行初始化。當然輸入參數,改成引用效果更佳
改進要點:
CLBTimeSpan::CLBTimeSpan(consttime_t &t): m_timeSpan(t)
{
//m_timeSpan = t;
}
擴展閱讀:
其實在構造函數里面調用等於號並不是真正意義上的“初始化”(未改進之前的方式)。這個過程相當於:
1. 系統創建成員變量;
2. 創建完后再進行賦值操作。
而在構造函數后面跟冒號,就相當於:
1. 系統創建成員變量並且初始化。也就是系統為成員變量分配了一塊內存並且把相應的數據給填了進去。而構造函數里面調用等於號的方式是分配好后再進行賦值,多了一個步驟。
- 虛函數的作用范圍
提示:
Virtual function 'Open' is called from constructor 'CBackFile(char*strFileName,uint nOpenFlags)' at line 53. Dynamic binding is not used.
翻譯:
虛函數'Open'在第53行從構造函數'CBackFile(char * strFileName,uint nOpenFlags)'調用。不使用動態綁定。
對應源碼位置:
virtual bool Open(char *strFileName, uint nOpenFlags);
原因分析:
Open是虛函數,如果不是用於接口定義的話(也就是不存在動態綁定)且本身是有實現的話,建議是不用使用虛函數。
這個虛函數本身是作用為了將來派生用的(派生類會)。但實際代碼,的確未使用動態綁定的特性。另外虛函數是會占一定空間,導致編譯之后的可執行文件體積變大,效率方面也是有所降低,這里不推薦使用虛函數。
改進要點:
擴展閱讀:
一般虛函數是用於“實現多態性(Polymorphism),多態性是將接口與實現進行分離”還有就是析構函數這里經常要用到虛函數。
但是我們更應該要知道虛函數的缺點:
如果某個類不包含虛函數,那一般是表示它將不作為一個基類來使用。當一個類不准備作為基類使用時,使析構函數為虛一般是個壞主意。因為它會為類增加一個虛函數表,對象增加一個虛指針,使得對象的體積增大。所以基本的一條是:無故的聲明虛析構函數和永遠不去聲明一樣是錯誤的。
同理,也可以知道如果不是當基類用途的類,析構函數也是不需要使用虛函數的。
實驗證明,如果程序里減少虛函數,可以縮減可執行文件體積大小。
- 構造函數里未完全初始化成員變量
提示:
Member variable 'CLBTime::m_status' is not initialized in the constructor.
翻譯:
成員變量'CLBTime :: m_status'未在構造函數中初始化。
也有可能這樣提示:(同一個意思)
When an object of a class is created, the constructors of all member variables are called consecutively in the order the variables are declared, even if you don‘t explicitly write them to the initialization list. You could avoid assigning ‘m_strHrefOnPanReady‘ a value by passing the value to the constructor in the initialization list.
翻譯:
創建類的對象時,即使您沒有將它們顯式寫入初始化列表,也會按聲明變量的順序連續調用所有成員變量的構造函數。 您可以通過將值傳遞給初始化列表中的構造函數來避免為“m_strHrefOnPanReady”賦值。
對應源碼位置:
原因分析:
這個問題比較直接,就是成員變量未初始化。
改進要點:
擴展閱讀:
一般來說構造函數用來初始化成員變量(這是個好習慣),但如果其中有個變量未初始化,也存在很大風險。所以要核對是否所有的成員變量都初始化(比較安全的數)
- 成員變量是需要動態分配內存
標題可能有點問題,大概意思是成員變量有指針,那么要重載=
提示:
Class 'CLogFile' does not have a operator= which is recommended since it has dynamic memory/resource allocation(s).
翻譯:
類'CLogFile'沒有operator =,因為它具有動態內存/資源分配,所以建議使用它。
對應源碼位置:
原因分析:
意思是構造函數有動態分配內存,所以建議該類增加operator =函數。為什么呢?
因為是存在動態分配,因此如果默認調用A=B的時候,會默認調用淺拷貝,就好比只是B里成員變量的指針拷貝給A里成員變量,而不是拷貝“成員變量指針指向內容”,這樣就會帶來指針被多次引用,指向地方又是同一個地方,存在很大風險。
改進要點:
增加CLogFile 的operator =函數
擴展閱讀:
1.淺拷貝: 將原對象或原數組的引用直接賦給新對象,新數組,新對象/數組只是原對象的一個引用
2.深拷貝: 創建一個新的對象和數組,將原對象的各項屬性的“值”(數組的所有元素)拷貝過來,是“值”而不是“引用”
這個重載“=”,使用
CExample theObjthree;
theObjthree.init(60);//現在需要一個對象賦值操作,被賦值對象的原內容被清除,並用右邊對象的內容填充。
theObjthree = theObjone; //此時調用“=”操作符,若CExample內有申請操作,此時 theObjthree里的指針是指向 theObjone,從而忘記里對theObjthree內空間釋放導致內存的泄露。
- 指針類型缺少強制轉換
提示:
C-style pointer casting detected. C++ offers four different kinds of casts as replacements: static_cast, const_cast, dynamic_cast and reinterpret_cast. A C-style cast could evaluate to any of those automatically, thus it is considered safer if the programmer explicitly states which kind of cast is expected. See also: https://www.securecoding.cert.org/confluence/display/cplusplus/EXP05-CPP.+Do+not+use+C-style+casts.
翻譯:
檢測到C風格的指針轉換。 C ++提供了四種不同類型的轉換作為替換:static_cast,const_cast,dynamic_cast和reinterpret_cast。 C風格的強制轉換可以自動評估任何一個,因此如果程序員明確說明預期哪種類型的強制轉換,則認為它更安全。 另請參閱:https://www.securecoding.cert.org/confluence/display/cplusplus/EXP05-CPP.+Do+not+use+C-style+casts。
原因分析:
不要用C風格進行強制轉換
static_cast,const_cast,dynamic_cast和reinterpret_cast 替換原先
改進要點:
擴展閱讀:
實際上static_cast,const_cast,dynamic_cast和reinterpret_cast都是用於C++類型強制轉換,主要用於繼承。
static_cast 應該比較頻繁,static_cast主要是一些類型轉換。static_cast相當於傳統的C語言里的強制轉換,該運算符把expression轉換為new_type類型,用來強迫隱式轉換,例如non-const對象轉為const對象,編譯時檢查,用於非多態的轉換,可以轉換指針及其他,但沒有運行時類型檢查來保證轉換的安全性。它主要有如下幾種用法:
① 用於類層次結構中基類(父類)和派生類(子類)之間指針或引用的轉換。
進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;
進行下行轉換(把基類指針或引用轉換成派生類表示)時,由於沒有動態類型檢查,所以是不安全的。
②用於基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。
③把空指針轉換成目標類型的空指針。
④把任何類型的表達式轉換成void類型。
注意:static_cast不能轉換掉expression的const、volatile、或者__unaligned屬性。
- 缺少拷貝構造函數
這里是指當該類存在指針的成員變量時,需要增加拷貝構造函數
提示:
Class 'CLocalSocket' does not have a copy constructor which is recommended since it has dynamic memory/resource allocation(s).
翻譯:
類'CLocalSocket'沒有建議(默認)的復制構造函數,因為它具有動態內存/資源分配。
原因分析:
首先確定的是C++中每個類一定有一個拷貝構造函數(好習慣)。它可以用來創建一個對象,並用另一個對象的數據初始化新建對象。你的代碼中如果沒有顯示定義拷貝構造函數(用來實現你自己的一些特殊需求),則C++將為每個類隱式地提供一個缺省的拷貝構造函數。缺省的拷貝構造函數簡單地將參數對象的每個數據域復制給新建對象中相應的副本(注意如果數據域是一個指向其他對象的指針,那么僅簡單地復制指針保存的地址值,而不是復制對象的內容)(會產生淺拷貝,因此成員變量有動態內容)
改進要點:
增加CLocalSocket 的拷貝構造函數
擴展閱讀:
拷貝構造函數,又稱復制構造函數,是一種特殊的構造函數,它由編譯器調用來完成一些基於同一類的其他對象的構建及初始化。
在C++中,下面三種對象需要調用拷貝構造函數(有時也稱“復制構造函數”):
1) 一個對象作為函數參數,以值傳遞的方式傳入函數體;
例如:BOOL testfunc(CExample obj);
2) 一個對象作為函數返回值,以值傳遞的方式從函數返回;
CTest func()
{
CTest theTest;
return theTest;
}
3) 一個對象用於給另外一個對象進行初始化(常稱為賦值初始化);
CExample theObjtwo = theObjone; //注意此時調用的不是重載“=”
還有:CExample theObjthree(theObjone);。//顯式調用
- 減少變量的作用域
其實的意思,有些變量沒有必要在前面就申請。建議用到之前申明即可
提示:
The scope of the variable 'byDir' can be reduced.
翻譯:
可以減少變量'byDir'的范圍。
原因分析: byDir 實際上就是在for循環內部使用,建議在for循環進行初始化。(個人認為這個不是很好的建議,會增加構造函數調用次數,實際情況請適當改進) 改進要點: for (int i = 0; i < 0x0040; i++) { byte byDir = (byte)((i >> 4) & 0x0F); //高4,表 abc byte byNum = (byte)(i & 0x0F); //低3,表 數字 sprintf(strUPath, "/media/sd%c%d/", strUDiskDir[byDir],byNum); if (Utility::IsExist_Dir(strUPath)) { nRes = i; break; } }
- 迭代器使用后置疊加或減運算
增加效率的問題
提示:
Prefix ++/-- operators should be preferred for non-primitive types. Pre-increment/decrement can be more efficient than post-increment/decrement. Post-increment/decrement usually involves keeping a copy of the previous value around and adds a little extra code.
翻譯:
前綴++ / - 運算符應該是非基本類型的首選。 預增量/減量可以比后增量/減量更有效。 后遞增/遞減通常涉及保留前一個值的副本並添加一些額外的代碼。
原因分析:
迭代器前置++和后置++的運行效率是不同的,前置++效率更高,因為后置運算符需要做一個臨時的類對象拷貝。
這里是不是比較坑?
如果是后置:
調用過程(從匯編語言可以分析)
a. 先拷貝*this內容到_temp中;b. 調用前置++操作符,自增*this的內容;c. 返回_temp;
b. 從過程就可以輕易看出,后置++在前置++的基礎上,至少多了步驟a和c;
如果是前置:
調用過程(從匯編語言可以分析)
a. a.直接遞增;c. 返回(*this);
前置式迭代器不需要傳回舊值,所以不需要花費一個臨時對象來保存舊值。因此,面對任何迭代器(以及任何抽象數據型別),應該優先使用前置式。這條規則對遞減操作也同樣適用。
- 直接在函數參數中使用C風格字符串
提示:
The conversion from const char* as returned by c_str() to std::string creates an unnecessary string copy. Solve that by directly passing the string.
翻譯:
從c_str()返回到std :: string的const char *的轉換會創建一個不必要的字符串副本。 通過直接傳遞字符串來解決這個問題。
原因分析:
比如一個函數的參數類型是string,調用時實參直接使用了C風格(c_str())的字符串,於是就會有以上提示,主要還是因為這里會造成一個不必要的字符串拷貝,降低運行效率。
這里也有坑:大家可以度娘下 c_str()
擴展閱讀:
char* c;
string s="1234";
c = s.c_str(); //c最后指向的內容是垃圾,因為s對象被析構,其內容被處理,同時,編譯器也將報錯——將一個const char *賦與一個char *。野指針無意之中出現,很尷尬。
應該這樣用:
char c[20];
string s="1234";
strcpy(c,s.c_str());
這樣才不會出錯,c_str()返回的是一個臨時指針,不能對其進行操作
這樣說嗎c_str()會產生一個臨時空間,而且調用完c_str()就無效了。存在效率問題,也存在使用風險,好在大部分編譯器能報錯
- 使用無用的find
提示:
Either inefficient or wrong usage of string::find(). string::compare() will be faster if string::find's result is compared with 0, because it will not scan the whole string. If your intention is to check that there are no findings in the string, you should compare with std::string::npos.
翻譯:
string :: find ()的低效或錯誤用法。 如果將string :: find的結果與0進行比較,則string :: compare()會更快,因為它不會掃描整個字符串。 如果您打算檢查字符串中沒有發現,則應與std :: string :: npos進行比較。
原因分析:
find() 效率低,要用compare
代碼本身不會出錯,但是效率上是不被認可的,如cppcheck所說,如果你希望檢查某個字符串是不是某個期望的字符串,那你應該使用compare函數,因為這樣更快。
擴展閱讀:
很多時候,我們會寫的find代碼,值不等於-1則說明找到了該字符串,這樣做效率比較低,可以用compare比較
C++標准庫里面的string::rfind和string:find不是用快速匹配算法實現的,效率不是一般的差
在實踐中還發現,string::find(char)比較string::find(string)慢很多
- 函數參數使用傳值而不是傳引用
提示:
Parameter 'strShowTime' is passed by value. It could be passed as a const reference which is usually faster and recommended in C++.
翻譯:
參數'strShowTime'按值傳遞。 它可以作為const引用傳遞,通常更快並且在C ++中推薦。
原因分析:
值傳遞會產生賦值操作,會產生臨時變量,因此效率比較低(對於超過4字節的結構體、字符串)
可以改成 const & 方式
- 使用memset清空含有string(wstring)類型成員的結構體
提示:
Using 'memset' on struct that contains a 'std::wstring'. [memsetClass]
翻譯:
原因分析:
在C語言中,使用memset清空內存是常用操作,在C++中,和malloc一樣,使用memset直接操作內存是有很大風險的,因為它們都只是在內存層面做了改動,但是對類對象本身卻沒有任何行動,也就是說,他們不會調用類對象的構造或者析構函數,如果我們的結構體中含有string類型的成員,直接使用memset很可能導致內存泄漏!
這里涉及到一個POD參考的概念。如果一個類不是POD的,那么就不應該使用如mem*,malloc等內存操作函數,否則,我們將得不到我們想要的東西,甚至引發錯誤。
擴展閱讀:
測試程序以memcpy函數為例,運行本程序,雖然tt2可以正常的將str1拷貝進來,但是最后程序奔潰了!!
想想原因是為何?
程序崩潰在tt的析構函數中,因為無法探究memcpy到底做了哪些具體操作,所以我們可以猜測它是將tt的內存區域完整的拷貝到了tt2內,但是,tt2中的string類型的成員str1並沒有完成自己的構造,而是通過內存拷貝的方式完整的使用了tt的數據,那么執行完delete ,tt2的str1也會析構自身,這時其析構的其實是tt的str1。等到tt再去析構自己時,奔潰就發生了!以上只是自己的猜測。可能不是事實,但是,當我把delete tt2注釋后,崩潰就消失了,這也能證明自己上面的論述。
在C++中,除非是明確的POD類型,否則放棄使用mem*系包括malloc等傳統C內存操作函數。
首先,我們需要弄明白,我們為什么需要使用memet,因為我們需要將這個結構體的數據清零,所以我們真正需要的是一個結構體(類)的構造函數!在類中寫一個結構體,將類里的所有成員變量進行列表初始化就可以解決這個問題了。
話說回來,就好像,我們在寫C代碼時, 如果結構體某個成員類型需要是結構體類型,我們使用了該結構體指針一樣,我們同樣可以使用string類型的指針來表示該成員類型。畢竟在VS2010環境下,一個string類型的內存占用就是32byte,而一個string*只有4byte。如果擔心hold不住指針類型,可以使用智能指針來折中,如shared_ptr,內存的占用將減小到8。事實上,使用智能使用已經是一個最優方案了。
Ps:string不是常規類型,使用時不能簡單等同於“int”之類進行操作,否則很多坑的
- size() 判斷是否為空,效率低
提示:
Checking for 'm_listSendBuffer' emptiness might be inefficient. Using m_listSendBuffer.empty() instead of m_listSendBuffer.size() can be faster. m_listSendBuffer.size() can take linear time but m_listSendBuffer.empty() is guaranteed to take constant time.
翻譯:
檢查'm_listSendBuffer'空虛可能效率低下。 使用m_listSendBuffer.empty()而不是m_listSendBuffer.size()可以更快。 m_listSendBuffer.size()可以占用線性時間,但m_listSendBuffer.empty()保證需要恆定的時間。
在vocter里empty()的效率要高於size
擴展閱讀:
size_type size() const
{
size_type __result = 0;
distance(begin(), end(), __result);
return __result;
}