欢迎光临
我们一直在努力

指针 + 1 到底加了多少?——指针运算的本质,Day12 学习记录

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[] 的关键区别:

char *p = "hello";char s[] = "hello";
本质 指针,指向字符串常量区 数组,在栈上开辟空间并拷贝内容
可否修改内容 ❌ 不可改(常量区只读,修改会崩溃) ✅ 可改,如 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」

我当时的第一反应: 拆分单词存数组、反向拼接。但老师说有更简洁的两遍逆序法。

正确思路:

  • 先把整个字符串逆序:"how are you" → "uoy era woh"
  • 再把每个单词单独逆序:"uoy" → "you","era" → "are","woh" → "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 和元素粒度的概念就通了八成。

    赞(0)
    未经允许不得转载:171主机测评 » 指针 + 1 到底加了多少?——指针运算的本质,Day12 学习记录
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址