C语言入门之结构(3)

3/8/2008来源:C/C++教程人气:2989

   结构指针变量作函数参数

   在ANSI C标准中答应用结构变量作函数参数进行整体传送。 但是这种传送要将全部成员逐个传送, 非凡是成员为数组时将会使传送的时间和空间开销很大,严重地降低了程序的效率。 因此最好的办法就是使用指针,即用指针变量作函数参数进行传送。 这时由实参传向形参的只是地址,从而减少了时间和空间的开销。

   [例7.8]题目与例7.4相同,计算一组学生的平均成绩和不及格人数。

   用结构指针变量作函数参数编程。

strUCt stu
{
  int num;
  char *name;
  char sex;
  float score;}boy[5]={
   {101,"Li ping",'M',45},
   {102,"Zhang ping",'M',62.5},
   {103,"He fang",'F',92.5},
   {104,"Cheng ling",'F',87},
   {105,"Wang ming",'M',58},
  };
main()
{
  struct stu *ps;
  void ave(struct stu *ps);
  ps=boy;
  ave(ps);
}
void ave(struct stu *ps)
{
  int c=0,i;
  float ave,s=0;
  for(i=0;i<5;i++,ps++)
  {
   s+=ps->score;
   if(ps->score<60) c+=1;
  }
  PRintf("s=%f\n",s);
  ave=s/5;
  printf("average=%f\ncount=%d\n",ave,c);
}

   本程序中定义了函数ave,其形参为结构指针变量ps。boy 被定义为外部结构数组,因此在整个源程序中有效。在main 函数中定义说明了结构指针变量ps,并把boy的首地址赋予它,使ps指向boy 数组。然后以ps作实参调用函数ave。在函数ave 中完成计算平均成绩和统计不及格人数的工作并输出结果。与例7.4程序相比,由于本程序全部采用指针变量作运算和处理,故速度更快,程序效率更高。.

   topoic=动态存储分配

   在数组一章中,曾介绍过数组的长度是预先定义好的, 在整个程序中固定不变。C语言中不答应动态数组类型。例如: int n;scanf("%d",&n);int a[n]; 用变量表示长度,想对数组的大小作动态说明, 这是错误的。但是在实际的编程中,往往会发生这种情况, 即所需的内存空间取决于实际输入的数据,而无法预先确定。对于这种问题, 用数组的办法很难解决。为了解决上述问题,C语言提供了一些内存治理函数,这些内存治理函数可以按需要动态地分配内存空间, 也可把不再使用的空间回收待用,为有效地利用内存资源提供了手段。 常用的内存治理函数有以下三个:

   1.分配内存空间函数malloc

   调用形式: (类型说明符*) malloc (size) 功能:在内存的动态存储区中分配一块长度为"size" 字节的连续区域。函数的返回值为该区域的首地址。 “类型说明符”表示把该区域用于何种数据类型。(类型说明符*)表示把返回值强制转换为该类型指针。“size”是一个无符号数。例如: pc=(char *) malloc (100); 表示分配100个字节的内存空间,并强制转换为字符数组类型, 函数的返回值为指向该字符数组的指针, 把该指针赋予指针变量pc。

   2.分配内存空间函数 calloc

   calloc 也用于分配内存空间。调用形式: (类型说明符*)calloc(n,size) 功能:在内存动态存储区中分配n块长度为“size”字节的连续区域。函数的返回值为该区域的首地址。(类型说明符*)用于强制类型转换。calloc函数与malloc 函数的区别仅在于一次可以分配n块区域。例如: ps=(struet stu*) calloc(2,sizeof (struct stu)); 其中的sizeof(struct stu)是求stu的结构长度。因此该语句的意思是:按stu的长度分配2块连续区域,强制转换为stu类型,并把其首地址赋予指针变量ps。

   3.释放内存空间函数free

   调用形式: free(void*ptr); 功能:释放ptr所指向的一块内存空间,ptr 是一个任意类型的指针变量,它指向被释放区域的首地址。被释放区应是由malloc或calloc函数所分配的区域:[例7.9]分配一块区域,输入一个学生数据。

main()
{
  struct stu
  {
   int num;
   char *name;
   char sex;
   float score;
  } *ps;
  ps=(struct stu*)malloc(sizeof(struct stu));
  ps->num=102;
  ps->name="Zhang ping";
  ps->sex='M';
  ps->score=62.5;
  printf("Number=%d\nName=%s\n",ps->num,ps->name);
  printf("Sex=%c\nScore=%f\n",ps->sex,ps->score);
  free(ps);
}

   本例中,定义了结构stu,定义了stu类型指针变量ps。 然后分配一块stu大内存区,并把首地址赋予ps,使ps指向该区域。再以ps为指向结构的指针变量对各成员赋值,并用printf 输出各成员值。最后用free函数释放ps指向的内存空间。 整个程序包含了申请内存空间、使用内存空间、释放内存空间三个步骤, 实现存储空间的动态分配。链表的概念在例7.9中采用了动态分配的办法为一个结构分配内存空间。每一次分配一块空间可用来存放一个学生的数据, 我们可称之为一个结点。有多少个学生就应该申请分配多少块内存空间, 也就是说要建立多少个结点。当然用结构数组也可以完成上述工作, 但假如预先不能准确把握学生人数,也就无法确定数组大小。 而且当学生留级、退学之后也不能把该元素占用的空间从数组中释放出来。 用动态存储的方法可以很好地解决这些问题。 有一个学生就分配一个结点,无须预先确定学生的准确人数,某学生退学, 可删去该结点,并释放该结点占用的存储空间。从而节约了宝贵的内存资源。 另一方面,用数组的方法必须占用一块连续的内存区域。 而使用动态分配时,每个结点之间可以是不连续的(结点内是连续的)。 结点之间的联系可以用指针实现。 即在结点结构中定义一个成员项用来存放下一结点的首地址,这个用于存放地址的成员,常把它称为指针域。可在第一个结点的指针域内存入第二个结点的首地址, 在第二个结点的指针域内又存放第三个结点的首地址, 如此串连下去直到最后一个结点。最后一个结点因无后续结点连接,其指针域可赋为0。这样一种连接方式,在数据结构中称为“链表”。

   在链表中,第0个结点称为头结点, 它存放有第一个结点的首地址,它没有数据,只是一个指针变量。 以下的每个结点都分为两个域,一个是数据域,存放各种实际的数据,如学号num,姓名name,性别sex和成绩score等。另一个域为指针域, 存放下一结点的首地址。链表中的每一个结点都是同一种结构类型。例如, 一个存放学生学号和成绩的结点应为以下结构:

struct stu
{
  int num;
  int score;
  struct stu *next;
}


   前两个成员项组成数据域,后一个成员项next构成指针域, 它是一个指向stu类型结构的指针变量。链表的基本操作对链表的主要操作有以下几种:

   1.建立链表;

   2.结构的查找与输出;

   3.插入一个结点;

   4.删除一个结点;

   下面通过例题来说明这些操作。

   [例7.10]建立一个三个结点的链表,存放学生数据。 为简单起见, 我们假定学生数据结构中只有学号和年龄两项。

   可编写一个建立链表的函数creat。程序如下:

#define NULL 0
#define TYPE struct stu
#define LEN sizeof (struct stu)
struct stu
{
  int num;
  int age;
  struct stu *next;
};
TYPE *creat(int n)
{
  struct stu *head,*pf,*pb;
  int i;
  for(i=0;i<n;i++)
  {
   pb=(TYPE*) malloc(LEN);
   printf("input Number and Age\n");
   scanf("%d%d",&pb->num,&pb->age);
   if(i==0)
    pf=head=pb;
   else pf->next=pb;
   pb->next=NULL;
   pf=pb;
  }
  return(head);
}

   在函数外首先用宏定义对三个符号常量作了定义。这里用TYPE表示struct stu,用LEN表示sizeof(struct stu)主要的目的是为了在以下程序内减少书写并使阅读更加方便。结构stu定义为外部类型,程序中的各个函数均可使用该定义。

   creat函数用于建立一个有n个结点的链表,它是一个指针函数,它返回的指针指向stu结构。在creat函数内定义了三个stu结构的指针变量。head为头指针,pf 为指向两相邻结点的前一结点的指针变量。pb为后一结点的指针变量。在for语句内,用malloc函数建立长度与stu长度相等的空间作为一结点,首地址赋予pb。然后输入结点数据。假如当前结点为第一结点(i==0),则把pb值 (该结点指针)赋予head和pf。如非第一结点,则把pb值赋予pf 所指结点的指针域成员next。而pb所指结点为当前的最后结点,其指针域赋NULL。 再把pb值赋予pf以作下一次循环预备。

   creat函数的形参n,表示所建链表的结点数,作为for语句的循环次数。图7.4表示了creat函数的执行过程。

   [例7.11]写一个函数,在链表中按学号查找该结点。

TYPE * search (TYPE *head,int n)
{
  TYPE *p;
  int i;
  p=head;
  while (p->num!=n && p->next!=NULL)
   p=p->next; /* 不是要找的结点后移一步*/
   if (p->num==n) return (p);
   if (p->num!=n&& p->next==NULL)
   printf ("Node %d has not been found!\n",n
}

   本函数中使用的符号常量TYPE与例7.10的宏定义相同,等于struct stu。函数有两个形参,head是指向链表的指针变量,n为要查找的学号。进入while语句,逐个检查结点的num成员是否等于n,假如不等于n且指针域不等于NULL(不是最后结点)则后移一个结点,继续循环。如找到该结点则返回结点指针。 如循环结束仍未找到该结点则输出“未找到”的提示信息。

   [例7.12]写一个函数,删除链表中的指定结点。删除一个结点有两种情况:

   1. 被删除结点是第一个结点。这种情况只需使head指向第二个结点即可。即head=pb->next。其过程如图7.5所示。

   2. 被删结点不是第一个结点,这种情况使被删结点的前一结点指向被删结点的后一结点即可。即pf->next=pb->next。

   函数编程如下:

TYPE * delete(TYPE * head,int num)
{
  TYPE *pf,*pb;
  if(head==NULL) /*如为空表, 输出提示信息*/
  {
   printf("\nempty list!\n");
   goto end;
  }
  pb=head;
  while (pb->num!=num && pb->next!=NULL)
   /*当不是要删除的结点,而且也不是最后一个结点时,继续循环*/
  {
   pf=pb;pb=pb->next;}/*pf指向当前结点,pb指向下一结点*/
   if(pb->num==num)
   {
    if(pb==head) head=pb->next;
     /*如找到被删结点,且为第一结点,则使head指向第二个结点,
      否则使pf所指结点的指针指向下一结点*/
    else pf->next=pb->next;
    free(pb);
    printf("The node is deleted\n");}
   else
    printf("The node not been foud!\n");
    end:
   return head;
  }

   函数有两个形参,head为指向链表第一结点的指针变量,num删结点的学号。 首先判定链表是否为空,为空则不可能有被删结点。若不为空,则使pb指针指向链表的第一个结点。进入while语句后逐个查找被删结点。找到被删结点之后再看是否为第一结点,若是则使head指向第二结点(即把第一结点从链中删去),否则使被删结点的前一结点(pf所指)指向被删结点的后一结点(被删结点的指针域所指)。如若循环结束未找到要删的结点, 则输出“末找到”的提示信息。最后返回head值。

   [例7.13]写一个函数,在链表中指定位置插入一个结点。在一个链表的指定位置插入结点, 要求链表本身必须是已按某种规律排好序的。例如,在学生数据链表中, 要求学号顺序插入一个结点。设被插结点的指针为pi。 可在三种不同情况下插入。

   1. 原表是空表,只需使head指向被插结点即可。

   2. 被插结点值最小,应插入第一结点之前。这种情况下使head指向被插结点,被插结点的指针域指向原来的第一结点则可。即:

pi->next=pb;
head=pi;

   3. 在其它位置插入。这种情况下,使插入位置的前一结点的指针域指向被插结点,使被插结点的指针域指向插入位置的后一结点。即为:pi->next=pb;pf->next=pi;

   4. 在表末插入。这种情况下使原表末结点指针域指向被插结点,被插结点指针域置为NULL。即:

pb->next=pi;
pi->next=NULL; TYPE * insert(TYPE * head,TYPE *pi)
{
  TYPE *pf,*pb;
  pb=head;
  if(head==NULL) /*空表插入*/
   (head=pi;
   pi->next=NULL;}
  else
  {
   while((pi->num>pb->num)&&(pb->next!=NULL))
   {
    pf=pb;
    pb=pb->next;
   }/*找插入位置*/
   if(pi->num<=pb->num)
   {
    if(head==pb)head=pi;/*在第一结点之前插入*/
    else pf->next=pi;/*在其它位置插入*/
    pi->next=pb; }
   else
   {
    pb->next=pi;
    pi->next=NULL;
   } /*在表末插入*/
  }
  return head;
}


   本函数有两个形参均为指针变量,head指向链表,pi 指向被插结点。函数中首先判定链表是否为空,为空则使head指向被插结点。表若不空,则用while语句循环查找插入位置。找到之后再判定是否在第一结点之前插入,若是则使head 指向被插结点被插结点指针域指向原第一结点,否则在其它位置插入, 若插入的结点大于表中所有结点,则在表末插入。本函数返回一个指针, 是链表的头指针。 当插入的位置在第一个结点之前时, 插入的新结点成为链表的第一个结点,因此head的值也有了改变, 故需要把这个指针返回主调函数。


   [例7.14]将以上建立链表,删除结点,插入结点的函数组织在一起,再建一个输出全部结点的函数,然后用main函数调用它们。

#define NULL 0
#define TYPE struct stu
#define LEN sizeof(struct stu)
struct stu
{
  int num;
  int age;
  struct stu *next;
};
TYPE * creat(int n)
{
  struct stu *head,*pf,*pb;
  int i;
  for(i=0;i<n;i++)
  {
   pb=(TYPE *)malloc(LEN);
   printf("input Number and Age\n");
   scanf("%d%d",&pb->num,&pb->age);
   if(i==0)
    pf=head=pb;
   else pf->next=pb;
   pb->next=NULL;
   pf=pb;
  }
  return(head);
}
TYPE * delete(TYPE * head,int num)
{
  TYPE *pf,*pb;
  if(head==NULL)
  {
   printf("\nempty list!\n");
   goto end;
  }
  pb=head;
  while (pb->num!=num && pb->next!=NULL)
  {
   pf=pb;pb=pb->next;
  }
  if(pb->num==num)
  {
   if(pb==head) head=pb->next;
   else pf->next=pb->next;
   printf("The node is deleted\n");
  }
  else
   free(pb);
   printf("The node not been found!\n");
  end:
  return head;
}
TYPE * insert(TYPE * head,TYPE * pi)
{
  TYPE *pb ,*pf;
  pb=head;
  if(head==NULL)
  {
   head=pi;
   pi->next=NULL;
  }
  else
  {
   while((pi->num>pb->num)&&(pb->next!=NULL))
   {
    pf=pb;
    pb=pb->next;
   }
   if(pi->num<=pb->num)
   {
    if(head==pb) head=pi;
    else pf->next=pi;
    pi->next=pb;
   }
   else
   {
    pb->next=pi;
    pi->next=NULL;
   }
  }
  return head;
}
void print(TYPE * head)
{
  printf("Number\t\tAge\n");
  while(head!=NULL)
  {
   printf("%d\t\t%d\n",head->num,head->age);
   head=head->next;
  }
}
main()
{
  TYPE * head,*pnum;
  int n,num;
  printf("input number of node: ");
  scanf("%d",&n);
  head=creat(n);
  print(head);
  printf("Input the deleted number: ");
  scanf("%d",&num);
  head=delete(head,num);
  print(head);
  printf("Input the inserted number and age: ");
  pnum=(TYPE *)malloc(LEN);
  scanf("%d%d",&pnum->num,&pnum->age);
  head=insert(head,pnum);
  print(head);
}

   本例中,print函数用于输出链表中各个结点数据域值。函数的形参head的初值指向链表第一个结点。在while语句中,输出结点值后,head值被改变,指向下一结点。若保留头指针head, 则应另设一个指针变量,把head值赋予它,再用它来替代head。在main函数中,n为建立结点的数目, num为待删结点的数据域值;head为指向链表的头指针,pnum为指向待插结点的指针。 main函数中各行的意义是:

   第六行输入所建链表的结点数;

   第七行调creat函数建立链表并把头指针返回给head;

   第八行调print函数输出链表;

   第十行输入待删结点的学号;

   第十一行调delete函数删除一个结点;

   第十二行调print函数输出链表;

   第十四行调malloc函数分配一个结点的内存空间, 并把其地址赋予pnum;

   第十五行输入待插入结点的数据域值;

   第十六行调insert函数插入pnum所指的结点;

   第十七行再次调print函数输出链表。

   从运行结果看,首先建立起3个结点的链表,并输出其值;再删103号结点,只剩下105,108号结点;又输入106号结点数据, 插入后链表中的结点为105,106,108。联合“联合”也是一种构造类型的数据结构。 在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,答应装入该“联合”所定义的任何一种数据。 这在前面的各种数据类型中都是办不到的。例如, 定义为整型的变量只能装入整型数据,定义为实型的变量只能赋予实型数据。