複雜的東西寫多了,現在寫點簡單的好了。因爲功能上的須要,Vczh Library++3.0被我搞得很離譜。爲了開發維護的遍歷、減小粗心犯下的錯誤以及加強單元測試、迴歸測試和測試工具,所以記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。
C++實謂各類語言中的軟肋,功能強大,陷阱更強大。固然我認爲一門語言用得很差徹底是程序員的責任,不過由於C++涉及到的概念實在是太多,想用好實在也不是一件容易的事情。C++開發的時候老是會遇到各類各樣的問題,其中最嚴重的無非是內存相關的。C語言因爲結構簡單,內存處理起來雖然不得力,但總的來講慣用法已經深刻人心,所以也不會形成什麼很難發現的錯誤。C++就不同了。有了虛函數、構造函數、析構函數、複製構造函數和operator=重載以後,仍是有不少人喜歡把一個類直接寫進文件流,或者拿來memset,代碼一團亂麻,不知悔改也。可是不能所以因噎廢食,就像某人由於C++帶來的心智問題太多,本身搞不定,本身團隊也搞不定,就說C++很差同樣。
所以第一篇文章主要針對內存來說。咱們處理內存,第一件事就是不要有內存泄露。內存泄露不能等到測試的時候,經過長時間運行程序並觀察任務管理器的方法來作,這顯然已經晚了。幸虧Visual C++給了咱們一個十分好用的工具:_CrtDumpMemoryLeaks函數。這個函數會在Debug模式下往Visual Studio的output窗口打印出那個時候你new(malloc)了可是還沒delete(free)的全部內存塊的地址、長度、前N個字節的內容和其餘信息。怎麼作呢?其實很簡單:
1 #define _CRTDBG_MAP_ALLOC
2 #include <stdlib.h>
3 #include <crtdbg.h>
4 #include <windows.h>
5
6 int wmain(vint argc , wchar_t* args[])
7 {
8 // 這裏運行程序,並在下面的函數調用以前delete掉全部new的東西
9 _CrtDumpMemoryLeaks();
10 return 0;
11 }
咱們只須要在註釋的地方完成咱們程序的功能,而後確信本身已經delete掉全部應該delete的東西,最後_CrtDumpMemoryLeaks()函數調用的時候就能夠打印出沒被delete的東西了。這個方法十分神奇,由於你只須要在main函數所在的cpp文件這麼#include一下,全部的cpp文件裏面的new都會受到監視,跟日常所用的用宏把new給換掉的這種破方法大相徑庭。若是你使用了全局變量的話也要當心,由於全局變量的析構函數是在main函數結束以後才執行的,所以若是在全局變量的析構函數裏面delete的東西仍然會被_CrtDumpMemoryLeaks函數當成泄露掉的資源對待。固然本人認爲全局變量能夠用,可是全局變量的賦值必須在main裏面作,釋放也是,除非那個全局變量的構造函數沒有申請任何內存,因此這也是一個很好的檢查方法。
不過上面也僅僅是一個告訴你有沒有內存泄漏的方法罷了。那麼如何避免內存泄露呢?固然在設計一些性能要求沒有比操做系統更加嚴格的程序的時候,可使用如下方法:
一、若是構造函數new了一個對象並使用成員指針變量保存的話,那麼必須在析構函數delete它,而且不能有爲了某些便利而將這個對象的全部權轉讓出去的事情發生。
二、在能使用shared_ptr的時候,儘可能使用shared_ptr。shared_ptr只要你不發生循環引用,那麼這個東西能夠安全地互相傳遞、隨便你放在什麼容器裏面添加刪除、你想放哪裏就放在哪裏,不再用考慮這個對象的生命週期問題了。
三、不要在有構造函數和析構函數的對象上使用memset(或者memcpy)。若是一個對象須要memset,那麼在該對象的構造函數裏面memset本身。若是你須要memset一個對象數組,那也在該對象的構造函數裏面memset本身。若是你須要memset一個沒有構造函數的複雜對象,那麼請爲他添加一個構造函數,除非那是別人的API提供的東西。
四、若是一個對象是繼承了其餘東西,或者某些成員被標記了virtual的話,絕對不要memset。對象是獨立的,也就是說父類內部結構的演變不須要對子類負責。哪天父類裏面加了一個string成員,被子類一memset,就欲哭無淚了。
五、若是須要爲一個對象定義構造函數,那麼連複製構造函數、operator=重載和析構函數都所有寫全。若是不想寫複製構造函數和operator=的話,那麼用一個空的實現寫在private裏面,確保任何試圖調用這些函數的代碼都出現編譯錯誤。
六、若是你實在很喜歡C語言的話,那麻煩換一個只支持C不支持C++的編譯器,全面杜絕由於誤用了C++而致使你的C壞掉的狀況出現。
什麼是循環引用呢?若是兩個對象互相使用一個shared_ptr成員變量直接或者間接指向對方的話,就是循環引用了。在這種狀況下引用計數會失效,由於就算外邊的shared_ptr全釋放光了,引用計數也不會是0的。
今天就說到這裏了,過幾天我高興的話再寫一篇續集,若是我持續高興的話呢……嗯嗯……。
複雜的東西寫多了,現在寫點簡單的好了。因爲功能上的須要,Vczh Library++3.0被我搞得很離譜。爲了開發維護的遍歷、減小粗心犯下的錯誤以及加強單元測試、迴歸測試和測試工具,所以記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。
上一篇文章講到了如何檢查內存泄露。其實只要肯用C++的STL裏面的高級功能的話,內存泄露是很容易避免的。我在開發Vczh Library++ 3.0的時候,全部的測試用例都保證跑完了沒有內存泄露。可是很惋惜有些C++團隊不能使用異常,更甚者不容許寫構造函數析構函數之類,前一個還好,後一個簡直就是在用C。固然有這些變態規定的地方STL都是用不了的,因此咱們更加須要紮實的基礎來開發C++程序。
今天這一篇主要仍是講指針的問題。由於上一篇文章一筆帶過,今天就來詳細講內存泄漏或者野指針發生的各類狀況。固然我不可能一會兒舉出所有的例子,只能說一些常見的。
1、錯誤覆蓋內存。
以前提到的不能隨便亂memset其實就是爲了不這個問題的。其實memcpy也不能亂用,咱們來看一個例子,最簡單的:
1 #define MAX_STRING 20;
2
3 struct Student
4 {
5 char name[MAX_STRING];
6 char id[MAX_STRING];
7 int chinese;
8 int math;
9 int english;
10 };
你們對這種結構確定十分熟悉,畢竟是大學時候常常要寫的做業題……好了,你們很容易看得出來這實際上是C語言的經典寫法。咱們拿到手以後,通常會先初始化一下,而後賦值。
1 Student vczh;
2 memset(&vczh, 0, sizeof(vczh));
3 strcpy(vczh.name, "vczh");
4 strcpy(vczh.id, "VCZH'S ID");
5 vczh.chinese=70;
6 vczh.math=90;
7 vczh.english=80;
爲何要在這裏使用memset呢?memset的用處是將一段內存的每個字節都設置成同一個數字。這裏是0,所以兩個字符串成員的全部字節都會變成0。所以在memset了Student以後,咱們經過正常方法來訪問name和id的時候都會獲得空串。並且若是Student裏面有指針的話,0指針表明的是沒有指向任何有效對象,所以這個時候對指針指向的對象進行讀寫就會馬上崩潰。對於其餘數值,0通常做爲初始值也不會有什麼問題(double什麼的要當心)。這就是咱們寫程序的時候使用memset的緣由。
好了,現在社會進步,人民當家作主了,死程們不再須要受到可惡的C語言剝削了,咱們可使用C++!所以咱們藉助STL的力量把Student改寫成下面這種帶有C++味道的形式:
1 struct Student
2 {
3 std::string name;
4 std::string id;
5 int chinese;
6 int math;
7 int english;
8 };
咱們仍然須要對Student進行初始化,否則三個分數仍是隨機值。可是咱們又不想每一次建立的時候都對他們分別進行賦值初始化城0。這個時候你內心可能仍是想着memset,這就錯了!在memset的時候,你會把std::string內部的不知道什麼東西也給memset掉。假如一個空的std::string裏面存放的指針指向的是一個空的字符串而不是用0來表明空的時候,一會兒內部的指針就被你刷成0,等下std::string的析構函數就沒辦法delete掉指針了,因而內存泄露就出現了。有些朋友可能不知道上面那句話說的是什麼意思,咱們如今來模擬一下不能memset的std::string要怎麼實現。
爲了讓memset必定出現內存泄露,那麼std::string裏面的指針必須永遠都指向一個有效的東西。固然咱們還須要在字符串進行復制的時候複製指針。咱們這裏不考慮各類優化技術,用最簡單的方法作一個字符串出來:
1 class String
2 {
3 private:
4 char* buffer;
5
6 public:
7 String()
8 {
9 buffer=new char[1];
10 buffer[0]=0;
11 }
12
13 String(const char* s)
14 {
15 buffer=new char[strlen(s)+1];
16 strcpy(buffer, s);
17 }
18
19 String(const String& s)
20 {
21 buffer=new char[strlen(s.buffer)+1];
22 strcpy(buffer, s.buffer);
23 }
24
25 ~String()
26 {
27 delete[] buffer;
28 }
29
30 String& operator=(const String& s)
31 {
32 delete[] buffer;
33 buffer=new char[strlen(s.buffer)+1];
34 strcpy(buffer, s.buffer);
35 }
36 };
因而咱們來作一下memset。首先定義一個字符串變量,其次memset掉,讓咱們看看會發生什麼事情:
1 string s;
2 memset(&s, 0, sizeof(s));
第一行咱們構造了一個字符串s。這個時候字符串的構造函數就會開始運行,所以strcmp(s.buffer, "")==0。第二行咱們把那個字符串給memset掉了。這個時候s.buffer==0。因而函數結束了,字符串的析構函數嘗試delete這個指針。咱們知道delete一個0是不會有問題的,所以程序不會發生錯誤。咱們活生生把構造函數賦值給buffer的new char[1]給丟了!鐵定發生內存泄露!
好了,提出問題總要解決問題,咱們不使用memset的話,怎麼初始化Student呢?這個十分好作,咱們只須要爲Student加上構造函數便可:
1 struct Student
2 {
3 .//不重複那些聲明
4
5 Student():chinese(0),math(0),english(0)
6 {
7 }
8 };
這樣就容易多了。每當咱們定義一個Student變量的時候,全部的成員都初始化好了。name和id由於string的構造函數也本身初始化了,所以全部的成員也都初始化了。加入Student用了一半咱們想再初始化一下怎麼辦呢?也很容易:
1 Student vczh;
2 .//各類使用
3 vczh=Student();
通過一個等號操做符的調用,舊Student的全部成員就被一個新的初始化過的Student給覆蓋了,就如同咱們對一個int變量從新賦值同樣常見。固然由於各類複製常常會出現,所以咱們也要跟上面貼出來的string的例子同樣,實現好那4個函數。至此我十分不理解爲何某些團隊不容許使用構造函數,我猜就是爲了能夠memset,實際上是很沒道理的。
2、異常。
咋一看內存泄露跟異常好像沒什麼關係,但實際上這種狀況更容易發生。咱們來看一個例子:
1 char* strA=new char[MAX_PATH];
2 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
3 char* strB=new char[MAX_PATH];
4 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
5
6 DoSomething(strA, strB);
7
8 RELEASE_STRB:
9 delete[] strB;
10 RELEASE_STRA:
11 delete[] strA;
相信這確定是你們的經常使用模式。我在這裏也不是教唆你們使用goto,不過對於這種例子來講,用goto是最優美的解決辦法了。可是你們能夠看出來,咱們用的是C++,由於這裏有new。若是DoSomething發生了異常怎麼辦呢?若是GetXXX發生了異常怎麼辦呢?咱們這裏沒有任何的try-catch,一有異常,函數裏克結束,兩行可憐的delete就不會被執行到了,因而內存泄漏發生了!
那咱們如何避免這種狀況下的內存泄露呢?一些可愛的小盆友可能會想到,既然是由於沒有catch異常才發生的內存泄露,那咱們來catch吧:
1 char* strA=new char[MAX_PATH];
2 try
3 {
4 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
5 char* strB=new char[MAX_PATH];
6 try
7 {
8 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
9 DoSomething(strA, strB);
10 }
11 catch()
12 {
13 delete[] strB;
14 throw;
15 }
16 }
17 catch()
18 {
19 delete[] strA;
20 throw;
21 }
22
23 RELEASE_STRB:
24 delete[] strB;
25 RELEASE_STRA:
26 delete[] strA;
你能接受嗎?固然是不能的。問題出在哪裏呢?由於C++沒有try-finally。你看這些代碼處處都是雷同的東西,顯然咱們須要編譯器幫咱們把這些問題搞定。最好的解決方法是什麼呢?顯然仍是構造函數和析構函數。總之記住,若是想要事情成對發生,那麼使用構造函數和析構函數。
第一步,GetXXX顯然只能支持C模式的東西,所以咱們要寫一個支持C++的:
1 bool GetXXX2(string& s)
2 {
3 char* str=new char[MAX_PATH];
4 bool result;
5 try
6 {
7 result=GetXXX(str, MAX_PATH);
8 if(result)s=str;
9 }
10 catch()
11 {
12 delete[] str;
13 throw;
14 }
15 delete[] str;
16 return result;
17 }
藉助這個函數咱們能夠看到,由於有了GetXXX這種C的東西,致使咱們多了多少麻煩。不過這老是一勞永逸的,有了GetXXX2和修改以後的DoSomething2以後,咱們就能夠用更簡單的方法來作了:
1 string a,b;
2 if(GetXXX2(a) && GetXXX2(b))
3 {
4 DoSomething2(a, b);
5 }
多麼簡單易懂。這個代碼在任何地方發生了異常,全部new的東西都會被delete。這就是析構函數的一個好處。一個變量的析構函數在這個變量超出了做用域的時候必定會被調用,不管代碼是怎麼走出去的。
今天就說到這裏了。說了這麼多仍是想讓你們不要小看構造函數和析構函數。那種微不足道的由於一小部分不是瓶頸的性能問題而放棄構造函數和析構函數的作法,終究是要爲了修bug而加班的。只要明白並用好了構造函數、析構函數和異常,那麼C++的特性也能夠跟C同樣清楚明白便於理解,並且寫出來的代碼更好看的。你們期待第三篇哈。
複雜的東西寫多了,現在寫點簡單的好了。因爲功能上的須要,Vczh Library++3.0被我搞得很離譜。爲了開發維護的遍歷、減小粗心犯下的錯誤以及加強單元測試、迴歸測試和測試工具,所以記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。
今天是關於內存的最後一篇了。上一篇文章講了爲何不能對一個東西隨便memset。裏面的demo代碼出了點小bug,不過我不喜歡在發文章的時候裏面的demo代碼也拿去編譯和運行,因此你們有什麼發現的問題就評論吧。這樣也便於後來的人不會受到誤導。此次說的仍然是構造函數和析構函數的事情,不過咱們將經過親手開發一個智能指針的方法,知道引用計數如何幫助管理資源,以及錯誤使用引用計數的狀況。
首先先來看一下智能指針是如何幫助咱們管理內存的。如今智能指針的實現很是多,我就假設這個類型叫Ptr<T>吧。這跟Vczh Library++ 3.0所使用的實現同樣。程序員
1 class Base
2 {
3 public:
4 virtual ~Base(){}
5 };
6
7 class Derived1 : public Base
8 {
9 };
10
11 class Derived2 : public Base
12 {
13 };
14
15 //---------------------------------------
16
17 List<Ptr<Base>> objects;
18 objects.Add(new Derived1);
19 objects.Add(new Derived2);
20
21 List<Ptr<Base>> objects2;
22 objects2.Add(objects[0]);
固然這裏的List也是Vczh Library++3.0實現的,不過這玩意兒跟vector也好跟C#的List也好都是一個概念,所以也就不須要多加解釋了。咱們能夠看到智能指針的一個好處,只要沒有循環引用出現,你不管怎麼複製它,最終老是能夠被析構掉的。另外一個例子告訴咱們智能指針如何處理類型轉換:
1 Ptr<Derived1> d1=new Derived1;
2 Ptr<Base> b=d1;
3 Ptr<Derived2> d2=b.Cast<Derived2>();
4 // d2是空,由於b指向的是Derived1而不是Derived2。
這就如同咱們Derived1*能夠隱式轉換到Base*,而當你使用dynamic_cast<Derived2*>(static_cast<Base*>(new Derived1))會獲得0同樣。智能指針在幫助咱們析構對象的同時,也要作好類型轉換的工做。
好了,如今先讓咱們一步一步作出那個Ptr<T>。咱們須要清楚這個智能指針所要實現的功能是什麼,而後咱們一個一個來作。首先讓咱們列出一張表:
一、沒有參數構造的時候,初始化爲空
二、使用指針構造的時候,擁有那個指針,而且在沒有任何智能指針指向那個指針的時候刪除掉該指針。
三、智能指針進行復制的時候,兩個智能指針共同擁有該內部指針。
四、智能指針可使用新的智能指針或裸指針從新賦值。
五、須要支持隱式指針類型轉換,static_cast不支持而dynamic_cast支持的轉換則使用Cast<T2>()成員函數來解決。
六、若是一個裸指針直接用來建立兩個智能指針的話,指望的狀況是當兩個智能指針析構掉的時候,該指針會被delete兩次從而崩潰。
七、不處理循環引用。
最後兩點其實是錯誤使用智能指針的最多見的兩種狀況。咱們從1到5一個一個實現。首先是1。智能指針能夠隱式轉換成bool,能夠經過operator->()拿到內部的T*。在沒有使用參數構造的時候,須要轉換成false,以及拿到0:
1 template<typename T>
2 class Ptr
3 {
4 private:
5 T* pointer;
6 int* counter;
7
8 void Increase()
9 {
10 if(counter)++*counter;
11 }
12
13 void Decrease()
14 {
15 if(counter && --*counter==0)
16 {
17 delete counter;
18 delete pointer;
19 counter=0;
20 pointer=0;
21 }
22 }
23
24 public:
25 Ptr():pointer(0),counter(0)
26 {
27 }
28
29 ~Ptr()
30 {
31 Decrease();
32 }
33
34 operator bool()const
35 {
36 return counter!=0;
37 }
38
39 T* operator->()const
40 {
41 return pointer;
42 }
43 };
在這裏咱們實現了構造函數和析構函數。構造函數把內部指針和引用計數的指針都初始化爲空,而析構函數則進行引用計數的減一操做。另外兩個操做符重載很容易理解。咱們主要來看看Increase函數和Decrease函數都分別作了什麼。Increase函數在引用計數存在的狀況下,把引用計數加一。而Decrease函數在引用計數存在的狀況下,把引用計數減一,若是引用計數在減一過程當中變成了0,則刪掉擁有的資源。
固然到了這個時候智能指針還不能用,咱們必須替他加上覆制構造函數,operator=操做符重載以及使用指針賦值的狀況。首先讓咱們來看使用指針賦值的話咱們應該加上什麼:
1 Ptr(T* p):pointer(0),counter(0)
2 {
3 *this=p;
4 }
5
6 Ptr<T>& operator=(T* p)
7 {
8 Decrease();
9 if(p)
10 {
11 pointer=p;
12 counter=new int(1);
13 }
14 else
15 {
16 pointer=0;
17 counter=0;
18 }
19 return *this;
20 }
這裏仍是偷工減料了的,構造函數接受了指針的話,仍是轉給operator=去調用了。當一個智能指針被一個新指針賦值的時候,咱們首先要減掉一個引用計數,由於原來的指針不再被這個智能指針共享了。以後就進行判斷,若是來的是0,那麼就變成空。若是不是0,就擁有該指針,引用計數初始化成1。因而咱們就能夠這麼使用了:
1 Ptr<Base> b=new Derived1;
2 Ptr<Derived2> d2=new Derived2;
讓咱們開始複製他們吧。複製的要領是,先把以前擁有的指針脫離掉,而後鏈接到一個新的智能指針上面去。咱們知道非空智能指針有多少個,總的引用計數的和就是多少,只是分配到各個指針上面的數字不同而已:
1 Ptr(const Ptr<T>& p):pointer(p.pointer),counter(p.counter)
2 {
3 Increase();
4 }
5
6 Ptr<T>& operator=(const Ptr<T>& p)
7 {
8 if(this!=&p)
9 {
10 Decrease();
11 pointer=p.pointer;
12 counter=p.counter;
13 Increase();
14 }
15 return *this;
16 }
在上一篇文章有朋友指出重載operator=的時候須要考慮是否是本身賦值給本身,其實這是很正確的。咱們寫每一類的時候,特別是當類擁有本身控制的資源的時候,須要很是注意這件事情。固然若是隻是複製幾個對象而不會new啊delete仍是close什麼handle,那檢查不檢查也無所謂了。在這裏咱們很是清楚,當增長一個新的非空智能指針的時候,引用計數的總和會加一。當修改一個非空智能指針的結果也是非空的時候,引用計數的和保持不變。固然這是應該的,由於咱們須要在全部非空智能指針都被毀掉的時候,釋放受保護的全部資源。
到了這裏一個智能指針基本上已經能用了,可是還不能處理父類子類的狀況。這個是比較麻煩的,一個Ptr<Derived>事實上沒有權限訪問Ptr<Base>的內部對象。所以咱們須要經過友元類來解決這個問題。如今讓咱們來添加兩個新的函數吧,從一個任意的Ptr<C>複製到Ptr<T>,而後保證只有當C*能夠隱式轉換成T*的時候編譯可以經過:
1 template<X> friend class Ptr;
2
3 template<typename C>
4 Ptr(const Ptr<C>& p):pointer(p.pointer),counter(p.counter)
5 {
6 Increase();
7 }
8
9 template<typename C>
10 Ptr<T>& operator=(const Ptr<C>& p)
11 {
12 Decrease();
13 pointer=p.pointer;
14 counter=p.counter;
15 Increase();
16 return *this;
17 }
注意這裏咱們的operator=並不用檢查是否是本身給本身賦值,由於這是兩個不一樣的類,相同的話會調用上面那個operator=的。若是C*不能隱式轉換到T*的話,這裏的pointer=p.pointer就會失敗,從而知足了咱們的要求。
如今咱們可以作的事情就更多了:
1 Ptr<Derived1> d1=new Derived1;
2 Ptr<Base> b=d1;
因而咱們只剩下最後一個Cast函數了。這個函數內部使用dynamic_cast來作判斷,若是轉換失敗,會返回空指針:
1 tempalte<typename C>
2 Ptr<C> Cast()const
3 {
4 C* converted=dynamic_cast<C*>(pointer);
5 Ptr<C> result;
6 if(converted)
7 {
8 result.pointer=converted;
9 result.counter=counter;
10 Increase();
11 }
12 return result;
13 }
這是一種hack的方法,平時是不鼓勵的……不過由於操做的都是Ptr,並且特化Ptr也是使用錯誤的一種,因此這裏就無論了。咱們會檢查dynamic_cast的結果,若是成功了,那麼會返回一個非空的新智能指針,並且這個時候咱們也要記住Increase一下。
好了,基本功能就完成了。固然一個智能指針還要不少其餘功能,譬如說比較什麼的,這個就大家本身搞定哈。
指針和內存就說到這裏了,下一篇講如何利用一個好的IDE構造輕量級單元測試系統。咱們都說好的工具可以提升生產力,所以這種方法不能脫離一個好的IDE使用。
複雜的東西寫多了,現在寫點簡單的好了。因爲功能上的須要,Vczh Library++3.0被我搞得很離譜。爲了開發維護的遍歷、減小粗心犯下的錯誤以及加強單元測試、迴歸測試和測試工具,所以記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。
以前的文章講了指針和內存的一些問題,今天說一下單元測試的問題。若是在團隊裏面沒有對單元測試的框架有要求的話,其實咱們可使用一個最簡單的方法來搭建在IDE裏面運行的單元測試框架,整個框架只需十幾行代碼。咱們先來考慮一下功能最少的單元測試框架須要完成什麼樣的內容。首先咱們要運行一個一個的測試用例,其次在一個測試用例裏面咱們要檢查一些條件是否成立。舉個例子,咱們寫一個函數將兩個字符串鏈接起來,通常來講要進行下面的測試:
1 #include "MyUnitTestFramework.h"//等一下咱們會展現一下如何用最少的代碼完成這個頭文件的內容
2 #include ""
3
4 TEST_CASE(StringConcat)
5 {
6 TEST_ASSERT(concat("a", "b")=="ab");
7 TEST_ASSERT(concat("a", "")=="a");
8 TEST_ASSERT(concat("", "b")=="b");
9 TEST_ASSERT(concat("", "")=="");
10 .
11 }
12
13 int wmain()
14 {
15 return 0;
16 }
若是咱們的單元測試框架能夠這麼寫,那顯然作起什麼事情來都會方便不少,並且不須要向一些其餘的測試框架同樣註冊一大堆東西,或者是寫一大堆配置函數。固然此次咱們只作功能最少的測試框架,這個框架除了運行測試之外,不會有其餘功能,譬如選擇哪些測試能夠運行啦,仍是在出錯的時候log一些什麼啦之類。之因此要在IDE裏面運行,是由於咱們若是作到TEST_ASSERT中出現false的話,馬上在該行崩潰,那麼IDE就會幫你定位到出錯的TEST_ASSERT中去,而後給你顯示全部的上下文信息,譬如說callstack啦什麼的。友好的工具不用簡直對不起本身啊,幹嘛非得把單元測試作得那麼複雜捏,凡是單元測試,老是要所有運行經過才能提交代碼的。
那麼咱們來看看上面的單元測試的代碼。首先寫了TEST_CASE的那個地方,大括號裏面的代碼會自動運行。其次TEST_ASSERT會在表達式是false的時候崩潰。先從簡單的入手吧。如何製造崩潰呢?最簡單的辦法就是拋異常:
1 #define TEST_ASSERT(e) do(if(!(e))throw "今晚沒飯吃。";}while(0)
這裏面有兩個要注意的地方。首先e要加上小括號,否則取反操做符就有可能作出錯誤的行爲。譬如說當e是a+b==c的時候,加了小括號就變成if(!(a+b==c))...,沒有加小括號就變成if(!a+b==c)...,意思就徹底變了。第二個主意的地方是我使用do{...}while(0)把語句包圍起來了。這樣作的好處是能夠在任什麼時候候TEST_ASSERT(e)都像一個語句。譬如咱們可能這麼寫:
1 if(a)
2 TEST_ASSERT(x1);
3 else if(b)
4 {
5 TEST_ASSERT(x2);
6 TEST_ASSERT(x3);
7 }
若是沒有do{...}while(0)包圍起來,這個else就會被綁定到宏裏面的那個if,你的代碼就被偷偷改掉了。
那麼如今剩下TEST_CASE(x){y}了。什麼東西能夠在main函數外面自動運行呢?這個我想熟悉C++的人都會知道,就是全局變量的構造函數啦。因此TEST_CASE(x){y}那個大括號裏面的y只能在全局變量的構造函數裏面調用。可是咱們知道寫一個類的時候,構造函數的大括號寫完了,後面還有類的大括號,全局變量的名稱,和最終的一個分號。爲了把這些去掉,那麼顯然{y}應該屬於一個普通的函數。那麼全局變量如何可以使用這個函數呢?方法很簡單,把函數前置聲明一下就好了:
1 #define TEST_CASE(NAME) \
2 extern void TESTCASE_##NAME(); \
3 namespace vl_unittest_executors \
4 { \
5 class TESTCASE_RUNNER_##NAME \
6 { \
7 public: \
8 TESTCASE_RUNNER_##NAME() \
9 { \
10 TESTCASE_##NAME(); \
11 } \
12 } TESTCASE_RUNNER_##NAME##_INSTANCE; \
13 } \
14 void TESTCASE_##NAME()
那咱們來看看TEST_CASE(x){y}究竟會被翻譯成什麼代碼:
1 extern void TESTCASE_x();
2 namespace vl_unittest_executors
3 {
4 class TESTCASE_RUNNER_x
5 {
6 public:
7 TESTCASE_RUNNER_x()
8 {
9 TESTCASE_x();
10 }
11 } TESTCASE_RUNNER_x_INSTANCE;
12 }
13 void TESTCASE_x(){y}
到了這裏是否是很清楚了捏,首先在main函數運行以前TESTCASE_RUNNER_x_INSTANCE變量會初始化,而後調用TESTCASE_RUNNER_x的構造函數,最後運行函數TESTCASE_x,該函數的內容顯然就是{y}了。這裏還能學到宏是如何鏈接兩個名字成爲一個名字,和如何寫多行的宏的。
因而MyUnittestFramework.h就包含這兩個宏,其餘啥都沒有,是否是很方便呢?打開Visual C++,創建一個工程,引用這個頭文件,而後寫你的單元測試,最後F5就運行了,多方便啊,啊哈哈哈。
這裏須要注意一點,那些單元測試的順序是不受到保證的,特別是你使用了多個cpp文件的狀況下。因而你在使用這個測試框架的同時,會被迫保證執行一次單元測試不會對你的全局狀態帶來什麼反作用,以便兩個測試用例交換順序執行的時候仍然能穩定地產生相同的結果。這對你寫單元測試有幫助,並且爲了讓你的代碼可以被這麼測試,你的代碼也會寫的有條理,不會依賴全局狀態,真是一箭雙鵰也。並且說不定單元測試用例比你的全局變量的初始化還先執行呢,所以爲了使用這個測試框架,你將會不得不把你的全局變量隱藏在一個cpp裏面,而暴露出隨時能夠被調用的一組函數出來。這樣也可讓你的代碼在使用全局狀態的時候更加安全。
今天就講到這裏了。下一篇要寫什麼我還沒想好,到時候再說吧。