1. 本章导读
指针是 C 语言最核心、也最容易让初学者感到困惑的知识点之一。谭浩强老师在《C程序设计(第五版)》中用了整整一章来讲解指针,足见其重要性。本章从「指针是什么」讲起,逐步深入到指针与数组、指针与字符串、函数指针、动态内存分配等内容,最终目标是让读者能够灵活运用指针处理一批数据、优化函数传参、构建动态数据结构。
本文将按照教材第 8 章的章节脉络,梳理知识要点,配合典型案例分析,并对课后习题给出解析与参考代码,帮助读者系统掌握指针。
2. 知识要点梳理
2.1 指针与指针变量
- 地址:内存中每个字节都有一个编号,这个编号就是地址。变量在内存中占用若干字节,其「首地址」就是该变量的地址。
- 指针:在 C 语言中,地址被形象地称为「指针」,即通过地址可以「指向」某个存储单元。
- 指针变量:专门用来存放地址(指针)的变量。定义指针变量时必须指定它指向的数据类型,例如
int *p;表示p是一个指向整型数据的指针变量。 - 指针变量的定义与初始化:
inta=10;int*p=&a;/* p 保存 a 的地址 */两个关键运算符:
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
& | 取地址 | &a | 得到变量 a 的地址 |
* | 间接访问(解引用) | *p | 得到 p 所指向的变量 |
2.2 指针变量作函数参数
C 语言函数调用采用「值传递」——形参只是实参的一份拷贝。如果想在函数内部修改主调函数中的变量,就必须把变量的地址传进去。这是指针最典型的应用之一。
voidswap(int*x,int*y){inttmp=*x;*x=*y;*y=tmp;}intmain(void){inta=3,b=5;swap(&a,&b);/* 调用后 a=5, b=3 */return0;}思考:如果
swap的参数写成int x, int y,调用后a、b会交换吗?答案是不会,因为交换的只是形参副本。
2.3 通过指针引用数组
数组名代表数组首元素的地址,因此可以用指针遍历数组,提高程序执行效率。
inta[5]={1,2,3,4,5};int*p=a;/* 等价于 int *p = &a[0]; */- 当
p = a时:p + i指向a[i],*(p + i)等价于a[i]。 - 指针变量可以自增:
p++使p指向下一个元素;但数组名 a 是常量指针,不能自增。 - 结合数组作函数参数:形参
int a[]在函数内部实际上等价于int *a。
voidprint_arr(int*p,intn){for(inti=0;i<n;i++)printf("%d ",*(p+i));}2.4 通过指针引用字符串
C 语言没有专门的字符串类型,字符串本质上是以'\0'结尾的字符数组。可以用字符指针指向字符串常量:
char*str="Hello, C!";printf("%s\n",str);两种方式的区别:
charstr1[]="hello";/* 字符数组,内容可以修改 */char*str2="hello";/* 指针指向字符串常量,内容通常不可修改 */用字符指针处理字符串更灵活,例如字符串复制、比较、连接等操作:
voidmy_strcpy(char*dest,constchar*src){while((*dest++=*src++)!='\0');}2.5 指向函数的指针
函数在内存中也占存储空间,函数的入口地址同样可以用指针保存。函数指针常用于回调函数和菜单驱动程序中。
intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmain(void){int(*func)(int,int);/* 定义函数指针 */func=add;printf("%d\n",func(10,20));/* 输出 30 */func=sub;printf("%d\n",func(10,20));/* 输出 -10 */return0;}2.6 返回指针值的函数
函数可以返回地址(指针),典型场景是返回数组中某个元素或某段内存的地址。注意:绝不能返回局部变量的地址,因为函数结束后局部变量已被释放。
int*max_elem(int*arr,intn){int*max=arr;for(inti=1;i<n;i++)if(arr[i]>*max)max=&arr[i];returnmax;}2.7 指针数组与多重指针
- 指针数组:数组的每个元素都是指针。常用于保存多个字符串:
char*names[]={"Alice","Bob","Carol"};printf("%s\n",names[1]);/* 输出 Bob */- 指向指针的指针(多重指针):指针变量本身也有地址,可以用另一个指针指向它。
char*name="hello";char**pp=&name;printf("%s\n",*pp);/* 输出 hello */2.8 动态内存分配
动态分配允许程序在运行期间按需申请内存,是构建链表等动态数据结构的基础。
#include<stdlib.h>int*p=(int*)malloc(sizeof(int));/* 分配一个 int 大小 */int*arr=(int*)malloc(n*sizeof(int));/* 分配 n 个 int 的连续空间 */free(p);/* 使用完毕必须释放 */free(arr);常用的相关函数:malloc、calloc、free、realloc。使用动态内存时必须注意「配对释放」,防止内存泄漏。
3. 典型案例分析
案例一:用指针实现数组逆序
题目:输入 10 个整数,用指针方法将它们按逆序重新存放并输出。
思路:设首指针p指向第一个元素,尾指针q指向最后一个元素,二者相向移动并交换所指内容,直到p >= q。
#include<stdio.h>voidreverse(int*a,intn){int*p=a;int*q=a+n-1;while(p<q){inttmp=*p;*p=*q;*q=tmp;p++;q--;}}intmain(void){inta[10];inti;printf("请输入 10 个整数:\n");for(i=0;i<10;i++)scanf("%d",&a[i]);reverse(a,10);printf("逆序后:\n");for(i=0;i<10;i++)printf("%d ",a[i]);putchar('\n');return0;}分析:本题体现了「指针 + 数组」的经典用法。函数reverse通过指针直接操作原数组,不需要返回新数组,避免了内存浪费。
案例二:函数指针实现简易计算器
题目:编写程序,根据用户输入的四则运算符号,调用相应函数完成计算。
思路:把加、减、乘、除四个函数存入函数指针数组,用运算符作为下标索引。
#include<stdio.h>intadd(inta,intb){returna+b;}intsub(inta,intb){returna-b;}intmul(inta,intb){returna*b;}intdiv(inta,intb){returna/b;}intmain(void){int(*ops[4])(int,int)={add,sub,mul,div};charsymbols[]="+-*/";intx,y;charop;printf("请输入表达式(如 8+2):");scanf("%d%c%d",&x,&op,&y);for(inti=0;i<4;i++){if(op==symbols[i]){printf("%d %c %d = %d\n",x,op,y,ops[i](x,y));break;}}return0;}分析:本题展示了函数指针数组的威力。如果不使用函数指针,往往需要写if-else或switch逐个分支调用,代码冗长且不易扩展。
案例三:字符串排序
题目:用指针数组对若干字符串按字典序从小到大排序。
思路:使用指针数组保存每个字符串的首地址,排序时只交换指针,不移动字符串本身,从而提升效率。
#include<stdio.h>#include<string.h>voidsort_strings(char*arr[],intn){for(inti=0;i<n-1;i++){for(intj=0;j<n-1-i;j++){if(strcmp(arr[j],arr[j+1])>0){char*tmp=arr[j];arr[j]=arr[j+1];arr[j+1]=tmp;}}}}intmain(void){char*names[]={"pear","apple","orange","banana"};intn=4;sort_strings(names,n);for(inti=0;i<n;i++)printf("%s\n",names[i]);return0;}分析:排序过程中交换的是指针变量(地址),而不是字符串内容,这就是「通过指针引用字符串」带来的典型优势。
4. 课后习题解析(节选)
习题 4:输入三个整数,按由小到大的顺序输出
要求:用指针方法处理。
#include<stdio.h>voidswap(int*p,int*q){inttmp=*p;*p=*q;*q=tmp;}intmain(void){inta,b,c;int*p1=&a,*p2=&b,*p3=&c;printf("请输入三个整数:");scanf("%d%d%d",&a,&b,&c);if(*p1>*p2)swap(p1,p2);if(*p1>*p3)swap(p1,p3);if(*p2>*p3)swap(p2,p3);printf("从小到大:%d %d %d\n",a,b,c);return0;}解析:通过swap交换指针所指向的变量的值,经过三轮比较后,a必为最小、c必为最大。
习题 5:输入三个字符串,按由小到大的顺序输出
#include<stdio.h>#include<string.h>intmain(void){chars1[80],s2[80],s3[80];char*p1=s1,*p2=s2,*p3=s3,*tmp;printf("请输入三个字符串:\n");scanf("%s%s%s",s1,s2,s3);if(strcmp(p1,p2)>0){tmp=p1;p1=p2;p2=tmp;}if(strcmp(p1,p3)>0){tmp=p1;p1=p3;p3=tmp;}if(strcmp(p2,p3)>0){tmp=p2;p2=p3;p3=tmp;}printf("从小到大:%s %s %s\n",p1,p2,p3);return0;}解析:这里借助指针变量交换「指向关系」,字符串内容本身无需移动。
习题:用函数指针求两个整数的最大值、最小值、和
#include<stdio.h>intmax(inta,intb){returna>b?a:b;}intmin(inta,intb){returna<b?a:b;}intsum(inta,intb){returna+b;}voidcompute(intx,inty,int(*func)(int,int)){printf("结果为:%d\n",func(x,y));}intmain(void){inta=10,b=20;compute(a,b,max);compute(a,b,min);compute(a,b,sum);return0;}解析:compute函数的第三个参数是函数指针,通过传入不同函数,实现了同一框架下的多种运算,体现「回调」思想。
5. 易错点与避坑指南
- 指针未初始化就解引用:
int *p; *p = 10;是危险的,因为p中存的是随机地址。务必先让它指向合法空间。 - 混淆
*在不同位置的含义:定义时int *p表示声明指针变量;使用时*p表示间接访问。两者作用完全不同。 - 忘记区分数组名与指针变量:数组名是常量,
a++非法;指针变量可以自增。 - 返回局部变量地址:函数返回指向局部变量的指针是未定义行为,应避免。
- 动态内存不释放:
malloc分配的每一块内存都应在适当时机free,否则会造成内存泄漏。 - 字符数组与字符指针的差异:通过字符指针指向的字符串常量通常不可修改,直接写
*str = 'H'可能引发运行时错误。
6. 本章小结
指针是 C 语言的灵魂。学好指针,才能真正理解 C 程序在内存中的运行机制,也才能熟练运用动态数据结构和高效算法。学习本章建议遵循「由浅入深」的路径:
建议读者在学习中多画内存示意图:明确「谁存地址、谁存数据」,每个指针变量指向哪里、解引用后得到什么。配合上机调试,逐步体会指针带来的灵活与高效,本章的难点便能迎刃而解。