新闻详情

新闻详情

首页 / 资讯中心 / 详情

C/C++指针与引用:从内存地址到智能指针的彻底理解

发布时间:2026/10/1 16:54:46来源:尧图网络
C/C++指针与引用:从内存地址到智能指针的彻底理解
指针这东西说玄乎也玄乎说简单也简单。搞了这么多年C/C我最大的感受是很多人被指针劝退不是因为它真的难而是因为一开始就把概念学拧了。尤其是*这个符号一会儿用来声明指针一会儿用来解引用再加上取地址和C里的引用四个东西搅在一起不懵才怪。这篇内容我就从实际开发角度把指针、取地址、解引用、引用一次讲透。不管是刚入门C语言的新手还是学C卡在智能指针和右值引用上的朋友都能从里面找到你想要的答案。我会尽量避开教科书那种“指针是指向变量的变量”的绕口令式定义用真正好懂的方式拆开讲顺便把那些面试常考、工作常用的坑点一并聊清楚。1. 先搞懂内存指针到底在“指”什么1.1 从变量到地址一次门牌号的类比要理解指针得先理解一个朴素的事实程序里的变量本质上是内存中的一块区域。你可以把内存想象成一栋巨大的公寓楼每个房间都有一个唯一编号这个编号就是内存地址。你声明一个int a 10相当于在公寓里租了一个房间房间号是某个地址比如0x7ffe1234房间里放着数值10。变量名a只是这间房的“户主名牌”方便你找人真正定位到房间靠的是门牌号——地址。这里有个很多人一直没绕过来的弯变量名和地址之间隔着“编译器帮你做的事”。你在代码里写a编译器知道你要访问的是那个地址里存的数值你写a编译器知道你要的是那个地址本身。这个转换在编译期就完成了运行时根本不存在“变量名”这个概念只有寄存器和内存地址。所以指针真正做的事情是把“地址”这个东西当成值来保存和传递。这就是指针变量的本质——它是一个用来存地址的变量存的是别人家的门牌号不是自己的值。1.2 指针变量的本质类型只是“解释方式”来看这段最基础的代码int a 10; int *p a;很多教程会说p是指向int的指针存了a的地址。这没错但我想强调的是指针变量本身也是一个变量它也有自己的内存地址。p自己也有门牌号你甚至可以取p的地址int **pp p;这就是指针的指针。更重要的一个认知是指针变量存的值本质上就是一个非负整数地址但为什么需要类型因为运算符要知道怎么解释这段内存。同样是地址如果你把它当成int*那么解引用时读取4个字节假设32位int如果你把它当成char*解引用只读1个字节。类型决定了指针的“步长”和解引用时的解释方式。这就是为什么int*和char*做加减法时实际移动的字节数不同——p 1移动4个字节cp 1移动1个字节。注意C语言里void*是万能指针它可以接收任意类型的地址但它不能直接解引用也不能做指针运算因为编译器不知道它指向什么类型、步长是多少。必须先强转成具体类型才能操作。2. 三个操作符一次说清声明、取地址、解引用2.1 声明时*的含义我是指针先明确一个容易混淆的点*在声明语句中不是“解引用”的意思它的作用是告诉编译器“我声明的是一个指针类型”。int *p; // p 是一个指向 int 的指针 char *cp; // cp 是一个指向 char 的指针这里有个C语言老生常谈的问题int* p1, p2;到底声明了什么正确答案是p1是指针p2是普通的int。因为*只作用于紧随其后的变量名。所以写代码时我习惯把*靠变量名放即int *p1而不是int* p1就是为了避免这个歧义。很多人喜欢写int* p觉得类型统一好看但int* p1, p2这种写法坑了不少人。我个人的建议是一行只声明一个指针变量别搞组合声明谁爱看谁看反正排错的时候难受的是自己。2.2 取地址运算符拿到门牌号是取地址运算符作用就是获取变量的内存地址。注意只有左值有明确内存位置的东西才能取地址。你没法对(a 1)取地址因为表达式结果没有独立的内存位置也没法对字面量取地址比如10这在C语言里也是非法的。int a 42; int *p a; // p 存的是 a 的地址取地址时有个细节得到的指针类型是“指向该变量类型的指针”。你取char变量的地址得到char*取数组名得到的类型是指向数组首元素的指针而不是数组本身的指针——这个细节在数组章节会展开。2.3 解引用*按门牌号找上门解引用运算符*作用于指针意思是“访问这个地址上存储的值”。它有个经典比喻p存的是门牌号*p就是顺着门牌号找上门看到房间里的人。int a 10; int *p a; *p 20; // 把 a 的值改成 20 printf(%d\n, a); // 输出 20这里要特别强调*p 20的含义不是“把20存到p里”而是“把20存到p指向的内存位置”。很多人出错就是把指针变量本身和它指向的目标搞混了。区分方法很简单看等号左边。如果等号左边是*p改的是目标如果等号左边是p改的是指针本身存的地址。2.4 为什么“声明”和“解引用”长得一模一样这是新手最容易炸的点int *p里的*和*p 20里的*明明长得一样为什么含义不同我的理解方式是*在不同上下文里身份不同。在声明语句中*是类型构造符的一部分它构造出“指针类型”在表达式语句中*是解引用运算符它执行“访问目标”的操作。就像“打”字在“打人”和“打饭”里动作完全不同上下文决定语义。类似的还有。在声明中int r a;这里是C的引用声明在表达式中a是取地址。同一个符号声明和表达式语境下含义完全不同。这个矛盾没法消除只能在脑子里建好“上下文切换”的潜意识。3. 指针的进阶形态const、数组、函数与多级指针3.1 const与指针顶层指针和底层指针能相互赋值吗这是面试高频题也是很多工作两三年的朋友都容易答错的地方。先说定义顶层指针top-level const指针本身是常量也就是指针变量存下的地址不能变。写法int *const p a;底层指针low-level const指针指向的对象是常量也就是不能通过指针修改目标值。写法const int *p a;两者的区别用大白话说顶层指针说“门牌号不能换”底层指针说“房间里东西不能动”。常见写法整理const int *p; // 底层可指向 int 或 const int但 *p 不可写 int *const p; // 顶层p 不能改但 *p 可写 const int *const p; // 既是底层又是顶层全都不能改那“顶层指针和底层指针可以相互赋值吗”答案不能简单用能或不能来回答要分方向底层指针赋值给顶层指针允许。int *const p2 p1;假设p1是底层指针因为p1指向的东西不能被改赋值后p2虽然可以改指向的内容但p2本身不能重新指向——这里真正要保证的是把一个限制更多的指针赋给限制更少的指针是允许的。顶层指针赋值给底层指针允许。const int *p2 p1;假设p1是顶层指针因为p1只是不能改地址但指向的内容是普通的int现在换成只读视角没有任何安全问题。非const指针赋给底层指针允许。const int *p a;把一个普通指针或普通变量地址赋给底层指针是扩大限制安全。底层指针赋给非const指针禁止。比如int *p cp;cp是const int*编译器会直接报错。因为你通过p可能修改原本const限定的内容这是类型系统不允许的。一句话总结赋值规则指针赋值时目标指针底层const限制不能比源指针更松。也就是“从只读到读写”是不行的反向则可以。3.2 指针与数组的纠缠数组名、指针数组、数组指针C语言里数组和指针关系极其密切也极其容易搞混。先记结论数组名在多数表达式中会“退化”为指向首元素的指针。int arr[5] {1,2,3,4,5}; int *p arr; // arr 退化为 arr[0]但注意两件事第一sizeof(arr)返回整个数组大小不会退化第二arr的类型是int(*)[5]是指向整个数组的指针而不是指向首元素的指针。arr和arr指向的地址虽然相同但类型不同所以arr 1移动4字节arr 1移动20字节整个数组大小。然后是两个高频易混淆概念指针数组本质是数组数组里存的是指针。写法int *arr[5];。想象成一个抽屉柜每个抽屉里放着一张写着门牌号的纸条。数组指针本质是指针指向一个数组。写法int (*p)[5];。它像一个指引指向一整排连续房间的起始处。我在实际开发中看得最多的应用场景是指针数组用来存多个字符串字符指针比如命令行参数char *argv[]数组指针常用于二维数组的函数传参。至于热搜词里提到的“指针数组存放字符串”经典例子const char *strs[] {hello, world, foo};这样定义的好处是每个字符串可以长度不同指针数组只存地址字符串常量存储在只读区内存利用更灵活。3.3 函数指针、结构体指针与万能指针函数指针也是个让很多人头疼的东西。它的作用是把函数当成一个值来传递典型应用是回调函数、状态机、插件接口。int add(int a, int b) { return a b; } int (*func_ptr)(int, int) add; int result func_ptr(3, 4);注意声明方式int (*func_ptr)(int, int)一个小括号把*func_ptr括起来因为()优先级高于*如果不加括号就变成了int *func_ptr(int, int)——一个返回int*的函数。函数指针还经常配合typedef使用让代码更清爽typedef int (*MathOp)(int, int); MathOp op add;结构体指针就比较直观了C语言里访问结构体成员有两种方式(*p).member和p-member后者是前者的语法糖。实际开发中极少有人写(*p).member都是直接-。操作符的优先级决定了解引用时要加括号否则*p.member会被解释为*(p.member)直接编译错误。**万能指针void***前面提过一嘴这里补充实际用法C语言中malloc返回void*它不是一个“什么都能干”的指针而是一个“未定型”的指针。它最大的价值是通用接口比如memcpy的形参就是void*。但代价是类型安全完全丢失C中更推荐用模板或auto来避免裸void*。4. 引用指针的“安全替身”4.1 引用的本质与规则它是个别名C的引用和指针有根本性的不同引用不是对象它只是一个已存在对象的别名。创建引用时不会分配额外内存至少逻辑上不分配它绑定到一个变量后终生无法改变绑定关系。int a 5; int r a; // r 是 a 的别名 r 10; // a 变成 10引用声明时必须初始化没有空引用也没有“引用的引用”。这些限制让引用比指针安全得多。C设计它的目的就是解决指针最危险的几个场景空指针、野指针、忘记初始化。函数传参是引用最经典的用途之一。对比三种方式void func(int a); // 值传递拷贝一份 void func(int *a); // 指针传递传地址但可能为空 void func(int a); // 引用传递直接操作原变量不会为空用引用传参有两个好处一是避免拷贝大对象的开销二是让函数内部可以直接修改外部变量。相比指针引用在语法上不用写取地址、不用写*解引用代码更干净。4.2 引用与指针的核心对比到底该用谁简单列个对比表维度指针引用是否可空可以且常常为空危险不能为空必须初始化是否可改绑可以指针可以被重新赋值不能绑定后终身不变是否需要解引用是*p访问目标否直接用引用名指针运算支持自增自减等算术运算不支持内存占用通常占用一个指针大小的变量逻辑上不占独立空间数组/动态分配指针是唯一选择不能指向动态分配的数组除非借助指针那我个人在实际开发中怎么选默认用引用确实需要“可空”或“可重新指向”时才用指针。函数参数如果只是想读取一个对象用const T想在函数里修改实参用T可能需要表示“没有”的情况用T*并检查空指针。4.3 右值引用为什么它能让代码跑得更快C11引入的右值引用T是个很有意思的话题。热搜词里有“右值引用为什么会提高效率”这里用一个例子说明白。假设有一个类String内部管理了动态内存。老式写法String a createString(); // 临时对象拷贝到 a如果拷贝构造函数是深拷贝会多一次分配拷贝如果给String实现了移动构造函数参数是String编译器会发现createString()返回的临时对象是右值于是调用移动构造函数直接把临时对象内部的内存指针“偷”过来再把自己的指针置空。整个过程只是指针交接不涉及内存分配和逐字节拷贝效率自然高。关键理解右值引用本身不提高效率提高效率的机制是“移动语义”——右值引用只是提供了一个入口让编译器知道这个对象马上就要销毁了你可以放心“偷”它的资源。生活类比就是搬家时如果旧房子马上要拆了你直接开卡车把家具整个拉走而不是一件件搬下来再搬上去。卡车的“通行证”就是T。在参数列表里T能同时绑定左值和右值完美转发场景还能实现移动构造这就是为什么现代C推荐用值传递加std::move而不是老式的手动管理生命周期。5. 智能指针现代C对裸指针的“降维打击”5.1 为什么要用智能指针裸指针的三大原罪裸指针原始指针在C里之所以让人头疼主要是三个问题第一忘记释放内存导致内存泄漏。从new到delete之间如果出现异常或提前返回delete就不会执行。你可能会说“我编程习惯好会加try/catch”但当你的代码规模上到几十万行、多人协作时靠自觉维护new/delete配对几乎一定会出问题。第二悬空指针野指针。提前释放了对象但还有指针指向那块地址再访问就是未定义行为。第三所有权模糊。一个裸指针被多个模块持有谁最后释放释放后别人还在用怎么办这个问题在C语言时代靠注释和约定解决在C中直接用智能指针从类型系统层面解决。传统做法里最头疼的就是“我释放了但其他模块不知道”的问题。智能指针的核心思想是把堆内存的生命周期和某个栈上对象绑定起来栈上对象析构时自动释放堆内存。也就是有名的RAII资源获取即初始化。5.2 unique_ptr、shared_ptr和weak_ptr怎么选C11提供的三种智能指针适用场景完全不同。std::unique_ptr是独占所有权的智能指针。它不可拷贝只能移动。它的适用场景是一块内存只有一个明确的拥有者。生活类比就是每个人都是自己房间的唯一房主想换房主只能搬家std::move。std::unique_ptrFoo p std::make_uniqueFoo(); std::unique_ptrFoo q std::move(p); // 所有权转移p 变为空现代C实践里unique_ptr应该是默认选择因为它的开销和裸指针几乎一样但自动管理生命周期性能和安全兼得。std::shared_ptr是共享所有权的智能指针。它通过引用计数跟踪多少个shared_ptr指向同一对象计数归零时自动删除对象。适用场景是多个对象确实共同持有一份资源。代价是引用计数的原子操作有额外开销而且循环引用会让引用计数永远达不到零导致内存泄漏。std::weak_ptr就是为了解决shared_ptr循环引用而出现的“旁观者”。它不增加引用计数只是弱引用地观察对象是否还活着。需要使用时通过lock()获取一个shared_ptr如果对象已被释放lock()返回空。std::shared_ptrFoo sp std::make_sharedFoo(); std::weak_ptrFoo wp sp; // 不增加计数 if (auto locked wp.lock()) { // 对象还活着可以使用 } else { // 对象已被释放 }我实际编码时的心法是能栈上直接声明就用栈上对象必须用到堆内存时优先unique_ptr确实多方共享再用shared_ptr检测到有循环引用时在适当位置插入weak_ptr打断环。这个决定层级能覆盖绝大多数场景主动写裸new的情况几乎为零。贴一段稍微完整的循环引用对比struct Node { std::shared_ptrNode next; // 如果这里改成下面的写法就不会泄漏 // std::weak_ptrNode next; }; // 构建一个两个节点的环如果不加 weak_ptr两个节点引用计数都是2离开作用域后永远不为06. 常见问题排查与避坑指南6.1 空指针、野指针和悬空指针别让程序“静默崩溃”空指针是最基础的一类问题指针值为nullptr访问*p就是未定义行为大多数现代操作系统上直接段错误Segmentation fault。我见过的菜鸟错误里很多是忘记检查malloc返回值就往下走。野指针和悬空指针的区别很多人分不清。野指针是声明了但没初始化它存着一个随机地址悬空指针是原来指向的对象已经释放但指针里的地址没有清空。共同点是你永远不知道它们指向什么访问时可能碰巧工作也可能随机崩溃。这种随机性是最恶心的它破坏了“每次运行结果一致”的可预期性。要避开这些坑我的习惯是声明指针时立即初始化要么给有效地址要么给nullptr。动态内存释放后立刻把指针置为nullptr防止二次释放。函数入参如果是指针一进来就检查非空。统一用nullptr而非NULL或0C11之后nullptr有类型安全优势。6.2 编译错误解读那些让你抓狂的报错信息开发中会碰到许多和指针相关的编译错误这里挑几个最常见的聊一下。error: invalid conversion from ‘const int’ to ‘int’**就是前面讲的底层指针赋给非const指针的错误。编译器在告诉你你把一个只读视角的指针赋给了一个可写视角的指针。解决办法是重新梳理设计确认到底谁拥有写权限。error: assignment to ‘int’ from ‘int’ makes integer from pointer without a cast*常见于忘了解引用。比如int a p;忘了写*p。根据报错提示很快能定位到是赋值时的类型不匹配。segmentation fault (core dumped)这是运行时最常见的崩溃。可能触发点包括空指针解引用、数组越界、栈溢出、访问已释放内存。排查时先用gdb跑bt命令看调用栈十有八九能定位到崩溃行。我之前排查过一个诡异段错误最后发现是结构体里的指针成员没初始化就在析构函数里delete这类问题在C里如果你用了智能指针就基本不会出现。6.3 调试与工具技巧怎么高效定位指针问题调试指针问题我个人最常用的三样工具gdb、AddressSanitizerASan和valgrind。gdb可以查看指针的值和指向的内存区域比如用p *p打印解引用内容用x/4wx 0x...查看指定地址的内容。调试多级指针时print输出会带类型信息比如$1 (int **) 0x7fffffffd8c0能让你直观看到指针层级。AddressSanitizer是编译期插桩工具编译时加-fsanitizeaddress -g程序里任何内存越界、释放后使用、堆溢出都会在发生处直接报错输出带调用栈。我强烈建议开发调试阶段统一开ASan它的检测能力比valgrind快一个量级也更精准。valgrind侧重内存泄漏检测在ASan不支持的场景比如需要检查“是否有某段内存永远不会被释放”可以配合使用。但它的运行速度慢一般在CI环境或者发布前检查时跑。另外还有一个很实用的技巧给指针变量起名字时带上类型维度。比如pInt、pArr、ppNode让代码在可读性上减少臆测。别小看这个习惯写出“自解释”的变量名能帮你减少大量低级错误。7. 总结之外我的切身体会从接触指针到现在十几年了我踩过的坑五花八门。说几个感受最深的点给后来人做个参考。第一别背概念去画内存图。画内存图是理解指针和引用最快的方法。遇到指针问题不要靠脑子想拿起笔把每个变量、每块内存都画出来标注好类型、地址、值问题一般一两分钟就清楚了。第二现代C不等于C with Class。很多从C转过来的人还在用裸指针手动管理内存这不是说完全不行而是会让你多出大量本不必要的调试时间。unique_ptr、shared_ptr、weak_ptr这套工具早就成熟了该用就用。第三引用传参是一种品味。我见过太多代码参数列表一堆T*然后函数内部反复判空、解引用。如果能用引用代码逻辑会清晰得多也顺便把“不可为空”这个约束写进了类型系统。类型能表达的东西就不要依赖注释和口头约定。最后指针和引用不是什么高深的秘籍它只是C/C想让你理解“内存在哪里、数据从哪来到哪去”的一种方式。把内存模型想清楚这些都只是顺理成章的结果。希望你读完这篇能少走一些我当年走过的弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

C++设计模式实战:RAII、智能指针与模板如何重塑经典模式 2026/10/1 17:35:57

C++设计模式实战:RAII、智能指针与模板如何重塑经典模式

我第一次用C在真实项目里写业务代码时,犯过一个非常典型的错误:把《Head First设计模式》里那套Java写法原封不动搬进了C工程。类图画得漂漂亮亮的菱形结构,接口用纯虚函数模拟,new出来的对象统一塞进智能指针,编译器居…

阅读更多 →
GitHub搜索全攻略:掌握限定符与筛选语法,高效定位优质开源项目 2026/10/1 17:35:57

GitHub搜索全攻略:掌握限定符与筛选语法,高效定位优质开源项目

1. 先搞清楚GitHub搜索到底在搜什么:它是个筛选器,不是搜索引擎用了十几年GitHub,我发现自己对搜索的理解经历过三个阶段:最早我把搜索框当百度用,直接敲关键词,结果经常一无所获或返回一堆不相关的东西&am…

阅读更多 →
优化实战指南:从系统到数据库再到算法的通用方法论 2026/10/1 17:35:50

优化实战指南:从系统到数据库再到算法的通用方法论

“优化”这个词,可能是整个技术圈里被用滥得最严重的一个。你说电脑卡了要优化,慢 SQL 要优化,Hive 小文件要优化,向量数据库要优化,游戏帧率低要优化,甚至无人机投送路线也要优化。看起来各个领域都在谈优…

阅读更多 →
3 步完成 RomM BIOS 固件配置:gba_bios.bin 校验入位,GBA 游戏不再黑屏 2026/10/1 17:35:36

3 步完成 RomM BIOS 固件配置:gba_bios.bin 校验入位,GBA 游戏不再黑屏

3 步完成 RomM BIOS 固件配置:gba_bios.bin 校验入位,GBA 游戏不再黑屏 【免费下载链接】romm A beautiful, powerful, self-hosted ROM manager and player. 项目地址: https://gitcode.com/GitHub_Trending/rom/romm 在 RomM 里启动 GBA 游戏&a…

阅读更多 →
《动手学深度学习》word2vec 近似训练深度解析:负采样与层次 Softmax 的原理、推导与源码实现 2026/10/1 17:35:36

《动手学深度学习》word2vec 近似训练深度解析:负采样与层次 Softmax 的原理、推导与源码实现

人工智能深度学习机器学习教程 【免费下载链接】d2l-zh 《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。 项目地址: https://gitcode.com/GitHub_Trending/d2/d2l-zh 点击查看 免费下载 当词典规模达到…

阅读更多 →
ROS2相机接入与标定全指南:工业相机、深度相机到零拷贝传输 2026/10/1 17:35:30

ROS2相机接入与标定全指南:工业相机、深度相机到零拷贝传输

搞ROS2机器人开发,相机这一关基本绕不开。我见过太多朋友把相机驱动装好了,结果rviz2里看不到话题,或者点云全是错位的噪点,又或者标定出来的内参怎么都不过关。这篇文章不打算从ROS2安装开始罗列,而是围绕“相机”这个…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉