资讯详情

0基础C语言积跬步之深入理解指针(1)

📅 2026/11/5 3:33:05 | 华诺云谱 👁 阅读
0基础C语言积跬步之深入理解指针(1)
目录内存和地址指针变量和地址指针变量类型的意义const 修饰指针指针运算野指针assert 断言指针的使用和传址调用一、内存和地址内存和地址的关系内存是房间地址是门牌号靠地址定位内存靠内存存数据。内存分为一个个小的内存单元一个内存单元为一个字节每一个内存单元都对应着一个唯一的编号我们把这个编号称之为地址我们又给地址取了个别名叫指针。我们可以理解为内存单元的编号地址指针那怎么理解编址内存中的地址是在硬件生产的时候就已经创建好的是固定的不会因为电脑的重启而改变只有有了这些地址cpu才能和内存正常交互找到想读的位置比如上图中如果cpu要读取数据cpu是先通过地址总线在内存找到要操作的内存空间再通过控制总线cpu发指令给内存是要读数据还是写数据最后通过数据总线将内存中的数据传给cpu进行处理。如果还要修改内存空间的数据内容CPU 在运算处理完成后会再次通过地址总线指定存储位置、控制总线下发写命令然后由数据总线写回到内存中保存完成一次完整的内存读写交互。只读不改只走「读流程」不用写回内存。读完还要修改赋值读完运算 → 再走一遍「写流程」存回内存。32位的机器总共有三十二条地址总线每根地址总线都对应着一个二进制位0或1,32根地址总线就能组成2³²种不同的组合对应着2³²种不同的地址编码也就能存2³²4294967296个地址也就是4294967296字节为4GB1GB2³ºByte二、指针变量和地址2.1取地址操作符首先在C语言中我们创建变量就是在向内存空间申请一块内存比如int a我们就向内存的栈区中申请了四个字节的内存空间用来存放变量a我们可以用取地址操作符来看到变量a的地址打印地址用占位符%p#include stdio.h int main() { int a 0; printf(%p\n, a); //打印a的地址 return 0; }运行截图我们调试在内存地址中可以看到变量a占了4个字节但打印a时只打印了最小的地址所以我们可以知道a取出的是a所占四个字节中地址较小的那个地址2.2指针变量和解引用操作符*2.2.1指针变量指针变量就是用来存放地址的存放到指针变量里的值都会被理解为地址int a 10; int* p a;int *p就是一个指针我们拆解来看*是在说明p是一个指针变量前面的int是在说明p中所存的地址所指向的内存空间中的数据是一个int类型的数据比如要存一个char类型变量的地址我们就要用char*类型的指针变量char ch a; char* pc ch;2.2.2解引用操作符*解引用操作符的作用是找到指针变量所存地址指向的内存空间中所存储的内容也就可以通过对此地址的解引用去访问和修改里面的内容#include stdio.h int main() { int a 10; printf(a%d\n, a); int* p a; //将a的地址传给指针变量p printf(*p%d\n, *p); //验证解引用p可以找到a的值 *p 5; //通过解引用p找到a的值并修改 printf(a%d\n, a); //验证是否修改成功 return 0; }运行截图可以看到*p成功找到a原来的值10并打印在运行完*p5代码后也确实成功通过*p找到了a的值并修改成功将a的值改成了52.3指针变量的大小指针变量是用来存放地址的而32位的电脑下地址是由32位地址线构成的也就是每个地址需要32个二进制数32个bit位4个字节的大小所以32位的电脑下指针变量的大小就需要4个字节才能存放的下地址同理在64位的电脑下需要64根地址线也就是64个bit位8个字节所以指针变量大小需要8个字节才能存放的下地址而且无论哪个类型的指针大小都是由电脑的位数来决定的和所指向什么类型的数据无关我们来验证一下#include stdio.h int main() { printf(%zu\n, sizeof(char*)); printf(%zu\n, sizeof(int*)); printf(%zu\n, sizeof(float*)); printf(%zu\n, sizeof(double*)); return 0; }在x8632电脑的平台下运行截图在x6464位电脑的平台下运行截图结论32位平台下地址是32个bit位指针变量大小是4个字节64位平台下地址是64个bit位指针变量大小是8个字节三、指针变量类型的意义既然指针变量的大小都是一样的那为什么要需要不同类型的指针都用一个类型的指针来存放所有地址不就可以了吗下面我们来一步步解答这个问题3.1指针的解引用我们来看两串代码1代码1用int*来接收int类型的地址#include stdio.h int main() { int n 0x11223344; int* pi n; *pi 0; return 0; }我们调试n的地址来看可以看到原本n中存的是十六进制的11223344当我们用int*类型的指针变量pi用*pi等于0后四个字节全部变成了0这是因为int类型占四个字节所以在*pi找到n的值后会往后找四个字节进行修改将其四个字节都变成02代码2用char*来接收int类型的地址#include stdio.h int main() { int n 0x11223344; char* pc (char*)n; //不强制转换会报警告从“int *”到“char *”的类型不兼容 *pc 0; return 0; }我们同样调试n的地址来看可以看到原本n中存的是十六进制的11223344当我们用char*类型的指针变量pc用*pc等于0后只有一个字节变成了0这是因为char类型占1个字节所以在*pc找到n的值后会往后找一个字节进行修改将地址最低的一个字节变成0后续修改不到结论指针的类型决定了对指针解引用的时候有多大的权限⼀次能操作几个字节char*的指针解引用一次就只能访问一个字节而int*的指针的解引用一次就能访问四个字节3.2指针加减整数我们来观察一段代码#include stdio.h int main() { int n 10; char* pc (char*)n; int* pi n; printf(n%p\n, n); printf(pc%p\n, pc); printf(pc1%p\n, pc 1); printf(pi%p\n, pi); printf(pi1%p\n, pi 1); return 0; }运行截图可以看到char*类型的指针加1时只跳过了一个字节int*类型的指针加1时跳过了四个字节所以指针的类型可以决定指针向前走或向后走一步一次能跳过多少个字节的地址结论指针的类型决定了指针向前或者向后走一步有多大距离char*加1跳过一个字节short*加1跳过两个字节int*加1跳过4个字节3.3void*指针void*类型的指针为无具体类型的指针也可理解为泛型指针它可以存进去任何类型的指针但有局限性void*类型的指针无法直接进行整数加减和解引用的运算#include stdio.h int main() { int a 10; void* pa a; *pa 10; printf(%p\n, pa 1); return 0; }运行截图可以看到*pa无法被解引用且无法进行赋值、整数加减的运算显示未知的大小void*一般用于函数的参数部分实现泛型编程使得函数可以处理多种指针类型的数据四、const 修饰指针4.1const修饰变量const用于修饰常变量会把变量赋予常属性使其像常量一样无法修改注意一个点是static修饰的局部变量才会将变量从栈区放入静态区使其获得和静态区的变量相同特性无法修改但const修饰的局部变量还是在栈区只是给变量加了一个只读权限标记使得其无法直接修改比如用const修饰的变量是无法直接修改的#include stdio.h int main() { const int n 10; //const修饰的变量无法直接修改 n 5; //修改失败会报错 return 0; }运行截图运行错误无法修改虽然const修饰的变量在语法上无法直接修改直接修改会报错但我们可以通过指针间接修改const修饰的变量比如绕过n通过n的地址去修改n#include stdio.h int main() { const int n 10; //n被const修饰无法直接修改 int* p n; //我们把n的地址传给指针变量p *p 5; //通过解引用p间接找到n的值并修改 printf(%d\n, n); //验证是否修改成功 return 0; }运行截图可以看到虽然成功修改但是利用指针间接修改变量是不合理的这样const就失去了意义所以应该让const修饰的变量也无法通过指针间接修改我们就有了const修饰指针变量4.2const修饰指针变量我们来看几串代码1代码1无const修饰#include stdio.h int main() { int n 10; int m 5; int* pn n; *pn 20; printf(n%d\n, n); pn m; printf(*pn%d\n, *pn); return 0; }运行截图2代码2有const修饰const放在*左边#include stdio.h int main() { int n 10; int m 5; int const* pn n; //*pn被const修饰无法修改 *pn 20; printf(n%d\n, n); return 0; }运行截图可以看到const放在*左边时pn所存地址指向的内容无法修改会报错那pn指针变量所存放的地址内容可以修改吗我们来看#include stdio.h int main() { int n 10; int m 5; int const* pn n; //*pn被const修饰无法修改 pn m; //pn指针变量所存的地址是可以修改的指向其他地址 printf(*pn%d\n, *pn); return 0; }运行截图可以看到pn所指向的地址成功修改并且*pn打印出了m的值如果const放在了最前方数据类型前面那么效果和放在*前面的效果是一样的像这样const int* pn n;等同于int const* pn n;结论const在*左边时修饰的是指针变量原本所存地址指向的内容指向的内容无法被修改但指针变量本身指向的地址可以修改3代码3有const修饰const放在*右边#include stdio.h int main() { int n 10; int m 5; int* const pn n; //指针变量pn被const修饰pn所指向的地址无法修改 pn m; return 0; }运行截图可以看到当const放在*右边时将pn本身给限制住了pn无法再修改为指向其他地方的地址会报错那pn指针变量所存放的地址指向的内容可以修改吗我们来看#include stdio.h int main() { int n 10; int m 5; int* const pn n; //pn被const修饰pn所指向的内容无法修改 *pn 20;//*pn指针变量所存的地址所指向的内容是可以修改的 printf(*pn%d\n, *pn); return 0; }运行截图可以看到*pn20成功修改了n的值并打印出来结论const在*右边时修饰的是指针变量所存的地址地址无法被修改但指针变量原本存的地址所指向的内容是可以修改的4代码4有const修饰const放在*两边#include stdio.h int main() { int n 10; int m 5; int const* const pn n; *pn 20; pn m; return 0; }运行截图可以看到当指针变量pn和*pn都被const修饰时指针变量pn所存地址指向的内容既不能被修改也不能重新指向其他的地址结论当const放在*两边时既不能修改指针指向的地址也不能修改所存地址指向的内容五、指针运算指针有三种运算指针-整数指针-指针指针的关系运算5.1指针-整数指针-整数还是指针只是指向的位置变了比如打印一个一维数组一维数组在内存中的存储是连续的所以我们只需要知道首元素的地址再让这个地址不断自增就能顺藤摸瓜找到所有其他元素#include stdio.h int main() { int arr[10] { 1,2,3,4,5,6,7,8,9,10 }; int sz sizeof(arr) / sizeof(arr[0]); int* p arr; for (int i 0; i sz; i) { printf(%d , *p); p; } return 0; }我们在其中就运用到了指针整数的知识点因为p是int*类型的指针变量所以每次1p都会跳过一个int类型的字节大小从而找到数组的下一位元素对应的地址5.2指针-指针指针-指针不再是指针得到的时两个指针之间相差的元素个数比如我们想知道一个字符串中有多少个元素我们创建了一个计算字符串长度函数my_strlen()#include stdio.h int my_strlen(char* s) { char* p s; //把字符串的初始地址存入指针变量p中 while (*p ! \0) //若*p不是\0,p就直到p指向\0不再 p; return p - s; //此时p指向\0,s指向第一个元素初始地址p-s就是\0到第一个元素之间一共有的元素个数 } int main() { printf(%d\n, my_strlen(abc)); return 0; }运行截图可以看到正常计算出了字符串abc的长度我们首先把字符串的初始地址存入到了指针变量p中然后建立了一个while循环若*p不是\0,p就直到p指向\0不再此时p指向\0,s指向第一个元素初始地址我们再returnp-sp-s就是\0到指向首元素的地址之间相差元素个数5.3指针的关系运算指针和指针之间大小的比较称之为指针的关系运算#include stdio.h int main() { int arr[10] { 1,2,3,4,5,6,7,8,9,10 }; int* p arr[0]; int sz sizeof(arr) / sizeof(arr[0]); while (p arr sz) //指针的⼤⼩⽐较 { printf(%d , *p); p; } return 0; }运行截图arrsz刚好指向第10个元素再加上一个int大小的地址p是arr的首元素地址只要parrsz就让p不断自增最后就能找到所有元素的地址这个打印数组的方法就运用到了指针的关系运算六、野指针野指针就是指向未知地址的指针随机的、不正确的、未明确限制的6.1野指针成因1指针未初始化#include stdio.h int main() { int* p;//局部变量指针未初始化默认为随机值 *p 20; return 0; }指针变量未初始化放的是随机值无法对其地址进行解引用这块空间属于未知的不合规的无法使用。此时指针变量p就是野指针。运行截图2指针越界访问#include stdio.h int main() { int arr[10] { 0 }; int* p arr; int i 0; for (i 0; i 11; i) { //当指针指向的范围超出数组arr的范围时p就是野指针 *(pi) i; } return 0; }运行截图程序因为使用了野指针而崩溃只创建了大小为10的一维数组却访问了arr[10]和arr[11]这就是未知的未定义的未限制的地址属于危险地址为野指针3指针指向的空间被释放我们再看样一串代码#include stdio.h int* test() { int n 100; return n; } int main() { int* p test(); printf(%d\n, *p); return 0; }我们调用了test() 函数 返回了n的地址并传给指针变量p接着对这串地址进行了解引用操作看似是正确的但是我们要知道n是test() 函数里面的局部变量当调用完成此函数出函数的时候这个局部变量会自动销毁自动销毁是指栈区回收n对这块内存空间的使用权但值依然存在于这个地址中等待着被系统覆盖这个局部变量原先的地址不再属于它这块地址空间变成没有指定限制的无管理的地址是为野指针运行截图可以看到虽然我们使用的p是野指针也成功访问到了调用test() 函数时n的地址也成功打印了n的值但这并不代表着n没被销毁也不代表着*p这样访问就是合法的只是因为在栈区中这块地址的值还没被其他值覆盖100仍然放在这块地址里面所以我们访问这块地址打印出来的值就还是100但局部变量n已经被销毁了这块空间已经是危险空间了是未被指定未被限制的危险空间6.2如果规避野指针1指针初始化我们可以在指针在创建的时候就明确给它一个初始化如果不知道指针该指向什么地址那就给指针赋个空地址NULL不能不初始化在C语言中NULL是由宏定义的标识符常量值为0被强制转化为void*类型所以0也是地址但是这个不能被使用会报错C语言定义#ifdef __cplusplus // 如果是 C 环境 #define NULL 0 // 把 NULL 定义成 0 #else // 如果是 C 环境 #define NULL ((void *)0) // 把 NULL 定义成 空指针 #endif所以我们在创建指针变量时就给它初始化或者赋值为空指针NULL#include stdio.h int main() { int num 10; int* p1 num; //明确指向地址 int* p2 NULL; //赋值为空指针 return 0; }2指针越界在使用指针的时候我们要小心指针越界访问比如访问数组时小心越界访问数组里没有的元素。一个指针指向的内容向内存申请了多少空间我们的指针就只能访问多少的空间不能超过访问范围超过了就是指针越界。3变量不再使用时及时置为NULL指针使用之前检查有效性如果指针变量不再使用我们就把它置为NULL给它赋上一个空指针给它拴住不被误用而且在使用每个指针之前我们都检查一下这个指针是否为空指针NULL如果是则不能使用#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; for (i 0; i 10; i) { printf(%d , *p); p; } printf(\n); //此时p已经越界了可以把p置为NULL p NULL; //下次使⽤的时候判断p不为NULL的时候再使⽤ p arr[0];//重新让p获得地址 if (p ! NULL) //判断 ,如果p不为空指针我们再对其进行使用 { printf(%d\n, *p); } return 0; }运行截图指针越界访问打印出来了一串随机值-858993460此时指针已经越界就算没越界因为我们无需再使用此指针所以我们将此指针置为空指针需要使用时再次赋地址给它并判断其是不是空指针进行使用4避免返回局部变量的地址避免像野指针成因3中讲的指针指向的空间被释放的情况因为函数中局部变量出了函数范围之后就会被销毁我们要小心使用七、assert断言assert(表达式)和if语句很像都是在判断括号内的条件是否成立如果成立就继续执行下方的程序如果不成立则程序终止。但assert()断言还有一个好处是在判断条件是否成立时如果条件成立assert不会产生任何作用。但如果错误会直接终止程序显示没有通过的表达式以及包含这个表达式的文件名和行号以让程序员即使修改小bug防止酿造成大bug使用assert断言需要包括头文件#includeassert.h#include stdio.h #include assert.h int main() { int* p NULL; assert(p ! NULL); //assert断言 *p 5; printf(%d\n, *p); return 0; }运行截图程序直接崩溃显示了没有通过的表达式、包含这个表达式的文件名、行号如果确认程序没有问题不需要再做断言,我们可以定义一个宏#define NDEBUG在头文件#include assert.h前面NDEBUGno debug关闭调试它是一个宏定义专门用来关闭所有 assert 断言。我们一般在debug调试版本中使用assert断言用来校验程序逻辑、排查非法情况。在release发布版本中我们就在引用头文件前使用宏定义#define NDEBUG来禁用所有assert断言。这样做的好处是在release版本中断言被优化删掉不做多余判断、不损耗运行效率不影响普通用户使用体验。#include stdio.h #define NDEBUG #include assert.h int main() { int* p NULL; assert(p ! NULL); //assert断言失效 *p 5; printf(%d\n, *p); return 0; }运行截图assert断言被禁用程序没有终止和报错但因为p是空指针无法进行解引用系统明确表明NULL这个空地址是禁止使用的所以并没有打印出来任何东西八、指针的使用和传址调用8.1strlen的模拟实现strlen()函数是用来求字符串长度的用于统计\0之前的字符个数strlen()函数原型size_t strlen ( const char * str );模拟实现strlen就需要接收一个起始地址从起始地址开始往后遍历只要没找到\0就让计数器就最后返回计数器的值达到模拟实现统计字符个数的效果,返回值类型可以用int也可以用size_t#include stdio.h #include assert.h size_t my_strlen(const char* p) { size_t count 0; assert(p ! NULL); while (*p ! \0) { count; p; } return count; } int main() { size_t ret my_strlen(abcdef); printf(%zu\n, ret); return 0; }运行截图:8.2传值调用和传址调用1传值调用void swap(int x, int y) { int temp 0; temp x; x y; y temp; } int main() { int a 5, b 10; printf(交换前a%d b%d\n, a, b); swap(a, b); printf(交换后a%d b%d\n, a, b); return 0; }运行截图像这样把变量的值直接传给函数就叫做传值调用但这样做并不能修改a和b的值因为形参只是对形参的一份临时拷贝swap函数中创建的int xint y会重新开辟地址在出函数后这两份地址将会被销毁和ab的地址都不一样所以修改xy的值并不是修改ab的值并不会对a和b造成什么影响想要在函数中修改ab的值这时我们就需要用到传址调用2传址调用#include stdio.h void swap(int* x, int* y) { int temp 0; temp *x; *x *y; *y temp; } int main() { int a 5, b 10; printf(交换前a%d b%d\n, a, b); swap(a, b); printf(交换后a%d b%d\n, a, b); return 0; }运行截图当我们把a和b的地址传给函数时我们在函数里对指针进行解引用的操作时其实就是在对我们ab本身进行操作因为我们解引用的地址就是我们ab本身的地址此时再交换a和b就能达到交换的效果所以当我们想函数中的操作和我们主函数建立联系时如果我们只是想在函数中操作得到一个结果那我们可以选择传值调用如果我们是想在函数中修改我们主调函数的值那我们需要使用传址调用。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑