指针的本质:我用“门牌号“这个比喻,帮三个实习生搞懂了C++
上篇聊了union和enum,有人后台说想看指针。
来了。
指针这个东西,在C++面试里出现的频率,大概仅次于虚函数。特别是机器人方向,你写驱动、操作内存、处理传感器数据……哪一样离得开指针?
但很多人学了几年C++,对指针的理解还是模糊的。
讲个真事。
去年带了三个实习生,都是大三的,C++课本都学过。我让他们看一段传感器驱动的代码,里面有指针。
第一个实习生问我:"int* p和int *p有区别吗?"——没区别,只是风格问题。
第二个实习生问我:"*p是取地址还是取值?"——*p是取值,&p才是取地址。
第三个实习生更猛,问我:"指针和数组是一回事吗?"——不是一回事,但关系很密切。
三个问题,三个不同的误区。但归根到底,都是同一个原因:没真正理解指针到底是什么。
指针到底是什么?别背定义,理解"门牌号"
课本上说:"指针是一个变量,它的值是另一个变量的地址。"
这个定义没错,但太抽象了。
我用一个比喻来解释:内存就像一栋大楼,每个房间都有门牌号。指针就是门牌号。
你有一个int a = 10;,这个a就是大楼里的一个房间,里面住着数字10。a是房间名,方便你找到它。
但大楼还有个更底层的编号系统——内存地址。每个房间都有一个唯一的地址,比如0x7fff5a2b。
指针是什么?指针就是记下了这个地址的变量。
int a = 10;
int* p = &a; // p存的是a的地址
&a就是取a的地址,p把这个地址存了下来。现在p指向a。
你想通过指针找到房间里的值?用*:
cout << *p << endl; // 输出10,*p就是"这个地址里住的数据"
所以*有两个身份:声明的时候int* p表示"p是个指针",使用的时候*p表示"取指针指向的值"。
记住这个比喻:变量是房间名,地址是门牌号,指针是记着门牌号的纸条,*是"按门牌号找到房间,看里面住着谁"。
指针为什么要有类型?
很多人有个疑问:指针存的不就是个地址吗?为什么还要分int*、float*、double*?
答案是:类型告诉编译器,这个地址后面的数据占多大空间、怎么解读。
int a = 10;
int* ip = &a;
float* fp = (float*)&a;
cout << *ip << endl; // 输出10
cout << *fp << endl; // 输出一个奇怪的浮点数
ip知道从&a开始读4个字节,按int来解读。fp也知道读4个字节,但按float来解读。同样的内存,不同的类型,读出来的值完全不一样。
在机器人开发里,这个问题特别容易出在协议解析的时候。你收到一段原始数据,如果类型搞错了,读出来的值就是错的。传感器数据解析bug,十个里有三个是类型不匹配。
指针最容易翻车的三个地方
面试的时候,指针最爱考的就是那些"坑"。我总结了三个最常见的:
第一个坑:空指针。
int* p = nullptr;
cout << *p << endl; // 崩溃!
p存的是0(nullptr),这个地址不属于你的程序。你去访问它,操作系统直接把你杀了。
在机器人开发里,空指针崩溃是最常见的bug之一。特别是传感器驱动,你拿到一个指针,没检查是不是空就去用,程序就崩了。
好习惯:拿到指针,先判空。
void processData(SensorData* data) {
if (data == nullptr) {
// 错误处理
return;
}
// 正常使用data
}
第二个坑:野指针(悬空指针)。
int* p = new int(10);
delete p;
cout << *p << endl; // 未定义行为!
p指向的内存已经被释放了,但p里还存着那个地址。你去访问它,可能读到垃圾数据,也可能程序崩溃。
这种bug最恶心,因为它不一定每次都崩。有时候跑得好好的,上线之后才出问题。
好习惯:delete之后,立刻把指针置为nullptr。
delete p;
p = nullptr;
第三个坑:内存泄漏。
void loadData() {
int* data = new int[10000];
// 处理数据
// 忘了delete
}
每次调用loadData(),都分配了40KB内存,但从不释放。调用一千次,就泄漏了40MB。机器人程序是长时间运行的,内存越用越少,最后就OOM了。
我见过一个真实的case。一个同事写的激光雷达驱动,每帧点云都new了一块内存,但忘了delete。程序跑了两个小时,内存从500MB涨到了4GB,最后被系统kill了。
后来他改成用std::vector或者智能指针,问题就消失了。
指针在机器人开发里的实战用法
说了这么多坑,那指针在机器人开发里到底怎么用?
场景一:动态内存分配。 你不知道传感器一帧数据有多大,只能运行时分配:
int* buffer = new int[num_points];
// 读取数据
delete[] buffer;
当然,现在更推荐用std::vector,自动管理内存。
场景二:函数参数传递。 大的数据结构用指针传,避免拷贝。这个上篇聊值传递的时候讲过了。
场景三:硬件寄存器映射。 写嵌入式驱动的时候,硬件寄存器的地址是固定的,你用指针直接访问:
volatile uint32_t* reg = (volatile uint32_t*)0x40021000;
*reg = 0x01; // 写寄存器
volatile告诉编译器不要优化这个指针的访问,因为硬件可能随时改变寄存器的值。
场景四:回调函数。 机器人系统里经常用函数指针做回调:
typedef void (*Callback)(int event);
void registerCallback(Callback cb);
ROS2里虽然用Lambda更多了,但底层还是函数指针。
面试中指针的常见考法
总结一下,面试中关于指针,最爱考这几个点:
基础题: 指针是什么?指针和引用的区别?*和&分别是什么操作?
进阶题: 空指针和野指针的区别?怎么避免内存泄漏?volatile指针是什么?
底层题: 指针占多少字节?(64位系统上通常是8字节。)不同平台的指针大小一样吗?
我面试的时候,如果候选人能把指针的本质讲清楚——地址、类型、生命周期——我基本就认定这个人C++基础过关了。
给正在准备面试的你一点建议
指针这个东西,光看书是不够的。
我的建议是:
打开你的IDE,写几个小程序,用调试器看看指针的值、看看内存地址、看看*和&到底在干什么。亲眼看到内存布局,比看十遍课本都管用。
然后把指针的常见坑都踩一遍——空指针崩溃、野指针、内存泄漏。踩过了,你才知道怎么避免。
最后,去看看开源的机器人代码,比如ROS2的rclcpp、PCL,看看人家是怎么用指针的。特别是智能指针的用法,这是现代C++的标准做法。
还有一点特别重要,面试现在基本必问智能指针。shared_ptr、unique_ptr、weak_ptr这三个东西,你得搞清楚各自的使用场景。unique_ptr用于独占所有权,性能最好,零开销抽象;shared_ptr用于共享所有权,内部靠引用计数管理,但要注意循环引用问题;weak_ptr就是用来打破循环引用的。我面试候选人的时候,经常追问一句:"你知道shared_ptr的引用计数是怎么实现的吗?"答得上来的,说明真的看过源码或者深入学过,加分不少。实际项目中我们团队已经基本不用裸指针了,全部用智能指针替代,代码审查的时候发现裸指针直接打回去重写。
分享一个调试指针的小技巧:在GDB里用print p可以看到指针的值也就是地址,用print *p可以看到指向的数据,用print &p可以看到指针变量自己的地址。这三个地址分清楚了,指针的三层关系就彻底明白了。很多实习生分不清这三层,面试时被追问就慌了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列 上一篇:第10篇 联合体与枚举——状态机设计中的常用工具 下一篇预告:第12篇 指针运算与数组的关系——面试官最爱挖的底层细节
有任何问题欢迎评论区留言,我会尽量回复。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)