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;
}
⚠️ 重要:数组名的两个例外情况
数组名不是首元素地址的情况只有两个,这是面试必考点!
-
`sizeof(数组名)`:计算的是整个数组的总大小,不是指针的大小
-
`&数组名`:取的是整个数组的地址,不是首元素地址(虽然值一样)
除此之外,所有情况下数组名都自动退化为首元素的地址!
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:野指针
什么是野指针? 指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
产生原因:
-
指针变量未初始化
-
指针释放后未置空
-
指针操作超越了变量的作用域
错误代码 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还指着那个地址,但那个地址已经不属于我们了
// 这就是"悬空指针",非常危险!
总结
好了,今天的指针教程就到这里!让我们最后总结一下重点:
-
指针的本质:就是存放内存地址的变量,没什么神秘的
-
两个操作符:&取地址,*解引用
-
指针大小:32位4字节,64位8字节,和类型无关
-
数组名:大多数情况是首元素地址,只有sizeof和&时例外
-
函数传参:要改实参就传地址,数组传参要传长度
-
二级指针:用来修改一级指针本身
-
五大易错点:野指针、空指针、越界、返回局部地址、free后未置空
指针是C语言的灵魂,也是难点,但只要你理解了内存模型,多写代码多调试,一定能掌握它!
记住:学习指针最好的方法就是------把每个指针的地址都打印出来,亲眼看看内存里到底发生了什么!
加油,你一定可以的!�