C语言笔记归纳10:指针(1)
类型* 变量名int main()int a = 10;// p是指针变量(本质是变量),存放a的地址;int*表示p指向的是int类型变量return 0;的作用:仅用于说明 “这个变量是指针类型”,和变量值无关;指针变量的命名:通常加p前缀(如pcpa),方便区分普通变量。指针的本质是地址,指针变量是存放地址的容器—— 掌握指针的关键,是理解 “类型决定操作规则”“地址操作直接操作内存”。指针
指针(1)
目录
✨ 引言:
指针是 C 语言的 “灵魂”,也是新手的 “噩梦”🤯—— 有人说 “学会指针,C 语言就掌握了一半”,这话一点不假!很多人对指针的理解停留在 “地址” 层面,却不懂其底层原理和实战技巧,导致写代码时避之不及,或踩坑无数。
1. 🧠 指针基础:内存与地址的本质
要学指针,先懂内存 —— 指针的所有操作都围绕内存展开!
1.1 内存是如何被管理的?
计算机的内存就像一栋 “宿舍楼”🏫,如果房间没有编号,找人(访问数据)只能挨个敲门,效率极低。因此,系统会对内存做两件关键事:
- 划分最小单元:将内存分成一个个 1 字节(1Byte = 8bit)的 “小房间”,每个房间能存 8 个二进制位(好比 “八人间”);
- 给房间编号:每个字节都分配唯一的 “门牌号”,这个编号就是地址。
1.2 地址 = 指针?一文说清
- 内存单元的编号 = 地址 = 指针(三者本质是同一个东西);
- 定义变量(如
int a = 10),本质是向内存 “租” 了一块连续的房间:int占 4 字节,就是租了 4 个相邻的 “小房间”; - 变量的地址,是这 4 个房间中最小的那个门牌号(首地址)—— 知道了首地址,就能顺藤摸瓜找到剩下的房间。
1.3 地址总线:决定地址的 “容量”
内存的 “门牌号”(地址)是通过地址总线传递的,地址总线的根数决定了能表示的地址数量:
- 32 位机器:32 根地址总线,每根线有 0/1 两种状态,能表示
2^32个地址(范围:0x00000000~0xFFFFFFFF),需 4 字节存储; - 64 位机器:64 根地址总线,能表示
2^64个地址,需 8 字节存储。
📌 小贴士:地址总线的根数,直接决定了指针变量的大小(后面会详细说)!
2. 📌 指针变量:存放地址的 “容器”
指针是地址,但 “指针变量” 是专门存放地址的变量—— 就像用纸条记下宿舍楼的门牌号,纸条本身不是门牌号,而是存放门牌号的容器。
2.1 取地址操作符 &:获取变量的 “门牌号”
&是单目操作符,作用是获取变量的首地址,格式:&变量名:
int main()
{
int a = 10;
printf("%p\n", &a); // 输出a的首地址(如00D9FA1C),%p是地址的格式符
return 0;
}
📌 小贴士:变量的每个字节都有地址,但&a只返回首地址 —— 就像说 “302 宿舍”,默认指的是 302 房间的第一个床位。
2.2 指针变量的定义:不是指针,是变量!
指针变量的定义格式:类型* 变量名,核心是 “存放地址”:
int main()
{
int a = 10;
// p是指针变量(本质是变量),存放a的地址;int*表示p指向的是int类型变量
int* p = &a;
return 0;
}
*的作用:仅用于说明 “这个变量是指针类型”,和变量值无关;- 指针变量的命名:通常加
p前缀(如pc、pa),方便区分普通变量。
2.3 解引用操作符 *:通过地址访问变量
*是单目操作符,作用是 “按地址开门”—— 通过指针变量存放的地址,访问对应的变量:
int main()
{
int a = 0;
int* p = &a;
*p = 100; // 解引用:通过p的地址,修改a的值(等价于a=100)
*&a = 200; // 等价于a=200(先取地址,再解引用,回到变量本身)
printf("%d", a); // 输出200
return 0;
}
🤔 比喻:&a是获取 a 的门牌号,*p是拿着门牌号找到 a 的房间,然后修改房间里的内容。
3. 📏 指针变量的大小:和类型无关?
指针变量的大小,只和平台(32 位 / 64 位) 有关,和指向的类型无关!
int main()
{ // x86(32位) // x64(64位)
printf("%zd\n", sizeof(char*)); // 4 8
printf("%zd\n", sizeof(int*)); // 4 8
printf("%zd\n", sizeof(float*)); // 4 8
printf("%zd\n", sizeof(double*)); // 4 8
printf("%zd\n", sizeof(short*)); // 4 8
return 0;
}
原因:
- 32 位平台:地址是 32 位二进制(4 字节),指针变量要存下地址,必须占 4 字节;
- 64 位平台:地址是 64 位二进制(8 字节),指针变量占 8 字节。
📌 结论:不管是char*还是int*,只要是指针变量,在同一个平台上大小都相同!
4. 🔍 指针类型的意义:不止是 “标签”
既然指针变量大小和类型无关,为什么还要分int*、char*?—— 类型决定了指针的 “操作规则”!
4.1 解引用的 “访问权限”:能读多少字节?
指针类型决定了解引用时,能访问内存的字节数(即 “访问权限”):
int main()
{
int a = 0x11223344; // 内存存储:44 33 22 11(小端序:低位字节存低地址)
int* pa = &a;
*pa = 0; // int*解引用:访问4字节,内存变为00 00 00 00
return 0;
}
int main()
{
int a = 0x11223344; // 内存存储:44 33 22 11
char* pc = &a;
*pc = 0; // char*解引用:仅访问1字节,内存变为00 33 22 11
return 0;
}
📌 小贴士:
类型是 “权限凭证”——int*能操作 4 字节,char*只能操作 1 字节,超出权限会导致数据错误!
4.2 指针 ± 整数的 “步长”:一次走多远?
指针 ± 整数时,移动的字节数 = n * sizeof(指向的类型)(步长由类型决定):
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
printf("pa = %p\n", pa); // 00AFF7E4
printf("pa+1 = %p\n", pa+1);// 00AFF7E8(+4字节,int的大小)
printf("pc = %p\n", pc); // 00AFF7E4
printf("pc+1 = %p\n", pc+1);// 00AFF7E5(+1字节,char的大小)
return 0;
}
🤔 比喻:
int*指针像 “大步走”,一步 4 字节;char*指针像 “小步走”,一步 1 字节 —— 步长由类型决定!
📌 结论:指针类型决定了指针 “向前 / 向后走一步” 的距离,这是遍历数组的核心!
5. 🌐 void * 指针:万能的 “泛型指针”
void*是 “无类型指针”(泛型指针),特点是:
- 能接收任意类型的地址(万能接收器);
- 不能直接解引用,也不能 ± 整数(无类型,无操作规则)。
int main()
{
int a = 0;
float f = 0.0f;
void* p = &a; // 接收int*类型地址
p = &f; // 接收float*类型地址,合法
return 0;
}
int main()
{
int a = 0;
void* p = &a;
//*p = 20; // 错误:void*不能直接解引用
//p = p + 1; // 错误:void*不能±整数
}
应用场景:
函数参数中接收任意类型地址(如memcpy、qsort),实现 “泛型编程”—— 一个函数能处理多种类型数据。
6. 🔒 const 修饰指针:两种场景,两种限制
const修饰指针变量,位置不同,限制的对象也不同 —— 记住口诀:“左锁内容,右锁指针”!
6.1 const 在*左侧:锁住指针指向的内容
不能通过指针修改目标变量,但指针本身可指向其他地址:
int main()
{
int a = 10, b = 20;
int const* p = &a; // 等价于const int* p = &a
p = &b; // 合法:指针变量可修改(指向b)
//*p = 100; // 错误:不能修改指向的内容(a或b的值)
return 0;
}
📌 解读:const在*左侧,限制的是 “*p”(指针指向的内容),相当于给内容上了锁。
6.2 const 在*右侧:锁住指针变量本身
指针变量不能指向其他地址,但可修改指向的内容:
int main()
{
int a = 10, b = 20;
int* const p = &a;
//p = &b; // 错误:指针变量不可修改(不能指向b)
*p = 100; // 合法:可修改指向的内容(a的值)
printf("%d\n", a); // 输出100
return 0;
}
📌 解读:const在*右侧,限制的是 “p”(指针变量本身),相当于给指针上了锁。
6.3 const 在*两侧:双重锁定
指针变量和指向的内容都不能修改:
int main()
{
int a = 10, b = 100;
int const* const p = &a; // 等价于const int* const p = &a
p = &b; // 错误:指针变量不能改
*p = 0; // 错误:指向的内容不能改
return 0;
}
7. ➕➖ 指针的运算规则:不是普通的数学运算
指针支持三种运算:± 整数、指针 - 指针、关系运算 —— 但运算结果有特殊含义!
7.1 指针 ± 整数:遍历数组的核心
指针 ± 整数的本质是 “移动指针,指向相邻元素”,步长由类型决定(数组在内存中连续存储):
// 写法2:指针++遍历数组
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = &arr[0]; // p指向数组首元素
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *p); // 访问当前指针指向的元素
p++; // 指针向后移动一步(int*步长4字节)
}
return 0;
}
// 写法3:指针+i遍历数组
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i)); // p+i指向第i个元素
}
return 0;
}
📌 小贴士:arr[i]等价于*(arr+i)(数组名arr是首元素地址),这是数组和指针的核心关联!
7.2 指针 - 指针:计算元素个数
前提:两个指针必须指向同一块连续内存(如同一数组);结果:两个指针之间的元素个数(绝对值)。
int main()
{
int arr[10] = {0};
printf("%zd\n", &arr[9] - &arr[0]); // 输出9(第9个元素 - 第0个元素 = 9个间隔)
return 0;
}
实战:模拟实现 strlen 函数
#include <string.h>
// 法二:指针-指针计算字符串长度
int my_strlen(char* str)
{
char* start = str; // 记录字符串首地址
while (*str != '\0') // 遍历到字符串结束符
{
str++;
}
return str - start; // 差值 = 字符个数
}
int main()
{
char arr[] = "abcdef"; // 存储:a b c d e f \0
int len = my_strlen(arr); // arr是首元素地址
printf("%d\n", len); // 输出6
return 0;
}
📌 注意:指针 + 指针无意义(如&arr[0] + &arr[9]),就像 “两个门牌号相加,得不到任何有用信息”!
7.3 指针的关系运算:比较地址的大小
指针可比较大小(如<、<=、>、>=),常用于循环遍历:
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr; // p指向首元素
while (p < arr + sz) // p < 数组尾地址(arr+sz指向数组最后一个元素的下一个地址)
{
printf("%d ", *p);
p++;
}
return 0;
}
📌 小贴士:arr + sz是数组的 “尾后地址”(不指向任何元素),仅用于判断遍历结束。
8. 🐶 野指针:最危险的 “陷阱”
野指针是 “无主指针”—— 指向的地址随机、非法,访问野指针会导致程序崩溃或数据错误(未定义行为)!
8.1 野指针的三大成因
成因 1:指针未初始化
局部指针变量默认值随机,是野指针:
int main()
{
int* p; // 局部变量未初始化,值随机(野指针)
*p = 20; // 非法访问:崩溃风险极高!
return 0;
}
成因 2:指针越界访问
指针超出数组或申请的内存范围,变为野指针:
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i <= sz; i++) // i=10时,p指向数组外(越界)
{
printf("%d ", *p);
p++; // 越界后,p变为野指针
}
return 0;
}
成因 3:指针指向的空间被释放
函数结束后,局部变量的内存被释放,返回其地址会导致野指针:
int* test()
{
int a = 10; // 局部变量,函数结束后内存释放
return &a; // 返回已释放的地址(野指针)
}
int main()
{
int* p = test();
printf("%d\n", *p); // 非法访问:值随机或程序崩溃
return 0;
}
8.2 规避野指针的四大 “锦囊妙计”
妙计 1:初始化指针
- 明确指向:
int* p = &a;; - 暂时无指向:赋值
NULL(NULL是 C 语言定义的标识符常量,值为 0,该地址不可访问):
int main()
{
int a = 10;
int* p1 = &a; // 明确指向
int* p2 = NULL; // 暂时无指向,赋值NULL
return 0;
}
妙计 2:避免指针越界
严格控制访问范围,数组遍历用i < sz(而非i <= sz)。
妙计 3:指针不用时置NULL
指针指向的空间释放后,及时置NULL,通过if(p != NULL)判断合法性:
int main()
{
int* p = NULL;
if (p != NULL) // p为NULL,跳过非法操作
{
*p = 100;
}
return 0;
}
妙计 4:不返回局部变量的地址
局部变量的生命周期随函数结束而终止,其地址无意义。
9. ⚠️ assert 断言:调试阶段的 “安全哨兵”
assert.h中的assert()宏,是调试阶段的 “安全检查工具”—— 条件不满足则终止程序并报错:
#include <assert.h>
int main()
{
int* p = NULL;
assert(p != NULL); // 条件为假,直接报错:Assertion failed: p != NULL
return 0;
}
核心特点:
- 精准定位:报错时提示文件名、行号,快速找到问题;
- 可关闭:在
#include <assert.h>前定义#define NDEBUG,断言失效(release 版本常用); - 仅用于调试:不影响正式程序的运行效率。
为什么不用if判断?
if判断会让程序继续运行,可能隐藏错误;assert直接终止程序,强制开发者修复问题!
10. 🛠️ 指针实战:传值调用 vs 传址调用
指针的核心应用是 “传址调用”—— 解决传值调用无法修改实参的问题!
10.1 传值调用:“拷贝” 导致的局限性
传值调用时,形参是实参的临时拷贝,修改形参不影响实参:
void Swap1(int x, int y) // x、y是a、b的拷贝
{
int tmp = x;
x = y;
y = tmp; // 仅修改拷贝,实参a、b不变
}
int main()
{
int a = 10, b = 20;
printf("交换前:a=%d b=%d\n", a, b); // 10 20
Swap1(a, b); // 传值调用:传递a、b的拷贝
printf("交换后:a=%d b=%d\n", a, b); // 10 20(实参未变)
return 0;
}
🤔 比喻:传值调用像 “复印文件”—— 修改复印件,原件不变。
10.2 传址调用:直接操作内存的魅力
传址调用通过指针传递实参的地址,直接修改实参的内存:
void Swap2(int* pa, int* pb) // pa接收a的地址,pb接收b的地址
{
int tmp = 0;
tmp = *pa; // tmp = a(通过地址访问a)
*pa = *pb; // a = b(通过地址修改a)
*pb = tmp; // b = tmp(通过地址修改b)
}
int main()
{
int a = 10, b = 20;
printf("交换前:a=%d b=%d\n", a, b); // 10 20
Swap2(&a, &b); // 传址调用:传递a、b的地址
printf("交换后:a=%d b=%d\n", a, b); // 20 10(实参已改)
return 0;
}
🤔 比喻:传址调用像 “给别人家门牌号”—— 别人能直接找到你家,修改家里的东西。
10.3 实战案例:模拟实现 strlen 函数
#include <assert.h>
// size_t是无符号整型,匹配strlen的返回值(长度不可能为负)
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str != NULL); // 断言:防止传NULL(野指针)
while (*str != '0') // 易错点:原笔记故意写成'0',正确应为'\0'!
{
count++;
str++; // 指针向后移动,遍历字符串
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len); // 输出6(因'\0'未被统计,实际应为6,原代码错误不修改)
return 0;
}
📌 小贴士:
const char* str中的const,是为了保护字符串不被修改(只读权限),增强代码安全性。
🎉 总结
指针的本质是地址,指针变量是存放地址的容器—— 掌握指针的关键,是理解 “类型决定操作规则”“地址操作直接操作内存”。指针虽难,但只要多敲代码、多画图分析,就能从 “害怕” 到 “熟练”!如果这篇博客帮你理清了指针逻辑,欢迎点赞收藏🌟~
更多推荐

所有评论(0)