内存泄漏:
内存泄漏(Memory Leak)是指程序中已动态分配的内存由于某种原因程序未释放或无法释放,导致系统内存的浪费,严重时会导致程序运行缓慢甚至崩溃。这种情况在长时间运行的程序或大型系统中尤为常见,因为它会随着时间的推移逐渐累积未释放的内存。
内存泄漏的原因
-
忘记释放内存:这是最常见的内存泄漏原因。程序员在申请内存后,由于疏忽或其他原因,忘记了在不再需要时释放它。
-
异常终止:如果程序在执行过程中因为异常(如错误、崩溃等)而终止,那么它可能无法执行正常的清理代码,导致内存泄漏。
-
缓存机制不当:有时为了优化性能,程序会缓存一些数据。如果缓存的数据量没有得到有效控制,或者缓存的清理策略不合理,就可能导致内存泄漏。
-
全局变量和静态变量:全局变量和静态变量的生命周期与程序相同,如果它们被用于存储动态分配的内存地址,并且这些内存在使用完毕后没有被释放,就会导致内存泄漏。
-
第三方库或API的使用:使用第三方库或API时,如果没有正确遵循其内存管理规则,也可能导致内存泄漏。
valgrind: 它主要用于内存调试、内存泄漏检测以及程序性能分析。
内存碎片:
内存碎片是指系统中所有不可用的空闲内存,这些碎片之所以不能被使用,是因为负责动态分配内存的分配算法使得这些空闲的内存无法使用。内存碎片可以分为外碎片和内碎片两种类型,下面分别进行详细介绍:
一、内存碎片的定义及分类
1. 外碎片
- 定义:外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。
- 产生原因:频繁的内存分配和释放、不同大小的内存分配、内存对齐问题等,都可能导致外碎片的产生。
2. 内碎片
- 定义:内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间。
- 产生原因:假设有一块连续空闲内存空间,当某个进程请求的内存大小与空闲内存块不完全匹配时,系统可能会分配一个稍大一点的内存块给该进程,从而产生内部碎片。
二、内存碎片的产生原因
- 频繁的内存分配和释放:这是导致内存碎片的主要原因之一。
- 不同大小的内存分配:当系统中分配的内存大小不一致时,也会导致碎片的产生。
- 内存对齐的问题:内存分配时如果没有进行对齐,也容易导致碎片。
双向有头链表:
双向有头链表是一种链表数据结构,与单向链表不同,双向链表中的每个节点都包含两个指针:一个指向下一个节点,另一个指向前一个节点。
创建链表:
Dlink_t *creat_doulink()
{
Dlink_t *pdoulink = (Dlink_t *)malloc(sizeof(Dlink_t));
if(NULL == pdoulink)
{
perror("malloc fail");
return NULL;
}
pdoulink->phead = NULL;
pdoulink->clen = 0;
pthread_mutex_init(&(pdoulink->mutex),NULL);
return pdoulink;
}
遍历链表:
- 双向遍历:由于每个节点都包含前后两个指针,因此可以很方便地从前往后或从后往前遍历链表。
void doulink_for_each(DLink_t *pdoulink, int dir)
{
if (is_empty_doulink(pdoulink))
return;
DLink_Node_t *p = pdoulink->phead;
if (dir)//正向
{
while (p != NULL)
{
printf("%d %s %d\n", p->data.id, p->data.name, p->data.score);
p = p->pnext;
}
}
else//反向
{
while (p->pnext != NULL)
{
p = p->pnext;
}
while (p != NULL)
{
printf("%d %s %d\n", p->data.id, p->data.name, p->data.score);
p = p->ppre;
}
}
printf("\n");
}
头插:
int push_doulink_head(Dlink_t *pdoulink,DataType data)
{
Dlink_Node_t *pnode =(Dlink_Node_t*) malloc(sizeof(Dlink_Node_t));
if(NULL == pnode)
{
perror("fail malloc");
return -1;
}
pnode->data = data;
pnode->ppre = NULL;
pnode->pnext = NULL;
if(is_empty_doulink(pdoulink))
{
pdoulink->phead = pnode;
}
else
{
pnode->pnext = pdoulink->phead;
pdoulink->phead->ppre = pnode;
pdoulink->phead = pnode;
}
pdoulink->clen++;
return 0;
}
尾插:
int push_doulink_end(Dlink_t *pdoulink,DataType data)
{
Dlink_Node_t *pnode = (Dlink_Node_t *)malloc(sizeof(Dlink_Node_t));
if(pnode == NULL)
{
perror("fail malloc");
return -1;
}
pnode->data = data;
pnode->ppre = NULL;
pnode->pnext = NULL;
if(is_empty_doulink(pdoulink))
{
pdoulink->phead = pnode;
}
else
{
Dlink_Node_t *p = pdoulink->phead;
while(p->pnext != NULL)
{
p = p->pnext;
}
p->pnext = pnode;
pnode->ppre = p;
}
pdoulink->clen++;
return 0;
}
头删:
int pop_doulink_head(Dlink_t *plink)
{
if(is_empty_doulink(plink))
{
return 0;
}
Dlink_Node_t *p = plink->phead;
plink->phead = p->pnext;
p->ppre = NULL;
free(p);
plink->clen--;
return 0;
}
尾删:
int pop_doulink_end(Dlink_t *plink)
{
if(is_empty_doulink(plink))
return 0;
Dlink_Node_t *p = plink->phead;
if(NULL == p->pnext)
{
pop_doulink_head(plink);
}
while(p->pnext->pnext != NULL)
{
p = p->pnext;
}
free(p->pnext);
p->ppre = p;
p->pnext = NULL;
plink->clen--;
}
查找:
Dlink_Node_t *find_data(Dlink_t *plink,char *name)
{
if(is_empty_doulink(plink))
return 0;
int i = 1;
Dlink_Node_t *p = plink->phead;
while(p != NULL)
{
if(strcmp(name, p->data.name)==0)
{
break;
}
p = p->pnext;
if(p == NULL)
{
return NULL;
}
}
return p;
}
修改:
int change_data(Dlink_t *plink,char *name,int score)
{
Dlink_Node_t *ptmp;
ptmp = find_data(plink,name);
Dlink_Node_t *p = plink->phead;
while(p->pnext != NULL)
{
if(p == ptmp)
{
p->data.score = score;
}
p = p->pnext;
}
return 0;
}
销毁:
void destroy_doulink(Dlink_t *plink)
{
while(!is_empty_doulink(plink))
{
pop_doulink_head(plink);
}
free(plink);
}
双向链表的优势
双向链表相比于单向链表,具有以下几个显著的优势:
- 双向遍历能力:
- 双向链表中的每个节点都包含两个指针,一个指向前一个节点(ppre),另一个指向下一个节点(pnext)。这种结构使得双向链表可以从链表的任何一个节点开始,向前或向后遍历整个链表。相比之下,单向链表只能从头节点开始,逐个节点向后遍历,无法直接向前遍历。
- 高效的插入和删除操作:
- 在双向链表中插入或删除节点时,由于可以直接访问要插入或删除节点的前一个和/或后一个节点,因此可以更加高效地执行这些操作。例如,在双向链表中删除一个节点时,只需要修改该节点前后两个节点的指针,使其绕过被删除的节点即可,而不需要像单向链表那样可能需要遍历整个链表来找到要删除节点的前一个节点。
- 空间效率:
- 虽然双向链表中的每个节点需要额外的空间来存储指向前一个节点的指针,但从整体来看,它在内存使用上仍然具有较高的灵活性。链表可以在运行时动态地分配和释放内存,而不需要像数组那样预先分配固定大小的内存块。这种动态内存分配的特性使得双向链表在处理不确定大小的数据集时更加灵活。
单向链表的优势
-
内存占用较少:
单向链表的每个节点只需要存储一个指向下一个节点的指针(以及存储数据所需的空间)。而双向链表的每个节点除了存储数据外,还需要存储两个指针:一个指向前一个节点,另一个指向下一个节点。因此,在内存使用方面,单向链表更为节省。 -
实现更简单:
从实现的角度来看,单向链表的结构更为简单直接,因为它只涉及到一个方向的链接。这意味着在编写代码时,你可能需要处理更少的边界情况和指针操作,尤其是在实现一些基本的链表操作时(如插入和删除)。 -
特定的应用场景:
在某些特定的应用场景中,你可能只需要单向遍历链表,而不需要反向遍历。例如,当实现一个简单的栈(后进先出)或队列(先进先出)时,单向链表就足够了,因为它提供了所需的顺序访问功能,而无需额外的反向遍历能力。 -
兼容性:
在某些旧的或资源受限的系统中,双向链表的额外内存开销可能是一个问题。在这些情况下,单向链表由于其较小的内存占用而成为更合适的选择。 -
性能优势(在某些情况下):
虽然这一点可能并不总是成立,但在某些特定的情况下,单向链表可能具有更好的性能。例如,在遍历链表时,如果只需要从头到尾遍历一次,并且不需要反向遍历,那么单向链表可能会由于更简单的结构和更少的指针操作而表现出更好的缓存局部性,从而在某些情况下提高性能。