跳转至

第6章 · 指针和引用

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

本章题目对应教材第 6 章,按 6.1 指针基本概念、6.2 指向数组元素的指针、6.3 指针形参、6.5–6.6 行指针与指针数组的顺序排列。

6.1 指针的基本概念

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 · 指针与数组的关系

以下哪个选项是正确的?

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 就是 1、p[4] 就是 5。

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

D 的"指针数组"是 int *p[5](5 个指针组成的数组),和本题的 int *p(一根指针)不同;int (*p)[5] 是数组指针,也不一样。C 得写 p = arr + 4 才成立。


3 · 野指针

若有 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 = a 把 a 的值写进 n,等价于 n = 4,完全合法。

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

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

指针的用法是"先指向、再解引用",顺序不能颠倒。


4 · 数组名与指针

以下说法错误的是( )。

  • A. 字符串比较不能使用比较运算符
  • B. 指针可以指向任意类型的变量
  • C. sizeof 可以获得某个变量或类型占用内存大小
  • D. 数组名即为一个内存地址,但不能修改
答案

B

解析
  • A 对:if (s1 == s2) 比较的是地址而不是内容,字符串比较必须用 strcmp。
  • B 错:指针是有类型的,int * 和 double * 是不同的类型,不能无条件地互相赋值或指向任意对象(需要显式强制转换),否则解引用时类型解释就会出错。
  • C 对:sizeof 作用于变量或类型名都可以。
  • D 对:数组名就是一个地址常量。

易错:B 的说法"指针可以指向任意类型"看似正确,但 C 是强类型的,只有 void * 可以指向任意类型,而且把 void * 赋给别的指针类型仍需要转换。选项里说的"任意"对普通指针不成立。


5 · 字符数组 vs char *

设有 char b[5]="abcd", *p="abcd"; 则正确的语句为( )。

  • A. b=p;
  • B. strcpy(p,b);
  • C. b==p;
  • D. strlwr(p);
答案

C

解析

先分清两个对象的可写性:

char b[] = "abcd";   // b 是数组,内容可改 ✅
char *p  = "abcd";   // p 指向字符串字面量,内容只读 ❌

字面量存放在只读区,试图通过 p 或 p[i] 修改它 = 未定义行为(通常直接崩溃)。

  • A 错:数组名不是可修改左值。gcc 报 error: assignment to expression with array type,编译就过不去。
  • B 错:p 指向字符串字面量,strcpy 会往只读区写入。实测这段代码编译通过、连警告都没有,一到运行就以退出码 139(段错误)结束。
  • C 对:b 和 p 都是地址,比较两个地址是合法表达式。
  • D 错:strlwr 不是标准 C,只有 MSVC 才有。在 Linux 的 gcc / glibc 下报 error: implicit declaration of function 'strlwr',编译就不通过;即使在 Windows 上编过了,它要就地改写 p 指向的字面量,照样是写只读区。

易错:题干问的"正确的语句"指的是"合法、能正确运行",不是"表达式的值为真"。
b==p 的值其实是 0(两个地址不同),但作为一条语句它是合法的 —— 排除掉 A、B、D 之后只剩它。

gcc 会对这一行给出 statement with no effect 警告,也是同一个道理:算出来没用上。
char * 指向字面量时是"只读视图",只能读不能写。


6.2 指向数组元素的指针

6 · 数组名退化与行优先存储

执行下列语句后输出的结果是__。

int a[2][3] = { 1,2,3,4,5,6};
int* p;
p= a[1];
printf("%d", p[2]);
  • A. 4
  • B. 5
  • C. 6
  • D. 超出数组
答案

C(6)

解析

a[1] 是二维数组第二行的一维数组名,作为初值退化为 int *,指向 a[1][0],所以 p[2] 就是 a[1][2]。

二维数组按行优先存储、且初始化列表按行填满,故 a[1] 这一行是 {4,5,6},a[1][2] = 6。

考点:数组名退化(a[i] 类型是 int[3] → 变成 int *)与行优先,注意 p 只需要管列下标,不必再写行。


7 · 数组初始化补 0 与指针遍历

请写出以下程序运行所产生的输出。

#include <stdio.h>

int main() {
    int i, *ptr;
    int array[5] = {5,3,1};
    for (ptr=array, i=0; i<5; i++, ptr++)
    {
        if (*ptr == 0)
            putchar('X');
        else
            putchar('A' + *ptr);
    }
    return 0;
}
答案
FDBXX
解析

array[5] = {5,3,1} 只给了 3 个初值,后面两个元素自动补 0,所以数组实际是 {5,3,1,0,0}——这是本题的第一个考点,漏掉补 0 就写不出后面的 XX。

循环让 ptr 从 array 开始逐个后移(i++ 与 ptr++ 同步),依次读 5 个元素。

非 0 时走 putchar('A' + *ptr),即以 'A'(65) 为基准做字符编码偏移:5 → 'A'+5 = 'F'(70)、3 → 'D'(68)、1 → 'B'(66)。

两个 0 走 if 分支各输出一个 'X',拼起来就是 FDBXX。


8 · 数组元素访问与循环边界

以下程序段中有若干错误,请在不增删语句的情况下指明程序中的第几行有错误,并且写出正确的语句(每个错误 2 分)。

要求本程序的运行结果为 34512。

/* 第 1 行 */ int main() {
/* 第 2 行 */     int a[5] = { 1,2,3,4,5 };
/* 第 3 行 */     int i,index;
/* 第 4 行 */     for (i = 0;i <=5;i++) {
/* 第 5 行 */     index = (3 + i) % 5;
/* 第 6 行 */     printf("%d", (a+ index));
/* 第 7 行 */     }
/* 第 8 行 */     return 0;
/* 第 9 行 */ }
答案

3 处错误:

行 错误 改正 错因
4 for (i = 0;i <=5;i++) for (i = 0;i < 5;i++) 循环多跑一轮,会多输出一位数字(共 6 个字符)。注意:index = (3+i)%5 恒在 0~4,这一步并不越界,错在循环次数
5 index = (3 + i) % 5; index = (2 + i) % 5; 要从 a[2] 开始,才能得到 3 4 5 1 2
6 printf("%d", (a+ index)); printf("%d", *(a+index)); 或 printf("%d", a[index]); a+index 是地址,要打印元素必须解引用
解析
  • 第 4 行是差一错误(多循环一轮 → 多打一个字符 3,输出变成 345123)。看到 <= 就要警觉。

    "循环次数"与"下标越界"是两回事:本题的下标由 % 5 限制,index 恒在 0~4,不会越界,错在循环多跑了一轮。

  • 第 5 行需要从目标输出反推:要输出 3 4 5 1 2,就是依次取 a[2] a[3] a[4] a[0] a[1],所以 index = (2+i) % 5。

    这种"循环移位"用取模 % 实现是 C 里的常见技巧:(起始 + i) % 长度。

  • 第 6 行:a + index 是指针,*(a+index) 才是元素。这是 *(a+i) ≡ a[i] 的直接应用。

这类改错题的一般步骤:

  1. 从输出要求出发反推每一个式子(本题第 5 行只能这样定)。
  2. 有循环必有边界检查:< n 还是 <= n、n 还是 n-1,逐个数一遍。
  3. 分清"地址"和"元素":数组名、a+i、&a[i] 是地址;a[i]、*(a+i) 是元素。

9 · 指针遍历字符串(putchar)

关于以下代码段,哪个说法是正确的?

char str[] = "world";
char *p = str;
while (*p) {
    putchar(*p++);
}
  • A. 输出 world
  • B. 输出 world\n
  • C. 输出 drow
  • D. 输出 \nworld
答案

A

解析

*p++ 的语义是"先取 *p 交给 putchar,再把 p 后移一格",所以依次输出 w o r l d;当 p 走到结尾的 '\0' 时 *p 为 0,while 条件变假退出。

这里 p 是独立的指针变量(不是数组名),p++ 改的是指针自己,str 数组的内容一个字节都没动。B 是干扰项——代码里根本没有输出 '\n' 的语句。

while (*p) putchar(*p++); 是"指针版遍历字符串"的标准写法,等价于 for (i = 0; str[i]; i++) putchar(str[i]);。


10 · 指针遍历字符串(printf)

关于以下代码段,哪个说法是正确的?

char str[] = "Hello";
char *p = str;
while (*p) {
    printf("%c", *p++);
}
  • A. 输出 Hello
  • B. 输出 Hello\n
  • C. 输出 hello
  • D. 输出 hello\n
答案

A

解析

*p++ 先取值、后移针,while (*p) 在 p 指到 '\0' 时结束,所以正好输出 5 个字符 Hello,'\0' 本身不会被打印。

C、D 的 hello 是"大小写混淆"干扰项——C 语言区分大小写,'H'(72) 和 'h'(104) 是两个不同的字符,程序原样输出大写 H。

这道题只要抓住"取值用的是自增前的地址"就不会错。


11 · 指针与下标两个游标

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

#include <stdio.h>

int main(void)
{
    char str[100] = "I am a pig.";
    char* p = &str[7];
    int i;
    for (i = 0; str[i] != '\0'; i++, p++)
    {
        if (*p >= 'a' && *p < 'z')
            (*p)++;
        else if (*p == 'z')
            *p = 'a';
    }
    puts(str);
    return 0;
}
答案
I am a qjh.
解析

第一步:认清有两个游标,起点不同、速度相同。

i 从 0 起,p 从 &str[7] 起;循环的 i++, p++ 让它们每轮各走一格,所以 p 永远比 i 多走 7 格,p 指向的就是 str[i+7]。

第二步:循环跑几轮由 i 决定。条件是 str[i] != '\0':

下标 0 1 2 3 4 5 6 7 8 9 10 11
str[] I ␣ a m ␣ a ␣ p i g . \0

i 一直要走到 str[11] == '\0' 才停,所以循环共跑 11 轮(i = 0…10)——p 也就在这 11 轮里从 str[7] 一路走到了 str[17]。

第三步:这 11 个位置 p 到底读到了什么。

p 指向 str[7] str[8] str[9] str[10] str[11] str[12]~str[17]
字符 p i g . \0 0(数组零填充区)
变换后 q j h . 不变 不变

变换规则是"小写字母向后挪一位":*p >= 'a' && *p < 'z' 时 (*p)++(p→q、i→j、g→h);'z' 单独用 else if 绕回 'a'(因为它不满足 < 'z'),合起来就是26 个字母的循环移位。

'.'(46) 既不是小写字母也不是 'z',原样不动;后面的 0 更不会被碰。

所以最终字符串是 I am a qjh. ✅

需要注意的一点:p 并没有在字符串结尾的 '\0' 处停下——它继续走到了 str[17],越过了字符串末尾 7 个字节。这在别的题里往往就是越界 UB,但这里为什么不会崩溃?

因为 str 声明的是 char str[100],而 C 的规则是聚合初始化时没写到的成员一律补 0:= "I am a pig." 只写了前 12 字节,余下 88 字节全是 0。

p 走进的正是这片零填充区,读到的永远是 0,既不越出数组边界,也不改变输出。

可迁移的结论:判断"指针走过头会不会崩溃",要看的不是字符串有多长,而是数组开了多大。

char str[100] 装 11 个字符,后面 89 个字节是安全的零区;如果写成 char *str = "I am a pig."(指向字面量、没有多余空间),同样的循环就是实打实的越界了。

真机验证:gcc 15.2.0 下逐轮打印 p-str 与 *p,确认 p 从 7 走到 17,str[11] 起全为 0;开 -fsanitize=address,undefined 跑也是一片干净,无越界、无 UB。

(*p)++ 的括号不能省。 (*p)++ 是"把 p 指向的那个字符加 1";若写成 *p++,那是"取 *p 然后让 p 自增"——根据优先级 ++ 作用在 p 上而不是 *p 上,字符串内容一个字符都不会改。

要改的是"内容"就写 (*p)++,要移的是"指针"就写 p++,本题两者各要一次,正好并排出现。

另一点:i 在这段代码里只用来做循环条件(str[i] != '\0'),真正读字符的是 p,而 p == &str[i+7]——两个游标之间相差一个固定的偏移量 7。

核对时可以写一行 p → str[i+7],下标就能对上。

真机验证(gcc 15.2.0):输出 I am a qjh.。


12 · 指针的自增自减(*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 = *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 先加一。

字符串/数组的双指针交换,建议分四行写,不要为了方便把 ++ 写进解引用里。


13 · 指针与数组的等价写法(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 就是下标。因为 p 和 x 都指向同一数组,指针相减得到两者之间的元素个数,恰好就是下标。
  • 本题 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(按元素算),这也是"指针加减按元素个数走"的直接体现。


14 · 指针操作字符串(删除小写字母)(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++"。

材料状况说明

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


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

给出以下程序的输出结果,并说明 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 始终有序)。
解析

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

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) egjlnpw → efgjlnpw
'b' 0 → befgjlnpw
'l' 6(l 插在已有 l 之后) → befgjllnpw
'a' 0 → abefgjllnpw

易错:

  1. 以为是从头把 s2 拼上去(那样会得到 "fblaegjlnpw")
  2. 漏算重复字母 l 的处理——s2[k] >= s1[j] 用的是 >=,所以相等时继续往后走,新字符插在旧字符之后

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

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


16 · 指针操作字符串 ↔ 数值转换(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 行能跑完不崩溃——找错不能只靠编译器报错,要读语义。

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


6.3 指针形参

17 · 指针作函数参数

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

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 —— 原样输出,没有交换!

解析

这是"指针传参"常见的误解。

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

所以 *p、*q 仍是 2、5。

对比记忆:

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

要改实参的值,必须解引用形参指针。

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


18 · 值传递与地址传递(改错)

/*  1  */void swap(short *x, short *y) {
/*  2  */    short *z;
/*  3  */    z = *x;
/*  4  */    *x = *y;
/*  5  */    *y = z;
/*  6  */}

/*  7  */int main() {
/*  8  */    short a=3, b=5;
/*  9  */    swap(a, b);
/* 10  */    printf("a=%hd, b=%hd\n", a, b);
/* 11  */}

要求运行结果为 a=5, b=3。请指出错误行并写出正确语句(不增删语句、不改程序结构)。

答案

共 2 处错误:

行 错误 改正 错因
2 short *z; short z; 暂存变量只需存放一个 short 值,不该声明成指针
9 swap(a, b); swap(&a, &b); 形参是 short *,必须传地址,否则类型不匹配
解析
  • 只要把第 2 行改成 short z;,第 3 行 z = *x;(取出 *x 的值存进 z)、第 5 行 *y = z;(把值写回 *y)就都对了——这是一次完整的"通过指针交换两个变量"。
  • 第 9 行是本课程常见的错误:swap(a, b) 传的是值的副本,函数里怎么改都影响不到 main 里的 a、b(形参 x、y 是另外两个变量,函数一返回就消失)。要改实参,必须传地址,所以只能写 swap(&a, &b)。
  • 另一种改法行不通:如果把第 3 行改成 z = x;、第 5 行改成 *y = *z;(即把 z 当指针用),实测结果是错的:z 和 x 指向同一处,*x = *y 一执行,*z 读到的就已经是新值了,*y = *z 退化成自赋值,实测输出是 a=5, b=5,不是题目要的 a=5, b=3。所以本题只有"改第 2 行一行"这一条路——改错题优先选改动最小的方案。
  • 只要第 2 行改对了,第 9 行就一定还得加上 &;两处错误的本质是同一个:把"值"和"地址"搞混了。

19 · 值传递与地址传递(改错)

/* 第 1 行 */ void func(int *a, int *b)
/* 第 2 行 */ {
/* 第 3 行 */     int *x;
/* 第 4 行 */     x = *b;
/* 第 5 行 */     *b = *a;
/* 第 6 行 */     *a = x;
/* 第 7 行 */ }

/* 第 8 行 */ int main()
/* 第 9 行 */ {
/* 第 10 行 */     int u=8, v=4;
/* 第 11 行 */     func(u, v);
/* 第 12 行 */     printf("u=%d, v=%d\n", &u, &v);
/* 第 13 行 */     return 0;
/* 第 14 行 */ }

要求运行结果为 u=4, v=8。请指出错误行并写出正确语句(不增删语句、不改程序结构)。

答案

共 3 处错误:

行 错误 改正 错因
3 int *x; int x; 暂存一个 int 值,不需要指针
11 func(u, v); func(&u, &v); 形参是 int *,必须传地址
12 printf("u=%d, v=%d\n", &u, &v); printf("u=%d, v=%d\n", u, v); %d 要的是 int 值,&u 是地址
解析
  • 改完第 3 行之后,第 4 行 x = *b;(把 *b 的值存进 x)、第 6 行 *a = x;(把 x 写回 *a)就都对了——这是一次完整的"通过指针交换两个变量"的实现。
  • 第 11 行和第 12 行的错因是同一个:把"值"和"地址"写反了。取地址运算符 & 的正确用法是"调用时给形参指针(func(&u, &v))",而不是"打印时给 %d(那是 u, v)"。两个方向的误用都是本课程常见的错误。
  • 本题的形参是 int *a, int *b,错误是"暂存变量写成了指针" + "调用时没传地址",另外多出一个 printf 里误用 & 的错误。同一个错误点在本题中出现了两次。

6.5–6.6 行指针与指针数组

20 · 二维数组的地址表达式

数组元素 a[i][j] 的地址不能用下列哪个式子表达( )。

  • A. &a[i][j]
  • B. a[i]+j
  • C. *(a+i)+j
  • D. a+i+j
答案

D

解析

设 a 为 int a[M][N]。

  • a[i] 的类型是 int[N],退化后是 int *,指向第 i 行首元素 → a[i]+j 就是 &a[i][j] ✓
  • *(a+i) 中 a 是 int(*)[N](行指针),a+i 指向第 i 行,*(a+i) 得到 int[N] 再退化为 int * → *(a+i)+j = &a[i][j] ✓
  • a+i+j ✗:a 是行指针,+i 的步长是一整行(N*sizeof(int) 字节);再 +j 又是按行跳,得到的是 &a[i+j][0],不是 &a[i][j]。

等价关系:

a[i][j]  ≡  *(a[i] + j)  ≡  *(*(a + i) + j)
&a[i][j] ≡  a[i] + j      ≡  *(a + i) + j

易错:a+i+j 与 (a+i)+j 相同,都按行跳,容易误当作 a[i]+j。a 是行指针,a[i] 才是元素指针,步长不同。


21 · 行指针的元素访问

若有以下说明和语句,int c[4][5],(*p)[5]; p=c; 则正确访问 c 数组元素的表达式是__。

  • A. p+1
  • B. *(p+3)
  • C. *(p+1)+3
  • D. *(p[0]+2)
答案

D

解析

p 是行指针 int(*)[5],p[0] 等价于 *(p+0),取出第 0 行 c[0];

c[0] 退化为 int *,再加 2 解引用,*(p[0]+2) 就是元素 c[0][2] ✅。

A 的 p+1、C 的 *(p+1)+3 都还是地址(分别指向第 1 行、第 1 行第 3 列);B 的 *(p+3) 解引用行指针得到的是"第 3 行"这个一维数组名,本质仍是地址。

判断一个表达式是地址还是元素值:外层再解引用一次才是元素值。


22 · 元素地址的写法

表示数组 int a[2][3] 的第 i 行第 j 列元素地址的正确语句是__。

  • A. *(a[i]+j);
  • B. (a+i);
  • C. *(a+j);
  • D. a[i]+j;
答案

D

解析

由等价关系 &a[i][j] ≡ a[i]+j ≡ *(a+i)+j,D 正确。

A 的 *(a[i]+j) 已经解引用成了元素值(就是 a[i][j]);

B 的 (a+i) 是行指针,步长是一整行(3*sizeof(int) 字节),指向第 i 行而不是该元素;C 既把行下标写成了 j,解引用后得到的也不是地址。

问"地址"时不需解引用,问"元素"时才需要解引用。


23 · 指针数组与只读字面量

已知 char a[]="123", *p[] = {a,"abc"};,错误的表达式是( )。

  • A. p[1] = "456"
  • B. p[1][0] = 'B'
  • C. *(p+1) = "B"
  • D. *p = "B"
答案

B

解析

先看清类型——p 是指针数组 char *p[2],即两个 char *:p[0] 指向可写数组 a,p[1] 指向只读字面量 "abc"。

选项 含义 合法性
A p[1] = "456" 修改指针本身,让它改指别处 ✅ 合法
B p[1][0] = 'B' 修改 p[1] 指向的字面量内容 ❌ 写只读区
C *(p+1) = "B" 等价 p[1] = "B" ✅ 合法
D *p = "B" 等价 p[0] = "B" ✅ 合法

关键区分:p[1] = ...(改指针)✅ vs p[1][0] = ...(改内容)❌。

易错:C 里的 *(p+1) 看着复杂,其实恒等于 p[1]——记住 p[i] ≡ *(p+i)。

注意这里 p[0] 指向的 a 是可写的,所以如果选项写成 p[0][0] = 'B' 反而是合法的——判断可写性要看指针指向的是数组还是字面量。

易错:指针数组 vs 数组指针

char *p[2](指针数组)和 char (*p)[2](数组指针)不同:前者是 2 个指针,后者是 1 个指向 2 元素数组的指针。


24 · 字符串比较

以下程序在 5 个字符串中求最小字符串并输出结果。请指明第几行有错误并写出正确的语句(不增删语句,每个错误 2 分)。

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

/*  3  */void main(){
/*  4  */    char names[5] = {"Windows", "Word", "Excel",
/*  5  */                    "Powerpoint", "Outlook Express"};
/*  6  */    char *temp;
/*  7  */    int i;
/*  8  */    temp = names;
/*  9  */    for(i = 1; i < 5; i++)
/* 10  */        if(strcmp(temp, names[i]) < 0)
/* 11  */            temp = names[i];
/* 12  */    printf("%s\n", *temp);
/* 13  */}
答案

4 处错误:

行 错误 改正 错因
4 char names[5] = {...} char *names[5] = {...} 要装 5 个字符串,应该用指针数组,不能用一个字符数组装 5 个字符串
8 temp = names; temp = names[0]; names 是 char *[5],退化为 char **,不能赋给 char *temp
10 strcmp(temp, names[i]) < 0 strcmp(temp, names[i]) > 0 求最小字符串:当前值大于新值时替换
12 printf("%s\n", *temp) printf("%s\n", temp) %s 需要的是字符串地址 char *;*temp 是单个字符
解析
  • 第 4 行的关键:char names[5] 只能存 5 个字符,装不下 5 个字符串。正确写法是 char *names[5](指针数组),或者 char names[5][20](二维字符数组)。

    注意 "Powerpoint" 有 10 个字符、"Outlook Express" 有 15 个字符——用二维数组的话第二维至少得开 16(15 + '\0')。

  • 第 8 行的关键:names 作为 char *[5] 退化成 char **,赋给 char *temp 类型不匹配。要的是第一个字符串,所以写 names[0]。
  • 第 12 行是关于 %s 的常见问题:printf("%s", p) 传的是指针;传 *p(单个字符,值是 'E' = 69)会把这个小整数当成地址去解引用 → 段错误。
  • 第 10 行的比较逻辑:strcmp(a,b) > 0 表示 a 大于 b。要求最小值,就是"当前值大于新值就替换"。

说明

第 3 行的 void main() 不是标准写法(应为 int main(void)),但材料上标出的 4 处错误里没有它——这类题要按题目给的错误数量作答,不要改这种"不规范但能编译"的地方,否则会超出错误数限制。

求最小/最大时,保留当前最优值:求最小在"当前值大于新值"时替换,求最大在"当前值小于新值"时替换。


综合阅读

25 · 宏展开与运算符结合性

(程序阅读题,每题 3 分)下面程序的输出是?

#include <stdio.h>

#define M 1+2
void main()
{
    char str[6]={'a','b','\0','c','d','\0'};
    int x = 8, *p = &x;
    int b[5], i;
    printf("%s\n",str);
    for(i=0;i<=4;i++)   b[i]=i*3;
    printf("%d\n",b[4]);
    printf("%d\n",0<=x<=3);
    printf("%d\n",2*M*3);
    printf("%d\n",*p);
    printf("%c\n",str['\0']);
}
答案
ab
12
1
8
8
a
解析

逐行拆——

语句 输出 关键点
printf("%s\n",str) ab str 是 {'a','b','\0','c','d','\0'},%s 遇到第一个 '\0' 就停,后面的 c、d 不会被打印
b[4] 12 b[i]=i*3,i=4 → 12
0<=x<=3 1 关系运算符左结合:(0<=8)=1,再算 1<=3=1。恒为真——由关系运算符左结合造成
2*M*3 8 宏展开:M 是 1+2,展开为 2*1+2*3 = 2+6 = 8(不是 2*3*3=18)
*p 8 p=&x,x=8
str['\0'] a '\0' 的值是 0,所以这是 str[0] = 'a'

本题涉及的六个考点

本题同时涉及字符串结束符、数组越界、宏展开、关系运算左结合、指针解引用、字符常量的数值。

  • 宏只是文本替换,不会替你加括号。写宏要加足括号:#define M (1+2) 才是 2*3*3=18;带参数的宏更要写成 #define SQUARE(x) ((x)*(x))。
  • 0<=x<=3 这种"数学写法"在 C 里恒为真,需要写成 0<=x && x<=3。

说明

  1. 第 15 题:ostr 内部用的是下标 s1[j]、s2[k],没有用指针算术。同一件事既可以用指针写,也可以用下标写。
  2. 第 24 题:题干第 3 行的 void main() 不是标准 C(应为 int main(void))。材料上标出的 4 处错误里没有包含它,按材料上的收录作答;若按严格标准把这行也算错误,则本题应为 5 处。