函数定义与调用:面试官问“函数重载底层怎么实现的“,我愣住了
上个月面了一个候选人,简历上写着"熟悉C++",项目经验也挺丰富。
我问他:"你写过一个计算两点距离的函数吗?"
他说写过。
"那如果我要计算三维空间里两点的距离呢?"
他说可以再加一个参数。
"如果我要计算两个向量的距离、两个位姿的距离呢?"
他想了想说:"可以写不同的函数,或者用模板。"
我点点头,继续问:"那你知道函数重载在底层是怎么实现的吗?为什么C++支持重载,而C不支持?"
他愣住了。
讲真,这个问题确实有点深。但它能看出一个人对语言的理解,是停留在"会用"的层面,还是真正理解了背后的原理。
函数的基本要素:不只是"一段代码"
函数看起来是最基础的东西,但里面有很多细节。
一个函数由什么组成?函数名、参数列表、返回值类型、函数体。这些是语法层面的。
但从工程角度看,函数还应该有:明确的职责、清晰的接口、合理的副作用。
什么叫明确的职责?就是一个函数只做一件事。
我见过一个实习生写的代码,一个函数叫processData(),里面有200多行,从读取传感器数据、滤波、坐标变换、到发送控制指令,全在里面。
我问他:"这个函数是干什么的?"
他说:"处理数据的。"
"那如果我只需要滤波,不需要坐标变换呢?"
他沉默了。
这种函数在机器人开发里特别常见。因为机器人系统本身就是多个模块串联的,写代码的人图省事,就把所有逻辑堆在一起。结果就是代码难以测试、难以复用、难以维护。
我的建议是,一个函数最好只做一件事。读取传感器就只读取传感器,滤波就只滤波,坐标变换就只坐标变换。每个函数职责单一,组合起来就灵活了。
参数传递:值传递、指针传递、引用传递
函数参数传递有三种方式,面试的时候特别喜欢问。
值传递:把参数的副本传给函数,函数里修改参数不会影响原变量。
void modify(int x) { x = 100; // 原变量不受影响 }指针传递:把参数的地址传给函数,函数里可以通过指针修改原变量。
void modify(int* x) { *x = 100; // 原变量被修改 }引用传递:C++特有的语法,相当于给原变量起了个别名。
void modify(int& x) { x = 100; // 原变量被修改 }在机器人开发里,如果参数是大的数据结构,比如点云、图像,用值传递会有一次完整的拷贝,性能开销很大。这时候应该用引用传递或者指针传递。
void processPointCloud(const PointCloud& cloud) { // 不拷贝,直接引用 }加个const,表示不会修改原数据,既安全又高效。
面试的时候,如果你能主动提到const引用,面试官会觉得你有性能优化的意识。
不过有个细节要注意:引用传递虽然方便,但有时候会让人搞不清楚函数内部到底有没有修改参数。所以有些编码规范要求,如果函数要修改参数,就用指针;如果不修改,就用const引用。
我个人觉得这个规范有点死板。用const引用就挺好,语义清晰,代码也简洁。但团队里统一风格最重要,没必要在这个问题上较真。
函数重载:同名不同参的魔法
C++支持函数重载,就是同一个函数名可以有多个版本,只要参数列表不同。
double distance(const Point2D& a, const Point2D& b); double distance(const Point3D& a, const Point3D& b); double distance(const Pose& a, const Pose& b);这样调用的时候,编译器会根据参数类型自动选择正确的版本。
double d1 = distance(p1, p2); // 自动调用Point2D版本 double d2 = distance(p3, p4); // 自动调用Point3D版本这个特性在机器人开发里特别有用。因为你要处理的数据类型很多,点、向量、位姿、变换矩阵,都可能需要计算距离或者做运算。用重载,代码会更清晰。
但面试的时候,面试官可能会问:"函数重载在底层是怎么实现的?"
答案是:名字修饰(name mangling)。
C++编译器在编译的时候,会根据函数名和参数列表生成一个唯一的内部名字。比如distance(Point2D, Point2D)可能被编译成_Z8distanceRK6Point2DS1_这样的符号。
这就是为什么C++支持重载,而C不支持。C编译器只用函数名生成符号,如果有两个同名函数,符号就冲突了。
面试的时候,你能说出name mangling,面试官会觉得你对编译原理有了解。真的,我面试别人的时候,听到这个答案会眼前一亮。
不过有个坑要注意:默认参数和重载一起用的时候,可能会有歧义。
void print(int x, int y = 0); void print(int x); // 歧义! print(5); // 调用哪个?编译器会报错,因为它不知道调用哪个版本。这种代码在实际开发中要避免。
内联函数:用空间换时间的优化
有些函数很短,但调用很频繁。比如:
double square(double x) { return x * x; }如果这个函数在循环里被调用一万次,每次都要有函数调用的开销(压栈、跳转、返回)。
内联函数就是为了解决这个问题。用inline关键字修饰,建议编译器把函数体直接展开到调用处,省去函数调用的开销。
inline double square(double x) { return x * x; }这样编译器可能会把代码优化成:
// 原来的代码 double result = square(3.0); // 优化后 double result = 3.0 * 3.0;不过要注意,inline只是建议,编译器不一定采纳。如果函数体太复杂,编译器会忽略这个建议。
在机器人开发里,内联函数在控制循环、数学运算这些性能敏感的地方用得比较多。但说实话,现代编译器很聪明,很多时候你不用加inline,它也会自动内联。
我的习惯是,对于那些只有一两行、调用频繁的小函数,加上inline。其他的就交给编译器优化。
给正在准备面试的你一点建议
函数定义与调用,真的是最基础的东西了。但基础的东西,往往最能看出一个人的编程习惯。
我面试的时候,看候选人写函数,主要看几点:职责是否单一、参数传递方式是否合理、有没有用const修饰、命名是否清晰。这些细节做好了,说明这个人写过规范的代码,有工程素养。
怎么提高?我的建议是:
去看看开源的机器人代码,比如ROS2的功能包、PCL点云库,看看人家是怎么设计函数的。注意函数的参数列表、返回值类型、命名规范。
然后对比自己的代码,看看有哪些地方可以改进。是不是有函数职责太多?是不是参数传递方式不合理?是不是命名不够清晰?
最的是,养成好习惯。函数职责单一、参数用const引用、命名清晰有意义。这些习惯一旦养成,代码质量会有质的提升。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。
「机器人软件开发面试·从入门到精通」连载系列上一篇:第5篇 数组与字符串——传感器数据批量处理的第一步 下一篇预告:第7篇 值传递vs引用传递——面试高频陷阱题
有任何问题欢迎评论区留言,我会尽量回复。