C语言指针超详细教程——从入门到精通(面向初学者)

C语言指针超详细教程------从入门到精通(面向初学者)

作者:上弦月-编程 日期:2026-05-04

前言

大家好!今天我们来聊一聊C语言中最让人头疼,但也最核心的概念------指针。很多初学者一听到"指针"这两个字就头大,觉得这是个什么玄学东西。但其实,指针就像我们生活中的"地址"一样简单。

想象一下,你去快递站取快递,快递员问你:"你的快递在哪?"你不会说:"就在那个架子上,你自己找吧!"而是会说:"我的快递在A区3排2号。"这个"A区3排2号"就是地址。

在计算机中,每个数据都存放在内存的某个位置,这个位置就有一个"地址",而指针,就是专门用来存放这个地址的变量。

今天,我会用最通俗的语言,带你彻底搞懂指针!

什么是指针?指针的本质和内存模型

在讲指针之前,我们必须先搞懂:计算机的内存到底是什么样的?

1.1 内存模型:计算机的"大储物柜"

我们可以把计算机的内存想象成一个超大的储物柜,这个储物柜有很多很多小格子,每个小格子都有一个唯一的编号,就像酒店的房间号一样。

  • 每个小格子可以存放1个字节(Byte)的数据

  • 每个小格子都有一个唯一的编号,这个编号就是内存地址

  • 32位系统最多有2^32个格子(约4GB内存)

  • 64位系统最多有2^64个格子(理论上无限大)

让我们用文字来画一个内存示意图:

地址(十六进制) 存放的数据 ─────────────────────────────────── 0x000000000061FE10 │ 10 │ ← 这里存了一个整数10 0x000000000061FE11 │ 00 │ 0x000000000061FE12 │ 00 │ 0x000000000061FE13 │ 00 │ ─────────────────────────────────── 0x000000000061FE14 │ 0x61FE10 │ ← 这里存了上面那个数的地址! 0x000000000061FE15 │ 00 │ 这就是指针! 0x000000000061FE16 │ 00 │ 0x000000000061FE17 │ 00 │ ───────────────────────────────────

看到了吗?在地址`0x000000000061FE14`这个位置,存放的数据不是普通的数字,而是另一个内存的地址`0x000000000061FE10`!

这就是指针的本质:指针就是一个存放内存地址的变量!

就这么简单!指针没有什么神秘的,它和int、char这些变量一样,只是它存的不是普通数据,而是地址而已。

指针变量的定义和使用

现在我们知道了指针是什么,接下来看看怎么在代码中定义和使用指针。

2.1 取地址操作符 &:获取变量的门牌号

`&`符号叫做"取地址操作符",它的作用就是获取一个变量在内存中的地址。

就像你问服务员:"请问洗手间在哪?"服务员告诉你:"在走廊尽头左转。"这个过程就是"取地址"。

让我们看代码示例:

代码示例1:取地址操作

� 代码示例1:取地址操作

#include <stdio.h>

int main()

{

int a = 10; // 定义一个整型变量a,赋值为10

printf("a的值是:%d\n", a); // 输出a的值:10

printf("a的地址是:%p\n", &a); // 输出a的内存地址(%p专门用来打印地址)

// 注意:每次运行程序,地址可能都不一样,因为操作系统会重新分配内存

// 这就像你每次去酒店,房间号可能都不一样,但房间还是那个房间

return 0;

}

2.2 解引用操作符 *:根据地址找到数据

`*`符号叫做"解引用操作符",它的作用是:根据指针中存放的地址,找到那个地址里真正的数据。

这就像你拿着快递单号(地址),去快递站找到对应的快递(数据)。

让我们看完整的指针使用代码:

代码示例2:指针的完整使用

� 代码示例2:指针的完整使用

#include <stdio.h>

int main()

{

int a = 10; // 第1步:定义普通变量a,值为10

int *p; // 第2步:定义指针变量p,注意:*在这里是类型说明,不是解引用!

p = &a; // 第3步:把a的地址赋值给p

// 现在p中存的就是a的地址!

printf("a的值 = %d\n", a); // 直接访问a:10

printf("a的地址 = %p\n", &a); // a的地址

printf("p的值 = %p\n", p); // p中存的地址(和a的地址一样)

printf("*p的值 = %d\n", *p); // 根据p中的地址找到数据:10

// 修改指针指向的数据

*p = 20; // 通过指针修改a的值

printf("修改后a的值 = %d\n", a); // a变成了20!

// 生活类比:

// a就像酒店房间里的人

// &a就是房间号

// p就是写着房间号的纸条

// *p就是根据纸条上的房间号,找到房间里的人

return 0;

}

2.3 指针定义的几种写法

很多初学者会困惑指针定义时`*`的位置,其实这几种写法都是对的,但推荐第一种:

int *p; // ✅ 推荐写法:*和变量名挨在一起,清晰表明p是指针int* p; // ❌ 不推荐,容易误以为int*是一种类型,定义多个时会出错int * p; // 也可以,但空格没必要 // 反面教材:这样写只有p1是指针,p2只是普通int!int* p1, p2; // ❌ p2不是指针!int *p1, *p2; // ✅ 正确,两个都是指针

指针的类型

指针也是有类型的!就像快递单上会写"这是生鲜快递"、"这是文件快递",不同类型的快递处理方式不一样。

指针的类型决定了:当我们解引用时,一次能访问多少个字节

3.1 不同类型指针的区别

让我们用表格来对比一下不同类型指针的特点:

表3-1:不同指针类型对比

|--------------|------------------|--------------|------------------|
| 指针类型 | 解引用访问字节数 | 主要用途 | 特点说明 |
| int* | 4字节 | 指向整型数据 | 最常用,访问一个int的大小 |
| char* | 1字节 | 指向字符/字符串 | 字符串处理必备 |
| double* | 8字节 | 指向浮点型数据 | 处理高精度小数 |
| void* | 不确定 | 通用指针,传参用 | 不能直接解引用,需要强制类型转换 |

代码示例3:指针类型的意义

� 代码示例3:指针类型的意义

#include <stdio.h>

int main()

{

int a = 0x11223344; // 一个4字节的整数,十六进制表示方便观察

int *p_int = &a; // int型指针

char *p_char = (char*)&a; // char型指针,强制类型转换

printf("*p_int = 0x%x\n", *p_int); // 输出:0x11223344(读了4个字节)

printf("*p_char = 0x%x\n", *p_char); // 输出:0x44(只读了1个字节)

// 为什么只读到0x44?因为x86是小端存储,低地址存低位数据

// 内存中实际是这样存的:

// 地址: 0x100 0x101 0x102 0x103

// 数据: 0x44 0x33 0x22 0x11

return 0;

}

3.2 指针的大小:32位 vs 64位

这里有一个非常重要的结论:

在同一个系统中,不管是什么类型的指针,大小都是一样的!

为什么?因为指针存的是地址,地址的长度由系统决定:

  • 32位系统:地址是32位(4字节),所以所有指针都是4字节

  • 64位系统:地址是64位(8字节),所以所有指针都是8字节

这就像不管你寄的是手机还是衣服,快递单的大小都是一样的,因为快递单上写的都是地址!

代码示例4:指针的大小

� 代码示例4:指针的大小

#include <stdio.h>

int main()

{

printf("int* 大小:%zu字节\n", sizeof(int*));

printf("char* 大小:%zu字节\n", sizeof(char*));

printf("double*大小:%zu字节\n", sizeof(double*));

printf("void* 大小:%zu字节\n", sizeof(void*));

// 64位系统下输出:都是8字节

// 32位系统下输出:都是4字节

// 面试必考题:指针的大小是多少?

// 标准答案:看系统,32位4字节,64位8字节,和类型无关!

return 0;

}

指针与数组的关系

指针和数组的关系可以说是C语言中最微妙、最容易搞混的地方了。记住一句话:数组名在大多数情况下就是首元素的地址!

4.1 数组名的本质

先看代码:

代码示例5:数组名就是首元素地址

� 代码示例5:数组名就是首元素地址

#include <stdio.h>

int main()

{

int arr[5] = {1, 2, 3, 4, 5};

printf("arr = %p\n", arr); // 数组名

printf("&arr[0] = %p\n", &arr[0]); // 第一个元素的地址

printf("&arr = %p\n", &arr); // 整个数组的地址(值一样,但意义不同!)

// 三个打印出来的值是一样的!

// 但注意:它们的类型不一样!

// arr 是 int* 类型(指向第一个int)

// &arr 是 int(*)[5] 类型(指向整个数组)

return 0;

}

⚠️ 重要:数组名的两个例外情况

数组名不是首元素地址的情况只有两个,这是面试必考点!

  1. `sizeof(数组名)`:计算的是整个数组的总大小,不是指针的大小

  2. `&数组名`:取的是整个数组的地址,不是首元素地址(虽然值一样)

除此之外,所有情况下数组名都自动退化为首元素的地址!

4.2 用指针遍历数组

既然数组名就是首元素地址,那我们当然可以用指针来遍历数组:

代码示例6:指针遍历数组

� 代码示例6:指针遍历数组

#include <stdio.h>

int main()

{

int arr[5] = {1, 2, 3, 4, 5};

int *p = arr; // p指向数组第一个元素

int i;

// 方法1:用下标访问

printf("方法1:下标访问\n");

for (i = 0; i < 5; i++)

{

printf("%d ", arr[i]);

}

printf("\n");

// 方法2:用指针+偏移

printf("方法2:指针+偏移\n");

for (i = 0; i < 5; i++)

{

printf("%d ", *(p + i)); // p+i 就是第i个元素的地址

}

printf("\n");

// 方法3:移动指针本身

printf("方法3:移动指针\n");

for (p = arr; p < arr + 5; p++)

{

printf("%d ", *p);

}

printf("\n");

// 神奇的发现:arr[i] 等价于 *(arr + i)

// 甚至:i[arr] 也是对的!因为加法交换律!(但千万别这么写)

return 0;

}

指针与函数

指针最大的用途之一就是在函数间传递数据。还记得吗?C语言的函数参数传递本质上都是"值传递"。

5.1 传值调用 vs 传址调用

先看一个经典的例子:写一个函数交换两个变量的值。

初学者最容易犯的错误:

代码示例7:错误的交换函数(传值)

� 代码示例7:错误的交换函数(传值)

#include <stdio.h>

// 错误版本:传值调用

void swap(int x, int y)

{

int temp = x;

x = y;

y = temp;

printf("函数内:x=%d, y=%d\n", x, y); // 函数内确实交换了

}

int main()

{

int a = 10, b = 20;

swap(a, b);

printf("函数外:a=%d, b=%d\n", a, b); // 外面没变化!还是10,20

// 为什么?

// 因为传值调用是"拷贝一份"!

// x和y是a和b的副本,修改副本不影响原件

// 就像你把文件复印一份给别人,别人在复印件上乱画,你的原件不受影响

return 0;

}

代码示例8:正确的交换函数(传址)

� 代码示例8:正确的交换函数(传址)

#include <stdio.h>

// 正确版本:传址调用

void swap(int *x, int *y) // 接收地址!

{

int temp = *x; // 根据地址找到a的值

*x = *y;

*y = temp;

}

int main()

{

int a = 10, b = 20;

swap(&a, &b); // 把a和b的地址传过去!

printf("交换后:a=%d, b=%d\n", a, b); // 成功交换!20,10

// 原理:

// 虽然x和y还是副本,但它们存的是a和b的地址!

// 通过地址就能找到并修改原件!

// 就像你把家里的钥匙给别人,别人就能直接进你家改东西

return 0;

}

// 面试口诀:要想改实参,就得传地址!

5.2 数组作为函数参数的本质

这里又是一个大坑!当数组作为函数参数时,它会退化为指针!

也就是说:

void func(int arr[10]) // 写了10也没用!void func(int arr[]) // 等价于上面void func(int *arr) // 本质就是这个!

所以在函数内用sizeof(arr)得到的是指针的大小,不是数组的大小!这是90%的初学者都会踩的坑!

代码示例9:数组传参的本质

� 代码示例9:数组传参的本质

#include <stdio.h>

void print_size(int arr[]) // 等价于 int *arr

{

printf("函数内sizeof(arr) = %zu\n", sizeof(arr));

// 64位系统输出8,因为是指针!不是数组大小!

}

int main()

{

int arr[10] = {0};

printf("函数外sizeof(arr) = %zu\n", sizeof(arr)); // 40字节(10*4)

print_size(arr);

// 所以:数组传参时,一定要单独传数组长度!

return 0;

}

二级指针(指向指针的指针)

既然指针是变量,那变量就有地址,那指针的地址存在哪呢?答案是:二级指针!

6.1 二级指针的内存模型

还是用储物柜来理解:

  • 普通变量a:储物柜里放着数据

  • 一级指针p:储物柜里放着a的地址

  • 二级指针pp:储物柜里放着p的地址

文字化内存模型:

地址 内容 ─────────────────────────────── 0x100 │ 10 │ ← a = 10 ─────────────────────────────── 0x200 │ 0x100 │ ← p = &a ─────────────────────────────── 0x300 │ 0x200 │ ← pp = &p ───────────────────────────────

访问方式:

  • `a` = 10

  • `*p` = *0x100 = 10

  • `**pp` = *(*0x300) = *0x100 = 10

代码示例10:二级指针的使用

� 代码示例10:二级指针的使用

#include <stdio.h>

int main()

{

int a = 10;

int *p = &a; // 一级指针,存a的地址

int **pp = &p; // 二级指针,存p的地址

printf("a = %d\n", a);

printf("*p = %d\n", *p);

printf("****pp = %d\n",****pp); // 两次解引用

// 都输出10!

// 通过二级指针修改a的值

**pp = 100;

printf("修改后a = %d\n", a); // a变成100了!

return 0;

}

6.2 二级指针的使用场景

二级指针最常用的场景:在函数内修改一级指针本身的值

比如:我们想在函数内给一个指针分配内存(malloc),就需要传二级指针:

代码示例11:二级指针的实际应用

� 代码示例11:二级指针的实际应用

#include <stdio.h>

#include <stdlib.h>

// 在函数内给指针分配内存

void allocate(int **pp)

{

*pp = (int*)malloc(sizeof(int) * 5); // 修改p本身的值

}

int main()

{

int *p = NULL;

allocate(&p); // 传指针的地址!

// 现在p指向了分配的内存

p[0] = 10;

printf("p[0] = %d\n", p[0]);

free(p);

return 0;

}

// 如果不传二级指针,函数内修改的只是副本,外面的p还是NULL!

指针常见面试题和易错点总结

最后,我们来总结一下指针最容易出错的5个地方,这些都是面试高频考点!

7.1 易错点1:野指针

什么是野指针? 指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

产生原因:

  1. 指针变量未初始化

  2. 指针释放后未置空

  3. 指针操作超越了变量的作用域

错误代码 vs 正确代码

� 错误代码 vs 正确代码

// ========== 错误写法 ==========

void bad_code1()

{

int *p; // ❌ 未初始化!p是随机值

*p = 10; // 可能直接崩溃!

}

// ========== 正确写法 ==========

void good_code1()

{

int a = 10;

int *p = &a; // ✅ 初始化时就指向有效的地址

// 或者

int *p2 = NULL; // ✅ 暂时不用就初始化为NULL

}

// 原因分析:

// 局部变量不初始化就是随机值,指针存了随机地址就像拿着一把不知道开哪的钥匙

// 很可能访问到不该访问的内存,导致程序崩溃(段错误)

7.2 易错点2:空指针NULL

什么是空指针? `#define NULL ((void*)0)`,让指针指向0地址

注意:0地址是不能读写的!

错误代码 vs 正确代码

� 错误代码 vs 正确代码

// ========== 错误写法 ==========

void bad_code2()

{

int *p = NULL;

*p = 10; // ❌ 对空指针解引用!直接崩溃!

}

// ========== 正确写法 ==========

void good_code2()

{

int *p = NULL;

if (p != NULL) // ✅ 使用前一定要检查!

{

*p = 10;

}

}

// 原因分析:

// 0地址是操作系统保护的地址,不允许用户程序访问

// 对NULL解引用是C程序最常见的崩溃原因之一

7.3 易错点3:指针越界

指针访问了超出合法范围的内存

错误代码 vs 正确代码

� 错误代码 vs 正确代码

// ========== 错误写法 ==========

void bad_code3()

{

int arr[5] = {1,2,3,4,5};

int *p = arr;

for (int i = 0; i <= 5; i++) // ❌ i=5时越界了!

{

printf("%d ", *(p + i)); // 访问到arr[5],这是非法的

}

}

// ========== 正确写法 ==========

void good_code3()

{

int arr[5] = {1,2,3,4,5};

int *p = arr;

for (int i = 0; i < 5; i++) // ✅ 严格控制范围

{

printf("%d ", *(p + i));

}

}

// 原因分析:

// 数组只有5个元素,下标0-4

// 越界访问的后果是不可预测的,可能什么事都没有,也可能崩溃

// 这就是C语言的可怕之处:错了不一定马上报错!

7.4 易错点4:返回局部变量的地址

函数返回后,局部变量就被销毁了

错误代码 vs 正确代码

� 错误代码 vs 正确代码

// ========== 错误写法 ==========

int* bad_code4()

{

int a = 10;

return &a; // ❌ 返回局部变量地址!

} // 函数结束后a就不存在了!

// ========== 正确写法 ==========

int* good_code4()

{

static int a = 10; // ✅ static变量生命周期是整个程序

return &a;

// 或者用malloc动态分配内存

// int *p = malloc(sizeof(int));

// return p;

}

// 原因分析:

// 局部变量存在栈上,函数返回后栈帧就被销毁了

// 那个地址里的数据已经无效,再访问就是非法的

// 就像酒店退房了,你还拿着原来的房卡想去开门

7.5 易错点5:free后未置空

动态内存释放后,指针变成野指针

错误代码 vs 正确代码

� 错误代码 vs 正确代码

// ========== 错误写法 ==========

void bad_code5()

{

int *p = (int*)malloc(sizeof(int));

free(p); // 释放了内存

*p = 10; // ❌ p已经是野指针!

}

// ========== 正确写法 ==========

void good_code5()

{

int *p = (int*)malloc(sizeof(int));

free(p);

p = NULL; // ✅ free后立刻置空!

if (p != NULL) // 后面使用前检查

{

*p = 10;

}

}

// 原因分析:

// free只是把内存还给操作系统,p的值并没有变

// 这时候p还指着那个地址,但那个地址已经不属于我们了

// 这就是"悬空指针",非常危险!

总结

好了,今天的指针教程就到这里!让我们最后总结一下重点:

  1. 指针的本质:就是存放内存地址的变量,没什么神秘的

  2. 两个操作符:&取地址,*解引用

  3. 指针大小:32位4字节,64位8字节,和类型无关

  4. 数组名:大多数情况是首元素地址,只有sizeof和&时例外

  5. 函数传参:要改实参就传地址,数组传参要传长度

  6. 二级指针:用来修改一级指针本身

  7. 五大易错点:野指针、空指针、越界、返回局部地址、free后未置空

指针是C语言的灵魂,也是难点,但只要你理解了内存模型,多写代码多调试,一定能掌握它!

记住:学习指针最好的方法就是------把每个指针的地址都打印出来,亲眼看看内存里到底发生了什么!

加油,你一定可以的!�

相关推荐
ANnianStriver1 小时前
Java中的stream流的用法
java
莫等闲-1 小时前
代码随想录一刷记录Day44——leetcode1143.最长公共子序列 53. 最大子序和
数据结构·c++·算法·leetcode·动态规划
生成论实验室1 小时前
《事件关系阴阳博弈动力学:识势应势之道》第七篇:社会与情感关系——连接、表达与共鸣
人工智能·算法·架构·交互·创业创新
1104.北光c°1 小时前
【AI核心概念讲解】一口气搞懂 Agent:干翻传统后端!自主循环决策的秘密,ReAct 与 Plan-and-Execute 范式
java·人工智能·程序人生·ai·agent·react·智能体
承渊政道1 小时前
【动态规划算法】(背包问题经典模型与解题套路)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
Jul1en_1 小时前
Claude 迁移 Codex 工作流迁移与更新
java·服务器·前端·后端·ai编程
未若君雅裁2 小时前
Spring Statemachine 实战入门:从零实现一个订单状态流转 Demo
java·spring·状态模式
早日退休!!!2 小时前
操作系统锁
java·开发语言
yyy(十一月限定版)2 小时前
数电1对应latex代码
算法