Menu Close

指针

指针是C语言中广泛使用的一种数据类型。运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构;能很方便地使用数组和字符串;并能象汇编语言一样处理内存地址,从而编出精练而高效的程序。指针极大地丰富了C语言的功能。学习指针是学习C语言中最重要的一环,能否正确理解和使用指针是我们是否掌握C语言一个标志。

同时,指针也是C语言中最为困难的一部分,在学习中除了要正确理解基本概念,还必须要多编程,上机调试。只要作到这些,指针也是不难掌握的。

通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。所以,想要成为一名优秀的 C 程序员,学习指针是很有必要的。

正如您所知道的,每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址。

指针是一个变量,用于存储另一个变量的地址。 指针也可以用来引用另一个指针函数。 指针可以增加/减少,即指向下一个/上一个存储位置。 指针的目的是节省内存空间并缩短执行时间。

什么是指针 ?

在计算机中,所有的数据都是存放在存储器中的。一般把存储器中的一个字节称为一个内存单元,不同的数据类型所占用的内存单元数不等,如整型量占 2 个单元,字符量占 1个单元等,在前面已有详细的介绍。为了正确地访问这些内存单元,必须为每个内存单元编上号。根据一个内存单元的编号即可准确地找到该内存单元。内存单元的编号也叫做地址。既然根据内存单元的编号或地址就可以找到所需的内存单元,所以通常也把这个地址称为指针。 内存单元的指针和内存单元的内容是两个不同的概念。

可以用一个通俗的例子来说明它们之间的关系。我们到银行去存取款时, 银行工作人员将根据我们的帐号去找我们的存款单, 找到之后在存单上写入存款、取款的金额。在这里,帐号就是存单的指针, 存款数是存单的内容。对于一个内存单元来说,单元的地址即为指针,其中存放的数据才是该单元的内容。在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。

图中,设有字符变量 C,其内容为“K”(ASCII 码为十进制数 75),C 占用了 011A 号单元(地址用十六进数表示)。设有指针变量 P,内容为 011A,这种情况我们称为 P 指向变量 C,或说 P 是指向变量 C 的指针。

严格地说,一个指针是一个地址,是一个常量。而一个指针变量却可以被赋予不同的指针值,是变量。但常把指针变量简称为指针。为了避免混淆,我们中约定:“指针”是指地址,是常量,“指针变量”是指取值为地址的变量。定义指针的目的是为了通过指针去访问内存单元。

既然指针变量的值是一个地址,那么这个地址不仅可以是变量的地址,也可以是其它数据结构的地址。在一个指针变量中存放一个数组或一个函数的首地址有何意义呢? 因为数组或函数都是连续存放的。通过访问指针变量取得了数组或函数的首地址,也就找到了该数组或函数。这样一来,凡是出现数组,函数的地方都可以用一个指针变量来表示,只要该指针变量中赋予数组或函数的首地址即可。这样做,将会使程序的概念十分清楚,程序本身也精练,高效。在C语言中,一种数据类型或数据结构往往都占有一组连续的内存单元。 用“地址”这个概念并不能很好地描述一种数据类型或数据结构,而“指针”虽然实际上也是一个地址,但它却是一个数据结构的首地址,它是“指向”一个数据结构的,因而概念更为清楚,表示更为明确。 这也是引入“指针”概念的一个重要原因。

指针如何工作 ?

如果我们声明一个int类型的变量v,则v实际上将存储一个值。

v现在等于零。

但是,每个变量除自身的值外,还具有其内存的地址(或简单地说,位于内存中的位置)。 可以通过在变量名称前加上连字号(&)号来检索其内存地址。

如果在屏幕上打印变量的地址,它将看起来像一个完全随机的数字(而且,每次运行时它可能有所不同)。

实例:


#include <stdio.h> 
int main ()
{
   int  v = 0;  
   printf("V address %d\n", &v);   
   return 0;
}

什么是指针? 指针不存储值,而是存储变量的地址。指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。

指针变量声明的一般形式为:

Int *y = &v;

变量 – 存储在存储/内存地址中的值,该内存地址可以被跟踪
指针 – 指向另一个变量的存储/内存地址的变量

两个有关的运算符:

1) &:取地址运算符。
2) *:指针运算符(或称“间接访问” 运算符)。

C语言中提供了地址运算符&来表示变量的地址。
其一般形式为:
&变量名;
如&a 表示变量 a 的地址,&b 表示变量 b 的地址。变量本身必须预先说明。

声明一个指针

就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。 然后在程序中才能使用它们,指针只要符合C的命名规则。 指针声明具有以下形式:

 data_type * pointer_variable_name; 
  • data_type 是指针的基本类型,它必须是一个有效的 C 数据类型,反映了指向的变量的数据类型;
  • pointer_variable_name 是指针变量的名称;
  • * – 用来声明指针的星号 (间接寻址运算符)* 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。
int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;     /* 一个字符型的指针 */

所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

初始化指针

指针变量同普通变量一样,使用之前不仅要定义说明,而且必须赋予具体的值。未经赋值的指针变量不能使用,否则将造成系统混乱,甚至死机。指针变量的赋值只能赋予地址,决不能赋予任何其它数据,否则将引起错误。在C语言中,变量的地址是由编译系统分配的,对用户完全透明,用户不知道变量的具体地址。

声明指针之后,我们像使用变量地址的标准变量一样对其进行初始化。 如果指针未在程序中初始化和使用,则结果将不可预测,并且可能会造成灾难性的后果。

要获取变量的地址,我们使用连字号(&)运算符,并在需要其地址的变量名称之前放置。

 

指针初始化使用以下语法完成:

pointer = &variable; 

简单范例:

#include <stdio.h>
int main()
{
   int a=10;    //variable declaration
   int *p;      //pointer variable declaration
   p=&a;        //store address of variable a in pointer p
   printf("Address stored in a variable p is:%x\n",p);  //accessing the address
   printf("Value stored in a variable p is:%d\n",*p);   //accessing the value
   return 0;
}

%x以十六进制数形式输出整数

指针类型

NULL 指针

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针。

NULL 指针是一个定义在标准库中的值为零的常量。请看下面的程序:

#include <stdio.h>
int main()
{
	int *p = NULL; 	//null pointer
	printf("The value inside variable p is:\n%x",p);
	return 0;
}

在大多数的操作系统上,程序不允许访问地址为 0 的内存,因为该内存是操作系统保留的。然而,内存地址 0 有特别重要的意义,它表明该指针不指向一个可访问的内存位置。但按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

如需检查一个空指针,您可以使用 if 语句,如下所示:

if(ptr)     /* 如果 p 非空,则完成 */
if(!ptr)    /* 如果 p 为空,则完成 */

VOID 指针

在C编程中,void指针也称为通用指针。 它没有任何标准数据类型。 通过使用关键字void创建一个空指针。 它可以用来存储任何变量的地址。
以下程序说明了void指针的用法:

#include <stdio.h>
int main()
{
   void *p = NULL; 	//void pointer
   printf("The size of pointer is:%d\n",sizeof(p));
   return 0;
}

Wild 指针

如果未将指针初始化为任何内容,则称其为野指针。 这些类型的指针效率不高,因为它可能指向某些未知的内存位置,这可能会导致程序出现问题,并可能导致程序崩溃。 使用野生指针时,应始终小心。
以下程序说明了野指针的用法:

#include <stdio.h>
int main()
{
   int *p; 	//wild pointer
   printf("\n%d",*p);
   return 0;
}

 

直接和间接访问指针

在C语言中,有两种等效的方法来访问和操作变量内容

  • 直接访问:我们直接使用变量名
  • 间接访问:我们使用指向变量的指针

范例:

#include <stdio.h>

/* Declare and initialize an int variable */
int var = 1;
/* Declare a pointer to int */
int *ptr;

int main( void )
{
    /* Initialize ptr to point to var */
    ptr = &var;
    /* Access var directly and indirectly */
    printf("\nDirect access, var = %d", var);
    printf("\nIndirect access, var = %d", *ptr);
    /* Display the address of var two ways */
    printf("\n\nThe address of var = %d", &var);
    printf("\nThe address of var = %d\n", ptr);
    /*change the content of var through the pointer*/
    *ptr=48;
    printf("\nIndirect access, var = %d", *ptr);
    return 0;
}

指针的算法

下图总结了指针操作的算法

操作符优先顺序

使用指针时,我们必须遵守以下优先级规则:

  • 运算符*和&与一元运算符具有相同的优先级(否定!,递增++,减量–)。一些只需要一个操作数的运算符称为一元运算符(或单目运算符)
  • 在同一表达式中,一元运算符*,&,!,++,– 优先顺序从右到左进行

如果P指针指向X变量,则* P可以在可以写入X的任何地方使用。

假设:

int X =10
int *P = &X;
以下表达式为真
表达式 相当表达式
Y=*P+1

*P=*P+10

*P+=2

++*P

(*P)++

Y=X+1

X=X+10

X+=2

++X

X++

最后一个示例中,括号()是必要的,因为一元运算符从右向左运算。如果没有括号,P就先加加了,指针指向的存储位置加一,里面的值当然就不一样了。

下表显示了处理指针时可以使用的算术和基本运算。

操作符 Explanation (解释)
Assignment (赋值) int *P1,*P2 P1=P2; P1 and P2 point to the same integer variable (P1和P2指向同一整数变量)
Incrementation and decrementation (++,——) Int *P1; P1++;P1– ;
添加偏移量(常量) 例如,P1+5;  指针增加N次

 

指针和数组

一个变量有一个地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。所谓数组的指针是指数组的起始地址,数组元素的指针是数组元素的地址。一个数组是由连续的一块内存单元组成的。数组名就是这块连续内存单元的首地址。一个数组也是由各个数组元素(下标变量)组成的。每个数组元素按其类型不同占有几个连续的内存单元。一个数组元素的首地址也是指它所占有的几个内存单元的首地址。

定义一个指向数组元素的指针变量的方法,与以前介绍的指针变量相同。

例如:

int a[10]; /*定义 a 为包含 10 个整型数据的数组*/
int *p; /*定义 p 为指向整型变量的指针*/

应当注意,因为数组为 int 型,所以指针变量也应为指向 int 型的指针变量。下面是对指针变量赋值:
p=&a[0];
把 a[0]元素的地址赋给指针变量 p。

也就是说,p 指向 a 数组的第 0 号元素。

C 语言规定,数组名代表数组的首地址,也就是第 0 号元素的地址。因此,下面两个语句等价:
p=&a[0];
p=a;
在定义指针变量时可以赋给初值:
int *p=&a[0];
它等效于:
int *p;
p=&a[0];

我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。下面的程序递增变量指针,以便顺序访问数组中的每一个元素:


#include <stdio.h>
int main()
{
    int a[5]={1,2,3,4,5};   //array initialization
    int *p;     //pointer declaration
               /*the ptr points to the first element of the array*/

    p=a; /*We can also type simply ptr==&a[0] */
    
    printf("Printing the array elements using pointer\n");
    for(int i=0;i<5;i++)    //loop for traversing array elements
    {
        	printf("\n%x",*p);  //printing array elements
        	p++;    //incrementing to the next element, you can also write p=p+1
    }
    return 0;
}

把特定的数字添加到指针,会把指针位置移动到通过加法运算获得的值。

假设p是一个指针,该指针指向内存位置0,如果我们执行以下加法操作,则p + 1将以这种方式执行:

由于p当前指向位置0,在加1之后,因此该值将变为1,因此指针将指向存储位置1。

通过指针引用数组元素

C 语言规定:如果指针变量 p 已指向数组中的一个元素,则 p+1 指向同一数组中的下一个元素。引入指针变量后,就可以用两种方法来访问数组元素了。
如果 p 的初值为&a[0],则:

1) p+i 和 a+i 就是 a[i]的地址,或者说它们指向 a 数组的第 i 个元素。

2) *(p+i)或*(a+i)就是 p+i 或 a+i 所指向的数组元素,即 a[i]。例如,*(p+5)或*(a+5)
就是 a[5]。
3) 指向数组的指针变量也可以带下标,如 p[i]与*(p+i)等价。

根据以上叙述,引用一个数组元素可以用:

1) 下标法,即用 a[i]形式访问数组元素。在前面介绍数组时都是采用这种方法。
2) 指针法,即采用*(a+i)或*(p+i)形式,用间接访问的方法来访问数组元素,其中 a是数组名,p 是指向数组的指针变量,其处值 p=a。

举例1:输出数组中的全部元素。(通过数组名计算元素的地址,找出元素的值)

main(){
 int a[10],i;
 for(i=0;i<10;i++)
 *(a+i)=i;
 for(i=0;i<10;i++)
 printf("a[%d]=%d\n",i,*(a+i));
}

举例2:输出数组中的全部元素。(用指针变量指向元素)

main()
{
 int a[10],i;
 int *p;
 p=a;
 for(i=0;i<10;i++)
    *(p+i)=i;
 for(i=0;i<10;i++)
    printf("a[%d]=%d\n",i,*(p+i));
}

几个注意的问题:

1) 指针变量可以实现本身的值的改变。如 p++是合法的;而 a++是错误的。因为 a 是数组

名,它是数组的首地址,是常量。

2) 要注意指针变量的当前值。请查找以下错误:

main()
{
  int *p,i,a[10];
  p=a;
  for(i=0;i<10;i++)
  *p++=i;
  for(i=0;i<10;i++)
  printf("a[%d]=%d\n",i,*p++);
}

main()
{ 
  int *p,i,a[10];
  p=a; 
  for(i=0;i<10;i++) 
  *p++=i; 
  p=a; 
  for(i=0;i<10;i++) 
  printf("a[%d]=%d\n",i,*p++); 
}

3) 从上例可以看出,虽然定义数组时指定它包含 10 个元素,但指针变量可以指到数组以后的内存单元,系统并不认为非法。

4) *p++,由于++和*同优先级,结合方向自右而左,等价于*(p++)。

5) *(p++)与*(++p)作用不同。若 p 的初值为 a,则*(p++)等价 a[0],*(++p)等价 a[1]。

6) (*p)++表示 p 所指向的元素值加 1。

7) 如果 p 当前指向 a 数组中的第 i 个元素,则

*(p–)相当于 a[i–];

*(++p)相当于 a[++i];

*(–p)相当于 a[–i]。

a[i++]和a[++i]的区别:

相同点:都加1,都使i的值变成下一个元素的序号。
异同点:a[i++]中i++是后自增,必须先使用当前元素的值再使用下一个元素的值,a[++i]中++i是前自增,可以直接使用下一个元素的值。

举例:

#include <stdio.h>
int main ()
{
   int a[3] = {1,2,3};
   int i = 0;
   printf("%d\n",a[i++]);//本输出的值为1,因为是i++,所以是先使用a[0]的值,再加上1,即先输出a[0]的值。
   i = 0;
   printf("%d\n",a[++i]);//本输出的值为2,因为++i,所以直接使i加1,即输出a[1]的值。
   return 0;
}

 

指针和字符串

字符串是char数组,以空字符’\ 0’结尾。 我们可以使用指针来操作字符串。 这是解释此部分的示例:

#include <stdio.h>
#include <string.h>
int main()
{
    char str[]="Hello ZhiHuiXinPian";
    char *p;
    p=str;
    printf("First character is:%c\n",*p);
    p =p+1;
    printf("Next character is:%c\n",*p);
    printf("Printing all the characters in a string\n");
    p=str;  //reset the pointer
    for(int i=0;i<strlen(str);i++)
    {
       printf("%c\n",*p);
       p++;
    }
   return 0;
}

处理字符串的另一种方法是使用指针数组,如以下程序所示:


#include <stdio.h>
int main()
{
   char *materials[ ] = { "iron",  "copper",  "gold"};
   printf("Please remember these materials :\n");
   int i ;
   for (i = 0; i < 3; i++) {
   printf("%s\n", materials[ i ]);}
   return 0;
}