现在讨论用什么样的存储方式处理多个字符串比较合适假定有多个字符串,按照数组与指针的关系组合,可以有如下几种定义方式:char str[M][N];char *str[N];char **str;41在这3种声明中,每一种声明都使用了两个类型声明符(char、[ ]和*)在这种情况下,如何确定所声明的变量的含义呢?一般说来,这些类型声明符实际上是运算符在声明中的应用它们虽然在这里不是作为运算符使用,但在优先级和结合性上还是要按照运算符的规则与名字相结合因此可以采用下面的方法来理解: 首先按照优先级看哪个声明符应当与名字相结合在优先级相同的情况下按结合性看哪个声明符与名字结合 剩下的声明符是补充说明下面结合图来对上述3种表示方式的意义进行说明假定图中需要存储的字符串有下列5个:“C”42“C++”“Visual BASIC”“Java”“Ada”(1)char str[M][N];① 在这个声明中,有两个相同的数组类型说明符[],按照由左向右的结合性,可以首先确定str是一个大小为M的向量(一维数组)② 对于数组自然要说明类型由剩下的char和[N]补充说明,这个数组是长度为N的字符数组类型,即它的每个元素都是长度为N的字符数组。
所以,这个语句定义的str是字符数组类型的数组,或者说str是二维字符数组如果在定义数组时进行以下的初始化:char str[5][13]={ “C”,“C++”,“Visual BASIC”,“Java”,“Ada”};则数组中的存储情况如图所示用这种方式存储的几个字符串占有连续的存储空间43(2)char *str[N];① 在这个声明中,有两个不相同的数组类型声明符[]和*其中数组类型声明符的优先级别高,可首先确定str是一个大小为N的一维数组② 剩下的char和*补充声明:这个数组的每个元素都是字符类型指针所以,这个语句定义的str是字符指针数组其存储方式如图所示用这种方式存储的几个字符串长度可以不同,不一定占有连续的存储空间3)char **str;① 在这个声明中,有两个相同的类型声明符*按照由右向左的结合性,可以首先将后面的一个*与名字结合,得出结论:str是一个指针② 对于指针就要声明指向什么由剩下的char和*补充声明:这个指针是指向字符指针的所以,这个语句定义的str是指向字符指针的指针,即指向字符的二级指针44char str[M] [N]① []的结合为“左向右”故str是一个大小为M的数组②str的元素是字符数组ulBS\0AsiVI C\0 \0 \0 \0\0\0\0\0\0C\0 \0\0 \0 \0 \0\0\0\0++C\0 \0a \0 \0 \0\0\0\0vaJ\0 \0\0 \0 \0 \0\0\0\0adA\0 \0str[0]二维字符数组strstr[1]str[2]str[3]str[4]占有连续存储空间u lBS\0AsiVI C\0C\0++Ca \0vaJ\0adA字符指针数组str不一定占有连续存储空间char * str [N]① []优先,故str是一个大小为N的数组②str的元素是字符指针str[0]str[1]str[2]str[3]str[4]45u lBS\0AsiVI C\0C\0++Ca \0vaJ\0adA二级字符指针str不一定占有连续存储空间char * * str ① str是一个指针②str指向字符指针*str46#include #define M 5#define N 13int main(void){char *s1="abc",*s2="wxyz",*s3="ijklmn";char str1[M][N]={"C","C++","Visual BASIC","Java","Ada"};char *str2[M]={s2,str1[2],s1,"ijklm"};char **str3=&s2;int i;printf("\n ");printf("&s1=%ld,&s2=%ld,&s3=%ld\n",&s1,&s2,&s3);printf("\n ");47printf("&s1=%ld,&s2=%ld\n",&s1,&s2);printf("\n ");for(i=0;i<5;i++) printf("&str1[%d]=%ld:str1[%d]=%s\n ",i,&str1[i],i,str1[i]);printf("\n ");for(i=0;i<5;i++)printf("&str2[%d]=%ld:str2[%d]=%s\n ",i,&str2[i],i,str2[i]); printf("\n "); for(i=0;i<5;i++)printf("str3+%d=%ld:*(str3+%d)=%s\n ",i,str3+i,i,*(str3+i));printf("\n ");return 0;}48u lBS\0AsiVI C\0 \0 \0 \0\0\0\0\0\0C\0 \0\0 \0 \0 \0\0\0\0++C\0 \0a \0 \0 \0\0\0\0vaJ\0 \0\0 \0 \0 \0\0\0\0adA\0 \0\0 \0 \0 \0\0\0\0adA\0 \0临时字符数组①②③ulBS\0AsiVI C\0C\0++Ca \0vaJ\0adAstr[0]字符指针数组strstr[1]str[2]str[3]str[4]str[4]临时字符指针变量①②③49#include #include int main ( ){ char *s0="Java",*s1="Visual BASIC",*s2="C";char *string[3]={s0,s1,s2};char *p;int i;printf ("排序前:\n");for(i=0;i<3;i++) printf("&string[%d]=%ld->%s\n",i,&string[i],string[i]);printf ("&s0=%ld->%s\n",&s0,s0); ("&s1=%ld->%s\n",&s1,s1);printf ("&s2=%ld->%s\n",&s2,s2);printf("\n");50if (strcmp (string[0],string[1])>0){p=string[0];string[0]=string[1];string[1]=p;}if (strcmp (string[0],string[2])>0){p=string[0];string[0]=string[2];string[2]=p;}if (strcmp (string[1],string[2])>0){p=string[1];string[1]=string[2];string[2]=p;}printf ("排序后:\n");for(i=0;i<3;i++)printf("&string[%d]=%ld->%s\n",i,&string[i],string[i]);printf ("&s0=%ld->%s\n",&s0,s0);printf ("&s1=%ld->%s\n",&s1,s1);printf ("&s2=%ld->%s\n",&s2,s2);printf("\n");return 0;}51 (2)本程序实际上说明了冒泡排序的过程,并用strcmp(string[I],string[j])进行两个字符串的比较。
有了本例的基础,读者可以编写出顺序输出n个字符串的程序,也可以采用循环结构实现字符串的排序算法,例如用选择法,起泡法等 上例中,指针数组string的每一个元素都是一个地址(指向一个字符串),如果另外设一个指针变量p,用来指向string数组中的元素,那么这个p就是一个二级指针(又称“双重指针”)52采用二级指针的字符串冒泡排序程序include #include #define N 3int main ( ){ char *string[N]={ "Java","Visual BASIC","C"};char **p=&string[0];char *ptemp;int i,j;printf ("排序前:\n");for(i=0;i%s\n",i,*(p+i));printf("\n");for(j=0;j<=N-2;j++) for(i=0;i<=N-j-1;i++)if(strcmp(*(p+i),*(p+i+1))>0){ptemp=*(p+i);*(p+i)=*(p+i+1);*(p+i+1)=ptemp;}printf ("排序后:\n");for(i=0;i%s\n",i,*(p+i));printf("\n");return 0;}546.4.9 内存的动态分配与动态数组的建立内存的动态分配与动态数组的建立1. 动态分配的概念通过前面的讨论已经知道,使用指针方式进行内存空间的访问是非常危险的。
于是可以设想,如果能在程序运行时能为指针分配一个连续的存储空间,将会使得指针的使用变得安全C语言提供了这一功能这一功能称为内存的动态分配前面讨论过,全局变量是在编译时在内存静态存储区分配的,非静态的局部变量是程序运行时在栈区自动分配的,而为指针进行内存空间的动态分配是在程序运行过程中在自由内存区——堆(heap)区分配的堆可以形成比较大的存储空间,供动态分配使用动态分配的特点是,可以由程序员控制,在需要时分配,在不需要时释放,还可以根据需要改变所分配存储空间的大小这些功能重要通过stdlib.h库中的4个函数实现 55函数原型返 回功能说明void *malloc(unsigned int size);成功:返回所开辟空间首地址失败:返回空指针向系统申请size字节的堆存储空间void *calloc(unsigned int num, unsigned int size);成功:返回所开辟空间首地址失败:返回空指针按类型申请num个size大小的堆空间void free(void *p);无返回值释放p指向的堆空间void *realloc(void *p,unsigned int size);成功:返回新开辟空间首地址失败:返回空指针将p指向堆空间变为size大小56说明:(1)viod *p是说明p是void*类型指针,声明其基类型是未确定的类型,可以用强制转换的方法将其转换为任何别的类型。
例如double *pd=NULL;pd=(double *)calloc(10,sizeof(double));表示将向系统申请10个连续的double类型的存储空间,并用指针pd指向这个连续的空间的首地址并且用(double)对calloc()的返回类型进行转换,以便把double类型数据的地址赋值给指针pd2)使用sizeof的目的是用来计算一种类型的占有的字节数,以便适合不同的编译器3)由于动态分配不一定成功,为此要附加一段异常处理程序,不致程序运行停止,使用户不知所措通常采用这样的异常处理程序段:if(p==NULL) /* 或者if(!p)*/{printf(“No enough memry!\n”);exit(1);}572. 动态数组的建立 通过前面的学习,可以建立这样的概念:数组就是用于存储同类型数据的连续空间在C语言中,这个空间可以用下标形式表示,也可以用指针形式表示动态数组指的是不在程序开始时定义固定大小的数组,而是在需要时建立数组,不需要时释放在下面的例子中的动态数组用指针形式引用58#include #include #define STUDENT_NUM 3int main ( ){ double *p=NULL,sum=0.0;int i;p=(double *)calloc(STUDENT_NUM*sizeof(double)); if(!p){printf(“Memory request failed!\n”);exit(1);59}printf(“请输入学生的成绩:”);for(i=0;i
6.5 扩展知识与理论616.5.1 指针参数与函数的地址传送调用指针参数与函数的地址传送调用 指针作参数,就是传送地址的值,并且要求在实参与形参之间传送类型相同的数据的地址,其中包括同样的级别而就形式而言,形参与实参之间的关系各如图所示几种数组名变量地址指针变量指针变量数组名实参形参传送地址621. 简单变量地址传送#include int main(void){void s *p1,int *p2);int a=3,b=5;printf("交换前:a=%d,b=%d\n",a,b);s);/* 简单变量地址传送 */63printf("交换后:a=%d,b=%d\n",a,b);return 0;}void s *p1,int *p2)/* 用指针接收变量地址 */{int temp;temp=*p1,*p1=*p2,*p2=temp;}642. 数组地址传送 传输数组地址,有可能使主调函数和被调函数在同一个数组上进行操作,避免了传送数组实体所造成的程序效率不高下面介绍几种数组地址的传输方式1)数组名→数组名传输 数组名到数组名之间的数组地址传输方式。
65#include #define N 10int main(void){ void ArrMax(int [],int * ,int n);int array[N]={1,8,10,2,-5,0,7,15,4,-5};int max,*p=&max;ArrMax(array,p,N);printf (“max=%d”,max);66return 0;}void arrMax(int arr[],int *pt,int n){ int i;*pt=arr[0];for (i=1;i*pt)*pt=arr[i];}运行结果如下:max=1567说明: 本例的主函数调用函数main()时,传送了三个参数:• 函数名:在函数ArrMax()中使用下标方式引用主函数中的数组array• 指向变量max的指针:用于在函数ArrMax()中引用主函数中的变量max,把通过与array中的每个元素比较,始终在max中放最大的元素• 和数组大小:用于控制重复结构的循环次数68(2)数组名→指针传送 在上述程序中,也可以将函数ArrMax()的形参改用指针。
程序如下:#include #define N 10int main(void){ void ArrMax(int *,int * ,int n);int array[N]={1,8,10,2,-5,0,7,15,4,-5};int max,*p=&max;ArrMax(array,p,N);69printf (“max=%d”,max);return 0;}void ArrMax(int *arr,int *pt,int n){ int i;*pt=arr[0];for (i=1;i*pt)*pt=arr[i];}执行结果还是:max=15703. 字符指针参数int stringlen(const char *str){len=0;while(*str++) len++;return len;}71说明:(1)在项目五中具有相同功能的函数为int stringlen(char str[]){int i=0,len=0;while(str[i++])len++;return len;}由于指向字符的指针与字符数组类型相同,所以,*str++与str[i++]等价。
但是采用指针使程序更为简洁,不需要定义数组72(2)可能读者会问,这里会不会出现乱用指针的问题答案是:不会因为在参数传递时,实参是要计算长度的字符串名,即字符数组名而该字符数组是已经被分配类空间的这个函数中字符指针的活动范围是从字符串的起始地址到字符串结束标志符之间,当某一次循环中遇到字符串结束标志符’\0’时,*str的值为0(’\0’的ASCII码为0),while中表达式为假,循环中止,因而不会出现超界问题3)这个函数在参数表中使用了“const”,它表明在本函数的执行过程中,要将字符串“锁定”,即不允许对字符串作任何改变因为,函数的功能只是求字符串的长度73void *stringcopy(char *dest, const char *src){char *temp=dest;while(*dest++=*src++);return temp;}说明:(1)由于目标字符串要改变,而源字符串只是复制,所以仅用const修饰源字符串2)temp是一个指向字符的指针,用它来保存目的串的首地址因为在复制的过程中,随着一连串的自增操作,dest的值不再指向目的串的首地址74下面再看一个函数。
int stringcomp(const char *s1, const char *s2){for(;*s1==*s2;s1++,s2++)if(!*s1) return 0;return *s1-*s2;}75说明:(1)这个函数由一个for循环结构组成循环的初始条件就是s1指向进行比较的一个字符串的首地址,s2指向另一个字符喘的首地址这个条件已经在参数传递时实现了,所以在for结构中初始条件确省2)该函数中for循环的条件是:*s1==*s2,即两个指针的当前应用相等,也即对应位置的字符相同,就再比较下一个字符3)for循环中的修正表达式为:s1++,s2++从而保证两个指针分别指乡各自的字符串中的相同位置4)表达式if(!*s1) return 0的意思是当s1指向字符‘\0’时,函数返回0实际上,由于这个条件表达式是在for结构中的,而for结构的重复条件是*s1==*s2,即两个字符指针当前指向的字符相等所以当s1指向字符串结束符时,s2也一定是指向了字符串结束符,也就是两个字符串都结束,用返回0表示两个字符串完全相同76#include″stdio.h″#define N 80void delchar (char *p,char x){ char *q=p;for (;*p!='\0';p++)if (*p!=x)*q++=*p;*q='\0';}int main(void){ 77void delchar (char *p,char x);char c[N],*pt=c,x;printf("enter a string:");gets (pt);printf ("enter the character deleted:");x=getchar( );delchar (pt,x);printf ("The new string is:%s\n",c);}运行情况如下:enter a string: I have 50 Yuan. enter the character deleted:0 The new string is:I have 5 Yuan.78本程序的功能:在主函数中,定义字符数组c并使pt指向c;从键盘输入字符串和需要删去的字符;然后调用delchar()函数。
形参指针变量p和被删字符x由main函数中实参pt和x传递到函数delchar()在delchar()中实现删除字符请注意在delchar()中并未定义另一个数组,而是在c数组中删去指定的字符在刚开始执行此函数时,指针变量p和q都指向c数组中图6.17第一个字符当*p不等于x时,*p赋给*q,然后p和q都自加1,即同步下移,见图6.17(a)中p′和q′当某一次*p==x时,不执行“*q++=*p;”语句,q不自加1,而p继续加1,p与q不再指向同一元素,q仍指向c[8],而p指向c[9],见图6.17(b)中的p′和q′在执行下一次循环时由于(*p!=x)为真,执行“*q++=*p;”语句,将c[9]值赋给c[8],使c[8]中原值(字符0)被空格取代然后q和p均自加1,以后将c[10]c[9],c[11]c[10]…,c[13]c[12],c[14]c[13],最后用“*9=′\0′;”语句给c[14](即*9的当前值)赋予“\0”注意c数组中c[15]的值并未改变,因此a[15]仍为“\0”,a数组中有两个′\0′可以看到c数组各元素值改变了,在main函数中可以输出数组c中的字符串(遇第一个“\0”即停止)。
795e空格0ayun.hI空格av\0┇p,ptqp'q'5e空格0空格┇y┇.\0hIav\0┇p,pt'p'c[7]c[8]c[9]c[14]c[15]806.5.2 带参主函数带参主函数直到现在,用到的main函数都是不带参数的,因此main函数的第一行是:main( void)其实main函数也可以有参数有参数的main函数的原型为:int main (int argc,char *argv[]);也就是说,带参数main函数的第一个形参argc是一个整型变量,第二个形参argv是一个指针数组,其每个元素都指向字符型数据(即是一个字符串)这两个参数的值从哪里传递而来呢?main函数是主函数,它不能被程序中其它函数调用、因此显然不可能从其它函数向它传递所需的参数值,只能从程序以外传递而来也就是在启动一个程序时,从程序的命令行中给出的例如有个程序,程序名为c. 通常只要在操作系统的命令状态下,键入命令:cfile就可以开始执行这个程序81 不过,C语言还允许在这个命令行中键入要程序处理的其他字符串例如,要让cfile程序处理一个字符串“hardware”,可以键入命令行:c如果要让cfile程序处理两个字符串,如“hardware”和“software”,则可以键入命令行:c software那么,键入这些字符串后,C程序怎么接收的呢?实际上,这些字符串就是由main的两个参数接收的。
如下页图所示,当键入上述命令时,操作系统将把“cfile”、“hardware”和“software”保存在内存中,并把它们的地址依次存放在数组argv中,同时把字符串的个数,也即数组argv的大小存放在变量argc中82#include″stdio.h″int main (int argc, char * argv[]){ while (argc>1){++argv;printf (″%s\n″,*argv);--argc;} return 0;}leifcdwrahtwfosar\0erae\0\0argv[0]argv[1]argv[2]argvargc=383如果从键盘输入的命令行为:c software则输出为:hardware software 因为argc初值为3,每次循环减1,故其循环两次第一次时,先使argv指向argv[1],然后输出argv[1]指向的字符串“hardware”,第二次开始时,又使argv指向argv[2],然后输出“software”84用带参的main函数可以直接从命令行得到参数值(这些值是字符串),在程序运行时可以根据输入的命令行中的不同情况进行相应的处理。
例如,在使用数据文件时,可以根据不同的需要输入不同的命令行;以打开不同的文件利用main函数中的参数可以使程序从系统得到所需的数据,或者说,增加了一条系统向程序传递数据的渠道,增加了处理问题的灵活性其实main的形参名并不一定非用argc和argv不可,只是习惯上一般用这两个名字如果改用别的名字,其数据类型不能改变,即第一个形参为int型,第二个形参为指针数组85 顺便说明一个问题:在本例中用到argv++的运算,是使argv的值自加而argv是数组名以前说过数组名代表一个常量,它是数组起始地址,它是不能进行自加运算的,是不能改变其本身的值的例如下面程序是不能通过编译的:main ( ){ int a[5];int i;for (i=0;i<5;i++,a++)printf (“%d”,*a);}86错误在于a不能进行自加运算,a++不合法这是由于在编译时给a数组分配一段内存单元,a代表数组起始地址,是一常量注意a是main函数中的数组名,不是形参如果将a设为形参数组,情况就不同了:void fun(int a[],int n){ int i;for (i=0;i
在编译时并未分配其固定的内存单元只是在调用函数fun时才将arr的起始地址传给a,实际上a是一个指针变量,定义fun函数的第一行相当于:fun (int * a,int n) 这里,a是指针变量,a++是合法的所以例6-19中argv++是合法的它的作用是使argv指针下移一个元素886.3.3 返回指针值的函数返回指针值的函数 一个函数在被调用之后可以带回一个值返回到主调函数,这个值可以是整型、实型、字符型等类型,也可以带回一个指针类型的数据例如前面介绍的内存动态分配函数malloc()和calloc()都是返回指针的函数 例:编写一个函数,它的作用是在一个字符串中找一个指定的字符,返回该字符的地址(库函数中有标准函数strchr(),要求自己编写具有同样功能的stringchr())89函数如下:char *stringchr (char *str,char ch){ while (*str++!=’\0’) if (*str==ch) return (str);return (0);}可以用下面的main函数调用它include 90int main (void){char *stringchr (char *str, char ch);char *pt,ch,line[]=”I love China”;ch=’C’;pt=stringchr (line,ch);printf (“\n字符串的起始地址:%o。
\n”,line);printf (“最先出现字符%c的地址是:%o\n”,ch,pt);printf (“这是该字符串中的第 %d (从0开始)个字符\n”,pt-line);}91 这里4577550和457757是八进制的地址,十进制地址则分别是1245032和1245039下图表明了这个程序的执行过程:str的初值是数组line的起始地址,即& line[0]将*str与ch比较,如果*str(即str当前指向的字符)不等于ch的值,则使str++,即使str下移一个字符,直到str指向字符C为止(图中的str),将ptr值返回主调函数,str是字符C的地址92再看一例: 编写一个函数stringcat(),使一个字符串str2接到另一个字符串str1的后面,原来str1字符串最后的“\0”被str2的第一个字符取代函数返回str1的值(也是在标准库函数中有一个标准函数strcat()这里的stringcat()要求自己编)strovlIhCe\0anistr'数组linech=’C’93char *stringcat (char *str1,char *str2){char *p;for (p=str1;*p!=’\0’;p++);do{ *p++=*str2++;} while (*str2!=’\0’);*p=’\0’;return (str1);}94可以用main函数来调用它:#include int main(void){ char *stringcat (char *str1,char *str2);char string1[20]=”C language”,string2[]=” is fun.”,*pt;pt=stringcat (string1,string2);printf (“The new string is:%s\n”,pt);return 0;}95运行结果如下:The new string is: C language is fun.说明: stringcat()函数中的for语句作用是使p指向string1最后的“\0”。
do…while循环的作用是将字符串string2中的字符按照p的指示位置逐个传到string1中去开始,string2中第一个字符*str2的值赋给string1字符串中原来存放“\0”的单元然后str2和p都同步下移一个位置,再使*str2赋给*p,直到遇到string2中的“\0”为止注意应该再赋一个“\0”给*p,即加到新串的末尾这个过程如图6.21所示 函数stringcat()返回string1的首地址给主函数中的字符指针pt,并由printf()函数输出连接后的新字符串96str1string1a nlCgaug\0\0\0e\0 \0\0\0\0\0\0fsi\0.nustr2string2str1string1a nlCgaug\0\0\0e\0 \0\0\0\0\0\0pstr1string1a nlCgaugsien .uf\0\0p976.5.4 6.5.4 指向函数的指针指向函数的指针一个函数包括一系列的指令,在内存中占据一片存储单元,它有一个起始地址,即函数的入口地址,通过这个地址可以找到该函数,这个地址就称为函数的指针也可以定义一个指针变量,使它的值等于函数的入口地址,那么通过这个指针变量也能调用此函数,这个指针变量称为指向函数的指针变量。
定义一个指向函数的指针变量的一般形式如下:类型标识符 (*指针变量名)();例如:int(*p)();它表示p指向一个“返回整型值的函数”注意*p两侧的括弧不能省略,如果写成:“int *p();”就成了“返回指针值的函数”了98 由于函数可以返回一个值所以在C语言中,可以认为函数也具有数据类型定义函数时必须定义函数返回值的类型(void也是一种类型)同理,在定义一个指向函数的指针变量时,除了需要用指针声明符(*)和指针变量名外,还必须声明它所指向的函数的类型 在定义了指向函数的指针变量以后,可以将一个已经定义的函数的入口地址赋给它,使指针变量指向一个特定的函数如:p=fun1; fun1代表函数fun1的入口地址,函数名与数组名一样,都是常指针注意在将函数的入口地址赋给指针变量时,只写函数名而不要有括号和参数表,例如不应写成以下形式:p=fun1();99或p=fun1(a,b); 因为fun1(a,by)是函数调用,将得到一个函数值,而不是函数fun1的入口地址 定义并初始化一个指向函数的指针后,就可以通过指针调用它所指向的函数: (*指针变量) (实参表列)例如:(*p)(a,b),相当于fun1(a,b)。
100用指向函数的指针变量调用arradd()求二维数组中全部元素之和程序如下:#include #define N 3#define M 4int main( ){ int arradd (int arr[],int n);static int a[N][M]={1,3,5,7,9,11,13,15,17,19,21,23};int * p,total1,total2;int (*pt)();pt=arradd;101p=a[0];total1=arradd(p,N*M);total2=(*pt)(p,N*M);printf (“total1=%d\ntotal2=%d\n”,total1,total2);return 0;}arradd (int arr[],int n){ int i, sum=0;for (i=0;i
可以用指向函数的指针变量作为被调用函数的实参,由于该指针变量是指向某一函数的,因此先后使指针变量指向不同的函数,就可以在被调用函数中调用不同的函数103。