跳转至

知识点 09 · 指针

本页最后更新于 ,内容可能过时

1 · 指针能做什么、不能做什么

下列关于 C 语言指针的说法不正确的是?

  • A. 指针可以指向任何类型的变量
  • B. 指针可以指向函数
  • C. 指针可以进行加减乘除运算
  • D. 指针可以作为函数的参数
答案

C

解析

A、B、D 都对——函数指针是 C 的重要特性,指针作形参是全课程的基础。C 错:指针只能做加减(加减整数、两指针相减),不能乘除,更不能两指针相加。

指针运算速查:

运算 是否合法 说明
p + n / p - n 按所指类型的大小跳 n 个元素
p - q 结果是 ptrdiff_t,表示两指针间的元素个数
p++ / p--
p * q / p / q / p + q 无意义,编译报错

易错:p + 1 不是"地址加 1 字节",而是"加 1 个元素"。int *p; p+1 实际地址加了 4 字节(32/64 位平台)。

这一点是数组遍历、p - q 求下标的基石。


2 · 指针作函数参数(著名的坑)

写出以下程序的输出结果。

void swap(int *a, int *b)
{ int *t;
  t=a; a=b; b=t;
}

int main()
{ int i=2,j=5,*p=&i,*q=&j;
  swap(p,q); printf("%d %d\n",*p,*q);
}
答案

2 5 —— 原样输出,没有交换!

解析

这是"指针传参"最经典的误解。

  • mainpq 的值(即 &i&j)拷贝给形参 ab
  • 函数里 t=a; a=b; b=t; 交换的只是形参 ab 这两个局部变量;
  • 函数返回后 ab 就消失了,main 里的 pq 一点没变。

所以 *p*q 仍是 25

对比记忆:

函数体写法 效果
t=a; a=b; b=t;(交换指针本身) ❌ 实参指针不变
t=*a; *a=*b; *b=t;(交换指针所指的内容) ✅ 实参变量的值被交换

要改实参的值,必须解引用形参指针。 这正是一道典型题考的点。

一句话总结:C 只有值传递。指针作参数能"改外部变量",靠的不是"传引用",而是"传了地址,再解引用去写那块内存"。


3 · 指针的自增自减(*p++ 的坑)

写出以下程序的输出结果。

void fun(char* w, int n) {
    char t1,t2, *s1, *s2;
    s1 = w; s2 = w + n - 1;
    while (s1<s2) {
        t1 = *s1++;
        *s1 = *s2--;
        *s2 = t1;
    }
}

int main() {
    char p[8] ="1234567";
    fun(p, strlen(p));
    printf("%s",p);
    return 0;
}
答案

输出 1711717(不是预期的 7654321!)

解析

这个函数的本意是原地逆序,但写错了。关键在于 *s1++*s2--

t1 = *s1++;      /* 取走 s1 处的字符,然后 s1 自增 —— s1 已经指向下一个了! */
*s1 = *s2--;     /* 此时写入的是 s1 的【新】位置,不是原来的位置 → 错位 */
*s2 = t1;        /* 同理,s2 已经自减过,写到了【前一个】位置 */

正确的写法应该是先取值、后移动:

t1 = *s1;  *s1 = *s2;  *s2 = t1;   /* 先交换 */
s1++;      s2--;                    /* 再移动 */

或者一行解决:t1 = *s1; *s1++ = *s2; *s2-- = t1;

逐步演算(原代码),初始 w = "1234567"

轮次 s1 s2 t1 = *s1++ *s1 = *s2-- *s2 = t1 数组变为
1 0 6 t1='1's1→1 w[1]='7's2→5 w[5]='1' 1734517
2 1 5 t1='7's1→2 w[2]='1's2→4 w[4]='7' 1714717
3 2 4 t1='1's1→3 w[3]='7's2→3 w[3]='1' 1711717
4 3 3 3 < 3 为假,循环结束

这道题的价值在于:它让你看到"自增运算符写在解引用后面"有多危险。*s1++ 是"取 *s1 然后 s1++",指针已经移动了,接下来再写 *s1 就是写到隔壁去了。

三种写法的区别:*p++ 先取 *p 的值,然后把 p 移到下一个元素;(*p)++*p 的值,再让 *p 自己加一;++*p*p 先加一。

考试建议:字符串/数组的双指针交换,老老实实分四行写,不要图省事把 ++ 塞进解引用里。


4 · 指针与数组的等价写法(6 分)

以下程序的目的是在已知数组中找第一个与指定值相等的元素的下标。若找到,则返回该元素在已知数组的下标,否则返回 -1。

/* 1*/ #include <stdio.h>

/* 2*/ int search(int *x, int n, int key) {
/* 3*/     int *p;
/* 4*/     for (p = x; p < x+n; p++)
/* 5*/         if ( p == key )
/* 6*/             return *p - *x;
/* 7*/     return -1;
/* 8*/ }

/* 9*/ void main(){
/*10*/     int  a[8]={2,4,7,8,4,12,4}, n;
/*11*/     n = search( a[0], 8, 4);
/*12*/     printf("%d\n", n);
/*13*/ }
答案

3 处错误,共 6 分:

错误 改正 错因
5 if ( p == key ) if ( *p == key ) 比较的是指针和整数,类型不匹配;应比较指针所指的内容
6 return *p - *x; return p - x; 要返回的是下标,不是两个元素值之差;指针相减才得到下标
11 search( a[0], 8, 4) search( a, 8, 4) a[0]int 值,形参要的是 int *;数组名 a 才是地址
解析
  • p - x 就是下标——这是本题的灵魂。因为 px 都指向同一数组,指针相减得到两者之间的元素个数,恰好就是下标。
  • 本题 a = {2,4,7,8,4,12,4},第一个等于 4 的元素是 a[1],正确答案是 1;而 *p - *x = 4 - 2 = 2,虽然也是个"看起来像下标"的小整数,但它是元素值之差,语义完全不对。换个数组就更离谱了(比如 a = {10, 4}*p - *x = -6)。

这是找错类题目最典型的出题手法:写一个"看起来对、某些数据下也对"的表达式,诱导你不去改。 判断技巧:盯着函数的语义("返回下标"),而不是盯着"能不能跑通"。

顺带记住三条等价写法(本题把这三条串起来了):

指针写法 下标写法
int *p = x; …… p 走到第 k 个 int k = 0;
p - x k
*p x[k]
p < x + n k < n

在 64 位平台上 sizeof(int *) == 8,而 sizeof(int) == 4,所以 p < x+n 里的 n 会被自动乘以 4(按元素算),这也是"指针加减按元素个数走"的直接体现。


5 · 指针操作字符串(删除小写字母)(6 分)

以下程序将字符串中的小写字母删除,并输出删除小写字母后的字符串结果。

/* 1 */ #include <stdio.h>

/* 2 */ void main() {
/* 3 */     char *str="AmGhl", *s, *p;
/* 4 */     p = s = str;
/* 5 */     while(*s) {
/* 6 */         if(*s >= 'a' && *s <= 'z')
/* 7 */             for(p=s; *p; p++)  *p = *(p+1);
/* 8 */         else s++;
/* 9 */     }
/*10 */     while(*p; p++)
/*11 */         printf("%c", p);
/*12 */ }
答案

3 处错误:

错误 改正 错因
3 char *str="AmGhl" char str[]="AmGhl" 程序要就地修改字符串内容,而 char * 指向的是只读字面量,写入是未定义行为
10 while(*p; p++) for(p=str; *p; p++) while 条件里不能写分号(语法错误);② p 经过第 7 行后已停在串尾,必须重置回串首才能打印
11 printf("%c", p) printf("%c", *p) %c 要的是字符,p 是指针
解析
  • 第 3 行是本课程最爱考的"字面量只读"陷阱。char *str = "AmGhl";str 指向只读数据段里的字面量,第 7 行的 *p = *(p+1) 是在往只读区写 —— 未定义行为(常见结果是段错误)。要改就得改成数组:char str[]="AmGhl";,字符被拷到栈上的可写空间。
  • 第 10 行用 for(p=str; *p; p++) 一箭双雕:既修好语法,又把 p 拨回串首。这正是"不得增删行"约束下唯一优雅的解法。
  • 第 7 行的 for(p=s; *p; p++) *p = *(p+1); 本身是对的——它把 s 之后的所有字符左移一位,正好"抹掉"了 *s 这个小写字母。不要误改这一行。
  • 算法整体是对的:第 6~8 行"遇到小写字母就地删除(后续整体左移,s 原地不动继续比);否则 s++"。

材料状况说明

材料里这一题没有附答案,下面的结果是按题意推导得出的,建议自己编译验证一遍。


6 · 指针与字符串的插入/维护有序

给出以下程序的输出结果,并说明 ostr 函数功能。

#include <stdio.h>
#include <string.h>

void ostr(char *s1, char *s2)
{
    int i, j, k;
    for(k=0; s2[k]; k++)
    {
        for(j=0; s1[j] && s2[k] >= s1[j]; j++)
            ;
        for(i=strlen(s1); i>=j; i--)
            s1[i+1] = s1[i];
        s1[j] = s2[k];
    }
}

int main()
{
    char a[20] = "egjlnpw", b[20]= "fbla";
    ostr(a, b);
    printf("%s", a);
    return 0;
}
答案
  • 输出:abefgjllnpw
  • 功能:把字符串 s2 中的每个字符按升序插入到字符串 s1 中(即对 s1 做插入排序,保持 s1 始终有序)。
解析

ostrs2 的每个字符做"定位 → 后移腾位 → 插入"三步:

for(j = 0; s1[j] && s2[k] >= s1[j]; j++) ;            // ① 定位:找到第一个比 s2[k] 大的位置
for(i = strlen(s1); i >= j; i--) s1[i+1] = s1[i];     // ② 后移:从末尾开始整体右移一位
s1[j] = s2[k];                                        // ③ 插入

注意 ② 的循环方向:必须从后往前搬(i 递减)。如果从前往后会覆盖掉还没搬的数据——这是所有"数组插入/删除"题目的铁律。

逐步演算(s1 = "egjlnpw"s2 = "fbla"):

插入字符 定位 j s1 变化
'f' 1(e < f < g egjlnpwefgjlnpw
'b' 0 befgjlnpw
'l' 6(l 插在已有 l 之后) befgjllnpw
'a' 0 abefgjllnpw

易错:① 以为是从头把 s2 拼上去(那样会得到 "fblaegjlnpw");

② 漏算重复字母 l 的处理——s2[k] >= s1[j] 用的是 >=,所以相等时继续往后走,新字符插在旧字符之后。

另外注意 ② 里 i = strlen(s1) 而不是 strlen(s1)-1:这一步是真正在写 '\0' 之后一格,靠的是把整个串(含结尾的 '\0')一起右移一位来"扩容"。

s1 声明成 char a[20] 才有空间,这也是为什么形参必须是可写的 char *s1


7 · 指针操作字符串 ↔ 数值转换(12 分)

下面程序通过指针操作,输出由二进制数字组成的字符串对应的无符号十进制整数。 例如对 "0000000101001001" 转换后输出:

0000000101001001 is convered to integer number: 329
#include <stdio.h>
#include <string.h>

int main()
{
    char v2[17], * p;
    unsigned int v10, t;
    v10 = 0;
    scanf("%s", &v2);
    p = v2 + strlen(v2) - 1;
    while (*p != '\0')
    {
        t = *p - 0;
        v10 = v10 * 2 + t;
        p+1;
    }
    printf("%s is convered to integer number: %u\n", v2, v10);
    return 0;
}
答案

4 处错误:

行号 错误 改正
2 scanf("%s", &v2); scanf("%s", v2);
3 p = v2 + strlen(v2) - 1; p = v2;p = &v2[0];
6 t = *p - 0; t = *p - '0';
8 p+1; p++;p = p + 1;
解析
  • 第 2 行:数组名本身就是地址,&v2 的类型是 char(*)[17],类型不对(经典错误)。
  • 第 3 行:原写法让 p 指向最低位(串尾),但循环体里是 v10 = v10*2 + t 这种"高位先算"的秦九韶迭代——必须从左往右处理。所以 p 要指向串首。

    顺带一提:若坚持从低位开始,公式应改成 v10 += t * (1 << k),那就得额外维护 k。既然框架是 *2 + t,就只能左起。

  • 第 6 行:*p 是字符 '0'/'1'(ASCII 48/49),不是数字 0/1,必须减 '0' 做转换。

    易错点:写成 - 0 时值是 48/49,算出来的数完全错误;但它也"能跑",所以很多人不改。

  • 第 8 行:p+1; 只是算了个临时值然后丢弃,p 纹丝不动 → 死循环。必须 p++

本题是"找错类题目答题技巧"的教科书案例:4 个错误里,第 2 行、第 3 行、第 8 行都"能编译通过",第 6 行甚至"能跑完不崩溃"——找错不能只靠编译器报错来找 bug,要读语义。

验证:"0000000101001001" 按从左到右迭代: (((...(0*2+0)*2+0)...)) 最后得到 1·256 + 1·64 + 1·8 + 1·1 = 256+64+8+1 = 329


说明

  1. 第 6 题:ostr 内部用的是下标 s1[j]s2[k],并没有用指针算术——它其实是"数组下标"写法的经典题;可以和本文件里按指针写法给出的题对照着看:同一件事,既可以用指针写,也可以用下标写。

8 · 指针与数组的关系

以下哪个选项是正确的?

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
  • A. p 指向 arr 数组的第一个元素
  • B. p 是数组 arr 的副本
  • C. p 指向数组 arr 的最后一个元素
  • D. p 是一个指针数组
答案

A

解析

数组名 arr 用在表达式里会退化为首元素地址(即 &arr[0]),所以 p 指向第一个元素,*p 就是 1p[4] 就是 5

B 错在混淆了"指针"和"数组":p 只是一根指针,没有任何元素被拷贝,parr 的地址值虽然相同但类型不同(int * vs int [5],所以 sizeof(arr)==20sizeof(p)==8)。

D 的"指针数组"是 int *p[5](5 个指针组成的数组),和本题的 int *p(一根指针)完全是两回事,别和 int (*p)[5](数组指针)也搞混;C 得写 p = arr + 4 才成立。


9 · 野指针

若有 int *p, a=4, n; 下面正确的程序段是__。

  • A. p=&n; scanf("%d", &p);
  • B. p=&n; scanf("%d", *p);
  • C. scanf("%d", &n); *p=n;
  • D. p=&n; *p=a;
答案

D

解析

D 先让 p 指向 n(指针完成初始化),再通过 *p = aa 的值写进 n,等价于 n = 4,完全合法。

A 错在 &p 的类型是 int **,而 scanf("%d", ...) 要的是 int *,类型不匹配;B 错在 *pint 值(应传地址 p),scanf 会把 4 当成地址去写,直接崩溃。

C 最隐蔽也最危险——p 从未初始化就解引用,这是典型的"野指针":写进去的内存位置不确定,通常运行时崩溃。

记住一句话:指针"先指向、再解引用",顺序不能颠倒。