c语言释放echarts 内存释放问题的问题

C++里析构函数可用来释放内存,但是当关闭程序后不是就自动释放内存了吗,那为何还要用到析构函数呢。 - 极客问答 Geek Q and A
C++里析构函数可用来释放内存,但是当关闭程序后不是就自动释放内存了吗,那为何还要用到析构函数呢。
老师说析构函数释放内存,可是关闭程序自动释放内存,那么还写入析构函数干什么,我们可以不写析构函数吗。
10月 18, 2017
(2,760 分)
就算是你自定义了析构函数,默认析构函数同样会存在,而且会执行。当类中有类对象成员时,首先调用的是自定义的析构函数,然后这个类对象成员资源的释放,就是由默认析构函数来释放的。因为指针不属于对象成员,因此默认析构函数不会管他,需手动释放指针的资源(主要是new)例class B{public:~B(){cout&&"B";}}; class A{public: B ; ~A(){}}; void in(){ A ;}
在类A中的析构函数什么也没做,也就是没有释放类成员的资源,但类B的析构函数仍执行了,这个类对象成员的资源释放,就是使用的类A的默认析构函数进行的。
10月 18, 2017
(3,500 分)
12月 2, 2017
构造函数通常用于在实例化对象时自动对内部数据成员初始化,设置初始值、分配内存空间等。
而析构函数则用于删除对象时做收尾工作,比如释放申请的空间等。
10月 18, 2017
(4,304 分)
析构函数是在你程序运行的时候释放内存。和关闭程序不是一回事
10月 29, 2017
(7,390 分)
Powered byC语言释放链表内存的问题_百度知道
C语言释放链表内存的问题
void ListFree(LinkList L){
LNode *s = NULL;
}}一运行到free处就出错
我有更好的答案
1、如果是小程序,就算不释放一般没什么大后果,因为开内存比较小。因为这里的空间就是内存空间,如果不断申请的话,会占用大部分内存。如果不释放的话,就一直占用内存,使内存减小。直到关闭程序,内存才会释放。可以free函数释放内存。2、free函数:原型:void free(void *ptr);功能:释放malloc(或calloc、realloc)函数给指针变量分配的动态内存;头文件:malloc.h或stdlib.h;3、为了避免释放已经释放了内存的指针内存,或没有释放内存,在C语言中最好是在定义指针时赋初值NULL,释放后立即赋NULL,释放时检查指针值再决定释放就避免释放错误了,例如:int *a = NULLint *b = (int*) malloc(sizeof(int) * 10);a=/* 执行大量操作后 */if(a != NULL) {free(a);a=NULL;}if(b != NULL) {free(b);b=NULL;}
采纳率:74%
来自团队:
把链表创建的函数贴出来。否则难以判断。还有LNode 和LinkList的类型定义贴出来再请求一次,把LNode 和LinkList的类型定义贴出来! 从 L-&的用法,我推测LinkList是一个结构体指针类型,,作用是指向链表头。可是你在malloc的时候,用的sizeof(LinkList)),也就是对指针类型取size,通常固定是4,也就是说的你原意是要申请一个结构体大小的buff,结果是申请到一个指针大小的buf 如果LinkList是一个结构体类型,而不是指针类型,那么s=L 这样的语句就是彻底错误的,要知道void ListFree(LinkList L),这里的L是局部变量,进入函数时从堆栈中得到的,而不是你malloc出来的。所以你的free一定是失败的。
typedef struct LNode{struct LNode *}LNode,*LinkL其他部分写不下,已发你私信了
两个malloc都改成malloc(sizeof(LNode));
本回答被提问者采纳
LinkList 和 LNode* 是同一种类型吗,如果不是就会re,如果是也建议使用相同标识符
typedef struct LNode{
struct LNode *}LNode,*LinkL
为您推荐:
其他类似问题
链表的相关知识
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。还有很多小孩也没找到,你愿意帮助他们么?C语言中的内存分配与释放
  对C语言一直都是抱着学习的态度,很多都不懂,今天突然被问道C语言的内存分配问题,说了一些自己知道的,但感觉回答的并不完善,所以才有这篇笔记,总结一下C语言中内存分配的主要内容。
相关问题 
  刚刚在一篇看到一个简单的问题:
char* toStr()
char *s = "abcdefghijkl";
int main()
cout && toStr() &&
char* toStr()
char s[] = "abcdefghijkl";
int main()
cout && toStr() &&
  两段代码都很简单,输出一段字符,类型不同,一个是char*字符串,一个是char[]数据。
  结果你知道吗? 这个我确实知道,相信大部分人也都回知道,必然有一个不好使,或者两个都不好使!!!都对就没意思了~
  结果:第一个正确输出,第二个输出乱码。
  原因:在于局部变量的作用域和内存分配的问题,第一char*是指向一个常量,作用域为函数内部,被分配在程序的常量区,直到整个程序结束才被销毁,所以在程序结束前常量还是存在的。而第二个是数组存放的,作用域为函数内部,被分配在栈中,就会在函数调用结束后被释放掉,这时你再调用,肯定就错误了。
  什么是局部变量、全局变量和静态变量?
  顾名思义,局部变量就是在一个有限的范围内的变量,作用域是有限的,对于程序来说,在一个函数体内部声明的普通变量都是局部变量,局部变量会在栈上申请空间,函数结束后,申请的空间会自动释放。而全局变量是在函数体外申请的,会被存放在全局(静态区)上,知道程序结束后才会被结束,这样它的作用域就是整个程序。静态变量和全局变量的存储方式相同,在函数体内声明为static就可以使此变量像全局变量一样使用,不用担心函数结束而被释放。
相关函数:
void *malloc(size_t size);
void free(void *p);
/*一般这样用
Struct elem *p;
p = (struct elem*)malloc(sizeof(struct elem))
void free(p)
malloc原理
  malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。
栈区(stack)&由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap)&一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
全局区(静态区)(static)&全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态   & & & & & & & & & & & & & & & &变量在相邻的另一块区域。& 程序结束后由系统释放。
常量区&常量字符串就是放在这里的,直到程序结束后由系统释放。上面的问题就在这里!!!
代码区&存放函数体的二进制代码。
直接搬运的代码,确实很好!!容易理解
//main.cpp
int a = 0; //全局初始化区
char *p1; //全局未初始化区
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //在常量区,p3在栈上。
static int c =0;//全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);//分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
此外,还有realloc(重新分配内存)、calloc(初始化为0)、alloca(在栈上申请内存,自动释放)等。
阅读(...) 评论()C语言内存使用的常见问题及解决之道
& & 本文所讨论的&内存&主要指(静态)数据区、堆区和栈区空间(详细的布局和描述参考《虚拟地址空间布局》一文)。数据区内存在程序编译时分配,该内存的生存期为程序的整个运行期间,如全局变量和static关键字所声明的静态变量。函数执行时在栈上开辟局部自动变量的储存空间,执行结束时自动释放栈区内存。堆区内存亦称动态内存,由程序在运行时调用malloc/calloc/realloc等库函数申请,并由使用者显式地调用free库函数释放。堆内存比栈内存分配容量更大,生存期由使用者决定,故非常灵活。然而,堆内存使用时很容易出现内存泄露、内存越界和重复释放等严重问题。
二 &内存问题
2.1 数据区内存
2.1.1 内存越界
& & &内存越界访问分为读越界和写越界。读越界表示读取不属于自己的数据,如读取的字节数多于分配给目标变量的字节数。若所读的内存地址无效,则程序立即崩溃;若所读的内存地址有效,则可读到随机的数据,导致不可预料的后果。写越界亦称&缓冲区溢出&,所写入的数据对目标地址而言也是随机的,因此同样导致不可预料的后果。
& & &内存越界访问会严重影响程序的稳定性,其危险在于后果和症状的随机性。这种随机性使得故障现象和本源看似无关,给排障带来极大的困难。
& & &数据区内存越界主要指读写某一数据区内存(如全局或静态变量、数组或结构体等)时,超出该内存区域的合法范围。
& & &写越界的主要原因有两种:1) memset/memcpy/memmove等内存覆写调用;2) 数组下标超出范围。
&1 #define NAME_SIZE &5
&2 #define NAME_LEN & NAME_SIZE-1/*Terminator*/
&3 char gszName[NAME_SIZE] = &Mike&;
&4 char *pszName = &Jason&;
&5 int main(void)
&7 & & memset(gszName, 0, NAME_SIZE+1); //越界1
&8 & & gszName[NAME_SIZE] = 0; & & & & &//越界2
10 & & if(strlen(pszName) &= NAME_SIZE) &//越界3(注意'='号)
11 & & & & strcpy(gszName, pszName);
13 & & int dwSrcLen = strlen(pszName);
14 & & if(dwSrcLen & NAME_SIZE)
15 & & & & memcpy(gszName, pszName, dwSrcLen); //未拷贝结束符('\0')
17 & & return 0;
& & &使用数组时,经常发生下标&多1&或&少1&的操作,特别是当下标用于for循环条件表达式时。此外,当数组下标由函数参数传入或经过复杂运算时,更易发生越界。
&1 void ModifyNameChar(unsigned char ucCharIdx, char cModChar)
&3 & & gszName[ucCharIdx] = cModC &//写越界
&5 int main(void)
&7 & & ModifyNameChar(5, 'L');
&8 & & unsigned char ucIdx = 0;
&9 & & for(; ucIdx &= NAME_SIZE; ucIdx++) &//'='号导致读越界
10 & & & & printf(&NameChar = %c\n&, gszName[ucIdx]);
12 & & return 0;
& & &对于重要的全局数据,可将其植入结构体内并添加CHK_HEAD和CHK_TAIL进行越界保护和检查:
&1 #define CODE_SIZE & & & 4 &//越界保护码的字节数
&2 #if (1 == CODE_SIZE)
&3 & & #define CODE_TYPE & char
&4 & & #define CHK_CODE & &0xCC & & & //除0外的特殊值
&5 #elif (2 == CODE_SIZE)
&6 & & #define CODE_TYPE & short
&7 & & #define CHK_CODE & &0xCDDC & & //除0外的特殊值
&9 & & #define CODE_TYPE & int
10 & & #define CHK_CODE & &0xABCDDCBA //除0外的特殊值
12 #define CHK_HEAD & &CODE_TYPE ChkH
13 #define CHK_TAIL & &CODE_TYPE ChkT
14 #define INIT_CHECK(ptChkMem) do{ \
15 & & (ptChkMem)-&ChkHead = CHK_CODE; \
16 & & (ptChkMem)-&ChkTail = CHK_CODE; \
17 }while(0)
18 #define CHK_OVERRUN(ptChkMem) do{ \
19 & & if((ptChkMem)-&ChkHead != CHK_CODE || (ptChkMem)-&ChkTail != CHK_CODE) { \
20 & & & & printf(&[%s(%d)&%s&]Memory Overrun(ChkHead:0x%X,ChkTail:0x%X)!\n&, __FILE__, __LINE__, FUNC_NAME, \
21 & & & & (ptChkMem)-&ChkHead, (ptChkMem)-&ChkTail); \
22 & & } \
23 }while(0)
24 typedef struct{
25 & & CHK_HEAD; &
26 & & char szName[NAME_SIZE];
27 & & CHK_TAIL; &
28 }T_CHK_MEM;
29 T_CHK_MEM gtChkM
30 int main(void)
32 & & memset(&gtChkMem, 0, sizeof(T_CHK_MEM));
33 & & INIT_CHECK(&gtChkMem);
35 & & memset(&gtChkMem, 11, 6);
36 & & CHK_OVERRUN(&gtChkMem);
37 & & strcpy(gtChkMem.szName, &Elizabeth&);
38 & & CHK_OVERRUN(&gtChkMem);
40 & & return 0;
& & &执行结果如下,可见被检查的szName数组其头尾地址均发生越界:
1 [test.c(177)&main&]Memory Overrun(dwChkHead:0xB0B0B0B,dwChkTail:0xABCDDCBA)!
2 [test.c(179)&main&]Memory Overrun(dwChkHead:0xB0B0B0B,dwChkTail:0xABCD0068)!
& & &若模块提供有全局数据的访问函数,则可将越界检查置于访问函数内:
&1 #ifdef CHK_GLOBAL_OVERRUN
&2 & & #define CODE_SIZE & & & 4 &//越界保护码的字节数
&3 & & #if (1 == CODE_SIZE)
&4 & & & & #define CODE_TYPE & char
&5 & & & & #define CHK_CODE & &(CODE_TYPE)0xCC & & & //除0外的特殊值
&6 & & #elif (2 == CODE_SIZE)
&7 & & & & #define CODE_TYPE & short
&8 & & & & #define CHK_CODE & &(CODE_TYPE)0xCDDC & & //除0外的特殊值
&9 & & #else
10 & & & & #define CODE_TYPE & int
11 & & & & #define CHK_CODE & &(CODE_TYPE)0xABCDDCBA //除0外的特殊值
12 & & #endif
13 & & #define CHK_HEAD & & & &CODE_TYPE ChkHead
14 & & #define CHK_TAIL & & & &CODE_TYPE ChkTail
15 & & #define HEAD_VAL(pvGlblAddr) & & & & & & (*(CODE_TYPE*)(pvGlblAddr))
16 & & #define TAIL_VAL(pvGlblAddr, dwGlbSize) &(*(CODE_TYPE*)((char*)pvGlblAddr+dwGlbSize-sizeof(CODE_TYPE)))
18 & & #define INIT_CHECK(pvGlblAddr, dwGlbSize) do{\
19 & & & & HEAD_VAL(pvGlblAddr) = TAIL_VAL(pvGlblAddr, dwGlbSize) = CHK_CODE;}while(0)
20 & & #define CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine) do{\
21 & & & & if((HEAD_VAL(pvGlblAddr) != CHK_CODE) || (TAIL_VAL(pvGlblAddr, dwGlbSize) != CHK_CODE)) {\
22 & & & & & & printf(&[%s(%d)]Memory Overrun(ChkHead:0x%X,ChkTail:0x%X)!\n&, pFileName, dwCodeLine, \
23 & & & & & & HEAD_VAL(pvGlblAddr), TAIL_VAL(pvGlblAddr, dwGlbSize)); \
24 & & & & }}while(0)
26 & & #define INIT_GLOBAL(pvGlblAddr, dwInitVal, dwGlbSize) \
27 & & & & & & InitGlobal(pvGlblAddr, dwInitVal, dwGlbSize, __FILE__, __LINE__)
28 & & #define SET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) \
29 & & & & & & SetGlobal(pvGlblAddr, pvGlblVal, dwGlbSize, __FILE__, __LINE__)
30 & & #define GET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) \
31 & & & & & & GetGlobal(pvGlblAddr, pvGlblVal, dwGlbSize, __FILE__, __LINE__)
33 & & #define CHK_CODE & &0
34 & & #define CHK_HEAD
35 & & #define CHK_TAIL
36 & & #define HEAD_VAL(pvGlblAddr) & & & & & & & 0
37 & & #define TAIL_VAL(pvGlblAddr, dwGlbSize) & &0
38 & & #define INIT_CHECK(pvGlblAddr, dwGlbSize)
39 & & #define CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine)
41 & & #define INIT_GLOBAL(pvGlblAddr, dwInitVal, dwGlbSize) do{\
42 & & & & & & memset(pvGlblAddr, dwInitVal, dwGlbSize);}while(0)
43 & & #define SET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) do{\
44 & & & & & & memcpy(pvGlblAddr, pvGlblVal, dwGlbSize);}while(0)
45 & & #define GET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) do{\
46 & & & & & & memcpy(pvGlblVal, pvGlblAddr, dwGlbSize);}while(0)
49 void InitGlobal(void* pvGlblAddr, int dwInitVal, unsigned int dwGlbSize,
50 & & & & & & & & const char* pFileName, INT32U dwCodeLine)
52 & & if(NULL == pvGlblAddr)
54 & & & & printf(&[%s(%d)]Null Pointer!\n&, pFileName, dwCodeLine);
55 & & & &
58 & & memset(pvGlblAddr, dwInitVal, dwGlbSize);
59 & & INIT_CHECK(pvGlblAddr, dwGlbSize);
61 void SetGlobal(void* pvGlblAddr, void* pvGlblVal, unsigned int dwGlbSize,
62 & & & & & & & &const char* pFileName, INT32U dwCodeLine)
64 & & if((NULL == pvGlblAddr) || (NULL == pvGlblVal))
66 & & & & printf(&[%s(%d)]Null Pointer: (%p), (%p)!\n&, pFileName, dwCodeLine, pvGlblAddr, pvGlblVal);
67 & & & &
70 & & memcpy(pvGlblAddr, pvGlblVal, dwGlbSize);
71 & & CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine);
73 void GetGlobal(void* pvGlblAddr, void* pvGlblVal, unsigned int dwGlbSize,
74 & & & & & & & &const char* pFileName, INT32U dwCodeLine)
76 & & if((NULL == pvGlblAddr) || (NULL == pvGlblVal))
78 & & & & printf(&[%s(%d)]Null Pointer: (%p), (%p)!\n&, pFileName, dwCodeLine, pvGlblAddr, pvGlblVal);
79 & & & &
82 & & memcpy(pvGlblVal, pvGlblAddr, dwGlbSize);
83 & & CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine);
86 int main(void)
88 & & INIT_GLOBAL(&gtChkMem, 0, sizeof(T_CHK_MEM));
89 & & printf(&[%d]ChkHead:0x%X,ChkTail:0x%X!\n&, __LINE__, HEAD_VAL(&gtChkMem), TAIL_VAL(&gtChkMem, sizeof(T_CHK_MEM))); & &
90 & & T_CHK_MEM tChkM
91 & & GET_GLOBAL(&gtChkMem, &tChkMem, sizeof(T_CHK_MEM));
93 & & strcpy(tChkMem.szName, &Elizabeth&);
94 & & SET_GLOBAL(&gtChkMem, &tChkMem, sizeof(T_CHK_MEM));
96 & & return 0;
& & &其中,TAIL_VAL宏假定为1字节对齐(否则请置CODE_SIZE为4字节)。因0xCC默认为四字节(对应于0xFFFFFFCC),故需用(CODE_TYPE)0xCC做类型转换,否则CHK_OVERRUN宏内if判断恒为真。
& & &该检查机制的缺点是仅用于检测写越界,且拷贝和解引用次数增多,访问效率有所降低。读越界后果通常并不严重,除非试图读取不可访问的区域,否则难以也不必检测。
& & &数据区内存越界通常会导致相邻的全局变量被意外改写。因此若已确定被越界改写的全局变量,则可通过工具查看符号表,根据地址顺序找到前面(通常向高地址越界)相邻的全局数据,然后在代码中排查访问该数据的地方,看看有哪些位置可能存在越界操作。
& & &有时,全局数据被意外改写并非内存越界导致,而是某指针(通常为野指针)意外地指向该数据地址,导致其内容被改写。野指针导致的内存改写往往后果严重且难以定位。此时,可编码检测全局数据发生变化的时机。若能结合堆栈回溯(Call Backtrace),则通常能很快地定位问题所在。
& & &修改只读数据区内容会引发段错误(Segmentation Fault),但这种低级失误并不常见。一种比较隐秘的缺陷是函数内试图修改由指针参数传入的只读字符串,详见《关于Linux系统basename函数缺陷的思考》一文。
& & &因其作用域限制,静态局部变量的内存越界相比全局变量越界更易发现和排查。
& & 【对策】某些工具可帮助检查内存越界的问题,但并非万能。内存越界通常依赖于测试环境和测试数据,甚至在极端情况下才会出现,除非精心设计测试数据,否则工具也无能为力。此外,工具本身也有限制,甚至在某些大型项目中,工具变得完全不可用。
& & &与使用工具类似的是自行添加越界检测代码,如本节上文所示。但为求安全性而封装检测机制的做法在某种意义上得不偿失,既不及等高级语言的优雅,又损失了C语言的简洁和高效。因此,根本的解决之道还是在于设计和编码的审慎周密。相比事后检测,更应注重事前预防。
& & &时应重点走查代码中所有操作全局数据的地方,杜绝可能导致越界的操作,尤其注意内存覆写和拷贝函数memset/memcpy/memmove和数组下标访问。
& & &在内存拷贝时,必须确保目的空间大于或等于源空间。也可封装库函数使之具备安全校验功能,如:
&1 /******************************************************************************
&2 * 函数名称: &StrCopy
&3 * 功能说明: &带长度安全拷贝字符串
&4 * 输入参数: &dwSrcLen : 目的字符串缓冲区长度
&5 & & & & & & pSrcStr &: 源字符串
&6 & & & & & & dwSrcLen : 源字符串长度(含终止符'\0')
&7 * 输出参数: &pDstStr &: 目的字符串缓冲区
&8 * 返回值 &: &成功: ptD 失败: &Nil&
&9 * 用法示例: &char *pSrcStr = &HelloWorld&; char szDstStr[20] = {0};
10 & & & & & & StrCopy(szDstStr, sizeof(szDstStr), pSrcStr, strlen(pSrcStr))+1);
11 * 注意事项: &拷贝长度为min(dwDstLen, dwSrcLen) - 1{Terminator}
12 ******************************************************************************/
13 char *StrCopy(char *pDstStr, int dwDstLen, char *pSrcStr, int dwSrcLen)
15 & & if(((NULL == pDstStr) || (NULL == pSrcStr)) ||
16 & & & &((0 == dwDstLen) || (0 == dwSrcLen)))
17 & & & & return (char *)&Nil&;
19 & & int dwActLen = (dwDstLen &= dwSrcLen) ? dwDstLen : dwSrcL
20 & & pDstStr[dwActLen - 1] = '\0';
22 & & return strncpy(pDstStr, pSrcStr, dwActLen - 1);
& & &在使用memcpy和strcpy拷贝字符串时应注意是否包括结束符(memcpy不自动拷贝&\0&)。
& & &按照下标访问数组元素前,可进行下标合法性校验:
1 /* 数组下标合法性校验宏 */
2 #define CHECK_ARRAY_INDEX(index, maxIndex) do{\
3 & & if(index & maxIndex) { \
4 & & &printf(&Too large &#index&: %d(Max: %d)!!!\n\r&, index, maxIndex); \
5 & & &index = maxI \
7 }while(0)
2.1.2 多重定义
& & &函数和定义时已初始化的全局变量是强符号;未初始化的全局变量是弱符号。多重定义的符号只允许最多一个强符号。Unix链接器使用以下规则来处理多重定义的符号:
& & &规则一:不允许有多个强符号。在被多个源文件包含的头文件内定义的全局变量会被定义多次(预处理阶段会将头文件内容展开在源文件中),若在定义时显式地赋值(初始化),则会违反此规则。
& & &规则二:若存在一个强符号和多个弱符号,则选择强符号。
& & &规则三:若存在多个弱符号,则从这些弱符号中任选一个。
& & &当不同文件内定义同名(即便类型和含义不同)的全局变量时,该变量共享同一块内存(地址相同)。若变量定义时均初始化,则会产生重定义(multiple definition)的链接错误;若某处变量定义时未初始化,则无链接错误,仅在因类型不同而大小不同时可能产生符号大小变化(size of symbol `XXX' changed)的编译警告。在最坏情况下,编译链接正常,但不同文件对同名全局变量读写时相互影响,引发非常诡异的问题。这种风险在使用无法接触的第三方库时尤为突出。
& & &下面的例子编译链接时没有任何警告和错误,但结果并非所愿:
&1 //test.c
&2 int gdwCount = 0;
&3 int GetCount(void)
&5 & & return gdwC
&9 //main.c
10 extern int GetCount(void);
11 int gdwC
12 int main(void)
14 & & gdwCount = 10;
15 & & printf(&GetCount=%d\n&, GetCount());
16 return 0;
& & &编码者期望函数GetCount的返回值打印出来是0,但其实是10。若将main.c中的int gdwCount语句改为int gdwCount = 0,编译链接时就会报告multiple definition of 'gdwCount'的错误。因此尽量不要依赖和假设这种符号规则。
& & &关于全局符号多重定义的讨论,详见《C语言头文件组织与包含原则》一文。
& & 【对策】尽量避免使用全局变量。若确有必要,应采用静态全局变量(无强弱之分,且不会和其他全局符号产生冲突),并封装访问函数供外部文件调用。
2.1.3 volatile修饰
& & &关键字volatile用于修饰易变的变量,告诉编译器该变量值可能会在任意时刻被意外地改变,因此不要试图对其进行任何优化。每次访问(读写)volatile所修饰的变量时,都必须从该变量的内存区域中重新读取,而不要使用寄存器(CPU)中保存的值。这样可保证数据的一致性,防止由于变量优化而出错。
& & &以下几种情况通常需要volatile关键字:
外围并行设备的硬件寄存器(如状态寄存器);
中断服务程序(ISR)中所访问的非自动变量(Non-automatic Variable),即全局变量;
多线程并发环境中被多个线程所共享的全局变量。
& & &变量可同时由const和volatile修饰(如只读的状态寄存器),表明它可能被意想不到地改变,但程序不应试图修改它。指针可由volatile修饰(尽管并不常见),如中断服务子程序修改一个指向某buffer的指针时。又如:
1 //只读端口(I/O与内存共享地址空间,非IA架构)
2 const volatile char *port = (const volatile char *)0x01F7
& & &误用volatile关键字可能带来意想不到的错误,例如:
1 int CalcSquare(volatile int *pVal)
3 & & return (*pVal) * (*pVal);
4 } //deficient
& & &函数CalcSquare返回指针pVal所指向值的平方,但由于该值被volatile修饰,编译器将产生类似下面的代码:
1 int CalcSquare(volatile int *pVal)
3 & & int dwTemp1, dwTemp2;
4 & & dwTemp1 = *pV
5 & & dwTemp2 = *pV
6 & & return dwTemp1 * dwTemp2;
7 }//deficient
& & &多线程环境下,指针pVal所指向值在函数CalcSquare执行时可能被意想不到地该变,因此dwTemp1和dwTemp2的取值可能不同,最终未必返回期望的平方值。
& & &正确的代码如下(使用全局变量的拷贝也是提高线程安全性的一种方法):
1 long CalcSquare(volatile int *pVal)
3 & & int dwT
4 & & dwTemp = *pV
5 & & return dwTemp * dwT
6 }//deficient
& & &再举一例:
1 #define READ(val, addr) &(val = *(unsigned long *)addr)
& & &编译器优化这段代码时,若addr地址的数据读取太频繁,优化器会将该地址上的值存入寄存器中,后续对该地址的访问就转变为直接从寄存器中读取数据,如此将大大加快数据读取速度。但在并发操作时,一个进程读取数据,另一进程修改数据,这种优化就会造成数据不一致。此时,必须使用volatile修饰符。
& & 【对策】合理使用volatile修饰符。
2.2 栈区内存
2.2.1 内存未初始化
& & &未初始化的栈区变量其内容为随机值。直接使用这些变量会导致不可预料的后果,且难以排查。
& & &指针未初始化(野指针)或未有效初始化(如空指针)时非常危险,尤以野指针为甚。
& & 【对策】在定义变量时就对其进行初始化。某些编译器会对未初始化发出警告信息,便于定位和修改。
2.2.2 堆栈溢出
& & &每个线程堆栈空间有限,稍不注意就会引起堆栈溢出错误。注意,此处&堆栈&实指栈区。
1 #define MAX_SIZE &3200000 &//系统不同该值不同(ulimit &s: 10240kbytes)
2 int main(void){
3 & & int aStackCrasher[MAX_SIZE] = {0}; &//可能导致Segmentation fault
4 & & aStackCrasher[0] = 1;
5 & & return 0;
& & &堆栈溢出主要有两大原因:1) 过大的自动变量;2) 递归或嵌套调用层数过深。
& & &有时,函数自身并未定义过大的自动变量,但其调用的系统库函数或第三方接口内使用了较大的堆栈空间(如printf调用就要使用2k字节的栈空间)。此时也会导致堆栈溢出,并且不易排查。
& & &此外,直接使用接口模块定义的数据结构或表征数据长度的宏时也存在堆栈溢出的风险,如:
1 typedef struct{
2 & & unsigned short wV
3 & & unsigned char aMacAddr[6];
4 & & unsigned char ucMacT
5 }T_MAC_ADDR_ENTRY;
6 typedef struct{
7 & & unsigned int dwTotalAddrN
8 & & T_MAC_ADDR_ENTRY tMacAddrEntry[MAX_MACTABLE_SIZE];
9 }T_MAC_ADDR_TABLE;
& & &上层模块在自行定义的T_MAC_ADDR_TABLE结构中,使用底层接口定义的MAX_MACTABLE_SIZE宏指定MAC地址表最大条目数。接口内可能会将该宏定义为较大的值(如8000个条目),上层若直接在栈区使用TABLE结构则可能引发堆栈溢出。
& & &在多线程环境下,所有线程栈共享同一虚拟地址空间。若应用程序创建过多线程,可能导致线程栈的累计大小超过可用的虚拟地址空间。在用pthread_create反复创建一个线程(每次正常退出)时,可能最终因内存不足而创建失败。此时,可在主线程创建新线程时指定其属性为PTHREAD_CREATE_DETACHED,或创建后调用pthread_join,或在新线程内调用pthread_detach,以便新线程函数返回退出或pthread_exit时释放线程所占用的堆栈资源和线程描述符。
& & 【对策】应该清楚所用平台的资源限制,充分考虑函数自身及其调用所占用的栈空间。对于过大的自动变量,可用全局变量、静态变量或堆内存代替。此外,嵌套调用最好不要超过三层。
2.2.3 内存越界
& & &因其作用域和生存期限制,发生在栈区的内存越界相比数据区更易发现和排查。
& & &下面的例子存在内存越界,并可能导致段错误:
1 int bIsUniCommBlv = 1;
2 int main(void)
4 & & char szWanName[] = &OAM_WAN_VOIP&;
5 & & if(bIsUniCommBlv)
6 & & & & strcpy(szWanName, &OAM_WAN_MNGIP&);
8 & & return 0;
& & &但该例的另一写法则更为糟糕:
&1 int bIsUniCommBlv = 1;
&2 int main(void)
&4 & & char szWanName[] = &&; //字符数组szWanName仅能容纳1个元素('\0')!
&5 & & if(bIsUniCommBlv)
&6 & & & & strcpy(szWanName, &OAM_WAN_MNGIP&);
&7 & & else
&8 & & & & strcpy(szWanName, & OAM_WAN_VOIP&);
10 & & return 0;
& & &函数传递指针参数时也可能发生内存越界:
&1 typedef struct{
&2 & & int dwErrNo;
&3 & & int aErrInfo[6];
&4 }T_ERR_INFO;
&5 int PortDftDot1p(int dwPort, int dwDot1p, void *pvOut)
&7 & & int dwRet = 0;
&8 & & T_ERR_INFO *ptErrInfo = (T_ERR_INFO *)pvO
&9 & & //dwRet = DoSomething();
10 & & ptErrInfo-&dwErrNo & & = dwR
11 & & ptErrInfo-&aErrInfo[0] = dwP
12 & & return dwR
15 int main(void)
17 & & int dwOut = 0;
18 & & PortDftDot1p(0, 5, &dwOut);
19 & & return 0;
& & &上例中,接口函数PortDftDot1p使用T_ERR_INFO结构向调用者传递出错信息,但该结构并非调用者必知和必需。出于隐藏细节或其他原因,接口将出参指针声明为void*类型,而非T_ERR_INFO*类型。这样,当调用者传递的相关参数为其他类型时,编译器也无法发现类型不匹配的错误。此外,接口内未对pvOut指针判空就进行类型转换,非常危险(即使判空依旧危险)。从安全和实用角度考虑,该接口应该允许pvOut指针为空,此时不向调用者传递出错信息(调用方也许并不想要这些信息);同时要求传入pvOut指针所指缓冲区的字节数,以便在指针非空时安全地传递出错信息。
& & &错误的指针偏移运算也常导致内存越界。例如,指针p+n等于(char*)p + n * sizeof(*p),而非(char*)p + n。若后者才是本意,则p+n的写法很可能导致内存越界。
& & &栈区内存越界还可能导致函数返回地址被改写,详见《缓冲区溢出详解》一文。
& & &两种情况可能改写函数返回地址:1) 对自动变量的写操作超出其范围(上溢);2) 主调函数和被调函数的参数不匹配或调用约定不一致。
& & &函数返回地址被改写为有效地址时,通过堆栈回溯可看到函数调用关系不符合预期。当返回地址被改写为非法地址(如0)时,会发生段错误,并且堆栈无法回溯:
1 Program received signal SIGSEGV, Segmentation fault.
2 0x in ?? ()
& & &这种故障从代码上看特征非常明显,即发生在被调函数即将返回的位置。
& & 【对策】与数据区内存越界对策相似,但更注重代码走查而非越界检测。
2.2.4 返回栈内存地址
& & &(被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。详见《已释放的栈内存》一文。
&1 const static char *paMsgNameMap[] = {
&2 & & /* 0 */ & & &0&,
&3 & & /* 1 */ & & &1&,
&4 & & /* 2 */ & & &2&,
&5 & & /* 3 */ & & &3&,
&6 & & /* 4 */ & & &Create&,
&7 & & /* 5 */ & & &5&,
&8 & & /* 6 */ & & &Delete&,
&9 & & /* 7 */ & & &7&,
10 & & /* 8 */ & & &Set&,
11 & & /* 9 */ & & &Get&,
12 & & //... ...
13 & & /*28 */ & & &GetCurData&,
14 & & /*29 */ & & &SetTable&
16 const static unsigned char ucMsgNameNum = sizeof(paMsgNameMap) / sizeof(paMsgNameMap[0]);
18 char *ParseOmciMsgType(unsigned char ucMsgType)
20 & & if(ucMsgType & ucMsgNameNum)
21 & & & & return paMsgNameMap[ucMsgType];
23 & & char szStrMsgType[sizeof(&255&)] = {0}; &/* Max:&255& */
24 & & sprintf(szStrMsgType, &%u&, ucMsgType);
25 & & return szStrMsgT &//编译警告:
& & &编译上述代码,函数ParseOmciMsgType在返回szStrMsgType处产生function returns address of local variable的警告。可将szStrMsgType定义为静态变量:
1 char *ParseOmciMsgType(unsigned char ucMsgType)
3 & & if(ucMsgType & ucMsgNameNum)
4 & & & & return paMsgNameMap[ucMsgType];
6 & & static char szStrMsgType[sizeof(&255&)] = {0}; /* Max:&255& */
7 & & sprintf(szStrMsgType, &%u&, ucMsgType);
8 & & return szStrMsgT
& & &若将结果通过函数参数而非返回值传递,则代码会更为安全:
1 void ParseOmciMsgType(unsigned char ucMsgType, char *pszMsgType)
3 & & if(ucMsgType & ucMsgNameNum)
4 & & & & strcpy(pszMsgType, paMsgNameMap[ucMsgType]);
5 & & else
6 & & & & sprintf(pszMsgType, &%u&, ucMsgType);
& & &注意,不可采用下面的写法:
1 void ParseOmciMsgType(unsigned char ucMsgType, char *pszMsgType)
3 & & if(ucMsgType & ucMsgNameNum)
4 & & & & pszMsgType = paMsgNameMap[ucMsgType];
5 & & else
6 & & & & sprintf(pszMsgType, &%u&, ucMsgType);
& & &因为指针做为函数参数时,函数内部只能改变指针所指向地址的内容,并不能改变指针的指向。
& & &若线程在自身栈上分配一个数据结构并将指向该结构的指针传递给pthread_exit,则调用pthread_join的线程试图使用该结构时,原先的栈区内存可能已被释放或另作他用。
& & 【对策】不要用return语句返回指向栈内变量的指针,可改为返回指向静态变量或动态内存的指针。但两者都存在重入性问题,而且后者还存在内存泄露的危险。
2.3 堆区内存
2.3.1 内存未初始化
& & &通过malloc库函数分配的动态内存,其初值未定义。若访问未初始化或未赋初值的内存,则会获得垃圾值。当基于这些垃圾值控制程序逻辑时,会产生不可预测的行为。
& & 【对策】在malloc之后调用 memset 将内存初值清零,或使用 calloc代替malloc。
1 char *pMem = malloc (10);
2 memset(pMem, 0, 10); // memset前应对申请的动态内存做有效性检查
4 char *pMem = calloc (10, 1);
2.3.2 内存分配失败
& & &动态内存成功分配的前提是系统具有足够大且连续可用的内存。内存分配失败的主要原因有:
& & &1) 剩余内存空间不足;
& & &2) 剩余内存空间充足,但内存碎片太多,导致申请大块内存时失败;
& & &3) 内存越界,导致malloc等分配函数所维护的管理信息被破坏。
& & &剩余内存空间不足的情况相对少见,通常发生在申请超大块内存时。例如:
&1 #include &stdlib.h&
&2 #include &errno.h&
&3 #define ALLOC_BYTES & (24)
&4 int main(void){
&5 & & unsigned int dwRound = 0;
&6 & & while(1){
&7 & & & & char *pMem = malloc(ALLOC_BYTES);
&8 & & & & if(NULL == pMem){
&9 & & & & & & printf(&Alloc failed(%s)!\n&, strerror(errno));
10 & & & & & & return -1;&
11 & & & & }&
12 & & & & printf(&%d -& 0x%p\n&, dwRound, pMem);&
13 & & & & dwRound++;
15 & & return 0;
& & &执行后产生内存分配失败的错误:
1 0 -& 0x77f6b008
2 1 -& 0x37f6a008
3 Alloc failed(Cannot allocate memory)!
& & &内存越界导致内存分配失败的情况更为常见。此时,可从分配失败的地方开始回溯最近那个分配成功的malloc,看附近是否存在内存拷贝和数组越界的操作。
& & 【对策】若申请的内存单位为吉字节(GigaByte),可考虑选用64位寻址空间的机器,或将数据暂存于硬盘文件中。此外,申请动态内存后,必须判断内存是否是为NULL,并进行防错处理,比如使用return语句终止本函数或调用exit(1)终止整个程序的运行。
2.3.3 内存释放失败
& & &内存释放失败的主要原因有:
& & &1) 释放未指向动态内存的指针;
& & &2) 指向动态内存的指针在释放前被修改;
& & &3) 内存越界,导致malloc等分配函数所维护的管理信息被破坏;
& & &4) 内存重复释放(Double Free)。
& & &情况1属于低级错误,即指针并未执行malloc分配,却调用free释放该指针指向的内存。
1 int main(void)
3 & & int dwMem = 0; //具有迷惑性的变量名
4 & & int *pBuf = &dwM
5 & & free(pBuf);
7 & & return 0;
9 //执行后报错:*** glibc detected *** ./test: free(): invalid pointer: 0xbf84b35c ***
& & &情况2多发生在从申请内存到最后释放跨越多个模块历经大量处理逻辑时,指针初始值被修改掉。简单示例如下:
&1 int main(void)
&3 & & char *pMem = malloc(10);
&4 & & if(NULL == pMem)
&5 & & & & return -1;&
&7 & & pMem++;
&8 & & free(pMem);
10 & & return 0;
12 //执行后报错:*** glibc detected *** ./test: free(): invalid pointer: 0x082b5009 ***
& & &内存越界也可能导致内存释放失败:
&1 int main(void)
&3 & & char *pMem = malloc(2);
&4 & & if(NULL == pMem)
&5 & & & & return -1;&
&7 & & memset(pMem, 0, sizeof(int)*10);
&8 & & free(pMem);
&9 & & return 0;
11 //执行后报错:*** glibc detected *** ./test: free(): invalid next size (fast): 0x09efa008 ***
& & &内存重复释放最简单但最不可能出现的示例如下:
&1 int main(void)
&3 & & char *pMem = malloc(10);
&4 & & if(NULL == pMem)
&5 & & & & return -1;&
&7 & & free(pMem);
&8 & & free(pMem);
10 & & return 0;
12 //执行后报错:*** glibc detected *** ./test: double free or corruption (fasttop): 0x ***
& & &通常,编码者会封装接口以更好地管理内存的申请和释放。若释放接口内部在释放前未判断指向动态内存的指针是否为空,或释放后未将指向该内存的指针设置为空。当程序中调用关系或处理逻辑过于复杂(尤其是对于全局性的动态内存),难以搞清内存何时或是否释放,加之接口未作必要的防护,极易出现内存重复释放。
& & &此外,当程序中存在多份动态内存指针的副本时,很容易经由原内存指针及其副本释放同一块内存。
&1 int main(void)
&3 & & char *pMem = malloc(sizeof(char)*10);
&4 & & if(NULL == pMem)
&5 & & & & return -1;
&7 & & char *pMemTemp = pM
&8 & & //Do Something...
10 & & free(pMem);
11 & & free(pMemTemp);
12 & & return 0;
& & &上例中仅需释放pMem或pMemTemp其一即可。
& & 【对策】幸运的是,内存释放失败会导致程序崩溃,故障明显。并且,可借助静态或动态的内存检测技术进行排查。
& & &对于重复释放,可仿照《C语言通用双向循环链表操作函数集》一文中介绍的SAFE_FREE宏,尽可能地&规避&其危害(但当内存指针存在多个副本时无能为力)。
1 #define SAFE_FREE(pointer) & SafeFree(&(pointer)) &//与SAFE_ALLOC入参指针形式一致
2 void SafeFree(void **pointer)
4 & & if(pointer != NULL)
6 & & & & free(*pointer);
7 & & & & *pointer = NULL;
& & &此外,应在设计阶段保证数据结构和流程尽量地简洁合理,从根本上解决对象管理的混乱。
2.3.4 内存分配与释放不配对
& & &编码者一般能保证malloc和free配对使用,但可能调用不同的实现。例如,同样是free接口,其调试版与发布版、单线程库与多线程库的实现均有所不同。一旦链接错误的库,则可能出现某个内存管理器中分配的内存,在另一个内存管理器中释放的问题。此外,模块封装的内存管理接口(如GetBuffer和FreeBuffer)在使用时也可能出现GetBuffer配free,或malloc配FreeBuffer的情况,尤其是跨函数的动态内存使用。
& & 【对策】动态内存的申请与释放接口调用方式和次数必须配对,防止内存泄漏。分配和释放最好由同一方管理,并提供专门的内存管理接口。
2.3.5 内存越界
& & &除明显的读写越界外,关于动态内存还存在一种sizeof计算错误导致的越界:
&1 int main(void)
&3 & & T_CHK_MEM *pMem = malloc(sizeof(pMem));
&4 & & if(NULL == pMem)
&5 & & & & return -1;
&7 & & memset(pMem, 0, sizeof(T_CHK_MEM));
&8 & & free(pMem);
&9 & & return 0;
11 //执行后报错:*** glibc detected *** ./test: free(): invalid next size (fast): 0x ***
& & &这种越界也是内存释放失败的一个原因。正确的内存申请写法应该是:
1 T_CHK_MEM *pMem = malloc(sizeof(*pMem));
3 T_CHK_MEM *pMem = malloc(sizeof(T_CHK_MEM));

我要回帖

更多关于 c语言释放内存空间 的文章

 

随机推荐