C++实用技巧Word下载.docx
- 文档编号:16934749
- 上传时间:2022-11-27
- 格式:DOCX
- 页数:15
- 大小:24.68KB
C++实用技巧Word下载.docx
《C++实用技巧Word下载.docx》由会员分享,可在线阅读,更多相关《C++实用技巧Word下载.docx(15页珍藏版)》请在冰豆网上搜索。
这个方法十分神奇,因为你只需要在main函数所在的cpp文件这么#include一下,所有的cpp文件里面的new都会受到监视,跟平常所用的用宏把new给换掉的这种破方法截然不同。
如果你使用了全局变量的话也要小心,因为全局变量的析构函数是在main函数结束之后才执行的,因此如果在全局变量的析构函数里面delete的东西仍然会被_CrtDumpMemoryLeaks函数当成泄露掉的资源对待。
当然本人认为全局变量可以用,但是全局变量的赋值必须在main里面做,释放也是,除非那个全局变量的构造函数没有申请任何内存,所以这也是一个很好的检查方法。
不过上面也仅仅是一个告诉你有没有内存泄漏的方法罢了。
那么如何避免内存泄露呢?
当然在设计一些性能要求没有比操作系统更加严格的程序的时候,可以使用以下方法:
1、如果构造函数new了一个对象并使用成员指针变量保存的话,那么必须在析构函数delete它,并且不能有为了某些便利而将这个对象的所有权转让出去的事情发生。
2、在能使用shared_ptr的时候,尽量使用shared_ptr。
shared_ptr只要你不发生循环引用,那么这个东西可以安全地互相传递、随便你放在什么容器里面添加删除、你想放哪里就放在哪里,再也不用考虑这个对象的生命周期问题了。
3、不要在有构造函数和析构函数的对象上使用memset(或者memcpy)。
如果一个对象需要memset,那么在该对象的构造函数里面memset自己。
如果你需要memset一个对象数组,那也在该对象的构造函数里面memset自己。
如果你需要memset一个没有构造函数的复杂对象,那么请为他添加一个构造函数,除非那是别人的API提供的东西。
4、如果一个对象是继承了其他东西,或者某些成员被标记了virtual的话,绝对不要memset。
对象是独立的,也就是说父类内部结构的演变不需要对子类负责。
哪天父类里面加了一个string成员,被子类一memset,就欲哭无泪了。
5、如果需要为一个对象定义构造函数,那么连复制构造函数、operator=重载和析构函数都全部写全。
如果不想写复制构造函数和operator=的话,那么用一个空的实现写在private里面,确保任何试图调用这些函数的代码都出现编译错误。
6、如果你实在很喜欢C语言的话,那麻烦换一个只支持C不支持C++的编译器,全面杜绝因为误用了C++而导致你的C坏掉的情况出现。
什么是循环引用呢?
如果两个对象互相使用一个shared_ptr成员变量直接或者间接指向对方的话,就是循环引用了。
在这种情况下引用计数会失效,因为就算外边的shared_ptr全释放光了,引用计数也不会是0的。
今天就说到这里了,过几天我高兴的话再写一篇续集,如果我持续高兴的话呢……嗯嗯……。
(二)
上一篇文章讲到了如何检查内存泄露。
其实只要肯用C++的STL里面的高级功能的话,内存泄露是很容易避免的。
我在开发VczhLibrary++3.0的时候,所有的测试用例都保证跑完了没有内存泄露。
但是很可惜有些C++团队不能使用异常,更甚者不允许写构造函数析构函数之类,前一个还好,后一个简直就是在用C。
当然有这些变态规定的地方STL都是用不了的,所以我们更加需要扎实的基础来开发C++程序。
今天这一篇主要还是讲指针的问题。
因为上一篇文章一笔带过,今天就来详细讲内存泄漏或者野指针发生的各种情况。
当然我不可能一下子举出全部的例子,只能说一些常见的。
一、错误覆盖内存。
之前提到的不能随便乱memset其实就是为了避免这个问题的。
其实memcpy也不能乱用,我们来看一个例子,最简单的:
1#defineMAX_STRING20;
2
3structStudent
4{
5
charname[MAX_STRING];
6
charid[MAX_STRING];
7
intchinese;
intmath;
intenglish;
10};
大家对这种结构肯定十分熟悉,毕竟是大学时候经常要写的作业题……好了,大家很容易看得出来这其实是C语言的经典写法。
我们拿到手之后,一般会先初始化一下,然后赋值。
1Studentvczh;
2memset(&
vczh,0,sizeof(vczh));
3strcpy(vczh.name,"
vczh"
);
4strcpy(vczh.id,"
VCZH'
SID"
5vczh.chinese=70;
6vczh.math=90;
7vczh.english=80;
为什么要在这里使用memset呢?
memset的用处是将一段内存的每一个字节都设置成同一个数字。
这里是0,因此两个字符串成员的所有字节都会变成0。
因此在memset了Student之后,我们通过正常方法来访问name和id的时候都会得到空串。
而且如果Student里面有指针的话,0指针代表的是没有指向任何有效对象,因此这个时候对指针指向的对象进行读写就会立刻崩溃。
对于其他数值,0一般作为初始值也不会有什么问题(double什么的要小心)。
这就是我们写程序的时候使用memset的原因。
好了,如今社会进步,人民当家做主了,死程们再也不需要受到可恶的C语言剥削了,我们可以使用C++!
因此我们借助STL的力量把Student改写成下面这种带有C++味道的形式:
1structStudent
2{
3
std:
:
stringname;
4
stringid;
8};
我们仍然需要对Student进行初始化,不然三个分数还是随机值。
但是我们又不想每一次创建的时候都对他们分别进行赋值初始化城0。
这个时候你心里可能还是想着memset,这就错了!
在memset的时候,你会把std:
string内部的不知道什么东西也给memset掉。
假如一个空的std:
string里面存放的指针指向的是一个空的字符串而不是用0来代表空的时候,一下子内部的指针就被你刷成0,等下std:
string的析构函数就没办法delete掉指针了,于是内存泄露就出现了。
有些朋友可能不知道上面那句话说的是什么意思,我们现在来模拟一下不能memset的std:
string要怎么实现。
为了让memset一定出现内存泄露,那么std:
string里面的指针必须永远都指向一个有效的东西。
当然我们还需要在字符串进行复制的时候复制指针。
我们这里不考虑各种优化技术,用最简单的方法做一个字符串出来:
1classString
3private:
char*buffer;
6public:
String()
{
buffer=newchar[1];
buffer[0]=0;
11
}
12
13
String(constchar*s)
14
15
buffer=newchar[strlen(s)+1];
16
strcpy(buffer,s);
17
18
19
String(constString&
s)
20
21
buffer=newchar[strlen(s.buffer)+1];
22
strcpy(buffer,s.buffer);
23
24
25
~String()
26
27
delete[]buffer;
28
29
30
String&
operator=(constString&
31
32
33
34
35
36};
于是我们来做一下memset。
首先定义一个字符串变量,其次memset掉,让我们看看会发生什么事情:
1strings;
s,0,sizeof(s));
第一行我们构造了一个字符串s。
这个时候字符串的构造函数就会开始运行,因此strcmp(s.buffer,"
"
)==0。
第二行我们把那个字符串给memset掉了。
这个时候s.buffer==0。
于是函数结束了,字符串的析构函数尝试delete这个指针。
我们知道delete一个0是不会有问题的,因此程序不会发生错误。
我们活生生把构造函数赋值给buffer的newchar[1]给丢了!
铁定发生内存泄露!
好了,提出问题总要解决问题,我们不使用memset的话,怎么初始化Student呢?
这个十分好做,我们只需要为Student加上构造函数即可:
.//不重复那些声明
4
Student():
chinese(0),math(0),english(0)
这样就容易多了。
每当我们定义一个Student变量的时候,所有的成员都初始化好了。
name和id因为string的构造函数也自己初始化了,因此所有的成员也都初始化了。
加入Student用了一半我们想再初始化一下怎么办呢?
也很容易:
2.//各种使用
3vczh=Student();
经过一个等号操作符的调用,旧Student的所有成员就被一个新的初始化过的Student给覆盖了,就如同我们对一个int变量重新赋值一样常见。
当然因为各种复制经常会出现,因此我们也要跟上面贴出来的string的例子一样,实现好那4个函数。
至此我十分不理解为什么某些团队不允许使用构造函数,我猜就是为了可以memset,其实是很没道理的。
二、异常。
咋一看内存泄露跟异常好像没什么关系,但实际上这种情况更容易发生。
我们来看一个例子:
1char*strA=newchar[MAX_PATH];
2if(GetXXX(strA,MAX_PATH)==ERROR)gotoRELEASE_STRA;
3char*strB=newchar[MAX_PATH];
4if(GetXXX(strB,MAX_PATH)==ERROR)gotoRELEASE_STRB;
6DoSomething(strA,strB);
7
8RELEASE_STRB:
9delete[]strB;
10RELEASE_STRA:
11delete[]strA;
相信这肯定是大家的常用模式。
我在这里也不是教唆大家使用goto,不过对于这种例子来说,用goto是最优美的解决办法了。
但是大家可以看出来,我们用的是C++,因为这里有new。
如果DoSomething发生了异常怎么办呢?
如果GetXXX发生了异常怎么办呢?
我们这里没有任何的try-catch,一有异常,函数里克结束,两行可怜的delete就不会被执行到了,于是内存泄漏发生了!
没有catch异常才发生的内存泄露,那我们来catch吧:
2try
3{
if(GetXXX(strA,MAX_PATH)==ERROR)gotoRELEASE_STRA;
char*strB=newchar[MAX_PATH];
try
if(GetXXX(strB,MAX_PATH)==ERROR)gotoRELEASE_STRB;
DoSomething(strA,strB);
catch()
12
delete[]strB;
throw;
16}
17catch()
18{
delete[]strA;
21}
22
23RELEASE_STRB:
24delete[]strB;
25RELEASE_STRA:
26delete[]strA;
你能接受吗?
当然是不能的。
问题出在哪里呢?
因为C++没有try-finally。
你看这些代码到处都是雷同的东西,显然我们需要编译器帮我们把这些问题搞定。
最好的解决方法是什么呢?
显然还是构造函数和析构函数。
总之记住,如果想要事情成对发生,那么使用构造函数和析构函数。
第一步,GetXXX显然只能支持C模式的东西,因此我们要写一个支持C++的:
1boolGetXXX2(string&
char*str=newchar[MAX_PATH];
boolresult;
result=GetXXX(str,MAX_PATH);
if(result)s=str;
delete[]str;
returnresult;
17}
借助这个函数我们可以看到,因为有了GetXXX这种C的东西,导致我们多了多少麻烦。
不过这总是一劳永逸的,有了GetXXX2和修改之后的DoSomething2之后,我们就可以用更简单的方法来做了:
1stringa,b;
2if(GetXXX2(a)&
&
GetXXX2(b))
DoSomething2(a,b);
5}
多么简单易懂。
这个代码在任何地方发生了异常,所有new的东西都会被delete。
这就是析构函数的一个好处。
一个变量的析构函数在这个变量超出了作用域的时候一定会被调用,无论代码是怎么走出去的。
今天就说到这里了。
说了这么多还是想让大家不要小看构造函数和析构函数。
那种微不足道的因为一小部分不是瓶颈的性能问题而放弃构造函数和析构函数的做法,终究是要为了修bug而加班的。
只要明白并用好了构造函数、析构函数和异常,那么C++的特性也可以跟C一样清楚明白便于理解,而且写出来的代码更好看的。
大家期待第三篇哈。
(三)
今天是关于内存的最后一篇了。
上一篇文章讲了为什么不能对一个东西随便memset。
里面的demo代码出了点小bug,不过我不喜欢在发文章的时候里面的demo代码也拿去编译和运行,所以大家有什么发现的问题就评论吧。
这样也便于后来的人不会受到误导。
这次说的仍然是构造函数和析构函数的事情,不过我们将通过亲手开发一个智能指针的方法,知道引用计数如何帮助管理资源,以及错误使用引用计数的情况。
首先先来看一下智能指针是如何帮助我们管理内存的。
现在智能指针的实现非常多,我就假设这个类型叫Ptr<
T>
吧。
这跟VczhLibrary++3.0所使用的实现一样。
1classBase
3public:
virtual~Base(){}
5};
6
7classDerived1:
publicBase
8{
9};
10
11classDerived2:
12{
13};
14
15//---------------------------------------
16
17List<
Ptr<
Base>
>
objects;
18objects.Add(newDerived1);
19objects.Add(newDerived2);
20
21List<
objects2;
22objects2.Add(objects[0]);
当然这里的List也是VczhLibrary++3.0实现的,不过这玩意儿跟vector也好跟C#的List也好都是一个概念,因此也就不需要多加解释了。
我们可以看到智能指针的一个好处,只要没有循环引用出现,你无论怎么复制它,最终总是可以被析构掉的。
另一个例子告诉我们智能指针如何处理类型转换:
1Ptr<
Derived1>
d1=newDerived1;
2Ptr<
b=d1;
3Ptr<
Derived2>
d2=b.Cast<
();
4//d2是空,因为b指向的是Derived1而不是Derived2。
这就如同我们Derived1*可以隐式转换到Base*,而当你使用dynamic_cast<
Derived2*>
(static_cast<
Base*>
(newDerived1))会得到0一样。
智能指针在帮助我们析构对象的同时,也要做好类型转换的工作。
好了,现在先让我们一步一步做出那个Ptr<
。
我们需要清楚这个智能指针所要实现的功能是什么,然后我们一个一个来做。
首先让我们列出一张表:
1、没有参数构造的时候,初始化为空
2、使用指针构造的时候,拥有那个指针,并且在没有任何智能指针指向那个指针的时候删除掉该指针。
3、智能指针进行复制的时候,两个智能指针共同拥有该内部指针。
4、智能指针可以使用新的智能指针或裸指针重新赋值。
5、需要支持隐式指针类型转换,static_cast不支持而dynamic_cast支持的转换则使用Cast<
T2>
()成员函数来解决。
6、如果一个裸指针直接用来创建两个智能指针的话,期望的情况是当两个智能指针析构掉的时候,该指针会被delete两次从而崩溃。
7、不处理循环引用。
最后两点实际上是错误使用智能指针的最常见的两种情况。
我们从1到5一个一个实现。
首先是1。
智能指针可以隐式转换成bool,可以通过operator->
()拿到内部的T*。
在没有使用参数构造的时候,需要转换成false,以及拿到0:
1template<
typenameT>
2classPtr
4private:
T*pointer;
int*counter;
voidIncrease()
9{
if(counter)++*counter;
voidDecrease()
if(counter&
--*counter==0)
deletecounter;
18
deletepointer;
counter=0;
pointer=0;
23
24public:
Ptr():
pointer(0),counter(0)
28
29
~Ptr()
Decrease();
33
operatorbool()const
36
returncounter!
=0;
37
38
39
T*operator->
()const
40
41
returnpointer;
42
43};
在这里我们实现了构造函数和析构函数。
构造函数把内部指针和引用计数的指针都初始化为空,而析构函数则进行引用计数的减一操作。
另外两个操作符重载很容易理解。
我们主要来看看Increase函数和Decrease函数都分别做了什么。
Increase函数在引用计数存在的情况下,把引用计数加一。
而Decrease函数在引用计数存在的情况下,把引用计数减一,如果引用计数在减一过程中变成了0,则删掉拥有的资源。
当然到了这个时候智能指针还不能用,我们必须替他加上复制构造函数,operator=操作符重载以及使用指针赋值的情况。
首先让我们来看使用指针赋值的话我们应该加上什么:
1
Ptr(T*p):
2
*this=p;
Ptr<
operator=(T*p)
if(p)
pointer=p;
counter=newint
(1);
else
return*this;
这里还是偷工减料了的,构造函数接受了指针的话,还是转给opera
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C+ 实用技巧