C指针

2015/09/13 C

C指针

指针基础入门

计算机最主要的两项构造是中央处理器(CPU)与主存储器(内存)。一般执行程序时必须将程序及其所需的数据加载至主存储器,CPU才能开始执行该程序。早期用低级语言进行程序开发时还要厘清程序代码中变量在内存中的地址。在内存中,每一个字节都有一个内存编号(地址),如同现实生活中的地址一样,每一个地址都可存储二进制编码的数据。

在C语言中,指针是一个非常强有力的工具,许多初学者认为指针是进入C语言后较难跨过的障碍,其实一点都不难。指针和其他数据类型一样,只是一种内存地址的数据类型,也就是记录变量地址的工具。当CPU需要存取某个数据时(指出要存取哪一个地址的内存空间),指针就是让CPU存取数据的工具,直接根据其指定的地址存取变量。指针也可以用于为一维数组、二维数组等数组动态分配内存空间,让内存空间运用起来更加有效率。

计算机中字节在内存中的地址通常采用十六进制表示法,这对于人类而言并不是那么浅显易懂,也不容易识别。要直接指明地址的存取方式,对程序员而言难免费时费力。因此大部分高级程序设计语言提供了声明变量与使用变量的功能,以此来解决直接使用内存地址的问题。当用户需要使用变量的时候,只要声明变量类型与名称就可以直接使用,较底层的问题(例如向系统索取内存的工作)就交给系统解决。在编写程序时,用户可以先给变量命名,然后在稍后的程序代码中以变量名称直接存取该变量的数据即可。

指针的作用

直接使用内存地址存取数据的方式当然是有好处的。也许有人认为在计算机中需要用到变量的时候直接声明一个变量就好,并不需要知道内存的位置呢。换个角度想一想,如果现实生活中你要到某一家商店或一位从未登门拜访的朋友家中,需要地址或者明显的地标才能够找到。

除了指定变量名称存取数据外,在计算机的运行中也需要针对内存地址存取的工具,就是指针(Pointer)。指针是一种变量类型,内容就是内存的地址。大家可以把身份证号码想象成变量的地址,有了身份证号码自然就可以知道这个人的个人资料(变量内容)了。

有了指针变量,程序代码可以直接存取该指针变量所指定的地址内容。基本上,使用指针就可以直接存取内存,增加了便利性。另外,编写程序时如果不能预估程序执行时需要多少内存、多少变量等信息,可以使用动态内存分配功能,这时只有通过指针变量记录系统给定的地址在哪里才能完成动态分配内存的工作。

变量地址的存取

在C语言中,为了针对地址与指针进行运算,特别定义了指针变量的形式与存取变量地址的方式。例如,当需要使用某个数据时,存取内存地址对应的内存空间内容即可。要了解变量所在的内存地址,可以通过取址运算符(&)获取,语法格式如下:

&变量名称;

下面的范例程序通过取址运算符(&)示范变量名称、变量值与内存地址之间的相互关系。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06      int num = 110;
07      char ch = 'A';
08  
09      puts( "变量名称  变量值  内存地址" );
10      puts( "-----------------------------" );
11      printf( "num\t   %d\t   %p\n", num, &num );
12      /* 输出num的值及地址 */ 
13      printf( "ch\t   %c\t   %p\n", ch, &ch );
14      /* 输出ch的值及地址 */ 
15      system("pause");
16      return 0;
17  }

第6、7行:声明两种不同类型的变量。

第11、13行:以%p格式表示十六进制的地址,要取出变量的地址,在变量前加上&运算符即可。通常我们不用直接处理内存地址的问题,因为变量中已经包括了内存地址的信息,会直接告诉程序应该到内存中的什么地方取出数值。

存取数组元素的地址

基本上,对已经定义的变量和数组都会分配内存空间供所存储的数据使用。因此,在程序中遇到需要数组元素的地址来运算时,可以使用取址运算符(&)获取该数组元素的地址。

下面的范例程序将使用&取得数组内每个元素的地址,只要在数组元素名称前加上&即可。例如,&Num[i]代表第i-1个元素所在的地址。屏幕上所显示的地址可能会因大家执行程序的计算机环境不同而显示不同的数值。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06   int Num[5]={ 33, 44, 55, 66, 77 };  /* 定义整数数组 Num[5] */
07   int i;      
08   
09    for( i=0; i< 5; i++)
10     {
11     printf("Num[%d] 的元素值:%d",i, Num[i]);  /* 输出数组元素的值 */
12     printf("  ");      /* 输出空白行调整位置 */
13     printf("Num[%d] 的地址:%p",i,&Num[i]);  /* %p 显示十六进制值 */
14     printf("\n");              
15     /* 换行 */
16     }
17   
18    system("pause");
19    return 0;
20  }

从第11、13行的输出结果可以看出,数组元素每移动一次下标值,要在内存位移4个字节(因为是整数类型)才能取出数组的下一项数据。

指针变量

在C语言中,要存储与操作内存的地址就要使用指针变量,指针变量的作用类似于变量,功能比一般变量更为强大。在程序中声明指针变量时,内存分配的情况与一般变量相同。声明指针变量时,首先必须定义指针的数据类型并在数据类型后加上“”号(取值运算符或值引用运算符),再给予指针名称,即可完成声明。“”的作用是取得指针指向内存地址中存放的内容。指针变量声明方式如下:

数据类型 *指针名称;
或
数据类型* 指针名称;

由于指针是一种变量,因此命名规则与一般变量的命名规则相同。通常建议命名指针时在变量名称前加上小写p,若是整数类型的指针,则可在变量名称前加上“pi”两个小写字母,“i”代表整数类型(int)。在此再次提醒大家,良好的命名规则对于程序日后的阅读与维护大有裨益。

一旦确定指针所指向的数据类型就不能更改了,指针变量也不能指向不同数据类型的变量。以下是几个整数指针变量的声明方式,所存放的地址必须是一个整数变量的地址。当然,指针变量声明时也可设置初值为0或者NULL增加可读性:

int* x;
int *x, *y;
int *x=0;
int *y=NULL

在声明指针变量后,如果没有设置初始值,指针所指向的内存地址就是未知的。不能对未初始化的指针进行存取,因为可能指向一个正在使用的内存地址。要设置指针的值,可以使用取址运算符(&)将某个变量所指向的内存地址赋值给指针,格式如下:

数据类型 *指针变量;
指针变量=&变量名称; /* 变量名称已定义或声明 */

将指针变量address1指向一个已声明的整数变量num1,语句如下:

int num1 = 10;
int *address1;
address1 = &num1;

不能直接将指针变量的初始值设置为一个数值,否则会造成指针变量指向不合法的地址。例如:

int* piVal=10;  /* 不合法指令 */

对指针“既期待又怕受到伤害”的读者不用担心,接下来再举一个例子来说明。假设程序代码中声明了3个变量a1、a2与a3,其值分别为40、58、71。程序代码语句如下:

int a1=40, a2=58, a3=71;  /* 声明三个整数变量 */

假设这3个变量在内存中分别占用第102、200与202号地址。我们以*运算符声明3个指针变量p1、p2与p3,程序代码如下:

int *p1,*p2,*p3;        /* 使用 *符号声明指针变量 */

其中,p1、p与*p3前方的int表示这3个变量都指向整数类型。接下来,以&运算符取出a1、a2与a3三个变量的地址并存储至p1、p2与p3三个变量中,程序代码如下:

p1 = &a1;
p2 = &a2;
p3 = &a3;

p1、p2与p3三个变量的内容分别是102、200、202。

下面的范例程序用于示范上述内容中指针与地址的关系。注意:由于每台计算机在分配内存时或许会有不同的结果,因此大家在执行程序时并不一定会得到与本书相同的内存地址编号。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06    int a1=40, a2=58, a3=71;
07    int temp;
08    int *p1,*p2,*p3;
09    
10    
11      p1 = &a1; /* p1指向a1的地址 */ 
12      p2 = &a2; /* p2指向a2的地址 */ 
13      p3 = &a3; /* p3指向a3的地址 */
14  
15      printf("p1的地址:%p,*p1的内容:%d\n",p1,*p1);
16      printf("p2的地址:%p,*p2的内容:%d\n",p2,*p2);
17      printf("p3的地址:%p,*p3的内容:%d\n",p3,*p3);
18    
19    system("PAUSE");
20    return 0;
21  }
  • 第11行:将a1地址赋值给指针变量p1。
  • 第12行:将a2地址赋值给指针变量p2。
  • 第13行:将a3地址赋值给指针变量p3。
  • 第15~17行:输出p1、p2、p3与p1、p2、*p3的值。

以下这个程序是相当经典的指针使用范例,弄懂这个程序后,相信大家会对取值运算符与取址运算符有更清楚的认识。这个程序将进一步说明使用指针变量存取其指向变量的用法,重新改变指针变量的数据内容后,指向同一地址的变量内容也会随之改变。

01  #include <stdio.h>
02  #include<stdlib.h>
03  #include<string.h>
04  
05  int main()
06  {
07    int a1=40, a2=58, a3=71;
08    int temp;
09    int *p1,*p2,*p3;
10    
11    
12    p1 = &a1;/* p1指向a1的地址 */ 
13    p2 = &a2;/* p2指向a2的地址 */ 
14    p3 = &a3;/* p3指向a3的地址 */ 
15  
16    printf("变量 a1的值:%d,*p1的值:%d\n",p1,*p1);
17    printf("变量 a2的值:%d,*p2的值:%d\n",p2,*p2);
18    printf("变量 a3的值:%d,*p3的值:%d\n",p3,*p3);
19    
20    a1=101; /*重新设置a1的值 */ 
21    *p2=103; /*重新设置*p2的值 */ 
22    p3=p2; /* 将p3指向p2 */ 
23    printf("------------------------------------\n");
24    printf("变量 a1的值:%d,*p1的值:%d\n",p1,*p1);
25    printf("变量 a2的值:%d,*p2的值:%d\n",p2,*p2);
26    printf("变量 a3的值:%d,*p3的值:%d\n",p3,*p3);
27      printf("------------------------------------\n");
28      
29    system("PAUSE");
30    return 0;
31  }
  • 第12~14行:将p1、p2、p3分别指向整数变量a1、a2与a3。
  • 第20行:重新设置a1的值为101。可以看出第24行指向a1的*p1值也会改为101。
  • 第21行:重新设置*p2的值为103。可以看出第25行a2的值也会改为103。
  • 第22行:将p3指向p2,所以p3的值就是p2的值,但a3的值仍为71,并未改变。

多重指针

由于指针变量存储的是指向内存的地址,它本身所占有的内存空间也拥有一个地址,因此我们可以声明“指针的指针”(指向指针变量的指针变量)来存储指针所使用到的内存地址与存取变量的值,或者称为“多重指针”。

双重指针就是指向指针的指针,通常以两个*表示,也就是**。事实上,双重指针并不是一个很难理解的概念。大家可以想象原本的指针指向基本数据类型(例如整数、浮点数等),而双重指针同样是一个指针,只是指向另一个指针。双重指针的语法格式如下:

数据类型 **指针变量;

下面使用一个范例来说明。假设整数a1为10、指针ptr1指向a,而指针ptr2指向ptr1,程序代码如下:

int a1=10;      /*设置基本整数值a为10*/
int *ptr1, **ptr2;  /*整数指针 ptr1 与双重指针ptr2*/
ptr1=&a1;      /* 将a1地址赋值给ptr1 */
ptr2=&ptr1;        /* 将ptr1地址赋值给双重指针ptr2 */

其中,int*ptr2是双重指针,指向“整数指针”。intptr1存放的是a1变量的地址,ptr2变量存放的是ptr变量的地址。从图9-6可以发现,变量a1、指针变量ptr1以及双重指针变量ptr2都占有内存地址,分别为0022FF74、0022FF70与0022FF6C。

事实上,从单个指针intptr1来看,ptr1变量可以视为指向“int”类型的指针。而从双重指针intptr2来看,ptr2变量就是指向“int*”类型的指针。

下面的程序范例相当简单,主要是双重指针的声明与使用,若ptr1指向a1的地址,则ptr1=10。另外,ptr2指向ptr1的地址,因此ptr2=ptr1。经过两次“值引用运算符”的运算,得到**ptr2=10。

01  #include <stdio.h>
02  #include<stdlib.h>
03  
04  int main()
05  {
06    int a1=10;
07    int *ptr1,**ptr2;
08    
09    ptr1=&a1;/* ptr指向a1的地址 */ 
10     ptr2=&ptr1;/* ptr2指向ptr1的地址 */ 
11    
12   printf("变量a1的地址:%p,内容:%d\n",&a1,a1);
13    printf("变量ptr1的地址:%p,内容:%p,*ptr1:%d\n",&ptr1,ptr1,*ptr1);
14    printf("变量ptr2的地址:%p,内容:%p,**ptr2:%d\n",&ptr2,ptr2,**ptr2);
15    
16   system("PAUSE");
17    return 0;
18  }
  • 第9行:ptr指向a1的地址。
  • 第10行:ptr2指向ptr1的地址。
  • 第12、13行:可以发现&a1的地址和ptr是一样的,*ptr的值也和a1相同。
  • 第13、14行:&ptr1和ptr2相同,ptr1与ptr2相同,ptr1与**ptr2相同。

多重指针

既然有双重指针,那么是否有三重指针或者更多重的指针呢?当然有。就像前面所说的,双重指针是指向指针的指针,那么三重指针就是指向“双重指针”的指针,语法格式为:

数据类型 ***指针变量名称;

假设整数a1为10、指针ptr1指向a,而指针ptr2指向ptr1、指针ptr3指向ptr2,程序代码如下:

int a1=10;      /*设置基本整数值a为10*/
int *ptr1, **ptr2;  /*整数指针 ptr1 与双重指针ptr2*/
int ***ptr3;      /*三重指针 ptr3*/
ptr1=&a1;         /*将a1地址赋值给ptr1*/
ptr2=&ptr1;        /*将ptr1地址赋值给双重指针ptr2*/
ptr3=&ptr2;        /*将ptr2地址赋值给双重指针ptr3*/

除了原本的a1、ptr1、ptr2外,我们又新增了三重指针ptr3。通过ptr3=&ptr2可将双重指针ptr2的地址赋值给三重指针ptr3。因此,ptr3指针变量的内容为0022FF6C,也是ptr2的地址。接下来使用ptr3即可存取a变量的内容,*ptr3的值为10

所以,一重指针是“指向基本数据”的指针,双重指针是指向“一重指针”的指针,三重指针是“指向双重指针”的指针,其他更多重的指针可以此类推。例如下面为四重指针:

  int  a1= 10;
  int *ptr1 = &num;
  int **ptr2 = &ptr1;
  int ***ptr3 = &ptr2;
  int ****ptr4 = &ptr3;

下面的范例程序示范了三重指针的应用与实现方式,可根据相同的方法自行练习声明多重指针(注意大家屏幕上显示的内存地址可能与书中显示的不同)。

01  #include <stdio.h>
02  #include<stdlib.h>
03  
04  int main()
05  {
06    int a1=10;
07    int *ptr1,**ptr2;
08    int ***ptr3;
09    
10   ptr1=&a1;  /* ptr1是指向a1的指针 */
11    ptr2=&ptr1;/* ptr2是指向ptr1的指针 */
12    ptr3=&ptr2;/* ptr3是指向ptr2的指针 */ 
13    
14    printf("变量a1的地址:%p,内容:%d\n",&a1,a1);
15    printf("变量ptr1的地址:%p,ptr1的内容:%p,*ptr1:%d\n",&ptr1,ptr1,*ptr1);
16    printf("变量ptr2的地址:%p,ptr2的内容:%p,**ptr2:%d\n",&ptr2,ptr2,**ptr2);
17    printf("变量ptr3的地址:%p,ptr3的内容:%p,***ptr3:%d\n",&ptr3,ptr3,***ptr3);
18    
19   system("PAUSE");
20    return 0;
21  }
  • 第10行:ptr1是指向a1的指针。
  • 第11行:ptr2是指向ptr1的整数类型的双重指针。
  • 第12行:ptr3是指向ptr2的整数类型的三重指针。
  • 第16行:ptr2所存放的内容为ptr1的地址(&ptr1),ptr2为ptr1所存放的内容。我们可把ptr2看成*(*ptr2),也就是*(ptr),因此ptr2=ptr1=10。
  • 第17行:ptr3所存放的内容为ptr2的地址&ptr2),ptr3为ptr2所存放的内容。另外,ptr3为ptr2所存放的内容,ptr2可以看成(ptr2),因此ptr3=**ptr2=10。

认识指针运算

学会使用指针存储变量的内存地址后,我们也可以针对指针使用+运算符或-运算符进行运算。然而当你对指针使用这两个运算符时,并不是进行如数值般的加法或减法运算,而是针对所存放的地址运算,也就是向右或向左移动几个单元的内存地址,移动的单位视所声明的数据类型占用的字节数而定。

不过,指针的加法或减法运算只能针对常数值(如+1或-1)进行,不可以进行指针变量之间的相互运算。因为指针变量的内容是存放的地址,地址间的运算并没有任何实质意义,而且容易让指针变量指向不合法的地址。

递增与递减运算

可以换个角度来想,现实生活中的门牌号码以数字的方式呈现,是否能够运算呢?运算后又有什么样的意义呢?例如将中山路10号加2,其实是往门牌号码较大的一方移动两个号码,可以找到中山路12号;如果将中山路10号减2,就可以找到中山路8号。这样地址的加法与减法才有意义。

然而,将地址进行乘法与除法运算似乎就没有意义了。例如中山路20号乘以10虽能得到200号,但对于搜索住址不见得有实质的帮助;而中山路20号除以4更没有实质的意义。

由于不同的变量类型在内存中所占的空间不同,因此当指针变量加一或减一时,是以指针变量所声明类型的内存大小为单位决定向右或向左移动多少单位。例如,一个整数指针变量名称为piVal,当指针声明时所取得iVal的地址值为0x2004,之后piVal进行递增(++)运算,值将改变为0x2008,代码如下:

int iVal=10;
int* piVal=&iVal; /* piVal=0x2004 */
piVal++; /* piVal=0x2008 */

从下面的范例程序可以发现,因为整数类型占4个字节,所以指针每进行一次加一(++)运算,内存地址就会向右移动4个字节;每进行一次减一运算(–),内存地址就会向左移动4个字节。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04   int main()
05   {
06       int *int_ptr,no;    /* 声明整数类型指针 */
07       int_ptr=&no;/* 初始化指针 */ 
08       
09       printf("最初的int_ptr地址:\n"); 
10       printf( "int_ptr = %p\n", int_ptr);
11       int_ptr++;
12       printf("int_ptr++后的地址:\n"); 
13       printf( "int_ptr = %p\n", int_ptr);
14       int_ptr--;
15       printf("int_ptr--后的地址:\n"); 
16       printf( "int_ptr = %p\n", int_ptr);
17       int_ptr=int_ptr+2;
18       printf("int_ptr+2后的地址:\n"); 
19       printf( "int_ptr = %p\n", int_ptr);
20       int_ptr=int_ptr-2;
21       printf("int_ptr-2后的地址:\n"); 
22       printf( "int_ptr = %p\n", int_ptr);
23       
24       system("pause");
25       return 0;
26  }
  • 第6行:声明整数类型指针。
  • 第7行:初始化指针并给予合法地址。
  • 第10行:输出最初的int_ptr地址。
  • 第11行:执行int_ptr++的递增运算。
  • 第13行:可以发现输出的int_ptr地址向右移动了4个字节。
  • 第14行:执行int_ptr–的递减运算。
  • 第16行:可以发现输出的int_ptr地址又向左移动了4个字节。
  • 第17行:执行int_ptr=int_ptr+2的加法运算。
  • 第19行:可以发现输出的int_ptr地址又向左移动了4×2个字节。
  • 第20行:执行int_ptr=Int_ptr-2的减法运算。
  • 第21行:可以发现输出的int_ptr地址又向右移动了4×2个字节。

指针常数与数组

指针运算也可以用于数组存取的操作。之前我们介绍过使用取址运算符(&)获取数组元素的地址。假设程序中声明了一个数组int array[5],要求系统提供一块连续的内存区段能够存储5个整数类型的数据。数组名array指向这块连续内存空间的起始地址,“下标值”就是其他元素相对于第一个元素内存地址的“位移量”(Offset)。

使用数组名的指针常数来存取数据可以达到与使用数组下标存取数组元素相同的效果。在C语言中,存取array数组中的第i个元素通常使用a[i]。如果要以指针的形式存取数组的第i个元素,使用*(array+i)即可。使用语法如下:

数组名[下标值]= *(数组名+下标值)
或
数组名[下标值]= *(&数组名[下标值])

内存是线性构造,无论是一维还是多维数组,在内存中都是以线性方式为数组分配可用空间的,例如二维数组的名称也可以代表第一个元素的内存地址。例如以下声明:

int  arr[3][5];

在这个例子中,arr数组是一个3×5的二维数组,可以看成由3个一维数组组成,每个一维数组各有5个元素。因为数组名可以直接当成指针常数来使用,所以二维数组可以看成是一种双重指针的应用。例如,*(arr+0)表示数组中第一维维数为0的第一个元素的内存地址,也就是arr[0][0]*(arr+1)表示数组中第一维维数为1的第一个元素的内存地址,也就是arr[1][0]*(arr+i)表示数组中第一维维数为i的第一个元素的内存地址

如果想获取元素arr[1][1]的内存地址,就要使用*(arr+1)+1来取得,注意*运算符的优先级高于+运算符;如果要获取arr[2][3]的内存地址,就要使用*(arr+2)+3来取得,其他各个数组项依此类推。总之,要获取元素arr[i][j]的内存地址,就要使用*(arr+i)+j来取得

如果加上一个*取值运算符,也就是*(*(arr+i)+j),就可以使用双重指针取出二维数组arr[I][j]的元素值。

下面的范例程序说明如何使用指针常数表示二维数组元素的地址,并用双重指针打印二维数组中的元素值。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06      
07       int i,j,no[2][4]={312,16,35,65,52,111,77,80};
08       
09       for (i = 0; i < 2; i++ )
10        for ( j = 0; j < 4; j++ )
11     {
12       printf( "&no[%d][%d]=%p\t *(no+%d)+%d=%p\n",
13      i,j,&no[i][j],i,j,*(no+i)+j);
14      /* 输出二维数组的元素地址与使用指针表示数组元素的地址*/ 
15      printf( "*(*(no+%d)+%d) = %d\n", i, j, *(*(no+i)+j) );
16      /*打印arr[i][j]元素值*/
17      printf("===========================================\n");
18      }
19         
20        system("pause");
21        return 0;
22   }
  • 第12、13行:输出使用“&”(取址运算符)获取的二维数组元素的地址与使用指针表示二维数组元素的地址。可以发现,要获取元素no[i][j]的内存地址,就要使用*(no+i)+j来取得。
  • 第15行:使用双重指针打印no[i][j]元素值。

指针变量与数组

在编写C程序代码时,大家不但可以把数组名直接当成一种指针常数来使用,也可以将指针变量指向数组的起始地址,并借助指针变量间接存取数组中的元素值。指针变量获取一维数组地址的方式如下:

数据类型 *指针变量=数组名;   
或
数据类型 *指针变量=&数组名[0]; 

注意:尽管数组可以直接当成指针常数来使用,数组名是数组第一个元素的地址,不过由于数组的地址是只读的,因此不能改变其值,这点是和指针变量最大的不同。例如:

int arr[2],value=100;
int *ptr=&value;
arr=ptr;  /* 此行不合法,因为arr是只读的,不能重新设置其值 */

由于二维数组是占用连续的内存空间,因此可借助指针变量指向二维数组的起始地址获取数组的所有元素值。声明一个指针变量并让它指向一个二维数组的起始地址,方法如下:

数据类型 指针变量=&二维数组名[0][0];  

声明一个int数据类型的二维数组int no[n][m],并将起始地址值赋给指针变量*ptr,这时如果使用指针变量*ptr存取二维数组中第i行的第j列元素,可以使用如下公式取出该元素值:

 *(ptr+i*m+j);

高级指针处理

在C语言的语法中,指针对一些初学者来说是较难掌握的,指针使用了“间接引用”的概念,使得初学者往往无法将内存地址与变量值之间的关系直接串联在一起。不过,如果想要真正掌握C语言的高级程序设计技能,熟悉与活用指针是必要的基本功。

使用指针时也要相当小心,否则容易造成内存访问上的问题,从而引发不可预期的后果。

指针与字符串

由于字符串在C语言中以字符数组实现,指针可以应用于数组,因此也可以应用于字符串。事实上,使用指针变量的概念处理字符串比使用数组方便许多。

使用指针设置字符串

之前介绍的字符串声明都是以字符数组来实现的,其中字符串与字符数组唯一的不同在于字符串最后一定要连接一个空字符(\0),以表示字符串结束了,以下是两种字符串表示法:

char name[] = { 'J', 'o', 'h', 'n', '\0'};  
或
char name[] = "John"; 

如果要使用指针变量表示字符串,就要使用字符指针变量指向字符串,声明格式如下:

char *指针变量="字符串内容";
例如:
char *ptr = "How are you ?"; 

下面的范例程序分别以字符数组与指针变量表示字符串,用户可输入一个字符串,并将此字符串输出在屏幕上。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06      
07        char name[15];/*声明字符数组*/ 
08        char *number="Please input your name:";
09        /* 声明字符串指针*/ 
10        printf("%s",number);
11        scanf("%s",&name);/*输入字符串*/ 
12        printf("Your name is:%s",name);
13        printf("\n");      
14  
15        system("pause");
16        return 0;
17   }
  • 第7行:声明字符数组。
  • 第8行:将指针变量指向字符串Please input your name:。
  • 第10行:输出number的数据值,在此不用加上“*”号。

如果使用字符数组,这个字符数组的值指向此字符串第一个字符的起始地址,而且为常数,无法修改也不能做任何运算。如果使用指针建立字符串,此指针的值也是指向字符串第一个字符的起始地址,不过是变量形式,就能够进行运算。请看以下程序代码:

char name[15];
char *number="Please input your name:";
name++; /* 不合法的指令,字符数组是指针常数不可运算 */
number++;/* 合法的指令,字符指针变量可以运算 */

下面的范例程序将要实现字符串指针的运算,大家可以观察输出结果及其所代表的意义,特别是当输出字符串中的某个字符时,除了要使用%c格式化符号外,还要加上“*”来取值。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06      
07        char *number="President";
08        /* 声明字符串指针*/ 
09        number++;/* 字符串指针加1的运算 */     
10        printf("%c\n",*(number+0));/*取出第一个字符*/ 
11        printf("%s\n",number);/*执行加1运算后的字符串*/ 
12        
13        system("pause");
14        return 0;
15   }
  • 第7行:声明字符串指针,并设置值为President。
  • 第9行:字符串指针加1的表达式,是指针移动到原来字符串的第二个字符。
  • 第10行:输出此字符串的第一个字符。
  • 第11行:输出执行加1运算后的新字符串。

指针数组

我们知道其他基本数据类型的变量都可以声明成数组,当然指针也可以声明成指针数组,同时结合指针与数组的功能。每个指针数组中的元素都是一个指针变量,而元素值是指向其他数据类型变量的地址。一维指针数组的声明格式如下:

数据类型 *数组名[元素名称]; /* 数组名前加上* 运算符 */

例如,声明一个名称为p的整数指针数组的语句,3个元素(p[i])可指向一个整数值。另外,声明一个名称为ptr的浮点数指针数组,并包含4个指向浮点数的元素,分别是ptr[0]、ptr[1]、ptr[2]与ptr[3]:

int *p[3];
float *ptr[4];

一维指针数组在存储字符串上相当实用。之前介绍过使用二维字符数组存储字符串数组,例如一个字符串数组的声明方式如下:

char name[4][11] = { "apple", "watermelon", "Banana", "orange" };

上面的语句将声明一个4×11的数组(包括每个字符串末尾的\0字符),使用这种方式声明字符串数组的缺点是:每个字符串必须拥有11个字符类型的内存空间,这是为了满足最长字符串的需求,如果有的字符串不到11个字符,对整个存储空间来说就是一种严重的浪费,这样会花费许多内存空间来存储空字符\0。

为了避免内存空间的浪费,可以使用“指针数组”存储字符串。我们可以将上述声明更改为以下方式:

char *name[4] = { "apple", "watermelon", "banana", "orange" };

这种声明方式是将指针指向各个字符串的起始地址,从而建立字符串的数组。这时name[0]指向字符串apple,name[1]指向字符串watermelon,name[2]指向字符串b anana,name[3]指向字符串orange。

每个数组元素name[i]都用来存储内存的地址,各自存储了指定字符串的内存地址,这样编译程序会自动分配正好足够使用的字符空间存储该字符串,从而不再浪费内存空间存储无用的空字符。

下面的范例程序将使用一维指针数组存储5个字符串,声明指针数组name并将每个元素指向不同长度的字符串。

01  #include <stdio.h>
02  #include <stdlib.h> 
03  
04   int main()
05   {
06       char *name[5] =  { "John", "David", "Kelvin", "Steve","Wilson" };
07       int i;
08   
09       for ( i = 0; i < 5; i++ )
10    printf( "name[%d] = %s\n", i, name[i]);
11    /* 输出指针name[i]所指向的字符串 */ 
12       
13       system("pause");
14       return 0;
15   }
  • 第6行:声明指针数组name,并将每个元素指向不同长度的字符串。
  • 第9、10行:使用for循环输出指针name[i]所指向的字符串。

动态分配

“动态分配”(Dynamic Allocation)的基本精神是让内存使用起来更有弹性,也就是在程序运行时根据用户的设置与需求适当地分配所需要的内存空间。

许多程序设计人员经常苦恼如何声明适当的数组大小,如果声明的长度过大,内存使用效率就会不佳,声明的长度过小又容易面临存储空间不足的问题,这时就可以使用动态分配数组的方式。

动态分配变量

在C语言中,可以分别使用malloc()与free()函数在程序运行期间动态分配与释放内存空间,这两个函数定义在头文件stdlib.h中。动态分配变量的方式如下,n=1表示分配一个变量:

数据类型* 指针名称=(数据类型*)malloc(sizeof(数据类型)*n);

如果使用动态分配内存,分配的内存不再使用时一定要清除掉并归还给系统。否则,这类内存就会一直被占用导致整个系统的总内存慢慢减少,造成“内存泄漏”(memory leak)现象。也就是说,变量或对象在使用动态方式分配内存后,分配的内存不再使用时必须进行释放内存的操作;如果变量或对象使用静态方式分配内存(如常规变量的声明),那么内存的释放由编译程序自动完成,不再需要特别操作。C语言中释放动态分配变量必须使用free关键字,使用方式如下:

free(指针名称);

举个简单的例子:

int *piVal=(int*)malloc(sizeof(int));/*指针变量指向动态分配的内存空间*/
  …
free(piVal); /*释放此变量的内存*/

下面的范例程序将动态分配一个单精度浮点数变量的内存空间,输入整数数值并打印出所指向的地址与内容,最后使用free()函数释放空间。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06      float* piF=(float*)malloc(sizeof(float));
07      /* 将指针指向浮点数动态分配的内存空间 */
08      
09      printf("请输入piF的值 =");
10      scanf("%f",piF);/* 输入piF的值 */ 
11      printf("\n");
12      printf("piF所指向的地址内容为 %f\n",*piF);
13      printf("piF所指向的地址为 %p\n", piF);
14  
15      free(piF);/* 将指针piF的空间释放 */
16      
17      system("pause");  
18      return 0;
19  }
  • 第6行:声明浮点数指针piF,并将指针指向浮点数动态分配的内存空间。
  • 第10行:自行输入piF的值。
  • 第12、13行:输出piF所指向的地址与存储内容。
  • 第15行:将指针piF的空间释放。

动态分配一维数组

通常将数据声明为数组时在编译阶段就要确定数组的长度,但这样很容易造成内存浪费或无法满足程序所需,这时可考虑采用动态分配数组的方式。例如动态分配一维数组,n=数组长度:

数据类型* 指针名称=(数据类型*)malloc(n*sizeof(数据类型));

在程序运行期间,如果动态分配的一维数组不再需要,就将其释放。释放动态分配一维数组的方式如下:

free(指针名称);

例如,按照整数类型动态分配一个长度为8个元素的连续整数数组内存空间,方法如下:

int* piArrVal=(int*)malloc(8*sizeof(int));
/*指针变量指向动态分配的内存空间*/
…
free(piArrVal); /*释放此数组的内存*/

接下来,以一个计算成绩的范例程序为大家详细说明动态分配一维数组的用法。这个程序允许用户自行输入学生人数,并以人数配合指针变量*grades动态分配一维数组。教师可以按序输入成绩,最后显示所有学生的成绩并计算出平均分。

在这个程序中,首先提示用户输入学生人数,并将该数值存入变量n中。接着,以n变量的值作为动态分配数组的长度,由*grades指针变量记录该数组的起始地址。格式如下:

grades=(int *)malloc(n*sizeof(int));

其中,malloc函数需要传入的参数作为数组大小及每个元素数据类型的大小,分别为n与sizeof(int)。分配完毕后,此函数会返回一个指向int类型的指针,并由*grades指针变量接收。

执行完这条语句后,动态分配的内存可以视为数组grades[n],具有n个元素。所以,稍后存取该数组时可以使用i变量(从0开始到n-1)存取数组grades[i]的元素。不再使用grades[i]数组时,记得要用free(grades)语句将内存释放,并交还给系统。

下面的范例程序将实现计算成绩的程序并说明动态分配一维数组的用法,其中数组元素个数n和学生成绩可由用户自行输入。

01  #include <stdio.h>
02  #include<stdlib.h>
03  
04  int main()
05  {
06    int *grades;  /*学生成绩数组指针*/ 
07    int n;    /*学生人数*/ 
08    int i;      
09    int sum=0;    /*成绩总和 */
10    
11      printf("请输入学生人数:");
12       scanf("%d",&n);
13       grades=(int *)malloc(n*sizeof(int));
14    /* 将指针grades指向动态分配的内存空间 */
15       printf("共有%d位学生\n",n);
16       printf("\n");
17    
18      for(i=0;i<n;i++){
19      printf("请输入第%d位学生的成绩:",i+1);
20      scanf("%d",&grades[i]);
21      sum+=grades[i];  /* 累加成绩 */      
22    }
23       printf("==座号==学生成绩==\n");
24    
25      for(i=0;i<n;i++){
26      printf("%4d    %4d\n",i+1,grades[i]);    
27    }  
28      printf("==================\n");
29      printf("共有%d位学生,平均成绩为%.2f\n",n,(float)sum/(float)n);
30    /* 计算平均成绩 */ 
31     free(grades);/* 释放指针指向的内存空间 */
32  
33      system("PAUSE");
34      return 0;
35  }
  • 第6行:声明学生成绩数组指针grades。
  • 第12行:用于输入要产生的动态一维数组的个数n。
  • 第13行:将整数指针指向动态分配一维数组的内存空间。
  • 第18~22行:使用for循环输入学生成绩。
  • 第21行:使用sum变量累加成绩。
  • 第29行:输出学生人数与平均成绩。
  • 第31行:释放指针指向的内存空间。

动态分配字符串

字符串其实就是字符数组,如果在程序运行前无法得知字符串的长度,也就是字符数组元素个数,就可以使用动态数组进行字符串的内存分配。

以动态指针分配一维整数数组时,无法以sizeof()函数求得该数组的大小。不过,对于字符串而言,可以使用strlen()函数取得字符串长度,使用strcat()函数串接两个字符串。

下面的范例程序将声明数组char*name,这是一个字符指针,用来动态分配一维字符数组,并要求用户自行输入字符串的长度,在此程序中将会使用到strlen()函数与strcat()函数,必须包含头文件。

01  #include <stdio.h>
02  #include <stdlib.h>
03  #include <string.h>/* 使用strlen()函数与strcat()函数 */ 
04  
05  int main()
06  {
07    char *name;
08    int i;
09    
10   printf("请输入英文字符串的长度:");
11    scanf("%d",&i);
12    name = (char *)malloc((i+1)*sizeof(char));
13    /* i+1是为了将字符串的结尾字符(\0)加入字符串的最后*/ 
14    printf("请输入英文字符串:");
15    scanf("%s",name);
16    strcat(name,"\0");
17    printf("-%s-\n",name);
18    printf("字符串的长度:%d\n",strlen(name));
19    
20      system("PAUSE");
21      return 0;
22  }
  • 第3行:使用了strlen()函数,因此要包含string.h头文件。
  • 第12行:i+1字符是为了将字符串的结尾字符(\0)加入字符串的最后。
  • 第16行:strcat()函数将空字符连接到name后面。
  • 第18行:使用strlen()函数求出此字符串的长度。

动态分配多维数组

动态分配多维数组与一维数组的声明方式类似,不同的地方在于多维数组由第一维逐一分配内存到第n维为止。例如,声明一个n×m动态分配内存的二维数组,可以使用双重指针分配第一维部分的内存,格式如下:

数据类型** 指针名称=(数据类型**)malloc(数组长度n*sizeof(数据类型*));

上述声明的意义是按照数据类型动态分配一个长度为n的连续内存空间,并将分配的空间地址赋值给双重指针变量。当完成第一维分配的声明后,再分配第二维数组。

简单来说,分配动态一维整数数组使用的是“指向整数的指针”。在分配二维数组时,可以将二维数组看成有“多个一维整数数组”,因此需要一个“指向”整数指针“的指针”来实现。其格式如下:

指针名称[0]=(数据类型*)malloc(m*sizeof(数据类型));
指针名称[1]=(数据类型*)malloc(m*sizeof(数据类型));
指针名称[2]=(数据类型*)malloc(m*sizeof(数据类型));
…
指针名称[m-1]=(数据类型*)malloc(m*sizeof(数据类型));

例如,分配一个6行与3列的二维数组,可以声明一个双重指针变量char**star,并使用star双重指针分配一个具有6个元素的一维数组:

star=(char **)malloc(6*sizeof(char *));

接下来使用for循环针对每一个int*指针分别产生一个具有3个元素的一维数组,每个元素的数据类型都是char。格式如下:

star[i]=(char *)malloc(col*sizeof(char));

分配完成后,每个元素都是char类型的数据。如此就完成了二维数组star[6][3]的分配,此数组共6×3=18个元素。

“二维字符数组”也可以看成“一维字符串数组”。换句话说,我们示范的是一个6×3=18个元素的字符数组,实际上也是6个元素的字符串数组,每个数组允许的长度为3。

下面的范例程序声明了一个双重指针变量char**star,要求用户输入行数与列数,接下来针对数组中每个元素分配数据类型为字符的一维数组,然后输出此二维数组中存放的字符。

01  #include <stdio.h>
02  #include<stdlib.h>
03  
04  int main()
05  {
06    char **star;  /* 声明字符指针 */ 
07    int row,col;
08    int i,j;
09    
10   printf("请输入行数:");
11    scanf("%d",&row);
12    printf("请输入列数:");
13    scanf("%d",&col);
14    
15    star=(char **)malloc(row*sizeof(char *));
16    /*使用star双重指针分配一个具有row个元素的一维数组*/
17    for(i=0;i<row;i++){
18      star[i]=(char *)malloc(col*sizeof(char));
19    /*产生一个具有col个元素的一维数组*/  
20    for(j=0;j<col;j++){
21        star[i][j]='*';
22      }  
23    }
24    
25      for(i=0;i<row;i++){
26      for(j=0;j<col;j++){
27        printf("%c ",star[i][j]);
28      }/* 输出此二维数组的内容 */ 
29      printf("\n");
30    }  
31    
32   system("PAUSE");
33    return 0;
34  }
  • 第6行:声明字符双重指针。
  • 第11、13行:输入此动态分配数组的行数和列数。
  • 第15行:使用star双重指针分配一个具有row个元素的一维数组。
  • 第18行:产生一个具有col个元素的一维数组。
  • 第25~27行:输出此二维数组的内容。

接下来修改上述范例程序,继续使用二维数组的概念,允许用户将十二个月份的名称输入到动态字符串数组中。首先,声明一个字符类型的双重指针char**month用来分配二维字符数组(一维字符串数组)。

为了让用户能够输入各个月份的名字并且存储在字符串数组中,我们用了12行的字符串数组,每个字符串最大长度为10。考虑月份名称最长的为九月(September),长度为9,再加上字符串结尾字符(\0),因而每个字符串的长度为10个字符。事实上,就month[12][10]来看,是一个二维字符数组,但就每一行month[0]month[11]而言,是字符串类型的一维数组。

下面的范例程序将动态分配一个有12个字符串的数组,每个字符串可以存储10个字符,最后输出数组中的每个字符串。

01  #include <stdio.h>
02  #include<stdlib.h>
03  
04  int main()
05  {
06    char **month;    
07    int row,col;
08    int i,j;
09    
10   char month_name[12][10]=
11       {"January","February","March","April","May","June","July","August",
12        "September","October","November","September"};
13     /* 声明二维字符数组存储12个月份的名称 */ 
14       month=(char **)malloc(12*sizeof(char *));
15     /* 动态分配12个字符串 */ 
16     for(i=0;i<12;i++){
17      month[i]=(char *)malloc(10*sizeof(char));
18      /* 动态分配10个字符 */ 
19      month[i]=month_name[i];
20      }
21     for(i=0;i<12;i++){
22      printf("%d 月的英文名称:%s\n",i+1,month[i]);
23     }
24    
25      system("PAUSE");
26    return 0;
27  }
  • 第10~12行:声明二维字符数组存储12个月份的名称。
  • 第14行:以双重指针动态分配12个字符串。
  • 第17行:以指针动态分配10个字符。
  • 第21~23行:输出此动态分配字符串数组的所有元素值。

通用类型指针

在C语言中也有通用类型指针,用来指向特定的内存地址,但不指定数据类型。这样的指针可以通过转型的方式将所指向的内存地址转成各种数据类型。通用类型指针的语法如下:

void *p;

基本上,通用型的指针void*可以指向任何类型的数据,并且可以双向转换,也就是将特定数据类型的指针转为通用类型指针,再将通用类型指针转回特定数据类型的指针。

下面的范例程序用于示范通用类型指针的基本用法,首先声明一个通用类型指针void*p,然后示范转换为各种类型的指针。

01  #include <stdio.h>
02  #include <stdlib.h>
03  
04  int main()
05  {
06     void * p=(void *)100;  
07     /*void *p 指针原始值为 (void *)类型的值 100*/
08    printf("Address: %p\n",p);
09    /*(void *) 100的地址为 0x00000064*/
10     printf("Integer: %d\n",(int*)p);
11     /*将(void *)100以整数类型转型,值为 100*/
12       printf("Character: %c\n",(char*)p);  
13     /*以字符类型转型,得到的结果是小写的“d”字符*/
14    
15    system("PAUSE");
16     return 0;
17  } 
  • 第8行:以%p的方式查看其内容,可以发现(void*)100的地址为0x00000064。
  • 第10行:将(void*)100以整数类型转型,值为100。如果以字符类型转型,得到的结果是小写的“d”字符。
  • 第12行:如果查阅ASCII字符映射表,可以发现100就是d字符的编码。

Search

    微信好友

    博士的沙漏

    Table of Contents