1. 指针运算的本质:p+1 不是地址加 1
学指针的时候,我一直以为 p+1 就是地址值直接加 1。直到有一天写代码发现,int *p 和 char *p 做 p+1 的结果完全不一样——同一个操作,打印出来的地址偏移居然差了 4 倍。于是我明白了一件事:指针加 1,加的不是 1 个字节,而是 1 个元素的大小。
提出问题
既然指针本身存的是一个地址值,那 p+1 到底加了多少?为什么 int* 和 char* 的表现不同?
示例
#include <stdio.h>
int main() {
int a = 10;
char c = 'A';
int *p_int = &a;
char *p_char = &c;
printf("p_int = %p\\n", p_int);
printf("p_int + 1 = %p\\n", p_int + 1);
printf("p_char = %p\\n", p_char);
printf("p_char + 1 = %p\\n", p_char + 1);
return 0;
}
在我的机器上输出类似这样(具体值每次运行不同,但偏移规律固定):
p_int = 0x7ffe1a3b5c2c
p_int + 1 = 0x7ffe1a3b5c30 ← 差 4 字节
p_char = 0x7ffe1a3b5c2b
p_char + 1 = 0x7ffe1a3b5c2c ← 差 1 字节
为什么这样
p + N 的实际偏移字节数等于 N × sizeof(基类型)。编译器在编译时就知道指针指向的类型占多大空间,所以自动帮你算好偏移量。
- int 在我的机器上占 4 个字节,所以 p_int + 1 地址向后跳 4。
- char 占 1 个字节,所以 p_char + 1 地址向后跳 1。
这个设计的好处是:你不用自己算偏移字节数,编译器帮你保证 p+1 刚好指向「下一个元素」的起始位置。
指针相减也是类似逻辑:两个同类型指针相减,结果是它们之间相差的元素个数,不是字节数。比如 (p + 5) – p 的结果是 5。
一句总结
指针运算的本质是元素级别的地址偏移,步长取决于指针指向的类型大小,不是机械的地址值加 1。
2. 指针操作一维数组:三种写法背后是同一条路
学过指针运算的本质之后,再回头看数组,很多曾经模糊的地方突然通了。
提出问题
明明数组和指针是两个东西,为什么 a[i] 和 *(p + i) 能访问到同一个元素?它们底层到底有什么关系?
示例
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // arr 就是首元素地址 &arr[0]
// 三种等价写法
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 下标法
printf("%d ", *(p + i)); // 指针偏移法
printf("%d ", p[i]); // 指针也可以像数组一样用下标
}
return 0;
}
三种写法输出的结果完全相同:10 20 30 40 50。
为什么这样
- 数组名 arr 在表达式中会被隐式转换成指向首元素的指针,所以 arr 和 p 存的是同一个地址。
- 编译器处理 arr[i] 时,内部就是按 *(arr + i) 来计算的——下标法只是指针偏移的语法糖。
- 反过来,指针 p 也能用 p[i] 这种写法,因为 p[i] 同样等价于 *(p + i)。
指针遍历数组的几种实际写法:
// 写法一:下标法(最直观)
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
// 写法二:指针偏移法(逐步后移)
int *p = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
// 写法三:指针自增法(最贴近“遍历”语义)
int *p = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *p);
p++; // 每次后移一个 int 元素
}
第三种写法最常见于实际项目:每轮循环打印当前元素,然后 p++ 让指针指向下一个元素,直到遍历完整个数组。
一句总结
数组名就是首元素地址,arr[i] 和 *(p+i) 和 p[i] 底层完全一致,选择哪种写法只是习惯问题。
3. 指针操作字符串:字符指针遍历与 char* 和 char[] 的区别
字符串本质上是一个以 \\0 结尾的字符数组,用指针操作字符串是 C 语言里非常高频的用法。
提出问题
给一个字符串,怎么用指针遍历打印每个字符?char *p 和 char s[] 看起来都能存字符串,它们到底有什么区别?
示例
#include <stdio.h>
int main() {
char s[] = "hello";
char *p = s; // p 指向 s 的首字符 'h'
while (*p != '\\0') {
printf("%c", *p);
p++; // 指针后移一个 char,即 1 个字节
}
printf("\\n");
return 0;
}
输出:hello。每次循环打印当前字符,然后 p++ 让指针指向下一个字符,直到遇见 \\0 终止。
为什么这样
- C 语言字符串以 \\0(ASCII 码 0)结尾,所以 while (*p != '\\0') 可以自然控制循环结束。
- p++ 每次偏移 sizeof(char) = 1 字节,刚好指向下一个字符。
char *p 与 char s[] 的关键区别:
| 本质 | 指针,指向字符串常量区 | 数组,在栈上开辟空间并拷贝内容 |
| 可否修改内容 | ❌ 不可改(常量区只读,修改会崩溃) | ✅ 可改,如 s[0] = 'H'; |
| 指针本身 | 可重新指向别的字符串 | 数组名不可重新赋值 |
踩坑实录: 我曾经写过 char *p = "hello"; p[0] = 'H'; 想改成大写,结果程序直接崩溃。原因就是 p 指向的是字符串常量区,那片内存只读不可写。如果你需要可修改的字符串,请用 char s[] = "hello";。
一句总结
字符指针遍历字符串用 *p != '\\0' 判断结束,char* 指向常量区不可修改,char[] 在栈上可自由修改。
4. const 修饰指针:离谁近就限定谁
const 修饰指针是初学者最容易混淆的点之一,四种写法只差一个 const 的位置,含义就完全不同。
提出问题
const char *s、char const *s、char *const s、const char *const s 到底怎么区分?有没有简单的记忆方法?
示例与对比
| const char *s | 指向的内容不可修改 | 不能通过 *s 改字符,但 s 可以指向别处 |
| char const *s | 同上(const 在 * 左边就行)——const char *s 是更常见的写法,char const *s 也合法但不常用 | 和上一条完全等价 |
| char *const s | 指针本身不可修改 | s 不能指向别处,但可以通过 *s 改内容 |
| const char *const s | 两者都不可修改 | 既不能改指向,也不能改内容 |
为什么这样
记忆原则只有一句话:const 离谁近,就限定谁。
- const 挨着 char,限定的是 *s(内容不可改)。
- const 挨着 *s(即 *const s),限定的是 s 本身(指针不可改)。
实际应用建议:函数形参如果只读不写,一律用 const char *。 比如 int puts(const char *s);,调用者传字符串常量、数组名、const char* 变量都能接住,函数内部还不会误修改字符串,代码更严谨。
一句总结
const 修饰指针看相对位置:挨着类型限定内容,挨着指针变量限定指向。
5. 今日练习题解:用指针思维实战两道题
到做练习的时候我才发现,光听懂原理不够,真正写代码才能暴露理解漏洞。
(1)单词倒置——「how are you」→「you are how」
我当时的第一反应: 拆分单词存数组、反向拼接。但老师说有更简洁的两遍逆序法。
正确思路:
整个过程用指针控制区间边界,逆序函数只交换首尾字符。
注意:第一步逆序整个字符串时,空格也被当作普通字符参与逆序,会从中间被移到两端。不过没关系——后续对每个单词单独逆序时,空格会自然归位,
完整代码:
#include <stdio.h>
#include <string.h>
// reverse: 将字符串区间 [start, end) 内的字符逆序
// 注意:end 指向最后一个字符的下一位置(即空格或'\\0'),不包含在交换中
void reverse(char *start, char *end) {
—end; // 让 end 指向最后一个有效字符
while (start < end) {
char tmp = *start; // 交换首尾字符
*start = *end;
*end = tmp;
start++;
end—;
}
}
int main() {
char str[] = "how are you";
printf("原字符串: %s\\n", str);
int len = strlen(str);
// 第一步:整个字符串逆序(不包含 '\\0')
reverse(str, str + len);
printf("整体逆序: %s\\n", str);
// 第二步:逐个单词逆序
char *p = str;
while (*p != '\\0') {
// 跳过空格
while (*p == ' ') p++;
if (*p == '\\0') break; // 已到字符串末尾
char *word_start = p; // 单词起始位置
// 找到单词结束(下一个空格或字符串结尾)
while (*p != ' ' && *p != '\\0') p++;
char *word_end = p; // 单词结束位置(指向空格或'\\0')
// 逆序当前单词
reverse(word_start, word_end);
}
printf("单词逆序后: %s\\n", str);
return 0;
}
编译运行结果:
原字符串: how are you
整体逆序: uoy era woh
单词逆序后: you are how
最终结果正确。
(2)间隔输出——「Computer Science」→「Cmue cec」
思路: 从首字符开始,每次跳过 1 个字符(步长 2),相当于只取偶数下标的字符。
用指针实现就是 p 初始指向首字符,每次 p += 2,
完整代码:
#include <stdio.h>
int main() {
char str[] = "Computer Science";
char *p = str;
printf("原字符串: %s\\n", str);
printf("间隔输出: ");
while (*p != '\\0') {
putchar(*p); // 打印当前字符
p += 2; // 步长 2,跳过下一个字符
}
printf("\\n");
return 0;
}
编译运行结果:
原字符串: Computer Science
间隔输出: Cmue cec
直到超出字符串末尾。
一句总结
指针解决字符串问题的关键是「用指针控制区间」而不是「用下标数位置」,代码会更紧凑也更贴近内存思维。
今日避坑指南
| p+1 以为是地址加 1 | 打印地址发现偏移数值不对 | 偏移量 = 1 × sizeof(基类型) |
| char *p = "abc"; p[0]='A'; | 程序崩溃 | 字面串在常量区不可写,用 char s[] |
| 用 == 比较两个字符串 | 永远不成立或结果不对 | == 比的是地址,应使用 strcmp() |
| const 位置一头雾水 | 四种写法混淆 | const 离谁近限定谁 |
| 数组名和指针划等号 | 以为 sizeof(s) 能拿到字符串长度 | 数组名在多数表达式中会退化为指针,但在 sizeof 和 & 运算中不会退化;因此 sizeof(数组名) 返回整个数组的字节数 |
今日学习感悟
今天才真正搞明白指针运算不是在玩地址数字游戏,而是编译器在默默帮我按元素大小算偏移。以前记不住 const 的那几种写法,现在用「离谁近就限定谁」这条口诀基本不会搞混了。写单词倒置的时候第一次没考虑 \\0,逆序完打印多了一堆乱码,找半天才发现是字符串结束符也被反转了——如果 reverse 函数把 \\0 也纳入交换范围,\\0 就会被移到开头,导致字符串被截断。实际实现时,reverse 的区间应排除 \\0,只处理有效字符部分。这种低级错误真得亲自动手写过才会长记性。
Day12 最大的收获:指针不是玄学,它是一套有规律可循的内存操作逻辑,理解 sizeof 和元素粒度的概念就通了八成。




