复杂的东西写多了,现在写点简单的好了。因为功能上的须要,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里面,而暴露出随时能够被调用的一组函数出来。这样也可让你的代码在使用全局状态的时候更加安全。
今天就讲到这里了。下一篇要写什么我还没想好,到时候再说吧。