指针是C语言中比较难懂的部分,但是指针又是C语言的精华,所以指针是每个C语言开发人员都必须掌握的知识。在学指针之前先来了解变量在内存中的分布。
在上图中,定义了3个变量a、b、c,这三个变量的地址分别是0x2000,0x2004和0x2008,变量的地址就代表这个变量所在的内存位置。举个例子,在一家酒店里有三个房间,门牌号分别是0x2000,0x2004,0x2008,这里的门牌号就相当于是地址,找到相应的门牌号就能找到相应的房间。那么旅客根据什么东西可以找到相应的房间呢,答案是钥匙,一把钥匙对应一个房间门牌号,通过钥匙找到相应的门牌号进而打开相应的房间。而这里的钥匙就是C语言里面的指针。
- 指针的使用
想要定义一个指针变量,只需在变量名字前加一个*
号即可,比如定义一个整形指针变量int *a
,如果需要把一个普通变量的地址赋值给指针变量需要使用&符号,比如int *a = &b
,这段语句意思是把变量b的地址赋值给指针变量a。当想要获取指针变量的值需要在变量名前加*
号即可,比如*a
就代表取a地址所在内存的值。下面来写一个程序来做个实验。
程序1:
#include <stdio.h>
int main(void)
{
int *a; // 定义整形指针变量
int b = 10; // 定义整形变量b
a = &b; // 将b变量的地址赋值给指针a
printf("*a = %d\r\n",*a); // 打印出指针变量a的值
printf("a = 0x%x\r\n",a); // 打印出指针变量a的地址
printf("&b = 0x%x\r\n",&b); // 打印出变量b的地址
return 0;
}
在程序1中,定义了指针变量a和普通变量b,然后将b的地址赋值给a,最后打印出a的值,指针a的地址和变量b的地址。
可以看到,*a的值与变量b的值一样,而且指针变量a的地址与变量b的地址也一样。
- 未初始化与非法指针
指针在定义时必须初始化,否则就会成为野指针。
#include <stdio.h>
int main(void)
{
int *a; // 定义整形指针变量
*a = 10;
printf("a* = %d\r\n",*a);
return 0;
}
上述程序中定义了指针变量a,但是没有给指针变量初始化,当程序给指针变量a赋值时,系统提示段错误。这其实就是指针变量a的地址没有初始化,导致其指向了内存中的限制地址,这就是常说的越界访问,所以操作系统提示段错误。当我们定义指针但是又不想初始化时,可以使用NULL来初始化指针。
#include <stdio.h>
int main(void)
{
int *a = NULL; // 定义整形指针变量并用NULL初始化
int b = 10;
a = &b;
printf("a* = %d\r\n",*a);
return 0;
}
先用NULL来初始化指针,防止其变成野指针,后续再给指针分配内存地址。
- 指针的运算
指针的运算只允许两种方式,一种是指针自加或自减,一种是指针减指针。
1、 - 指针自加或自减。
#include <stdio.h>
int main(void)
{
int array[] = {0,1,2,3,4};
int *a = &array[3];
printf("a* = %d\r\n",*a);
a++;
printf("a* = %d\r\n",*a);
a--;
printf("a* = %d\r\n",*a);
return 0;
}
通过程序和示例图可以了解指针的自加和自减的运算过程。由内存示意图可以知道,数组内的元素地址是挨个排列,其地址都是相差4个字节(这里的4个字节是因为数组的类型为int类型,如果数组是其他类型需要根据实际情况来定)。指针的自加和自减就是指针往前移动n * sizeof(int)
个字节或者向后移动n * sizeof(int)
个字节。
- 2、 - 指针的相减
指针的相减仅限于两个指针指向同一个数组的情况。相减的结果将除以数组元素类型的长度。
#include <stdio.h>
int main(void)
{
int array[] = {46,1,452,-893,4};
int *a = &array[3];
int *b = &array[0];
printf("a - b = %d\r\n",a - b);
return 0;
}
- 指针与数组的关系
在C语言中,指针和数组的关系可以说是十分密切,在上一篇讲解数组的文章中,就出现数组和指针的相互引用,下面就来更加深入了解一些指针和数组的关系。- 指针与一维数组
#include <stdio.h>
int main(void)
{
int i = 0;
int array[] = {0,1,2,3,4}; // 定义数组
int *a = array; // 定义指针变量a并指向数组的首地址
for(i = 0;i < 5;i++)
{
printf("*(a + %d) = %d\r\n",i,*(a + i)); // 循环打印出数组的值
}
return 0;
}
因为数组在内存中的地址是连续的,而数组名又正好是整个的数组的首地址,所以当用一个指针指针数组名时,就意味着这个指针的地址已经数组的首地址进行关联,所以就可以用指针来直接访问数组元素。
- 指针数组
指针数组存放的是指针变量,由多个指针变量结合成一个数组。指针数组的表现形式为char *p[10]
,根据C语言的优先级,p先跟[]结合,p[10]
是一个数组,然后p[10]
再跟*
结合,变成了*p[10]
的指针数组,根据前面的char类型,所以这个是一个存放char* 类型的指针数组。
#include <stdio.h>
int main(void)
{
int i = 0;
char *p[] = {
"hello world",
"123456",
"abcdefg",
};
for(i = 0;i < 3;i++)
{
printf("p[%d] = %s\r\n",i,p[i]);
}
return 0;
}
- 数组指针
数组指针的形式是int (*p)[2],根据优先级p先跟星号结合,变成*p
,然后再跟[]结合,变成数组指针。数组指针它是一个指针,指向数组。数组指针通常用来访问一个二维数组。
#include <stdio.h>
int main(void)
{
int i = 0,j = 0;
int array[3][2] = // 定义一个二维数组
{
{0,1},
{2,3},
{4,5},
};
int (*p)[2] = array; // 定义一个数组指针并指针二维数组的首地址
p++; // 数组指针加1,表示指指针向下移动一行。
printf("(p)[0] = %d\r\n",*(p)[0]);
return 0;
}
- 指向函数的指针
在C语言中函数名代表该函数的首地址,既然函数有地址,那么也可以用指针来指向函数,这种指针叫做函数指针。
#include <stdio.h>
typedef void (*pfunc)(void); // 定义一个函数指针,类型为void ()(void)
void printf_fun(void)
{
printf("hello world\r\n");
}
int main(void)
{
pfunc pf = printf_fun; // 定义一个函数指针并指向一个函数
printf("0x%x\r\n",printf_fun);
printf("0x%x\r\n",pf);
pf(); // 通过指针调用函数
return 0;
}
从程序的运行结果中可以看到,函数指针和函数的内存地址是一样的,所以通过指针也可以直接调用函数。函数指针在操作系统中是非常常见的。
- 使用指针的好处
- 1、指针可以直接访问内存地址,可以提高效率
在讲解数组的时候说过,访问数组元素有两种访问形式,一种是下标法,一种是指针法。下标法其实是对指针法的一种封装。当使用下标法访问数组元素的时候,在程序运行的时候最后还是要转换成指针进行访问。所以直接使用指针访问效率会更高。 - 2、在C语言中一些复杂的数据结构可以通过指针来实现。
在C语言中的一些复杂结构,比如链表、树二叉树、红黑树等数据结构都是用指针进行构建。 - 3、在C语言中函数传参是值传递的,函数的形参是不可以修改变量值,但是通过指针却可以。
函数传参是不可修改变量的值,但是指针就可以,下面可以通过一个经典的数据交换例子来说明。
- 1、指针可以直接访问内存地址,可以提高效率
#include <stdio.h>
void swap(int a,int b)
{
int c;
printf("swap &a = 0x%x\r\n",&a);
printf("swap &b = 0x%x\r\n",&b);
c = a;
a = b;
b = c;
}
int main(void)
{
int a = 5,b = 10;
printf("&a = 0x%x\r\n",&a);
printf("&b = 0x%x\r\n",&b);
swap(a,b);
printf("a = %d\r\n",a);
printf("b = %d\r\n",b);
return 0;
}
函数swap的作用交换两个变量的值,在main函数中定义a和b两个变量,先将两个变量的地址打印出来,然后调用swap函数并将a和b的值作为函数的参数。在swap函数中也打印出两个函数参数的地址并进行数据交换,最后打印出交换后的值。
最后运行的结果是在调用swap函数后,a和b的值并没有被交换,还是原来的值,为什么会这样呢。其实通过两个变量的地址就可以知道,在main函数定义的两个变量的地址跟函数参数的地址是完全不同的地址。虽然在swap函数中有将两个值进行互换,但是地址不同,最后互换的结果自然也就失败。
接下来看一下利用指针作为函数的参数
#include <stdio.h>
void swap(int *a,int *b)
{
int c;
printf("swap a = 0x%x\r\n",a);
printf("swap b = 0x%x\r\n",b);
c = *a;
*a = *b;
*b = c;
}
int main(void)
{
int a = 5,b = 10;
printf("&a = 0x%x\r\n",&a);
printf("&b = 0x%x\r\n",&b);
swap(&a,&b);
printf("a = %d\r\n",a);
printf("b = %d\r\n",b);
return 0;
}
上述程序只是将函数的参数改成了指针,这时swap函数需要传入变量的地址,从运行结果可以看到,在main函数中定义的变量地址和swap参数传入的地址是完全相同。所以在swap函数中修改两个指针的值其实就等于在修改main函数中定义的a和b变量的值。所以最后结果是两个参数的值互换成功。
- 4、可以利用指针实现面向对象编程。
众所周知,C语言是一种面向过程的语言,它本身不具备面向对象的功能,但是在很多操作系统中,可以利用C语言的函数指针来实现面向对象的功能。下面来写一个简单的程序来进行说明。
#include <stdio.h>
typedef int (*cal_func)(int,int); // 定义计算器操作函数
int add(int a,int b) // 加
{
return a + b;
}
int sub(int a,int b) // 减
{
return a - b;
}
int mul(int a,int b) // 乘
{
return a * b;
}
int div(int a,int b) // 除
{
return a / b;
}
typedef struct calculator
{
int a; // 计数值
int b; // 计数值
cal_func pfun; // 计数器操作指针函数
}calculator_t;
int main(void)
{
calculator_t cal;
char c;
printf("请输入运算类型\r\n");
scanf("%c",&c);
printf("请输入两个整形变量\r\n");
scanf("%d",&cal.a);
scanf("%d",&cal.b);
switch(c) // 根据用户输入的运行类型进行函数指针的赋值
{
case '+':
cal.pfun = add;
break;
case '-':
cal.pfun = sub;
break;
case '*':
cal.pfun = mul;
break;
case '/':
cal.pfun = div;
break;
}
printf("value = %d\r\n",cal.pfun(cal.a,cal.b)); // 运行函数指针
return 0;
}
上述程序中根据用户输入的运算类型,在switch中选择不同的函数赋值给函数指针,最后调用函数指针得出结果。可以看到结构体中只定义了一个函数指针,但是却可以实现4种不同的运算功能。