资料—链表 c中错误指示翻译

单向链表(单链表)是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始。

 动态单链表

单向链表的数据结构可以分为两部分:数据域和指针域,数据域存储数据,指针域指向下一个储存节点的地址。

/*線性表的單鏈表存儲结構*/

typedef struct LNode{

  ElemType data;

  struct LNode *next;

}LNode, *LinkList;

 

/*带有头结點的單鏈表的基本操作(12个)*/

void InitList(LinkLi *L)

{ /* 操作结果:構造一个空的線性表L */

  *L=(LinkList)malloc(sizeof(structLNode)); /* 產生头结點,並使L指向此头结點 */

  if(!*L) /* 存儲分配失敗 */

    exit(OVERFLOW);

  (*L)->next=NULL; /* 指针域為空 */

}

 

void DestroyList(LinkList *L)

{ /* 初始條件:線性表L已存在。操作结果:销毁線性表L */

  LinkList q;

  while(*L)

  {

    q=(*L)->next;

    free(*L);

    *L=q;

  }

}

 

void ClearList(LinkList L) /* 不改变L */

{ /* 初始条件:线性表L已存在。操作结果:将L重置为空表 */

  LinkList p,q;

  p=L->next; /* p指向第一个结点 */

  while(p) /* 没到表尾 */

  {

    q=p->next;

    free(p);

    p=q;

  }

  L->next=NULL; /* 头结点指针域为空 */

}

 

Status ListEmpty(LinkList L)

{ /* 初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */

  return L->next == NULL;

}

 

int ListLength(LinkList L)

{ /* 初始条件:线性表L已存在。操作结果:返回L中数据元素个数 */

  int i=0;

  LinkList p=L->next; /* p指向第一个结点 */

  while(p) /* 没到表尾 */

  {

    i++;

    p=p->next;

  }

  return i;

}

 

Status GetElem(LinkList L,int i,ElemType *e) /* 算法2.8 */

{ /* L为带头结点的单链表的头指针。当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR */

  int j=1; /* j为计数器 */

  LinkList p=L->next; /* p指向第一个结点 */

  while(p&&j < i) /* 顺指针向后查找,直到p指向第i个元素或p为空 */

  {

    p=p->next;

    j++;

  }

  if(!p||j>i) /* 第i个元素不存在 */

    return ERROR;

  *e=p->data; /* 取第i个元素 */

  return OK;

}

 

int LocateElem(LinkList L,ElemType e,Status(*compare)(ElemType,ElemType))

{ /* 初始条件: 线性表L已存在,compare()是数据元素判定函数(满足为1,否则为0) */

  /* 操作结果: 返回L中第1个与e满足关系compare()的数据元素的位序。 */

  /*           若这样的数据元素不存在,则返回值为0 */

  int i=0;

  LinkList p=L->next;

  while(p)

  {

    i++;

    if(compare(p->data,e)) /* 找到这样的数据元素 */

      return i;

    p=p->next;

  }

  return 0;

}

 

Status PriorElem(LinkList L,ElemType cur_e,ElemType *pre_e)

{ /* 初始条件: 线性表L已存在 */

  /* 操作结果: 若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱, */

  /*           返回OK;否则操作失败,pre_e无定义,返回INFEASIBLE */

  LinkList q,p=L->next; /* p指向第一个结点 */

  while(p->next) /* p所指结点有后继 */

  {

    q=p->next; /* q为p的后继 */

    if(q->data==cur_e)

    {

      *pre_e=p->data;

      return OK;

   }

    p=q; /* p向后移 */

  }

  return INFEASIBLE;

}

 

Status NextElem(LinkList L,ElemType cur_e,ElemType *next_e)

{ /* 初始条件:线性表L已存在 */

  /* 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, */

  /*           返回OK;否则操作失败,next_e无定义,返回INFEASIBLE */

  LinkList p=L->next; /* p指向第一个结点 */

  while(p->next) /* p所指结点有后继 */

  {

    if(p->data==cur_e)

    {

      *next_e=p->next->data;

      return OK;

    }

    p=p->next;

  }

  return INFEASIBLE;

}

 

Status ListInsert(LinkList L,int i,ElemType e) /* 算法2.9。不改变L */

{ /* 在带头结点的单链线性表L中第i个位置之前插入元素e */

  int j=0;

  LinkList p=L,s;

  while(p&&j < i-1) /* 寻找第i-1个结点 */

  {

    p=p->next;

    j++;

  }

  if(!p||j>i-1) /* i小于1或者大于表长 */

    return ERROR;

  s=(LinkList)malloc(sizeof(structLNode)); /* 生成新结点 */

  s->data=e; /* 插入L中 */

  s->next=p->next;

  p->next=s;

  return OK;

}

 

Status ListDelete(LinkList L,int i,ElemType *e) /* 算法2.10。不改变L */

{ /* 在带头结点的单链线性表L中,删除第i个元素,并由e返回其值 */

  int j=0;

  LinkList p=L,q;

  while(p->next&&j<i-1) /* 寻找第i个结点,并令p指向其前岖 */

  {

    p=p->next;

    j++;

  }

  if(!p->next||j>i-1) /* 删除位置不合理 */

    return ERROR;

  q=p->next; /* 删除并释放结点 */

  p->next=q->next;

  *e=q->data;

  free(q);

  return OK;

}

 

void ListTraverse(LinkList L,void(*vi)(ElemType))

/* vi的形参类型为ElemType,与bo2-1.c中相应函数的形参类型ElemType&不同 */

{ /* 初始条件:线性表L已存在。操作结果:依次对L的每个数据元素调用函数vi() */

  LinkList p=L->next;

  while(p)

  {

    vi(p->data);

    p=p->next;

  }

  printf("\n");

}

 静态单链表

/*线性表的静态单链表存储结构 */

#define MAX_SIZE 100 /* 链表的最大长度 */

typedef struct

{

  ElemType data;

  int cur;

}component,SLinkList[MAX_SIZE];

/*一个数组只生成一个静态链表的基本操作(11个))*/

#define DestroyList ClearList // DestroyList()和ClearList()的操作是一样的

void InitList(SLinkList L)

{ /* 构造一个空的链表L,表头为L的最后一个单元L[MAX_SIZE-1],其余单元链成 */

  /* 一个备用链表,表头为L的第一个单元L[0],“0”表示空指针 */

  int i;

 L[MAX_SIZE-1].cur=0; /* L的最后一个单元为空链表的表头 */

 for(i=0;i<MAX_SIZE-2;i++) /* 将其余单元链接成以L[0]为表头的备用链表 */

    L[i].cur=i+1;

 L[MAX_SIZE-2].cur=0;

}

void ClearList(SLinkList L)

{ /* 初始条件:线性表L已存在。操作结果:将L重置为空表 */

  int i,j,k;

 i=L[MAX_SIZE-1].cur; /* 链表第一个结点的位置 */

 L[MAX_SIZE-1].cur=0; /* 链表空 */

  k=L[0].cur; /* 备用链表第一个结点的位置 */

  L[0].cur=i; /* 把链表的结点连到备用链表的表头 */

  while(i) /* 没到链表尾 */

  {

    j=i;

    i=L[i].cur; /* 指向下一个元素 */

  }

  L[j].cur=k; /* 备用链表的第一个结点接到链表的尾部 */

}

Status ListEmpty(SLinkList L)

{ /* 若L是空表,返回TRUE;否则返回FALSE */

  if(L[MAX_SIZE-1].cur==0)/* 空表 */

    return TRUE;

  else

    return FALSE;

}

int ListLength(SLinkList L)

{ /* 返回L中数据元素个数 */

  intj=0,i=L[MAX_SIZE-1].cur; /* i指向第一个元素 */

  while(i) /* 没到静态链表尾 */

  {

    i=L[i].cur; /* 指向下一个元素 */

    j++;

  }

  return j;

}

Status GetElem(SLinkList L,int i,ElemType *e)

{ /* 用e返回L中第i个元素的值 */

  intl,k=MAX_SIZE-1; /* k指向表头序号 */

 if(i<1||i>ListLength(L))

    return ERROR;

 for(l=1;l<=i;l++) /* 移动到第i个元素处 */

    k=L[k].cur;

  *e=L[k].data;

  return OK;

}

int LocateElem(SLinkList L,ElemType e) /* 算法2.13(有改动) */

{ /* 在静态单链线性表L中查找第1个值为e的元素。若找到,则返回它在L中的 */

  /* 位序,否则返回0。(与其它LocateElem()的定义不同) */

  inti=L[MAX_SIZE-1].cur; /* i指示表中第一个结点 */

 while(i&&L[i].data!=e) /* 在表中顺链查找(e不能是字符串) */

    i=L[i].cur;

  return i;

}

Status PriorElem(SLinkList L,ElemType cur_e,ElemType*pre_e)

{ /* 初始条件:线性表L已存在 */

  /* 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱, */

  /*           否则操作失败,pre_e无定义 */

  intj,i=L[MAX_SIZE-1].cur; /* i指示链表第一个结点的位置*/

  do

  { /* 向后移动结点 */

    j=i;

    i=L[i].cur;

 }while(i&&cur_e!=L[i].data);

  if(i) /* 找到该元素 */

  {

   *pre_e=L[j].data;

    return OK;

  }

  return ERROR;

}

Status NextElem(SLinkList L,ElemType cur_e,ElemType*next_e)

{ /* 初始条件:线性表L已存在 */

  /* 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, */

  /*           否则操作失败,next_e无定义 */

  intj,i=LocateElem(L,cur_e); /* 在L中查找第一个值为cur_e的元素的位置 */

  if(i) /* L中存在元素cur_e */

  {

    j=L[i].cur; /*cur_e的后继的位置 */

    if(j) /* cur_e有后继 */

    {

     *next_e=L[j].data;

      return OK; /*cur_e元素有后继 */

    }

  }

  return ERROR; /*L不存在cur_e元素,cur_e元素无后继 */

}

Status ListInsert(SLinkList L,int i,ElemType e)

{ /* 在L中第i个元素之前插入新的数据元素e */

  intl,j,k=MAX_SIZE-1; /* k指向表头 */

 if(i<1||i>ListLength(L)+1)

    return ERROR;

  j=Malloc(L); /* 申请新单元 */

  if(j) /* 申请成功 */

  {

    L[j].data=e; /*赋值给新单元 */

    for(l=1;l<i;l++) /* 移动i-1个元素 */

      k=L[k].cur;

   L[j].cur=L[k].cur;

    L[k].cur=j;

    return OK;

  }

  return ERROR;

}

Status ListDelete(SLinkList L,int i,ElemType *e)

{ /* 删除在L中第i个数据元素e,并返回其值 */

  intj,k=MAX_SIZE-1; /* k指向表头 */

  if(i<1||i>ListLength(L))

    return ERROR;

  for(j=1;j<i;j++) /* 移动i-1个元素 */

    k=L[k].cur;

  j=L[k].cur;

 L[k].cur=L[j].cur;

  *e=L[j].data;

  Free(L,j);

  return OK;

}

void ListTraverse(SLinkList L,void(*vi)(ElemType))

{ /* 初始条件:线性表L已存在。操作结果:依次对L的每个数据元素调用函数vi() */

  inti=L[MAX_SIZE-1].cur; /* 指向第一个元素 */

  while(i) /* 没到静态链表尾 */

  {

    vi(L[i].data);/* 调用vi() */

    i=L[i].cur; /* 指向下一个元素 */

  }

 printf("\n");

}

 

一、 链表数据结构简介

链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。

通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:

1. 单链表


1 单链表
  

单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行。

2. 双链表


2 双链表
  

通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。

3. 循环链表

循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。

在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。

回页首

二、 Linux 2.6内核链表数据结构的实现

尽管这里使用2.6内核作为讲解的基础,但实际上2.4内核中的链表结构和2.6并没有什么区别。不同之处在于2.6扩充了两种链表数据结构:链表的读拷贝更新(rcu)和HASH链表(hlist)。这两种扩展都是基于最基本的list结构,因此,本文主要介绍基本链表结构,然后再简要介绍一下rcu和hlist。

链表数据结构的定义很简单(节选自[include/linux/list.h],以下所有代码,除非加以说明,其余均取自该文件):

struct list_head { struct list_head *next, *prev; };



list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。

和第一节介绍的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。

在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):

struct list_node { struct list_node *next; ElemType data; };



因为ElemType的缘故,对每一种数据项类型都需要定义各自的链表结构。有经验的C++程序员应该知道,标准模板库中的<list>采用的是C++ Template,利用模板抽象出和数据项类型无关的链表操作接口。

在Linux内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head成员,例如在[include/linux/netfilter.h]中定义了一个nf_sockopt_ops结构来描述Netfilter为某一协议族准备的getsockopt/setsockopt接口,其中就有一个(structlist_head list)成员,各个协议族的nf_sockopt_ops结构都通过这个list成员组织在一个链表中,表头是定义在[net/core/netfilter.c]中的nf_sockopts(struct list_head)。从下图中我们可以看到,这种通用的链表结构避免了为每个数据项类型定义自己的链表的麻烦。Linux的简捷实用、不求完美和标准的风格,在这里体现得相当充分。


3 nf_sockopts链表示意图
  

回页首

三、 链表操作接口

1. 声明和初始化

实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:

#define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)



当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:

static inline int list_empty(const struct list_head *head) { return head->next == head; }



除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表:

#define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0)



我们用INIT_LIST_HEAD(&nf_sockopts)来使用它。

2. 插入/删除/合并

a) 插入

对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:

static inline void list_add(struct list_head *new, struct list_head *head); static inline void list_add_tail(struct list_head *new, struct list_head *head);



因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用

__list_add(new, head, head->next);



__list_add(new, head->prev, head);



来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作:

list_add(&new_sockopt.list, &nf_sockopts);



从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。如何通过链表访问到new_sockopt呢?下面会有详细介绍。

b) 删除

static inline void list_del(struct list_head *entry);



当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作:

list_del(&new_sockopt.list);



被剔除下来的new_sockopt.list,prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。

c) 搬移

Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:

static inline void list_move(struct list_head *list, struct list_head *head); static inline void list_move_tail(struct list_head *list, struct list_head *head);



例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,并将其再链入nf_sockopts的表头。

d) 合并

除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);



假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2和list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。如图(虚箭头为next指针):


4 链表合并list_splice(&list1,&list2)
  

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:

static inline void list_splice_init(struct list_head *list, struct list_head *head);



该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

3. 遍历

遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

a) 由链表节点到数据项变量

我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量,则如此调用:

list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);



这里"list"正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。

list_entry的使用相当简单,相比之下,它的实现则有一些难懂:

#define list_entry(ptr, type, member) container_of(ptr, type, member) container_of宏定义在[include/linux/kernel.h]中: #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) offsetof宏定义在[include/linux/stddef.h]中: #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)



size_t最终定义为unsigned int(i386)。

这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。

container_of()和offsetof()并不仅用于链表操作,这里最有趣的地方是((type*)0)->member,它将0地址强制"转换"为type结构的指针,再访问到type结构中的member成员。在container_of宏中,它用来给typeof()提供参数(typeof()是gcc的扩展,和sizeof()类似),以获得member成员的数据类型;在offsetof()中,这个member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。

如果这么说还不好理解的话,不妨看看下面这张图:


5 offsetof()宏的原理
  

对于给定一个结构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。

b) 遍历宏

在[net/core/netfilter.c]的nf_register_sockopt()函数中有这么一段话:

…… struct list_head *i; …… list_for_each(i, &nf_sockopts) { struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i; …… } ……



函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。在[include/linux/list.h]中,list_for_each()宏是这么定义的:

#define list_for_each(pos, head) \ for (pos = (head)->next, prefetch(pos->next); pos != (head); \ pos = pos->next, prefetch(pos->next))



它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。

那么在nf_register_sockopt()中实际上就是遍历nf_sockopts链表。为什么能直接将获得的list_head成员变量地址当成struct nf_sockopt_ops数据项变量的地址呢?我们注意到在struct nf_sockopt_ops结构中,list是其中的第一项成员,因此,它的地址也就是结构变量的地址。更规范的获得数据变量地址的用法应该是:

struct nf_sockopt_ops *ops = list_entry(I, struct nf_sockopt_ops, list);



大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。对此Linux给出了一个list_for_each_entry()宏:

#define list_for_each_entry(pos, head, member) ……



与list_for_each()不同,这里的pos是数据项结构指针类型,而不是(struct list_head *)。nf_register_sockopt()函数可以利用这个宏而设计得更简单:

…… struct nf_sockopt_ops *ops; list_for_each_entry(ops,&nf_sockopts,list){ …… } ……



某些应用需要反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,使用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同。

如果遍历不是从链表头开始,而是从已知的某个节点pos开始,则可以使用list_for_each_entry_continue(pos,head,member)。有时还会出现这种需求,即经过一系列计算后,如果pos有值,则从pos开始遍历,如果没有,则从链表头开始,为此,Linux专门提供了一个list_prepare_entry(pos,head,member)宏,将它的返回值作为list_for_each_entry_continue()的pos参数,就可以满足这一要求。

4. 安全性考虑

在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个方面:

a) list_empty()判断

基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。

b) 遍历时节点删除

前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。

当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的"_safe"接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

回页首

四、 扩展

1.hlist


6 list和hlist
  

精益求精的Linux链表设计者(因为list.h没有署名,所以很可能就是Linus Torvalds)认为双头(next、prev)的双链表对于HASH表来说"过于浪费",因而另行设计了一套用于HASH表应用的hlist数据结构--单指针表头双循环链表,从上图可以看出,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的HASH表中存储的表头就能减少一半的空间消耗。

因为表头和节点的数据结构不同,插入操作如果发生在表头和首节点之间,以往的方法就行不通了:表头的first指针必须修改指向新插入的节点,却不能使用类似list_add()这样统一的描述。为此,hlist节点的prev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的"*(node->pprev)"访问和修改前驱节点的next(或first)指针。

2.read-copy update

在Linux链表功能接口中还有一系列以"_rcu"结尾的宏,与以上介绍的很多函数一一对应。RCU(Read-Copy Update)是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能。

我们知道,系统中数据读取操作远多于写操作,而rwlock机制在smp环境下随着处理机增多性能会迅速下降(见参考资料4)。针对这一应用背景,IBM Linux技术中心的Paul E. McKenney提出了"读拷贝更新"的技术,并将其应用于Linux内核中。RCU技术的核心是写操作分为写-更新两步,允许读操作在任何时候无阻访问,当系统有写操作时,更新动作一直延迟到对该数据的所有读操作完成为止。Linux链表中的RCU功能只是Linux RCU的很小一部分,对于RCU的实现分析已超出了本文所及,有兴趣的读者可以自行参阅本文的参考资料;而对RCU链表的使用和基本链表的使用方法基本相同。

回页首

五、 示例

附件中的程序除了能正向、反向输出文件以外,并无实际作用,仅用于演示Linux链表的使用。

为了简便,例子采用的是用户态程序模板,如果需要运行,可采用如下命令编译:

gcc -D__KERNEL__ -I/usr/src/linux-2.6.7/include pfile.c -o pfile



因为内核链表限制在内核态使用,但实际上对于数据结构本身而言并非只能在核态运行,因此,在笔者的编译中使用"-D__KERNEL__"开关"欺骗"编译器。



参考资料

1.       维基百科 http://zh.wikipedia.org,一个在GNU Documentation License下发布的网络辞典,自由软件理念的延伸,本文的"链表"概念即使用它的版本。

2.       《Linux内核情景分析》,毛德操先生的这本关于Linux内核的巨著几乎可以回答绝大部分关于内核的问题,其中也包括内核链表的几个关键数据结构。

3.       Linux内核2.6.7源代码,所有不明白的问题,只要潜心看代码,总能清楚。

4.       Kernel Korner: Using RCU in the Linux 2.5 Kernel,RCU主要开发者Paul McKenney 2003年10月发表于Linux Journal上的一篇介绍RCU的文章。在 http://www.rdrop.com/users/paulmck/rclock/上可以获得更多关于RCU的帮助。

 

C语言建立动态数组

 

       数组的有点在于随机存取,然而其不足也是明显的,就是一旦建立其大小就不能改变。若用数组存储数据,则必须创建一个可能存放的最大空间的数组,这无疑浪费了空间。动态数组解决了这个问题。动态数组的思路是:先建立一定大小的数组,向这个数组中存放数据,如果数组已满,则重新申请一个更大的空间来存放。每次重新申请时可以指定增量(inc)的大小,也可以固定大小。这样做的好处是空间浪费不多,最多浪费(inc-1)个元素空间,其不足是重新申请空间浪费时间,每次重新申请空间时须将原来的数据拷贝到新申请的空间,当数组很大时,这种浪费还是相当可观的。稍后将用链表和数组结合解决这一问题。

 

先建立动态数组的存储结构:

 

typedef unsigned char     BOOL;

typedef int elem_t;         //存放数据类型

typedef struct

...{

   int           iCount;          //数据个数(数组中实际存放元素的个数)

   int           iCapacity;     //容量(数组中能够容纳元素的最大个数)

   elem_t  * pData;         //数据指针(该指针指向存放数据空间的首地址)

}Array_t;

下面定义数组的基本操作,包括:

 

1、初始化;

2、设置元素的值;

3、取得元素的引用(C语言中指地址);

4、取得元素的值;

5、销毁数组

 

以下是上面五个操作的函数声明:

 

BOOL initArray( Array_t * array, int size);                             //初始化,size为指定初始化数组容量

BOOL setValue( Array_t * array, int index,elem_t val );    //设置指定位置元素的值

elem_t * getRef( Array_t * array, int index);                        //得到指定位置元素的引用(地址)

elem_t getValue( Array_t * array, int index);                      //得到指定位置元素的值

BOOL destroyArray( Array_t * array );                                  //销毁该数组

以下是函数实现:

 

#define INIT_DATA_NUM 10                                            //数组初始化大小,增量大小

 

BOOL initArray( Array_t * array, int size)                              //初始化,若size <= 0,则采用默认大小

...{

   BOOL bRet = FALSE;

   int initSize = (size > 0) ? size:INIT_DATA_NUM;

 

   array->pData = ( elem_t * )malloc( initSize * sizeof( elem_t) );

   if ( array->pData != NULL )

   ...{

        array->iCapacity = initSize;

       array->iCount = 0;

       bRet = TRUE;

    }

   return bRet;

}

 

BOOL setValue( Array_t * array, int index,elem_t val )     //设置指定位置元素的值

...{

   BOOL bRet = FALSE;

   if( index > 0 && index < array->iCount )

    ...{

       array->pData[index] = val;

       bRet = TRUE;

    }

   return bRet;

}

elem_t * getRef( Array_t * array, int index)                         //得到指定位置元素的引用(地址)

...{

   elem_t * eRet = NULL;

   if( index > 0 && index < array->iCount )

    ...{

       eRet = array->pData + index;

    }

   return eRet;

}

 

elem_t getValue( Array_t * array, int index)                       //得到指定位置元素的值(不检查数组越界)

...{

   return array->pData[index];

}

 

BOOL destroyArray( Array_t * array )                                   //销毁该数组

...{

   free( array->pData );

   array->pData = NULL;

   return TRUE;

}

 

 

基百科,自由的百科全书

(重定向自双链表

跳转到: 导航,搜索

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表

/* 线性表的双向链表存储结构 */

typedef struct DuLNode

{

  ElemType data;

  struct DuLNode*prior,*next;

}DuLNode,*DuLinkList;

/*带头结点的双向循环链表的基本操作(14个) */

void InitList(DuLinkList *L)

{ /* 产生空的双向循环链表L*/

 *L=(DuLinkList)malloc(sizeof(DuLNode));

  if(*L)

   (*L)->next=(*L)->prior=*L;

  else

    exit(OVERFLOW);

}

void DestroyList(DuLinkList *L)

{ /* 操作结果:销毁双向循环链表L*/

  DuLinkListq,p=(*L)->next; /* p指向第一个结点 */

  while(p!=*L) /* p没到表头 */

  {

    q=p->next;

    free(p);

    p=q;

  }

  free(*L);

  *L=NULL;

}

void ClearList(DuLinkList L) /* 不改变L */

{ /* 初始条件:L已存在。操作结果:将L重置为空表 */

  DuLinkList q,p=L->next;/* p指向第一个结点 */

  while(p!=L) /* p没到表头 */

  {

    q=p->next;

    free(p);

    p=q;

  }

 L->next=L->prior=L; /* 头结点的两个指针域均指向自身 */

}

Status ListEmpty(DuLinkList L)

{ /* 初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE*/

 if(L->next==L&&L->prior==L)

    return TRUE;

  else

    return FALSE;

}

int ListLength(DuLinkList L)

{ /* 初始条件:L已存在。操作结果:返回L中数据元素个数 */

  int i=0;

  DuLinkListp=L->next; /* p指向第一个结点 */

  while(p!=L) /* p没到表头 */

  {

    i++;

    p=p->next;

  }

  return i;

}

Status GetElem(DuLinkList L,int i,ElemType *e)

{ /* 当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR */

  int j=1; /* j为计数器 */

  DuLinkListp=L->next; /* p指向第一个结点 */

 while(p!=L&&jnext;

    j++;

  }

  if(p==L||j>i)/* 第i个元素不存在 */

    return ERROR;

  *e=p->data; /*取第i个元素 */

  return OK;

}

 

int LocateElem(DuLinkList L,ElemTypee,Status(*compare)(ElemType,ElemType))

{ /* 初始条件:L已存在,compare()是数据元素判定函数 */

  /* 操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。 */

  /*           若这样的数据元素不存在,则返回值为0 */

  int i=0;

  DuLinkListp=L->next; /* p指向第1个元素 */

  while(p!=L)

  {

    i++;

   if(compare(p->data,e)) /* 找到这样的数据元素 */

      return i;

    p=p->next;

  }

  return 0;

}

 

Status PriorElem(DuLinkList L,ElemType cur_e,ElemType*pre_e)

{ /* 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱, */

  /*           否则操作失败,pre_e无定义 */

  DuLinkList p=L->next->next;/* p指向第2个元素 */

  while(p!=L) /* p没到表头 */

  {

   if(p->data==cur_e)

    {

     *pre_e=p->prior->data;

      return TRUE;

    }

    p=p->next;

  }

  return FALSE;

}

 

Status NextElem(DuLinkList L,ElemType cur_e,ElemType*next_e)

{ /* 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继, */

  /*           否则操作失败,next_e无定义 */

  DuLinkListp=L->next->next; /* p指向第2个元素 */

  while(p!=L) /* p没到表头 */

  {

   if(p->prior->data==cur_e)

    {

     *next_e=p->data;

      return TRUE;

    }

    p=p->next;

  }

  return FALSE;

}

 

DuLinkList GetElemP(DuLinkList L,int i) /* 另加 */

{ /* 在双向链表L中返回第i个元素的地址。i为0,返回头结点的地址。若第i个元素不存在,*/

  /* 返回NULL */

  int j;

  DuLinkList p=L;/* p指向头结点 */

 if(i<0||i>ListLength(L)) /* i值不合法 */

    return NULL;

 for(j=1;j<=i;j++)

    p=p->next;

  return p;

}

 

Status ListInsert(DuLinkList L,int i,ElemType e)

{ /* 在带头结点的双链循环线性表L中第i个位置之前插入元素e,i的合法值为1≤i≤表长+1 */

  /* 改进算法2.18,否则无法在第表长+1个结点之前插入元素 */

  DuLinkList p,s;

 if(i<1||i>ListLength(L)+1) /* i值不合法 */

    return ERROR;

 p=GetElemP(L,i-1); /* 在L中确定第i个元素前驱的位置指针p */

  if(!p) /* p=NULL,即第i个元素的前驱不存在(设头结点为第1个元素的前驱) */

    return ERROR;

 s=(DuLinkList)malloc(sizeof(DuLNode));

  if(!s)

    returnOVERFLOW;

  s->data=e;

  s->prior=p; /*在第i-1个元素之后插入*/

 s->next=p->next;

 p->next->prior=s;

  p->next=s;

  return OK;

}

 

Status ListDelete(DuLinkList L,int i,ElemType *e)

{ /* 删除带头结点的双链循环线性表L的第i个元素,i的合法值为1≤i≤表长 */

  DuLinkList p;

  if(i<1) /* i值不合法 */

    return ERROR;

 p=GetElemP(L,i);  /* 在L中确定第i个元素的位置指针p */

  if(!p) /* p=NULL,即第i个元素不存在 */

    return ERROR;

  *e=p->data;

 p->prior->next=p->next;

 p->next->prior=p->prior;

  free(p);

  return OK;

}

 

void ListTraverse(DuLinkList L,void(*visit)(ElemType))

{ /* 由双链循环线性表L的头结点出发,正序对每个数据元素调用函数visit() */

  DuLinkListp=L->next; /* p指向头结点 */

  while(p!=L)

  {

    visit(p->data);

    p=p->next;

  }

 printf("\n");

}

 

void ListTraverseBack(DuLinkListL,void(*visit)(ElemType))

{ /* 由双链循环线性表L的头结点出发,逆序对每个数据元素调用函数visit()。另加 */

  DuLinkListp=L->prior; /* p指向尾结点 */

  while(p!=L)

  {

   visit(p->data);

    p=p->prior;

  }

 printf("\n");

}

 

 

 

 

函数指针的引用

1.直接引用       int (*fun)(int,int) fun

2.通过typedef引用   typedef int   (*show) (int,int)         show s

3.typedef int FUN(int,int)  FUN f

 

 

 

 

 

把后继节点的前驱指向自己 
把前驱节点的后继指向自己
这就连接好了双向箭头。
你画个图就清楚了
另外不是方便便历,是方便插入和删除某个项。

 

 

C语言中常见得错误汉语解释

Ambiguous operators need parentheses

不明确的运算需要用括号括起

Ambiguous symbol 'xxx'

不明确的符号

Argument list syntax error

参数表语法错误

Array bounds missing

丢失数组界限符

Array size toolarge

数组尺寸太大

Bad character in paramenters

参数中有不适当的字符

Bad file name format in include directive

包含命令中文件名格式不正确

Bad ifdef directive synatax

编译预处理ifdef有语法错

Bad undef directive syntax

编译预处理undef有语法错

Bit field too large

位字段太长

Call of non-function

调用未定义的函数

Call to function with no prototype

调用函数时没有函数的说明

Cannot modify a const object

不允许修改常量对象

Case outside of switch

漏掉了case 语句

Case syntax error

Case 语法错误

Code has no effect

代码不可述不可能执行到

Compound statement missing{

分程序漏掉"{"

Conflicting type modifiers

不明确的类型说明符

Constant expression required

要求常量表达式

Constant out of range in comparison

在比较中常量超出范围

Conversion may lose significant digits

转换时会丢失意义的数字

Conversion of near pointer not allowed

不允许转换近指针

Could not find file 'xxx'

找不到XXX文件

Declaration missing ;

说明缺少";"

Declaration syntax error

说明中出现语法错误

Default outside of switch

Default 出现在switch语句之外

Define directive needs an identifier

定义编译预处理需要标识符

Division by zero

用零作除数

Do statement must have while

Do-while语句中缺少while部分

Enum syntax error

枚举类型语法错误

Enumeration constant syntax error

枚举常数语法错误

Error directive :xxx

错误的编译预处理命令

Error writing output file

写输出文件错误

Expression syntax error

表达式语法错误

Extra parameter in call

调用时出现多余错误

File name too long

文件名太长

Function call missing )

函数调用缺少右括号

Fuction definition out of place

函数定义位置错误

Fuction should return a value

函数必需返回一个值

Goto statement missing label

Goto语句没有标号

Hexadecimal or octal constant too large

16进制或8进制常数太大

Illegal character 'x'

非法字符x

Illegal initialization

非法的初始化

Illegal octal digit

非法的8进制数字

Illegal pointer subtraction

非法的指针相减

Illegal structure operation

非法的结构体操作

Illegal use of floating point

非法的浮点运算

Illegal use of pointer

指针使用非法

Improper use of a typedefsymbol

类型定义符号使用不恰当

In-line assembly not allowed

不允许使用行间汇编

Incompatible storage class

存储类别不相容

Incompatible type conversion

不相容的类型转换

Incorrect number format

错误的数据格式

Incorrect use of default

Default使用不当

Invalid indirection

无效的间接运算

Invalid pointer addition

指针相加无效

Irreducible expression tree

无法执行的表达式运算

Lvalue required

需要逻辑值0或非0值

Macro argument syntax error

宏参数语法错误

Macro expansion too long

宏的扩展以后太长

Mismatched number of parameters indefinition

定义中参数个数不匹配

Misplaced break

此处不应出现break语句

Misplaced continue

此处不应出现continue语句

Misplaced decimal point

此处不应出现小数点

Misplaced elif directive

不应编译预处理elif

Misplaced else

此处不应出现else

Misplaced else directive

此处不应出现编译预处理else

Misplaced endif directive

此处不应出现编译预处理endif

Must be addressable

必须是可以编址的

Must take address of memory location

必须存储定位的地址

No declaration for function 'xxx'

没有函数xxx的说明

No stack

缺少堆栈

No type information

没有类型信息

Non-portable pointer assignment

不可移动的指针(地址常数)赋值

Non-portable pointer comparison

不可移动的指针(地址常数)比较

Non-portable pointer conversion

不可移动的指针(地址常数)转换

Not a valid expression format type

不合法的表达式格式

Not an allowed type

不允许使用的类型

Numeric constant too large

数值常太大

Out of memory

内存不够用

Parameter 'xxx' is never used

能数xxx没有用到

Pointer required on left side of ->

符号->的左边必须是指针

Possible use of 'xxx' before definition

在定义之前就使用了xxx(警告)

Possibly incorrect assignment

赋值可能不正确

Redeclaration of 'xxx'

重复定义了xxx

Redefinition of 'xxx' is not identical

xxx的两次定义不一致

Register allocation failure

寄存器定址失败

Repeat count needs an lvalue

重复计数需要逻辑值

Size of structure or array not known

结构体或数给大小不确定

Statement missing ;

语句后缺少";"

Structure or union syntax error

结构体或联合体语法错误

Structure size too large

结构体尺寸太大

Sub scripting missing ]

下标缺少右方括号

Superfluous with function or array

函数或数组中有多余的""

Suspicious pointer conversion

可疑的指针转换

Symbol limit exceeded

符号超限

Too few parameters in call

函数调用时的实参少于函数的参数不

Too many default cases

Default太多(switch语句中一个)

Too many error or warning messages

错误或警告信息太多

Too many type in declaration

说明中类型太多

Too much auto memory in function

函数用到的局部存储太多

Too much global data defined in file

文件中全局数据太多

Two consecutive dots

两个连续的句点

Type mismatch in parameter xxx

参数xxx类型不匹配

Type mismatch in redeclaration of 'xxx'

xxx重定义的类型不匹配

Unable to create output file 'xxx'

无法建立输出文件xxx

Unable to open include file 'xxx'

无法打开被包含的文件xxx

Unable to open input file 'xxx'

无法打开输入文件xxx

Undefined label 'xxx'

没有定义的标号xxx

Undefined structure 'xxx'

没有定义的结构xxx

Undefined symbol 'xxx'

没有定义的符号xxx

Unexpected end of file in comment startedon line xxx

从xxx行开始的注解尚未结束文件不能结束

Unexpected end of file in conditionalstarted on line xxx

从xxx 开始的条件语句尚未结束文件不能结束

Unknown assemble instruction

未知的汇编结构

Unknown option

未知的操作

Unknown preprocessor directive: 'xxx'

不认识的预处理命令xxx

Unreachable code

无路可达的代码

Unterminated string or character constant

字符串缺少引号

User break

用户强行中断了程序

Void functions may not return a value

Void类型的函数不应有返回值

Wrong number of arguments

调用函数的参数数目错

'xxx' not an argument

xxx不是参数

'xxx' not part of structure

xxx不是结构体的一部分

xxx statement missing (

xxx语句缺少左括号

xxx statement missing )

xxx语句缺少右括号

xxx statement missing ;

xxx缺少分号

xxx' declared but never used

说明了xxx但没有使用

xxx' is assigned a value which is neverused

给xxx赋了值但未用过

Zero length structure

结构体的长度为零

 

本文来源碧海潮声大学生网http://www.zjoubbs.com/thread-26029-1-1.html

 

 

 

首先: void* 这不叫空指针,这叫无确切类型指针.这个指针指向一块内存,却没有告诉程序该用何种方式来解释这片内存.所以这种类型的指针不能直接进行取内容的操作.必须先转成别的类型的指针才可以把内容解释出来.
    还有'\0',这也不是空指针所指的内容. '\0'是表示一个字符串的结尾而已,并不是NULL的意思.
    真正的空指针是说,这个指针没有指向一块有意义的内存,比如说: char* k; 这里这个k就叫空指针.我们并未让它指向任意地点. 又或者 char* k = NULL; 这里这个k也叫空指针,因为它指向NULL 也就是0,注意是整数0,不是'\0'
    一个空指针我们也无法对它进行取内容操作. 空指针只有在真正指向了一块有意义的内存后,我们才能对它取内容.也就是说要这样 k = "hello world!"; 这时k就不是空指针了.

转载自:http://www.cublog.cn/u/18517/showart_309917.html
你问我答:

1.               什么是空指针常量(null pointer constant)?

[6.3.2.3-3] An integer constant expression with the value 0, or such anexpression cast to type void *, is called a null pointer constant.

这里告诉我们:0、0L、'\0'、3 - 3、0 * 17 (它们都是“integerconstant expression”)以及 (void*)0 (tyc:我觉得(void*)0应该算是一个空指针吧,更恰当一点)等都是空指针常量(注意(char*) 0 不叫空指针常量,只是一个空指针值)。至于系统选取哪种形式作为空指针常量使用,则是实现相关的。一般的 C 系统选择 (void*)0 或者 0 的居多(也有个别的选择 0L);至于 C++ 系统,由于存在严格的类型转化的要求,void* 不能象 C 中那样自由转换为其它指针类型,所以通常选 0 作为空指针常量(tyc: C++标准推荐),而不选择 (void*)0。

2.               什么是空指针(null pointer)?

[6.3.2.3-3] If a null pointer constant is converted to a pointer type, theresulting pointer, called a null pointer, is guaranteed to compare unequal to apointer to any object or function.

因此,如果 p 是一个指针变量,则 p = 0;、p = 0L;、p = '\0';、p = 3 - 3;、p = 0 * 17; 中的任何一种赋值操作之后(对于C 来说还可以是 p = (void*)0;), p 都成为一个空指针,由系统保证空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。(tyc: 比如这里的(void*)0就是一个空指针。把它理解为null pointer还是null pointerconstant会有微秒的不同,当然也不是紧要了)

3.               什么是 NULL?

[6.3.2.3-Footnote] The macro NULL is defined in <stddef.h> (and otherheaders) as a null pointer constant

即NULL 是一个标准规定的宏定义,用来表示空指针常量。因此,除了上面的各种赋值方式之外,还可以用 p = NULL; 来使 p 成为一个空指针。(tyc:很多系统中的实现:#defineNULL (void*)0,与这里的“a null pointer constant”并不是完全一致的)

4.               空指针(null pointer)指向了内存的什么地方(空指针的内部实现)?

标准并没有对空指针指向内存中的什么 地方这一个问题作出规定,也就是说用哪个具体的地址值(0x0 地址还是某一特定地址)表示空指针取决于系统的实现。我们常见的空指针一般指向 0 地址,即空指针的内部用全 0 来表示(zero null pointer,零空指针);也有一些系统用一些特殊的地址值或者特殊的方式表示空指针(nonzero null pointer,非零空指针),具体请参见C FAQ

幸运的是,在实际编程中不需要了解在我们的系统上空指针到底是一个 zero null pointer 还是 nonzeronull pointer,我们只需要了解一个指针是否是空指针就可以了——编译器会自动实现其中的转换,为我们屏蔽其中的实现细节。注意:不要把空指针的内部表示等同于整数0 的对象表示——如上所述,有时它们是不同的。

5.               如何判断一个指针是否是一个空指针?

这可以通过与空指针常量或者其它的空指针的比较来实现(注意与空指针的内部表示无关)。例如,假设 p 是一个指针变量,q 是一个同类型的空指针,要检查 p 是否是一个空指针,可以采用下列任意形式之一——它们在实现的功能上都是等价的,所不同的只是风格的差别。

指针变量 p 是空指针的判断:
if ( p == 0 )
if ( p == '\0' )
if ( p == 3 - 3 )
if ( p == NULL )  
if ( NULL == p )
if ( !p )
if ( p == q )
...

指针变量 p 不是空指针的判断:
if ( p != 0 )
if ( p != '\0' )
if ( p != 3 - 3 )
if ( p != NULL )  
if ( NULL != p )
if ( p )
if ( p != q )
...

6.               可以用 memset 函数来得到一个空指针吗?

这个问题等同于:如果 p 是一个指针变量,那么

memset( &p, 0, sizeof(p) ); 和 p = 0;

是等价的吗?

答案是否定的,虽然在大多数系统上是等价的,但是因为有的系统存在着“非零空指针” (nonzero null pointer),所以这时两者不等价。由于这个原因,要注意当想将指针设置为空指针的时候不应该使用 memset,而应该用空指针常量或空指针对指针变量赋值或者初始化的方法。

7.               可以定义自己的 NULL 的实现吗?兼答"NULL 的值可以是 1、2、3 等值吗?"类似问题

[7.1.3-2] If the program declares or defines an identifier in a context inwhich it is reserved (other than as allowed by 7.1.4), or defines a reservedidentifier as a macro name, the behavior is undefined.

NULL 是标准库中的一个符合上述条件的 reserved identifier (保留标识符)。所以,如果包含了相应的标准头文件而引入了 NULL 的话,则再在程序中重新定义 NULL 为不同的内容是非法的,其行为是未定义的。也就是说,如果是符合标准的程序,其 NULL 的值只能是 0,不可能是除 0 之外的其它值,比如 1、2、3 等。

8.               malloc函数在分配内存失败时返回 0 还是 NULL?

malloc 函数是标准 C 规定的库函数。在标准中明确规定了在其内存分配失败时返回的是一个 “null pointer”(空指针):

[7.20.3-1] If the space cannot be allocated, a null pointer is returned.

对于空指针值,一般的文档(比如 man)中倾向于用 NULL 表示,而没有直接说成 0。但是我们应该清楚:对于指针类型来说,返回 NULL 和 返回 0 是完全等价的,因为 NULL 和 0 都表示 “null pointer”(空指针)。(tyc:一般系统中手册中都返回NULL,那我们就用NULL吧)


转载自:http://www.linuxsky.org/doc/dev/200712/193.html
void及void指针深层次探索:
1.概述

  本文将对void关键字的深刻含义进行解说,并详述void及void指针类型的使用方法与技巧。

  2.void的含义

  void的字面意思是“无类型”,void *则为“无类型指针”,void *可以指向任何类型的数据。

  void几乎只有“注释”和限制程序的作用,因为从来没有人会定义一个void变量,让我们试着来定义:

  void a;

  这行语句编译时会出错,提示“illegaluse of type 'void'”。不过,即使void a的编译不会出错,它也没有任何实际意义。

  void真正发挥的作用在于:

  (1) 对函数返回的限定;

  (2) 对函数参数的限定。

  我们将在第三节对以上二点进行具体说明。

  众所周知,如果指针p1和p2的类型相同,那么我们可以直接在p1和p2间互相赋值;如果p1和p2指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。

  例如:

  float *p1;

  int *p2;

  p1 = p2;

  其中p1 = p2语句会编译出错,提示“'=': cannot convert from 'int *' to 'float *'”,必须改为:

  p1 = (float*)p2;

  而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:

  void *p1;

  int *p2;

  p1 = p2;

  但这并不意味着,void*也可以无需强制类型转换地赋给其它类型的指针。因为“无类型”可以包容“有类型”,而“有类型”则不能包容“无类型”。道理很简单,我们可以说“男人和女人都是人”,但不能说“人是男人”或者“人是女人”。下面的语句编译出错:

  void *p1;

  int *p2;

  p2 = p1;

  提示“'=' :cannot convert from 'void *' to 'int *'”。

  3.void的使用

  下面给出void关键字的使用规则:

  规则一 如果函数没有返回值,那么应声明为void类型

  在C语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为void类型。例如:

  add ( int a,int b )

  {

  return a +b;

  }

  int main(intargc, char* argv[])

  {

  printf ("2 + 3 = %d", add ( 2, 3) );

  }

  程序运行的结果为输出:

  2 + 3 = 5

  这说明不加返回值说明的函数的确为int函数。

  林锐博士《高质量C/C++编程》中提到:“C++语言有很严格的类型安全检查,不允许上述情况(指函数不加类型声明)发生”。可是编译器并不一定这么认定,譬如在Visual C++6.0中上述add函数的编译无错也无警告且运行正确,所以不能寄希望于编译器会做严格的类型检查。

  因此,为了避免混乱,我们在编写C/C++程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没有返回值,一定要声明为void类型。这既是程序良好可读性的需要,也是编程规范性的要求。另外,加上void类型声明后,也可以发挥代码的“自注释” 作用。代码的“自注释”即代码能自己注释自己。

  规则二 如果函数无参数,那么应声明其参数为void

  在C++语言中声明一个这样的函数:

  intfunction(void)

  {

  return 1;

  }

  则进行下面的调用是不合法的:

  function(2);

  因为在C++中,函数参数为void的意思是这个函数不接受任何参数。

  我们在Turbo C 2.0中编译:

  #i nclude"stdio.h"

  fun()

  {

  return 1;

  }

  main()

  {

  printf("%d",fun(2));

  getchar();

  }

  编译正确且输出1,这说明,在C语言中,可以给无参数的函数传送任意类型的参数,但是在C++编译器中编译同样的代码则会出错。在C++中,不能向无参数的函数传送任何参数,出错提示“'fun' : function does not take 1parameters”。

  所以,无论在C还是C++中,若函数不接受任何参数,一定要指明参数为void。

  规则三 小心使用void指针类型

  按照ANSI(AmericanNational Standards Institute)标准,不能对void指针进行算法操作,即下列操作都是不合法的:

  void *pvoid;

  pvoid++;//ANSI:错误

  pvoid += 1;//ANSI:错误

  //ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。

  //例如:

  int *pint;

  pint++;//ANSI:正确

  pint++的结果是使其增大sizeof(int)。

  但是大名鼎鼎的GNU(GNU'sNot Unix的缩写)则不这么认定,它指定void *的算法操作与char *一致。

  因此下列语句在GNU编译器中皆正确:

  pvoid++;//GNU:正确

  pvoid += 1;//GNU:正确

  pvoid++的执行结果是其增大了1。

  在实际的程序设计中,为迎合ANSI标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:

  void *pvoid;

  (char*)pvoid++; //ANSI:正确;GNU:正确

  (char*)pvoid += 1; //ANSI:错误;GNU:正确

  GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法的支持。但是我们在真实设计时,还是应该尽可能地迎合ANSI标准。

  规则四 如果函数的参数可以是任意类型指针,那么应声明其参数为void *

  典型的如内存操作函数memcpy和memset的函数原型分别为:

  void *memcpy(void *dest, const void *src, size_t len);

  void *memset ( void * buffer, int c, size_t num );

  这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy和memset的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy和memset明显不是一个“纯粹的,脱离低级趣味的”函数!

  下面的代码执行正确:

  //示例:memset接受任意类型指针

  intintarray[100];

  memset (intarray, 0, 100*sizeof(int) ); //将intarray清0

  //示例:memcpy接受任意类型指针

  intintarray1[100], intarray2[100];

  memcpy (intarray1, intarray2, 100*sizeof(int) ); //将intarray2拷贝给intarray1

  有趣的是,memcpy和memset函数返回的也是void*类型,标准库函数的编写者是多么地富有学问啊!

  规则五 void不能代表一个真实的变量

  下面代码都企图让void代表一个真实的变量,因此都是错误的代码:

  void a; //错误

  function(voida); //错误

  void体现了一种抽象,这个世界上的变量都是“有类型”的,譬如一个人不是男人就是女人.

  void的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中“抽象基类”的概念,也很容易理解void数据类型。正如不能给抽象基类定义一个实例,我们也不能定义一个void(让我们类比的称void为“抽象数据类型”)变量。


 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值