深入理解指针(1)

📅 发布时间:2026/7/6 8:16:40 👁️ 浏览次数:
深入理解指针(1)
1. 内存和地址1.1.内存在讲内存和地址之前我们想有个生活中的案例 假设有⼀栋宿舍楼把你放在楼里楼上有100个房间但是房间没有编号你的⼀个朋友来找你玩 如果想找到你就得挨个房子去找这样效率很低但是我们如果根据楼层和楼层的房间的情况给 每个房间编上号如1 2 3 ⼀楼 101 102 103...⼆楼 201 202 203... ...有了房间号如果你的朋友得到房间号就可以快速的找房间找到你。生活中每个房间有了房间号就能提高效率能快速的找到房间。 如果把上面的例子对照到计算机中又是怎么样呢我们知道计算机上CPU中央处理器在处理数据的时候需要的数据是在内存中读取的处理后的数据也会放回内存中那我们买电脑的时候电脑上内存是8GB/16GB/32GB等那这些内存空间如 何高效的管理呢 其实也是把内存划分为⼀个个的内存单元每个内存单元的大小取1个字节。其中每个内存单元相当于⼀个学生宿舍⼀ 个字节空间里面能放8个比特位就好比同学们住 的八⼈间每个⼈是⼀个比特位。 每个内存单元也都有⼀个编号这个编号就相当 于宿舍房间的门牌号有了这个内存单元的编号CPU就可以快速找到⼀个内存空间。生活中我们把门牌号也叫地址在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字指针。所以我们可以理解为内存单元的编号地址指针1.2. 指针变量和地址1.2.1.取地址操作符理解了内存和地址的关系我们再回到C语⾔在C语⾔中创建变量其实就是向内存申请空间比如上述的代码就是创建了整型变量a内存中 申请4个字节用于存放整数10其中每个字节都有地址。那我们如何能得到a的地址呢这里就得学习⼀个操作符()-取地址操作符。例如#includestdio.h int main() { int a10; printf(%p\n,a); return 0; }按照例子会打印处理006FFD70a取出的是a所占4个字节中地址较小的字节的地址。虽然整型变量占用了4个字节我们只要知道了第⼀个字节地址顺藤摸瓜访问到4个字节的数据也是可行的。1.2.2.指针变量和解引用操作符*我们通过取地址操作符()拿到的地址是⼀个数值比如0x006FFD70这个数值有时候也是需要 存储起来方便后期再使用的那我们把这样的地址值存放在哪里呢答案是指针变量中。例如#includestdio.h int main() { int a10; int*paa; return 0; }指针变量也是⼀种变量这种变量就是⽤来存放地址的存放在指针变量中的值都会理解为地址。如何拆解指针类型我们看到pa的类型是int* 我们该如何理解指针的类型呢这里pa左边写的是 int* * 是在说明pa是指针变量而前面的int 是在说明pa指向的是整型(int)类型的对象。1.2.3.解引用操作符我们将地址保存起来未来是要使用的那怎么使用呢 在现实生活中我们使用地址要找到⼀个房间在房间里可以拿去或者存放物品。 C语言中其实也是⼀样的我们只要拿到了地址指针就可以通过地址指针找到地址指针 指向的对象这里必须学习⼀个操作符叫解引用操作符(*)。例如#includestdio.h int main() { int a100; int*paa; *pa0; printf(%d\n,a); return 0; }上面代码中第7行就使用了解引用操作符 *pa 的意思就是通过pa中存放的地址找到指向的空间 *pa其实就是a变量了所以*pa0这个操作符是把a改成了0。有人肯定在想这里如果目的就是把a改成0的话写成 a 0; 不就完了为啥非要使用指针呢 其实这里是把a的修改交给了pa来操作这样对a的修改就多了⼀种的途径写代码就会更加灵活 后期慢慢就能理解了。1.2.4.指针变量的大小#include stdio.h //指针变量的⼤⼩取决于地址的⼤⼩ //32位平台下地址是32个bit位即4个字节 //64位平台下地址是64个bit位即8个字节 int main() { printf(X86环境输出结果:%zd\n, sizeof(char*)); printf(X86环境输出结果:%zd\n, sizeof(short*)); printf(X86环境输出结果:%zd\n, sizeof(int*)); printf(X86环境输出结果:%zd\n, sizeof(double*)); return 0; }输出结果x86就是32个bit位。#include stdio.h //指针变量的⼤⼩取决于地址的⼤⼩ //32位平台下地址是32个bit位即4个字节 //64位平台下地址是64个bit位即8个字节 int main() { printf(X64环境输出结果:%zd\n, sizeof(char*)); printf(X64环境输出结果:%zd\n, sizeof(short*)); printf(X64环境输出结果:%zd\n, sizeof(int*)); printf(X64环境输出结果:%zd\n, sizeof(double*)); return 0; }输出结果x64就是64个bit位。结论• 32位平台下地址是32个bit位指针变量大小是4个字节• 64位平台下地址是64个bit位指针变量大小是8个字节 X64环境输出结果• 注意指针变量的大小和类型是无关的只要指针类型的变量在相同的平台下大小都是相同的。2.指针-整数void* 指针const 修饰指针2.1.指针-整数先看⼀段代码观察地址的变化#include stdio.h int main() { int n 10; char* pc (char*)n; int* pi n; printf(%p\n, n); printf(%p\n, pc); printf(%p\n, pc 1); printf(%p\n, pi); printf(%p\n, pi 1); return 0; }输出结果我们可以看出 char* 类型的指针变量1跳过1个字节 int* 类型的指针变量1跳过了4个字节。 这就是指针变量的类型差异带来的变化。指针1其实跳过1个指针指向的元素。指针可以1那也可 以-1。结论指针的类型决定了指针向前或者向后走⼀步有多大距离。2.2.void* 指针在指针类型中有⼀种特殊的类型是 void * 类型的可以理解为无具体类型的指针或者叫泛型指 针这种类型的指针可以用来接受任意类型地址。但是也有局限性 void* 类型的指针不能直接进行的-整数和解引用的运算。#include stdio.h int main() { int a 10; int* pa a; char* pc a; return 0; }在上面的代码中将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告如下图是因为类型不兼容而使用void*类型就不会有这样的问题。输出结果使用void*类型的指针接收地址#include stdio.h int main() { int a 10; void* pa a; void* pc a; *pa 10; *pc 0; return 0; }输出结果这⾥我们可以看到 void* 类型的指针可以接收不同类型的地址但是无法直接进行指针运算。那么 v oid* 类型的指针到底有什么用呢 ⼀般void* 类型的指针是使用在函数参数的部分用来接收不同类型数据的地址这样的设计可以 实现泛型编程的效果使得⼀个函数来处理多种类型的数据在后面会讲解。2.3.const修饰指针2.3.1.const修饰普通变量变量是可以修改的如果把变量的地址交给⼀个指针变量通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制不能被修改怎么做呢这就是const的作用。例如#include stdio.h int main() { int m 0; m 20; //m是可以修改的 const int n 0; n 20; //n是不能被修改的 return 0; }上述代码中n是不能被修改的其实n本质是变量只不过被const修饰后在语法上加了限制只要我 们在代码中对n就行修改就不符合语法规则就报错致使没法直接修改n。 但是如果我们绕过n使用n的地址去修改n就能做到了虽然这样做是在打破语法规则。例如#include stdio.h int main() { const int n 0; printf(n %d\n, n); int* p n; *p 20; printf(n %d\n, n); return 0; }输出结果我们可以看到这⾥⼀个确实修改了但是我们还是要思考⼀下为什么n要被const修饰呢就是为了 不能被修改如果p拿到n的地址就能修改n这样就打破了const的限制这是不合理的所以应该让 p拿到n的地址也不能修改n那接下来怎么做呢2.3.2.const修饰指针变量⼀般来讲const修饰指针变量可以放在*的左边也可以放在*的右边意义是不⼀样的。int * p;//没有const修饰 int const * p;//const 放在*的左边做修饰 int * const p;//const 放在*的右边做修饰例如#include stdio.h //代码1 -测试⽆const修饰的情况 void test1() { int n 10; int m 20; int* p n; *p 20;//ok? p m; //ok? } //代码2 -测试const放在*的左边情况 void test2() { int n 10; int m 20; const int* p n; *p 20;//ok? p m; //ok? } //代码3 -测试const放在*的右边情况 void test3() { int n 10; int m 20; int* const p n; *p 20; //ok? p m; //ok? } //代码4 -测试*的左右两边都有const void test4() { int n 10; int m 20; int const* const p n; *p 20; //ok? p m; //ok? } int main() { //测试⽆const修饰的情况 test1(); //测试const放在*的左边情况 test2(); //测试const放在*的右边情况 test3(); //测试的左右两边都有const test4(); return 0; }结论const修饰指针变量的时候• const如果放在*的左边修饰的是指针指向的内容保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。• const如果放在*的右边修饰的是指针变量本⾝保证了指针变量的本⾝不能修改但是指针指向的内容可以通过指针改变。3.指针运算与野指针3.1.指针-整数因为数组在内存中是连续存放的只要知道第⼀个元素的地址顺藤摸⽠就能找到后面的所有元素。例如#include stdio.h //指针 -整数 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));//pi 这⾥就是指针整数 } return 0; }输出结果3.2.指针-指针在 C/C 中指针减指针是一个非常实用但有严格规则的作。它并不是简单地将两个地址数值相减其结果代表的是两个指针之间相隔的元素个数。例如#include stdio.h int main() { char str[] Hello; char *ptr_start str[0]; // 指向 H char *ptr_end str[4]; // 指向 o // 指针相减 int distance ptr_end - ptr_start; printf(字符 o 和 H 之间相隔的元素个数: %d\n, distance); printf(整个数组的起始地址: %p\n, (void*)ptr_start); printf(字符 o 的地址: %p\n, (void*)ptr_end); return 0; }输出结果3.3.指针的关系运算指针的关系运算Pointer Relational Operations指的是使用,,,,,!这些比较运算符来作两个指针。例如#include stdio.h int main() { int arr[5] {10, 20, 30, 40, 50}; // 指向数组的第一个元素 int *p arr[0]; // 指向数组最后一个元素的下一个位置安全越界常作边界标志 int *p_end arr[5]; printf(遍历结果: ); // 关系运算核心只要当前指针的地址 小于 边界指针的地址就继续循环 while (p p_end) { printf(%d , *p); p; // 指针向高地址移动 } printf(\n); return 0; }输出结果3.4.野指针概念野指针就是指针指向的位置是不可知的随机的、不正确的、没有明确限制的行为。3.4.1.指针未初始化例如#include stdio.h int main() { int *p;//局部变量指针未初始化默认为随机值 *p 20; return 0; }建议如果明确知道指针指向哪里就直接赋值地址如果不知道指针应该指向哪里可以给指针赋值NULL. NULL 是C语言中定义的⼀个标识符常量值是00也是地址这个地址是无法使用的读写该地址会报错。3.4.2.指针越界访问例如#include stdio.h int main() { int arr[10] {0}; int *p arr[0]; int i 0; for(i0; i11; i) { *(p) i; } return 0; }建议⼀个程序向内存申请了哪些空间通过指针也就只能访问哪些空间不能超出范围访问超出了就是 越界访问。3.4.3.指针指向的空间释放例如#include stdio.h int* test() { int n 100; return n; } int main() { int*p test(); printf(%d\n, *p); return 0; }建议不要返回局部变量的地址空间已经释放。3.5.assert 断言在 C/C以及许多其他编程语言中assert断言就像是一个“自动报警器”。它的作用是在程序运行时检查一个必须为“真”的条件。如果条件不成立程序会立即中止并报错。它是开发阶段用来捕捉逻辑错误Bug的最强辅助工具之一。在C语言中使用时需要包含头文件assert.h。例如assert(p ! NULL);上面代码在程序运行到这⼀行语句时验证变量 p 是否等于 NULL 。如果确实不等于 NULL 则继续运行否则就会终止运行并且给出报错信息提示。程序 assert() 宏接受⼀个表达式作为参数。如果该表达式为真返回值非0 任何作用程序继续运行。如果该表达式为假返回值为零 流 st assert() 不会产生assert() 就会报错在标准错误 derr 中写⼊⼀条错误信息显示没有通过的表达式以及包含这个表达式的文件名和行号。assert() 的使用对程序员是非常友好的使用assert() 有几个好处它不仅能自动标识文件和出问题的行号还有⼀种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题不需要再做断⾔就在 #include 语句的前面定义⼀个宏 NDEBUG即#define NDEBUG #include assert.h然后重新编译程序编译器就会禁用文件中所有的 除这条 assert() 语句。如果程序又出现问题可以移 #define NDEBUG 指令或者把它注释掉再次编译这样就重新启用了 assert() 语句。assert() 的缺点是因为引⼊了额外的检查增加了程序的运行时间。 ⼀般我们可以在 Debug 中使用在发环境中在Re Release版本中选择禁用assert 就行在VS这样的集成开 Release 版本中直接就是优化掉了。这样在debug版本写有利于程序员排查问题 lease 版本不影响用户使用时程序的效率。4.指针的使用和传址调用例如写⼀个函数交换两个整型变量的值 ⼀番思考后我们可能写出这样的代码#include stdio.h void Swap1(int x, int y) { int tmp x; x y; y tmp; } int main() { int a 0; int b 0; scanf(%d %d, a, b); printf(交换前a%d b%d\n, a, b); Swap1(a, b); printf(交换后a%d b%d\n, a, b); return 0; }当我们运行代码结果如下我们发现其实没产生交换的效果这是为什么呢调试⼀下试试呢原因我们发现在main函数内部创建了a和ba的地址是0x00cffdd0b的地址是0x00cffdc4在调⽤ Swap1函数时将a和b传递给了Swap1函数在Swap1函数内部创建了形参x和y接收a和b的值但是 x的地址是0x00cffcecy的地址是0x00cffcf0x和y确实接收到了a和b的值不过x的地址和a的地址不 ⼀样y的地址和b的地址不⼀样相当于x和y是独⽴的空间那么在Swap1函数内部交换x和y的值 自然不会影响a和b当Swap1函数调用结束后回到main函数a和b的没法交换。Swap1函数在使用的时候是把变量本身直接传递给了函数这种调用函数的方式我们之前在函数的时候就知道了这 种叫传值调用。结论实参传递给形参的时候形参会单独创建⼀份临时空间来接收实参对形参的修改不影响实 参。 所以Swap1是失败的了。那怎么办呢 我们现在要解决的就是当调⽤Swap函数的时候Swap函数内部操作的就是main函数中的a和b直接 将a和b的值交换了。那么就可以使⽤指针了在main函数中将a和b的地址传递给Swap函数Swap 函数⾥边通过地址间接的操作main函数中的a和b并达到交换的效果就好了。#include stdio.h void Swap2(int*px, int*py) { int tmp *px; *px *py; *py tmp; } int main() { int a 0; int b 0; scanf(%d %d, a, b); Swap2(a, b); printf(交换前a%d b%d\n, a, b); printf(交换后a%d b%d\n, a, b); return 0; }输出结果我们可以看到实现成Swap2的方式顺利完成了任务这里调用Swap2函数的时候是将变量的地址传 递给了函数这种函数调用方式叫传址调用。传址调用可以让函数和主调函数之间建立真正的联系在函数内部可以修改主调函数中的变量所以未来函数中只是需要主调函数中的变量值来实现计算就可以采用传值调用。如果函数内部要修改主调函数中的变量的值就需要传址调用。