8.1 其他运算符
本节涉及的一些运算符(例如取地址/解引用、成员访问、下标、函数调用)需要更多基础概念,才能讲得严谨。
本节介绍取地址与解引用;函数调用运算符见 第 6 章,下标运算符见 7.1,成员访问运算符见 10.2。
1. 取地址与解引用:& 和 *
一元 & 产生对象的地址;一元 * 对指针解引用,访问该地址指向的对象。两者互为配对关系,但不是任意场景都可互换:只有“可取地址的左值表达式”才能使用 &;空指针或无效地址使用 * 会导致未定义行为。
int x = 10;
int *p = &x;
*p = 20; /* 修改 x */2
3
在项目代码里,解引用前先确认指针有效,是最基本的防御式写法。
2. 习题
不运行程序,写出下面程序的输出,再编译核对:
#include <stdio.h>
int main(void) {
int x = 5;
int y = 8;
int *p = &x;
int *q = p;
*p = 12;
p = &y;
*p += 1;
printf("%d %d %d\n", x, y, *q);
printf("%d %d\n", p == &y, *q == x);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
把 int *q = p; 改成 int *q = &y;,其余不动,再写一次输出。两段程序里,q 有没有跟着 p 一起改变指向?*p = 12 修改的是哪个对象,p = &y 又修改了哪个对象?
实现带输出参数的整数除法:
#include <stdbool.h>
#include <stddef.h>
bool divmod(int a, int b, int *quotient, int *remainder);2
3
4
要求:
- 成功时通过两个指针写回商与余数,返回
true。 b为 0,或quotient、remainder中任一为空指针时返回false,且两个输出参数都不得写入。- 商向零截断,
divmod(-17, 5, &q, &r)应得到q == -3、r == -2。 - 写一个
main,依次调用divmod(17, 5, &q, &r)、divmod(-17, 5, &q, &r)、divmod(17, 0, &q, &r)、divmod(17, 5, NULL, &r);每次调用前把q、r置为-1,调用之后无条件把两者打印出来,不要放进if里。
验收:用 gcc -std=c17 -Wall -Wextra -Werror 编译无警告,输出为
3 2
-3 -2
-1 -1
-1 -12
3
4
后两行是调用前的哨兵值。它们没有被改写,说明失败路径确实没有碰输出参数。
一元 & 的操作数必须是可取地址的左值。下面七条声明逐条放进最小程序,结果并不相同:
int x = 1;
int arr[3] = {1, 2, 3};
int *p = &x;
int *a = &x;
int *b = &arr[2];
int *c = &*p;
int *d = &(x + 1);
int *e = &42;
int *f = &x++;
int *g = &(x = 3);2
3
4
5
6
7
8
9
10
11
(1) 先判断哪几条能通过编译、哪几条不能,写出依据。
(2) 每次只保留一条声明,单独编译,抄下不能通过的那几条的第一条诊断,整理成一张表:声明、能否编译、诊断里的关键短语。
(3) 写程序验证 *&x 与 x 相等、&*p 与 p 相等。再分别试写 *&x = 20; 与 &*p = &x;,记下哪一条能编译、哪一条报错以及错误文字。直接写 *&x == x 时 GCC 在 -Wall 下会报 -Wtautological-compare,把左边的值先赋给一个 int 对象再比较即可。用两条赋值语句的结果说明 *&x 与 &*p 分属哪种表达式类别。
下面的程序想演示三件事:用指针保存两数之和、把 x 加 1、取两数中较大的那个的地址。它现在编不过;把编译错误改掉以后,程序里仍然留着未定义行为。
#include <stdio.h>
static int *larger(int *a, int *b) {
if (*a >= *b) {
return a;
}
return b;
}
int main(void) {
int x = 3;
int y = 7;
int *p;
int *q = &(x + y);
int r = &x;
int *s = &x;
*p = x + y;
*s++;
int *m = larger(x, y);
printf("%d %d %d %d %d\n", x, *p, *q, *m, r);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(1) 逐行找出有问题的语句并说明原因。指出哪些会让编译失败、哪些只产生警告;有没有哪一处的问题要等别的错误改掉之后才会显示出来? (2) 用 gcc -std=c17 -Wall -Wextra 编译,抄下每一条诊断。每改掉一处编译错误就重新编译,记录哪些警告是之前没有出现过的。 (3) 继续修改,保持原有语句顺序,使程序在 -Wall -Wextra -Werror 下无警告编过,运行输出为 4 10 10 7 3。列出每一处改动,并说明 *s++ 与 (*s)++ 在这一行里各做了什么。