欢迎光临
我们一直在努力

一篇拿下,C 语言指针全方位终极解析与易错点避坑指南

 🔥个人主页:代码不加冰(欢迎来访) 🎬作者简介:java后端学习者 ❄️个人专栏:LeetCode刷题日记 ,  苍穹外卖日记,SSM框架深入,JavaWeb, ✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!


前言:

大家好,我是代码不加冰,还有两天就要开始考C语言了,其实没看多少,这里总结一下指针的知识点吧,我感觉对于初学者,最不容易搞懂的就是指针了,让我们一起来看看吧。


摘要:

本文是一篇关于C语言指针知识点的总结文章,主要面向初学者和即将参加C语言考试的学生。文章通过生动的比喻(如将内存比作快递柜)和清晰的示例代码,详细讲解了指针的核心概念、语法规则以及常见应用场景。内容包括:指针的基本概念(地址、解引用)、指针与数组的关系、字符串指针与字符数组的区别、二级指针的作用、函数指针的使用、动态内存分配注意事项,以及二维数组指针的解析。文章特别强调了期末易错点,如指针类型转换、const修饰符、指针数组与数组指针的区别等,并提供了实用的记忆口诀和思维导图。最后通过典型例题解析帮助读者巩固知识,旨在帮助读者攻克指针难关,顺利通过考试。


一、 关于指针

很多人学指针卡壳,是因为把“地址”想得太抽象。我们可以把内存想象成一整条街的快递柜

  • 每个柜子(内存单元)都有一个唯一的编号(地址)

  • 指针变量就是一张写了“柜子编号”的纸条。拿着这张纸条,你就能找到对应的柜子,取出里面的东西(数据)

核心语法:

c

int a = 10; // 普通柜子,里面放了数字10
int *p = &a; // 纸条p,上面写着柜子a的编号
printf("%d", *p); // *p 是“顺着纸条去找那个柜子”,输出10

画重点:

  • &是“取地址”运算符,作用是获取门牌号。

  • *是“解引用”运算符(间接访问),作用是根据门牌号进屋拿东西。

  • 在定义变量时(如 int *p),这里的*只是告诉编译器:这是一个指针变量,用来存放地址。

重点!

当 * 出现在变量定义/声明中时,它表示这个变量是指针类型。

int *p; // p 是一个指向 int 的指针
char *q; // q 是一个指向 char 的指针
double **pp; // pp 是一个指向指针的指针(二级指针)

关键理解:这里的 * 不是"操作",而是"类型标记"!

int *p, q; // p 是 int*(指针),q 是 int(普通变量)!不是两个指针!

如果你想定义两个指针,必须每个都加 *:

int *p, *q; // p 和 q 都是 int*

二、 指针的身份

指针是有类型的(int *,char *,double *)。类型决定了两个关键属性:

  • 解引用时访问几个字节:int *访问4个字节,char *访问1个字节。

  • 指针加减整数时的步长:p+1跳过sizeof(类型)个字节。

  • c

    int a = 0x12345678;
    int *p = &a;
    char *q = (char *)&a;

    printf("%x", *p); // 输出 12345678(读4个字节)
    printf("%x", *q); // 输出 78(小端存储,只读1个字节)

    printf("%d", p+1 – p); // 输出 1,但地址值相差4

    期末考点:p+1到底加几,答案是加 sizeof(指针指向的类型)。

    三、 数组与指针的关系

    数组名在绝大多数情况下(注意是绝大多数)会被隐式转换为指向数组首元素的指针

    c

    int arr[5] = {1,2,3,4,5};
    int *p = arr; // 等价于 int *p = &arr[0];

    表达式含义类型
    arr 指向首元素a[0]的指针 int *
    &arr[0] 指向首元素a[0]的指针 int *
    &arr 指向整个数组的指针 int (*)[5]

    易错点:&arr + 1 跳过了整个数组(5个int),而 arr + 1 只跳过了1个int。

    c

    int arr[5] = {1,2,3,4,5};
    int *p = (int *)(&arr + 1);
    printf("%d", *(p – 1)); // 输出 5
    // 解析:&arr+1 指向数组尾部的下一个位置,再减1就指向了最后一个元素。

    四、 字符串指针与字符数组

    内存模型对比:

    arr (字符数组):
    栈上 (或静态区) 开辟了6个字节的空间,把"hello\\0"复制进来
    ┌────┬────┬────┬────┬────┬────┐
    │ 'h'│ 'e'│ 'l'│ 'l'│ 'o'│'\\0'│ ← 这块内存属于 arr,是变量
    └────┴────┴────┴────┴────┴────┘
    地址:1000

    ptr (字符串指针):
    栈上只开辟了1个指针的空间 (4/8字节),指向常量区的字符串
    ┌──────┐
    │ ptr │──────► 常量区: "hello\\0" (只读)
    └──────┘
    地址:2000 地址:3000
    核心区别:

    arr 是数组,它拥有自己的内存空间,存储的是字符串的副本。

    ptr 是指针,它只存了一个地址,指向常量区的字符串原文。

    • arr[] 是在栈上开辟的空间,内容可以随意读写。

    • ptr 指向常量区(也叫只读数据段),操作系统禁止修改这块内存。强行修改会导致程序崩溃。

    这是期末挂科率最高的地方,一定要分清指向常量区的指针和栈上的数组。

    给未初始化的指针赋值

    c

    char *ptr;
    strcpy(ptr, "hello"); // ❌ 危险!ptr是野指针,不知道指向哪里

    // 改正:分配空间
    char *ptr = (char*)malloc(6);
    strcpy(ptr, "hello"); // ✅ 安全

    混淆 = 和 strcpy

    c

    char arr[10];
    arr = "hello"; // ❌ 编译错误!数组名不能赋值

    char *ptr;
    ptr = "hello"; // ✅ 正确:让ptr指向常量区

    char arr[10];
    strcpy(arr, "hello"); // ✅ 正确:复制内容到数组

    高频题:

    c

    char *getString() {
    char str[] = "Hello"; // 局部数组,在栈上
    return str; // 返回了局部变量的地址
    } // 函数结束,str被销毁,这块内存变成“垃圾”
    // 调用者拿到的是一个野指针!

    改正方案:使用static修饰,或者用malloc动态分配内存。

    五、 二级指针

    一级指针改值,二级指针改一级指针的指向。

    二级指针就是一个普通的变量,它存储的是另一个指针变量的地址。

    换句话说:

    • 一级指针:存的是"普通变量"的地址

    • 二级指针:存的是"一级指针变量"的地址

    经典场景:在函数内部修改外部指针的指向。

    变量名 地址 存储的内容
    ┌─────┐
    │ a │ 1000 │ 10 ← 普通变量
    └─────┘
    ┌─────┐
    │ p │ 2000 │ 1000 ← 一级指针:存a的地址
    └─────┘
    ┌─────┐
    │ pp │ 3000 │ 2000 ← 二级指针:存p的地址
    └─────┘
    c

    void swap(int **pp, int **qq) {
    int *tmp = *pp;
    *pp = *qq;
    *qq = tmp;
    }

    int main() {
    int a = 1, b = 2;
    int *p = &a, *q = &b;
    swap(&p, &q); // 交换的是p和q指针本身指向的变量
    // 现在 p 指向 b,q 指向 a
    return 0;
    }

    六、 函数指针

    函数名就是函数的入口地址。函数指针常用于回调函数(比如qsort排序)。

    c

    // 定义一个函数指针类型,指向 参数为(int,int),返回值为int 的函数
    int (*pFunc)(int, int) = NULL;

    // 指向加法函数
    int add(int a, int b) { return a + b; }
    pFunc = add;
    printf("%d", pFunc(3, 5)); // 输出 8

    // 变态写法(了解即可):void (*signal(int, void(*)(int)))(int);
    // 这是signal函数的声明,它接受一个int和一个函数指针,返回一个函数指针。

    七、 动态内存分配

    分配方式例子特点问题
    静态分配 int a[100]; 编译时确定大小 数组大小固定,浪费或不够用
    自动分配(栈) int arr[n];(VLA) 函数执行时分配 函数结束自动释放,大小仍受限
    动态分配(堆) malloc(100 * sizeof(int)) 运行时决定大小 需要手动管理,灵活但容易出错

    使用malloc/calloc在堆上申请内存,用完必须free。

    c

    int *p = (int *)malloc(10 * sizeof(int));
    if (p == NULL) { // 必须检查是否申请成功
    exit(1);
    }
    // 使用 p…
    free(p); // 释放内存
    p = NULL; // 必须置空,防止野指针

    三大致命伤:

  • 内存泄漏:忘了free,程序越跑越卡。

  • 野指针:free之后没有置NULL,继续使用。

  • 悬挂指针:返回了局部变量的地址(前面字符串部分已讲)。

  • 八、 期末易错点大合集

    1. sizeof 和 strlen 的区别

    c

    char arr[] = "abc";
    char *p = "abc";
    printf("%d", sizeof(arr)); // 4 (包含'\\0')
    printf("%d", strlen(arr)); // 3 (不包含'\\0')
    printf("%d", sizeof(p)); // 4/8 (指针本身的大小,取决于系统是32位还是64位)
    printf("%d", strlen(p)); // 3

    2. 指针与const的三种方式

    c

    const int *p; // 指向的内容不能变(p可以变),即 *p 不可改
    int const *p; // 同上(写法不同)
    int * const p; // 指针本身不能变(p不能变),即 p 不可改
    const int * const p; // 都不能变

    口诀:const在*左边修饰指向的内容,在*右边修饰指针变量本身。

    3. 指针数组 vs 数组指针

    • 指针数组:int *p[5]; —— [ ] 优先级高,p先和[ ]结合,所以p是数组,数组里存的是int*指针。

    • 数组指针:int (*p)[5]; —— ( ) 优先级高,p先和*结合,所以p是指针,指向一个长度为5的int数组。


    补充:二维数组指针:

    二级指针(int **p)和二维数组的行指针(int (*p)[N])是完全不同的两码事,它们在内存布局上有着本质的区别,绝对不能混用。

    写法第一次操作第二次操作底层逻辑
    二维数组 a[i][j] 计算偏移量(编译期已知步长) 真正的解引用(取数据) 编译器帮你算好地址,只解引用一次
    二级指针 p[i][j] 第一次解引用(取出一级指针) 第二次解引用(取出最终数据) 两次间接访问,跳转两次内存

    通俗版理解:

    • 二维数组:你手里有张地图,告诉你从起点走 i 行 j 列,一次性找到那个格子。

    • 二级指针:你先去信箱 A 取出一张新纸条(第一次解引用),再拿着新纸条去信箱 B 取东西(第二次解引用)。

    首先,我们要明白:C语言的内存是线性的,没有真正的“二维”。

    c

    int a[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
    };

    这个数组在内存中,就是连续排列的12个int: 1 2 3 4 5 6 7 8 9 10 11 12

    所谓的“二维”,只是编译器帮我们做的逻辑划分:3行,每行4个。


    核心:a 和 &a[0]

    这是所有混乱的根源,我们必须分清楚两个概念:

    表达式含义类型
    a 二维数组的数组名,代表首行的地址(第0行的地址) int (*)[4](行指针,指向含4个int的一整行)
    &a[0] 取第0行的地址 int (*)[4](和上面的类型完全一样)
    a[0] 第0行的数组名,代表该行首元素的地址 int *(列指针,指向一个int)
    &a[0][0] 取第0行第0列元素的地址 int *(列指针)

    最重要的记忆点:

    a 是“行指针”,它指向的是“一整行”。 a[0] 是“列指针”,它指向的是“一个具体的int元素”。

    虽然 a 和 a[0] 打印出来的地址数值是相同的,但它们的类型完全不同,导致 +1 跳过的距离不同!


    我们来写代码验证,这是期末笔试最常考的点:

    c

    #include <stdio.h>

    int main() {
    int a[3][4] = {0};

    printf("a = %p, a+1 = %p, 跨度 = %d 字节\\n", a, a+1, (char*)(a+1) – (char*)a);
    printf("a[0] = %p, a[0]+1= %p, 跨度 = %d 字节\\n", a[0], a[0]+1, (char*)(a[0]+1) – (char*)a[0]);

    return 0;
    }

    运行结果(假设地址从1000开始):

    text

    a = 1000, a+1 = 1016, 跨度 = 16 字节 // 跳过了4个int(一行)
    a[0] = 1000, a[0]+1= 1004, 跨度 = 4 字节 // 跳过了1个int(一个元素)

    结论:

    • a + 1 跳到了第1行(跳了16个字节)

    • a[0] + 1 跳到了第0行第1列(跳了4个字节)


    假设我们要访问 a[i][j](第 i 行第 j 列),有以下等价写法:

    方式1:最简单直观(最推荐)

    c

    a[i][j]

    方式2:把行指针拆开(理解本质)

    c

    *(a[i] + j)
    // 先取第i行的数组名 a[i](即该行首元素地址),再偏移j个元素,最后解引用

    方式3:完全用指针运算

    c

    *(*(a + i) + j)
    // 1. a + i:从第0行地址移动到第i行地址(行指针)
    // 2. *(a + i):对行地址解引用,得到第i行的首元素地址(降级为列指针)
    // 3. *(a + i) + j:在该行内偏移j个元素
    // 4. 最外层 *:取出该元素的值

    重点理解 *(a+i) 发生了什么: a 是 int (*)[4] 类型,a+i 还是 int (*)[4] 类型。 对它解引用 *(a+i),就变成了 int * 类型(行指针 → 列指针的降级)。


     二维数组做函数参数(期末必考)

    如果你想把二维数组传给函数,有且只有三种正确写法:

    写法1:用 int a[][4](行数省略,列数必须写)

    c

    void printArray(int arr[][4], int rows) {
    for(int i=0; i<rows; i++) {
    for(int j=0; j<4; j++) {
    printf("%d ", arr[i][j]);
    }
    printf("\\n");
    }
    }
    // 调用:printArray(a, 3);

    写法2:用行指针 int (*p)[4]

    c

    void printArray(int (*p)[4], int rows) {
    // 用法一模一样
    for(int i=0; i<rows; i++) {
    for(int j=0; j<4; j++) {
    printf("%d ", p[i][j]); // 甚至可以 p[i][j]
    }
    }
    }

    错误示范(初学者常犯):

    c

    void printArray(int **arr) { … } // ❌ 这是二级指针,不能接收二维数组!
    printArray(a); // 编译警告/错误

    因为 int ** 和 int (*)[4] 是完全不同的类型,内存布局不一样


    指针数组 vs 数组指针(复习巩固)

    既然讲到二维数组,这两个概念必须再提一次,因为它们经常一起考:

    • 指针数组:int *p[3];

      • 它是一个数组,里面有3个元素,每个元素都是 int* 指针。

      • 常用来存放多个字符串:char *str[3] = {"hello", "world", "c"};

    • 数组指针(行指针):int (*p)[4];

      • 它是一个指针,指向一个有4个int元素的数组。

      • 正好可以用来指向二维数组的每一行:p = a;(让p指向a的第0行)

    记忆口诀:[] 优先级高于 *,所以 int *p[3] 先结合 [] 是数组;加括号 (*p) 就先结合 * 是指针。


     综合实战:用行指针遍历二维数组

    c

    #include <stdio.h>

    int main() {
    int a[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
    };

    // 定义一个行指针,指向含4个int的数组
    int (*row)[4] = a; // row 指向第0行

    // 用行指针遍历
    for(int i=0; i<3; i++) {
    for(int j=0; j<4; j++) {
    // 三种等价写法:
    // printf("%d ", row[i][j]);
    // printf("%d ", *(*(row+i)+j));
    printf("%d ", *(row[i] + j)); // 最直观的指针写法
    }
    printf("\\n");
    }

    // 如果想让 row 指向第2行?
    row = &a[2]; // 或者 row = a + 2;
    printf("第2行第1个元素:%d\\n", (*row)[0]); // 输出 9
    // 注意:row 是指向一整行的指针,所以 *row 才得到该行的首元素地址

    return 0;
    }


    总结:

    如果你现在还是很晕,请记住下面这句话,反复念三遍:

    二维数组名 a 是“行地址”,相当于 int (*)[N]。 每一行的数组名 a[i] 是“列地址”,相当于 int *。 对“行地址”解引用 *,就得到“列地址”。

    思维导图:

    text

    a (行地址, 类型 int (*)[4])

    ├─ a + 0 → 指向第0行
    │ │
    │ ├─ *(a+0) 即 a[0] (列地址, 类型 int *)
    │ │ ├─ a[0] + 0 → &a[0][0]
    │ │ ├─ a[0] + 1 → &a[0][1]
    │ │ └─ *(a[0]+1) → a[0][1]

    ├─ a + 1 → 指向第1行
    │ └─ *(a+1) 即 a[1] (列地址)

    └─ a + 2 → 指向第2行
    └─ *(a+2) 即 a[2] (列地址)

    九、一个例子搞懂:

    题目:下面程序运行后输出什么

    c

    int a[5] = {1, 2, 3, 4, 5};
    int *p = (int *)(&a + 1);
    printf("%d, %d", *(a + 1), *(p – 1));

    答案:2, 5

    解析:

  • *(a+1):a是首元素地址,+1指向第二个元素,解引用得2。

  • &a+1:取整个数组的地址,+1跳过整个数组(5个int),指向数组末尾。

  • 强制转为int *赋给p,此时p指向数组末尾。

  • *(p-1):指针向前移动一个int,指向最后一个元素,得5。


  • 总结:

    不要把指针想得太玄乎。它本身就是一个存着整数的变量,只不过这个整数比较特殊——它是内存的编号。只要记住:

    • 传参想改变外部变量,必须传地址(一级指针传值,二级指针传址)。

    • 返回值不要返回局部变量的地址。

    • 动态内存申请了就要释放,释放了就置空。

    • 数组名当指针用,但别混淆&arr和arr。

    祝大家期末考神附体,指针题目全部拿下!如果有疑问,欢迎评论区留言讨论。

    赞(0)
    未经允许不得转载:171主机测评 » 一篇拿下,C 语言指针全方位终极解析与易错点避坑指南
    分享到: 更多 (0)

    评论 抢沙发

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