指针(1)

目录

指针(1)

1. 🧠 指针基础:内存与地址的本质

1.1 内存是如何被管理的?

1.2 地址 = 指针?一文说清

1.3 地址总线:决定地址的 “容量”

2. 📌 指针变量:存放地址的 “容器”

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

2.2 指针变量的定义:不是指针,是变量!

2.3 解引用操作符 *:通过地址访问变量

3. 📏 指针变量的大小:和类型无关?

原因:

4. 🔍 指针类型的意义:不止是 “标签”

4.1 解引用的 “访问权限”:能读多少字节?

4.2 指针 ± 整数的 “步长”:一次走多远?

5. 🌐 void * 指针:万能的 “泛型指针”

应用场景:

6. 🔒 const 修饰指针:两种场景,两种限制

6.1 const 在*左侧:锁住指针指向的内容

6.2 const 在*右侧:锁住指针变量本身

6.3 const 在*两侧:双重锁定

7. ➕➖ 指针的运算规则:不是普通的数学运算

7.1 指针 ± 整数:遍历数组的核心

7.2 指针 - 指针:计算元素个数

实战:模拟实现 strlen 函数

7.3 指针的关系运算:比较地址的大小

8. 🐶 野指针:最危险的 “陷阱”

8.1 野指针的三大成因

成因 1:指针未初始化

成因 2:指针越界访问

成因 3:指针指向的空间被释放

8.2 规避野指针的四大 “锦囊妙计”

妙计 1:初始化指针

妙计 2:避免指针越界

妙计 3:指针不用时置NULL

妙计 4:不返回局部变量的地址

9. ⚠️ assert 断言:调试阶段的 “安全哨兵”

核心特点:

为什么不用if判断?

10. 🛠️ 指针实战:传值调用 vs 传址调用

10.1 传值调用:“拷贝” 导致的局限性

10.2 传址调用:直接操作内存的魅力

10.3 实战案例:模拟实现 strlen 函数

🎉 总结


✨ 引言:

指针是 C 语言的 “灵魂”,也是新手的 “噩梦”🤯—— 有人说 “学会指针,C 语言就掌握了一半”,这话一点不假!很多人对指针的理解停留在 “地址” 层面,却不懂其底层原理和实战技巧,导致写代码时避之不及,或踩坑无数。

1. 🧠 指针基础:内存与地址的本质

要学指针,先懂内存 —— 指针的所有操作都围绕内存展开!

1.1 内存是如何被管理的?

计算机的内存就像一栋 “宿舍楼”🏫,如果房间没有编号,找人(访问数据)只能挨个敲门,效率极低。因此,系统会对内存做两件关键事:

  1. 划分最小单元:将内存分成一个个 1 字节(1Byte = 8bit)的 “小房间”,每个房间能存 8 个二进制位(好比 “八人间”);
  2. 给房间编号:每个字节都分配唯一的 “门牌号”,这个编号就是地址

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前缀(如pcpa),方便区分普通变量。

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*是 “无类型指针”(泛型指针),特点是:

  1. 能接收任意类型的地址(万能接收器);
  2. 不能直接解引用,也不能 ± 整数(无类型,无操作规则)。
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*不能±整数
}

应用场景:

函数参数中接收任意类型地址(如memcpyqsort),实现 “泛型编程”—— 一个函数能处理多种类型数据。

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;
  • 暂时无指向:赋值NULLNULL是 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;
}

核心特点:

  1. 精准定位:报错时提示文件名、行号,快速找到问题;
  2. 可关闭:在#include <assert.h>前定义#define NDEBUG,断言失效(release 版本常用);
  3. 仅用于调试:不影响正式程序的运行效率。

为什么不用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,是为了保护字符串不被修改(只读权限),增强代码安全性。

🎉 总结

指针的本质是地址,指针变量是存放地址的容器—— 掌握指针的关键,是理解 “类型决定操作规则”“地址操作直接操作内存”。指针虽难,但只要多敲代码、多画图分析,就能从 “害怕” 到 “熟练”!如果这篇博客帮你理清了指针逻辑,欢迎点赞收藏🌟~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐