6.4.4 指针与字符数组 6.5 指针的地址分配 6.6 指针数组
[例6-13] 上述程序也可采用指针变量作子程序的形式参数。 #include main( ) { int sub_max(); int n,a[10],*ptr=a; int max; for(n = 0; n<= 9; n++) scanf("%d", &a[n]); max = sub_max(ptr, 10); printf("max = %d\n",max); } int sub_max(b,i) / *形式参数为指针变量* / int *b,i; { int temp,j; temp = b[0]; / *数组元素指针的下标法表示* / for(j = 1; j <= i-1; j++) if(temp return temp; } 在子程序中,形式参数是指针,调用程序的实际参数p t r为指向一维数组a的指针,虚实结合,子程序的形式参数b得到ptr的值,指向了内存的一维数组。数组元素采用下标法表示,即一维数组的头指针为b,数组元素可以用b[j]表示。其内存中虚实参数的结合如图6 - 1 0所示。 运行程序: 1 3 5 7 9 2 4 6 8 0 max = 9
[例6-14] 上述程序的子程序中,数组元素还可以用指针表示。 #include main( ) { int sub_max(); int n,a[10],*ptr=a; int max; for(n = 0; n <= 9; n ++) scanf("%d", &a[n]); max = sub_max(ptr, 10); printf("max = %d\n", max); } int sub_max(b,i)/ *子程序定义* / int *b,i; { int temp,j; temp = *b ++; for( j = 1; j <= i - 1; j ++) if(temp<*b) temp=*b++; return temp; }
在程序中,赋值语句temp = * b ++;可以分解为:temp = *b;b ++;两句,先作temp = *b;后作b + +;程序的运行结果与上述完全相同。 对上面的程序作修改,在子程序中不仅找最大元素,同时还要将元素的下标记录下来。 #include main( ) { int *max();/* 函数声明* / int n,a[10],*s,i; for( i = 0; i < 10; i ++ ) / * 输入数据* / scanf("%d",a+i); s = max(a, 10); / *函数调用* / printf("max = %d, index = %d\n" , *s, s - a); } int *max(a,n) / *定义返回指针的函数* / int *a,n; { int *p,*t; / * p 用于跟踪数组,t用于记录最大值元素的地址* / for(p = a, t = a; p - a < n; p ++) if(*p>*t) t=p; return t; } 在max()函数中,用p - a < n来控制循环结束, a是数组首地址, p用于跟踪数组元素的地址,p - a正好是所跟踪元素相对数组头的距离,或者说是所跟踪元素相对数组头的元素个数,所以在main( )中,最大元素的下标就是该元素的地址与数组头的差,即s - a。运行程序: 1 3 5 7 9 2 4 6 8 0 max = 9, index = 4
[例6-15] 用指向数组的指针变量实现一维数组的由小到大的冒泡排序。编写三个函数用于输入数据、数据排序、数据输出。 在第5章的例题中,我们介绍过选择法排序及算法,此例再介绍冒泡排序算法。为了将一组n个无序的数整理成由小到大的顺序,将其放入一维数组a[0]、a[1]. . .a[n - 1]。冒泡算法如下: (开序) ① 相邻的数组元素依次进行两两比较,即a[0]与a[1]比、a[1]与a[2]比. . . a[n - 2]与a[n - 1]比,通过交换保证数组的相邻两个元素前者小,后者大。此次完全的两两比较,能免实现a[n - 1]成为数组中最大。 ② 余下n - 1个元素,按照上述原则进行完全两两比较,使a[n - 2]成为余下n - 1个元素中最大。 ③ 进行共计n - 1趟完全的两两比较,使全部数据整理有序。 下面给出一趟排序的处理过程: 原始数据3 8 2 5 第一次相邻元素比: 3 8 2 5 第二次相邻元素比: 3 2 8 5 第三次相邻元素比: 3 2 5 8 4个元素进行3次两两比较,得到一个最大元素。若相邻元素表示为a[j]和a[j + 1],用指针变量P指向数组,则相邻元素表示为*(P + j)和*(P + j + 1)程序实现如下: # include #define N 10 main( ) { void input(); / *函数声明* / void sort(); void output(); int a[N],*p; / *定义一维数组和指针变量* / input(a, N); / *数据输入函数调用,实参a是数组名* / p = a; / *指针变量指向数组的首地址* / sort(p, N); / *排序,实参p是指针变量* / output(p, N); / *输出,实参p是指针变量* / } void input(arr,n) / *无需返回值的输入数据函数定义,形参a r r 是数组* / int arr[],n; { int i; printf("input data:\n"); for(i = 0; i < n; i ++) / *采用传统的下标法*/ scanf("%d", &arr[i]); } void sort(ptr,n) / *冒泡排序,形参ptr是指针变量* / int *ptr,n; { int i,j,t; for(i = 0; i < n - 1; i ++) for(j = 0; j < n - 1 - i; j ++) if (*(ptr+j)>*(ptr+j+1))/* 相临两个元素进行比较* / { t = *(ptr + j); / *两个元素进行交换* / *(ptr + j) = *(ptr + j + 1); *(ptr + j + 1) = t; } } void output(arr,n) / *数据输出* / int arr[],n; { int i,*ptr=arr; / *利用指针指向数组的首地址* / printf("output data:\n"); for( ; ptr - arr < n; ptr ++) / *输出数组的n个元素* / printf("%4d", *ptr); printf("\n"); } 运行程序: 3 5 7 9 3 23 43 2 1 10 1 2 3 3 5 7 9 10 23 4 3 由于C程序的函数调用是采用传值调用,即实际参数与形式参数相结合时,实参将值传给形式参数,所以当我们利用函数来处理数组时,如果需要对数组在子程序中修改,只能传递数组的地址,进行传地址的调用,在内存相同的地址区间进行数据的修改。在实际的应用中,如果需要利用子程序对数组进行处理,函数的调用利用指向数组(一维或多维)的指针作参数,无论是实参还是形参共有下面四种情况:
实参 |
实参 |
形参 |
1 |
数组名 |
数组名 |
2 |
数组名 |
指针变量 |
3 |
指针变量 |
数组名 |
4 |
指针变量 |
指针变量 | 在函数的调用时,实参与形参的结合要注意所传递的地址具体指向什么对象,是数组的首址,还是数组元素的地址,这一点很重要。
[例6-16] 用指向二维数组的指针作函数的参数,实现对二维数组的按行相加。 #include #define M 3 #define N 4 main( ) { float a[M][N]; float score1,score2,score3, *pa=a[0];/* 指针变量p a指向二维数组* / /* score1,score2,score3 分别记录三行的数据相加* / int i,j; void fun(); for(i = 0; i < M; i ++) for(j=0;j scanf("%f", &a[i][j]); fun(pa, &score1, &score2, &score3); / *函数调用,不仅传递数组首地址,还要传递变量的地址* / printf("%.2f, %.2f, %.2f\n", score1, score2, score3); } void fun(b,p1,p2,p3) float b[ ][N],*p1,*p2,*p3; { int i,j; *p1 = *p2 = *p3 = 0 ; for(i = 0; i < M; i ++) for(j = 0; j < N; j ++) { if(i==0) *p1=*p1+b[i][j]; / *第0行的数据相加* / if(i==1) *p2=*p2+b[i][j]; / *第1行的数据相加* / if(i==2) *p3=*p3+b[i][j]; / *第2行的数据相加* / } } 程序中与形式参数p 1、p 2和p 3相对应的是实际参数&score1、&score2和&score3,其实际含义为p1 = &score1等,即将变量的地址传递给指针变量达到按行相加。 运行程序: 1 2 3 4 3 4 5 6 5 6 7 8 10.00,18.00,26.00
[例6-17] 求解二维数组中的最大值及该值在二维数组中的位置。 我们知道,二维数组在内存中是按行存放,假定我们定义二维数组和指针如下: int a[3][4],* p = a[0]; 则指针p就指向二维数组。其在内存的存放情况如图6 - 11所示。 从上述存放情况来看,若把二维数组的首地址传递给指针p,则映射过程如图6 - 11所示。我们只要找到用p所表示的一维数组中最大的元素及下标,就可转换为在二维数组中的行列数。
#include main( ) { int a[3][4],*ptr,i,j,max,maxi,maxj; / * max 是数组的最大, maxi 是最大元素所在行, maxj 是最大元素所在列* / for(i = 0; i < 3; i ++) for(j = 0; j < 4; j ++) scanf("%d", &a[i][j]); ptr = a[0]; / * 将二维数组的首地址传递给指针变量* / max_arr(ptr, &max, &maxi, 12); maxj = maxi % 4; / * 每行有四个元素,求该元素所在列* / maxi = maxi / 4; / * 求该元素所在行* / printf("max=%d,maxi=%d,maxj=%d",max,maxi,maxj); } int max_arr(b,p1,p2,n) int *b,*p1,*p2,n; / * b 指向二维数组的指针, p1指向最大值,p2 指向最大值在一维数组中的位置, * / / * n 是数组的大小* / { int i; *p1=b[0]; *p1=0; for(i = 1; i < n; i++) / * 找最大* / if (b[i]>*p1) { *p1=b[i]; *p2=i; } } 运行程序: 4 7 8 9 3 7 9 3 1 5 2 6 max=9,maxi=0,maxj=3
6.4.4 指针与字符数组 在前面的课程中,我们用过了字符数组,即通过数组名来表示字符串,数组名就是数组的首地址,是字符串的起始地址。下面的例子用于简单字符串的输入和输出。 #include main( ) { char str[20]; gets(str); printf("%s\n", str); } good morning! good morning! 现在,我们将字符数组的名赋予一个指向字符类型的指针变量,让字符类型指针指向字符串在内存的首地址,对字符串的表示就可以用指针实现。其定义的方法为: char str[20],*P = str;这样一来,字符串str就可以用指针变量P来表示了。 #include main( ) { char str[20],*p=str ; /* p=str则表示将字符数组的首地址传递给指针变量p */ gets(str); printf("%s\n",p); } good morning! good morning! 需要说明的是,字符数组与字符串是有区别的,字符串是字符数组的一种特殊形式,存储时以“\ 0”结束,所以,存放字符串的字符数组其长度应比字符串大1。对于存放字符的字符数组,若未加“\ 0”结束标志,只能按逐个字符输入输出。
[例6-18] 字符数组的正确使用方法。 #include main( ) { char str[10],*p=str; int i; scanf("%s", str); /*输入的字符串长度超过10*/ for(i=0;i<10;i++) printf("%c", *p++); / *正确输出* / printf("\n"); p = str; printf("%s", p); / *字符数组无' \ 0 ' 标志,输出出错* / puts(str); / *字符数组无' \ 0 ' 标志,输出出错* / } 对上述程序中字符数组以字符串形式输出,若无“ \ 0”标志,则找不到结束标志,输出出错。
[例6-19] 用指向字符串的指针变量处理两个字符串的复制。 字符串的复制要注意的是:若将串1复制到串2,一定要保证串2的长度大于或等于串1。 #include main( ) { char str1[30],str2[20],*ptr1=str1,*ptr2=str2; printf("input str1:"); gets(str1); / *输入str1*/ printf("input str2:"); gets(str2); /* 输入str2*/ printf("str1------------str2\n"); printf("%s.......%s\n", ptr1, ptr2); while(*ptr2) *ptr1++=*ptr2++;/ *字符串复制* / *ptr1 = '\0'; /* 写入串的结束标志* / printf("str1------------str2\n"); printf("%s.......%s\n", str1, str2); } 在程序的说明部分,定义的字符指针指向字符串。语句while(*ptr2) *ptr1++=*ptr2++;先测试表达式的值,若指针指向的字符是“\0”,该字符的ASCII码值为0,表达式的值为假,循环结束,表达式的值非零,则执行循环*ptr1++ = *ptr2 ++。语句*ptr1++按照运算优先级别,先算*ptr1,再算ptr1 ++。 运行程序: input str1: I love China! input str2: I love Chengdu! str1--------------------str2 I love China! ....... I love Chengdu! str1--------------------str2 I love Chengdu! ....... I love Chengdu! 现在,我们修改程序中语句printf("%s.......%s\n", str1,str2)为printf("%s.......%s\n",ptr1, ptr2); 会出现什么结果呢?请思考。 [例6-20] 用指向字符串的指针变量处理两个字符串的合并。 #include main( ) { char str1[50],str2[20],*ptr1=str1,*ptr2=str2; printf("input str1:"); gets(str1); printf("input str2:"); gets(str2); printf("str1------------str2\n"); printf("%s.......%s\n", ptr1, ptr2); while(*ptr1) ptr1++; / *移动指针到串尾* / while(*ptr2) *ptr1++=*ptr2++; /* 串连接* / *ptr1 = '\0'; /* 写入串的结束标志*/ ptr1=str1; ptr2=str2; printf("str1------------------str2\n"); printf("%s.......%s\n", ptr1, ptr2); } input str1: I love China! input str2: I love Chengdu! str1--------------------str2 I love China! ....... I love Chengdu! str1------------------------------------------str2 I love China! I love Chengdu! ...... I love Chengdu!. 需要注意的是,串复制时,串1的长度应大于等于串2;串连接时,串1的长度应大于等于串1与串2的长度之和。
6.5 指针的地址分配 我们可以定义指针变量指向任何类型的变量。在上述的处理过程中,指针变量指向的变量通过传递变量的地址来实现。指针变量的取值是内存的地址,这个地址应当是安全的,不可以是随意的,否则,写入内存单元的值将会使得已存放的数据或程序丢失。应使用编译系统提供的标准函数来实现地址分配。 ANSI标准建议设置了两个最常用的动态分配内存的函数malloc() 和free( ),并包含在stdlib.h中,但有些C编译却使用malloc.h包含。使用时请参照具体的C编译版本。 我们这里所指的动态内存分配其含义是指:当定义指针变量时,其变量的取值是随机的,可能指向内存的任一单元。若指针的指向是不安全的内存地址,在该地址空间上的数据交换就会产生意料不到的效果。为此,在程序的执行过程中,要保证指针操作的安全性,就要为指针变量分配安全地址。在程序执行时为指针变量所做的地址分配就称之为动态内存分配。当无需指针变量操作时,可以将其所分配的内存归还系统,此过程我们称之为内存单元的释放。malloc( )用以向编译系统申请分配内存; free( )用以在使用完毕释放掉所占内存。
[例6-21] 两个字符串的交换。 #include #include #include main( ) { char *ptr1,*ptr2,*temp; ptr1=malloc(30); /*动态为指针变量分配长度为3 0字节的存储空间* / ptr2 = malloc(20); temp = malloc(30); printf("input str1:"); gets(ptr1); / *输入字符串* / printf("input str2:"); gets(ptr2); printf("str1------------str2\n"); printf("%s.......%s\n", ptr1, ptr2); strcpy(temp,ptr1); /* 串复制*/ strcpy(ptr1, ptr2); strcpy(ptr2,temp); printf("str1------------str2\n"); printf("%s.......%s\n",ptr1,ptr2); free(ptr1); free(ptr2); } 为指针变量分配的存储空间长度取决于存放字符的多少。在上述的程序中,两个串的交换可以通过标准函数strcpy() 来完成,也可以通过串指针交换指向完成,用temp=ptr1; ptr1 = ptr2;ptr2 = temp;三条赋值语句实现。但是,利用指针交换指向,其物理意义与串通过函数进行的复制完全不同。前者是存放串地址的指针变量数据交换,后者是串在内存物理空间的数据交换。指针变量用完后,将指针变量所占的存储空间释放。 运行程序:r u n input str1: China input str2: Chengdu str1------------str2 China----------Chengdu str1------------str2 Chengdu----- China
6.6 指针数组 前面介绍了指向不同类型变量的指针的定义和使用,我们可以让指针指向某类变量,并替代该变量在程序中使用;我们也可以让指针指向一维、二维数组或字符数组,来替代这些数组在程序中使用,给我们在编程时带来许多方便。下面我们定义一种特殊的数组,这类数组存放的全部是指针,分别用于指向某类的变量,以替代这些变量在程序中的使用,增加灵活性。指针数组定义形式: 类型标识*数组名[数组长度] 例如: char *str[4]; 由于[ ] 比*优先权高,所以首先是数组形式str[4 ],然后才是与“*”的结合。这样一来指针数组包含4个指针str [0]、str[1]、str[2]、str[3],各自指向字符类型的变量。例如: int *ptr[5]; 该指针数组包含5个指针ptr[0]、ptr[1]、ptr[2]、ptr[3]、ptr[4],各自指向整型类型的变量。
[例6-22] 针对指针数组的应用,我们分别用指针数组的各指针指向字符串数组、指向一维整型数组、指向二维整型数组。 #include #include main( ) { char *ptr1[4]={"china","chengdu","sichuang","chongqin"}; /* 指针数组p t r 1 的4个指针分别依此指向4个字符串* / int i,*ptr2[3],a[3]={1,2,3},b[3][2]={1,2,3,4,5,6}; for (i=0;i<4;i++) printf("\n%s",ptr1[i]); /* 依此输出p t r 1 数组4个指针指向的4个字符串* / printf("\n"); for(i=0; i < 3; i ++) ptr2[i]=&a[i]; /*将整型一维数组a的3个元素的地址传递给指针数组ptr2*/ for(i = 0; i < 3; i ++) / * 依此输出ptr2 所指向的3个整型变量的值* / printf("%4d", *ptr2[i]); printf("\n"); for(i = 0; i < 3; i ++) ptr2[i]=b[i]; /*传递二维数组b的每行首地址给指针数组的4 个指针* / for(i = 0; i < 3; i ++) / * 按行输出* / printf("%4d %4d\n", *ptr2[i], *ptr2[i] + 1); } 程序中指针数组与所指对象的关系如图6 - 1 2所示。
ptr1指针数组中的4个指针分别指向4个字符串,如图6 - 11的a)所示,程序中依此输出;ptr2指针数组共有3个指针,若将整型一维数组a中各元素地址分别传递给指针数组的各指针,则ptr2[0]就指向a[0];ptr2[1]就指向a[1];ptr2[2]就指向a[2]。若将二维数组各行的首地址分别传递给指针数组的各指针,如图6 - 11 b)所示,这样一来, ptr2[0]就指向了b数组的第0行,该行有两个元素,其地址为ptr2[0]与ptr2[0] + 1;相应指针数组第i个元素ptr2[i]指向的b数组的第i行两个元素地址分别为ptr2[i]与ptr[i] + 1。 运行程序: china chengdu sichuang chongqin 1 2 3 1 2 2 4 5 6 在处理二维字符数组时,我们可以把二维字符数组看成是由多个一维字符数组构成,也就是说看成是多个字符串构成的二维字符数组,或称为字符串数组。指针数组对于解决这类问题(当然也可以解决其它问题)提供了更加灵活方便的操作。有一点需要说明,若定义一个指针数组后,指针数组各元素的取值(即地址)要注意安全性。 如定义指针数组: char *ptr[3]; 我们说该数组包含三个指针,但指针的指向是不确定的,指针现在可能指向内存的任一地址。假定现在作语句: scanf("%s", ptr[ i ] ), 则输入的字符串在内存的存放其地址由ptr[i]决定。除非给指针数组元素赋值安全的地址。
[例6-23] 定义字符指针数组,包含5个数组元素。同时再定义一个二维字符数组其数组大小为5 * 1 0,即5行1 0列,可存放5个字符串。若将各字符串的首地址传递给指针数组各元素,那么指针数组就成为名副其实的字符串数组。下面对各字符串进行按字典排序。 在字符串的处理函数中, strcmp(str1, str2)函数就可以对两个字符串进行比较,函数的返回值> 0、= 0、< 0分别表示串str1大于str2、str1等于str2、str1小于str2。再利用strcpy( )函数实现两个串的复制。下面选用冒泡排序法。 #include #include #include main( ) { char *ptr1[4],str[4][20],temp[20]; / *定义指针数组、二维字符数组、用于交换的一维字符数组* / int i,j; for(i=0;i<4;i++) gets(str[i]); / * 输入4个字符串* / printf("\n"); for(i = 0; i < 4; i ++) ptr1[i] = str[i]; / * 将二维字符数组各行的首地址传递给指针数组的各指针* / printf("original string:\n"); for(i = 0; i < 4; i ++) /*按行输出原始各字符串* / printf("%s\n", ptr1[i]); printf("ordinal string:\n"); for(i = 0; i < 3; i ++) /* 冒泡排序*/ for(j=0; j < 4 - i - 1; j ++) if( strcmp( ptr1[j], ptr1[j+1])>0) { strcpy(temp,ptr1[j]); strcpy(ptr1[j], ptr1[j+1]); strcpy(ptr1[j+1], temp); } for( i=0;i<4;i++) / *输出排序后的字符串* / printf("%s\n" , ptr1[i]); } 运行程序: jkjkdkddfs fhfgkjkfgkf hkfgkgfkklg jjkdjdk original string: jkjkdkddfs fhfgkjkfgkf hkfgkgfkklg jjkdjdk ordinal string: fhfgkjkfgkf hkfgkgfkklg jjkdjdk jkjkdkddfs 程序中一定要注意指针的正确使用。一旦将二维字符数组的各行首地址传递给指针数组的各指针,则相当于给指针分配了安全可操作的地址,地址空间大小由二维字符数组来决定。当然也可由编译系统为指针分配地址用于字符串的存放。
[例6-24] 利用malloc( )函数为指针分配存储空间,实现字符串的排序。 #include #include #include main( ) { char *ptr1[4],*temp; int i,j; for (i=0;i<4;i++) { ptr1[i] = malloc(20); / *为指针数组各指针分配2 0字节的存储空间* / gets(ptr1[i]); } printf("\n"); printf("original string:\n"); for(i=0; i<4;i++) printf("%s\n",ptr1[i]); printf("ordinal string:\n"); for(i=0;i<3;i ++) for(j=0;j<4-i-1;j++) if(strcmp(ptr1[j],ptr1[j+1])>0) { temp = ptr1[j]; /*利用指向字符串的指针,进行指针地址的交换*/ ptr1[j] = ptr1[j + 1]; ptr1[j+1] = temp; } for( i=0;i<4;i++) / *字符串输出* / printf("%s\n" , ptr1[i]); } 运行程序,其结果与上述例6 - 2 3完全相同。
| | |