15. 【C语言】C语言的灵魂:指针初体验
2026/7/5 14:56:57 网站建设 项目流程

前面十四篇文章,我们盖好了地基:变量存数据,函数拆模块,数组管批量。但你有没有觉得还缺一块关键拼图——为什么我们可以在函数里修改数组,却改不了普通变量?为什么scanf要用&取地址?为什么说 C 语言“贴近硬件”?

答案就在指针

指针是 C 语言最强大、也最让初学者头疼的特性。但不要怕——你已经有变量、内存、作用域的扎实基础,理解指针就只剩一层窗户纸。今天,我们就来捅破它。


一、为什么需要指针?

先看三个你或许已经遇到的困惑:

困惑一:为什么scanf要加&

intage;scanf("%d",&age);// 这个 & 是什么?

困惑二:为什么在函数里修改数组,外面也能看到变化?

voidfill_zero(intarr[],intn){for(inti=0;i<n;i++)arr[i]=0;// 外面的数组也变了!}

困惑三:为什么我们写的swap函数没用?

voidswap(inta,intb){inttemp=a;a=b;b=temp;// 外面纹丝不动}

这三个困惑都指向同一个核心概念:如何直接访问和操作内存中的数据。普通变量通过名字访问,但有时候我们需要通过地址来访问——这就引出了指针。


二、指针是什么?

简单说:指针就是一个变量,但它里面存的不是普通数值,而是另一个变量的内存地址。

inta=10;int*p=&a;// p 里存的是 a 的地址

这里:

  • a是一个普通的int变量,里面存的是10
  • &a取地址运算符,得到a在内存里的地址(比如0x7ffd1c)。
  • p是一个指针变量,它里面存的就是那个地址。
  • *p间接访问运算符(解引用),沿着p存的地址找到a,读写它的值。

关系图:

变量 a: [10] <-- 地址 0x100 ^ | 指针 p: [0x100] <-- p 自己的地址是 0x200

所以*p就是a本人。你写*p = 20;a就变成了 20。


三、声明指针变量

声明指针时,在类型后面加一个*

int*p;// p 是指向 int 的指针char*ch;// ch 是指向 char 的指针double*dp;// dp 是指向 double 的指针

声明时可以初始化:

inta=5;int*p=&a;// p 指向 a

也可以分开写:

int*p;p=&a;// 让 p 指向 a

注意int *p, q;中,只有p是指针,q是普通int。如果想声明两个指针,写int *p, *q;。这是 C 语法的一个小陷阱。


四、&取地址与*解引用

这两个运算符是指针的基本功,必须熟练掌握。

1.&取地址

把任意变量的地址取出来,返回指向该类型的指针。

intx=42;printf("%p\n",(void*)&x);// 打印 x 的地址

%p专门用来打印地址(指针值),需要强制转换为void*

2.*解引用(间接访问)

通过指针访问它所指向的变量。

inta=10;int*p=&a;printf("%d\n",*p);// 输出 10,等价于 printf("%d\n", a);*p=20;// 把 a 改成 20printf("%d\n",a);// 输出 20

*p可以出现在赋值号的左边(左值),用来修改指向的值。


五、指针作为函数参数:实现真正的“交换”

还记得第十二篇那个失败的swap吗?现在用指针来拯救它。

#include<stdio.h>voidswap(int*a,int*b){inttemp=*a;// 取 a 指向的值*a=*b;// 把 b 指向的值赋给 a 指向的位置*b=temp;}intmain(void){intx=5,y=10;printf("交换前: x=%d, y=%d\n",x,y);swap(&x,&y);// 传 x 和 y 的地址printf("交换后: x=%d, y=%d\n",x,y);return0;}

输出:

交换前: x=5, y=10 交换后: x=10, y=5

成功!因为swap接收的是xy的地址,它通过*a*b直接修改了main里那两个变量的内存。这就是指针的核心威力:让函数有能力修改外部的变量


六、指针与数组:一对亲兄弟

1. 数组名就是首元素的地址

intarr[5]={10,20,30,40,50};printf("%p\n",(void*)arr);// 数组名直接当指针用printf("%p\n",(void*)&arr[0]);// 和上面一样

数组名arr在表达式中会被自动转换成指向首元素的指针。所以:

int*p=arr;// p 指向 arr[0]

现在你可以用指针来访问数组元素:

printf("%d\n",*p);// arr[0] = 10printf("%d\n",*(p+1));// arr[1] = 20printf("%d\n",*(p+2));// arr[2] = 30

p + 1不是地址值加 1 个字节,而是加1 * sizeof(int)个字节——也就是跳过整个元素,指向下一个int。这称为指针算术

2. 指针算术

指针加整数 n,地址值增加n * sizeof(指向的类型)

intarr[5]={10,20,30,40,50};int*p=arr;// 指向 arr[0]p=p+1;// 指向 arr[1]p++;// 指向 arr[2]p+=2;// 指向 arr[4]

也可以用下标访问指针:

int*p=arr;printf("%d\n",p[2]);// 等价于 arr[2],输出 30

为什么数组和指针这么亲?因为arr[i]在底层被编译器翻译成*(arr + i)。这两者完全等价。甚至你可以写i[arr],会被翻译成*(i + arr),也是一样的(但千万别真这么写)。

3. 用指针遍历数组

#include<stdio.h>intmain(void){intarr[]={2,4,6,8,10};int*p;for(p=arr;p<arr+5;p++){printf("%d ",*p);}printf("\n");return0;}

p < arr + 5判断指针是否越过了数组末尾(arr + 5指向最后一个元素之后的位置,不能解引用,但可以做比较)。


七、数组作为函数参数的本质

现在我们可以解释那个困惑了:为什么函数里修改数组,外面也会变?

voidmodify(intarr[],intn){arr[0]=999;}

实际上,编译器看到int arr[]时,会把它当成int *arr。函数调用时,传进来的是数组首地址的副本,而不是整个数组的副本。通过这个地址,函数可以直接修改原数组。

modify(my_array,5);// 传的是 &my_array[0]

所以arr[0] = 999;等价于*(arr + 0) = 999;,直接写到了原数组的内存上。这就是为什么数组“按引用传递”的真相——它传的是地址值。


八、void*指针初识

有一种特殊的指针类型:void*(无类型指针)。它可以指向任何类型的数据,但不能直接解引用,因为编译器不知道它指向的数据类型大小。

inta=10;void*vp=&a;// 可以指向 int// printf("%d\n", *vp); // 错误!不能解引用 void*printf("%d\n",*(int*)vp);// 先强制转换回 int*,再解引用

void*常用于通用内存操作函数,比如mallocmemcpy,后面讲动态内存时会遇到。


九、常见错误与陷阱

1. 使用未初始化的指针

int*p;*p=10;// 危险!p 指向哪里?可能是随机地址,导致崩溃

指针必须指向合法的内存(已声明的变量、数组、动态分配的内存)才能解引用。

2. 返回局部变量的地址

int*bad_func(void){intx=100;return&x;// 函数返回后 x 已销毁,返回的地址无效}

这是经典的“悬空指针”错误。要返回指针,可以返回静态局部变量、全局变量或动态分配的内存的地址。

3. 解引用空指针

int*p=NULL;printf("%d\n",*p);// 段错误!NULL 是空地址,不允许访问

NULL是一个宏,表示空指针。在解引用前,一定要确保指针非空。

4. 指针类型不匹配

inta=10;double*dp=&a;// 编译器警告,类型不兼容printf("%f\n",*dp);// 未定义行为

不同类型的指针不要随便互指,除非你很明白自己在做什么(并且用强制转换)。

5. 野指针

指针指向的内存已经释放(free 之后),但指针还在,指向的地址无效。后面动态内存部分会细讲。


十、小结

今天你第一次触碰了 C 语言的灵魂——指针。它们不是什么神秘魔法,只是存地址的变量。但它们解锁了:

  • 直接修改外部变量的能力(swap终于能用了)
  • 高效操作数组的方式(指针算术)
  • 理解了数组作为函数参数的本质

你现在知道了:

  • &取地址,*解引用。
  • 指针变量声明用int *p;
  • 数组名就是首元素地址,arr[i]就是*(arr + i)
  • 函数传数组本质是传地址,所以能在函数内修改原数组。

指针的旅途才刚刚开始。下一篇我们会深入指针与数组更复杂的关系——指针数组、数组指针、多级指针,以及字符串与指针的紧密联系。当你能轻松玩弄指针时,你就真正拥有了 C 语言。


课后小练习

  1. 写一个函数void increment(int *p),让传入的整数加 1。在main中测试。
  2. 使用指针遍历一个double数组,打印所有元素,观察指针加 1 时地址增加了多少字节。
  3. 写一个函数int array_sum(int *arr, int n),用指针算术(而不是下标)计算数组元素的和。
  4. 分析以下代码错在哪里:
    int*get_pointer(void){intval=5;return&val;}intmain(void){int*p=get_pointer();printf("%d\n",*p);return0;}
    为什么输出可能不是 5?用静态局部变量怎么修正?

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询