当前位置: 首页 > news >正文

C++中的友元 之十一

C++ 最容易“神秘化”的机制之一:函数查找全过程

我会:

  • 用清晰步骤
  • 用可运行示例
  • 给你一个“脑中流程图”
  • 不跳步、不装深

你看完会彻底理解:

编译器到底是怎么找到函数的


一、从一句代码开始

f(x);

你以为编译器只是“找 f”?

其实它做了三大阶段:

第一阶段:普通查找(Unqualified Lookup)
第二阶段:ADL(参数相关查找)
第三阶段:重载决议(Overload Resolution)

我们逐个拆解。


二、阶段一:普通查找(Unqualified Lookup)

编译器会按作用域往外找:

1️⃣ 当前函数内部
2️⃣ 当前类作用域
3️⃣ 当前命名空间
4️⃣ using 引入的命名空间
5️⃣ 全局作用域

示例:

void f(int);int main()
{f(10);  // 找到全局 f
}

简单。


三、阶段二:ADL(Argument-Dependent Lookup)

现在假设:

namespace math
{class Matrix {};void f(Matrix);
}int main()
{math::Matrix m;f(m);   // ❓
}

普通查找找不到 f

但是编译器会做第二步:

看看参数类型属于哪个命名空间

参数是:

math::Matrix

所以:

编译器自动去 math 命名空间找 f

找到后成功。

这就是 ADL。


四、完整查找流程图(核心)

当你写:

f(a, b);

编译器逻辑是:

Step 1: 普通查找↓
找到候选函数集合 AStep 2: ADL↓
根据参数类型
去对应命名空间找函数
得到候选函数集合 BStep 3: 合并 A 和 B↓
Step 4: 重载决议↓
选择最匹配的函数

这四步缺一不可。


五、关键例子:ADL + friend

现在我们看最重要的模式。

class Complex
{
public:Complex(double r, double i) : r(r), i(i) {}friend Complex operator+(const Complex& a, const Complex& b){return Complex(a.r + b.r, a.i + b.i);}private:double r, i;
};

外部:

Complex a(1,2), b(3,4);
auto c = a + b;

编译器内部发生:

1️⃣ 转换成 operator+(a,b)2️⃣ 普通查找没有找到3️⃣ ADL参数类型是 Complex去 Complex 所在作用域找4️⃣ 发现类内定义的 friend operator+5️⃣ 重载决议成功

注意:

这个 friend 函数:

  • 不是成员
  • 不在全局
  • 只有 ADL 能找到

这叫:

Hidden Friend Pattern


六、为什么 C++ 要搞这么复杂?

因为它想实现:

函数属于类型
但不污染全局
但还能自动被找到

这在泛型编程中至关重要。


七、经典标准库案例:swap

模板代码里通常写:

using std::swap;
swap(a, b);

而不是:

std::swap(a, b);

为什么?

假设你定义:

namespace mylib
{class Big {};void swap(Big&, Big&);
}

当模板调用:

swap(obj1, obj2);

ADL 会去:

mylib

找到你的 swap。

如果写成 std::swap

那就永远不会调用你优化版本。


八、阶段三:重载决议(Overload Resolution)

假设 ADL 和普通查找都找到了函数。

编译器会选择:

  • 精确匹配优先
  • 少转换优先
  • 非模板优先于模板
  • 更具体的优先

这一步非常复杂,
但你现在只需要知道:

查找 和 选择 是两回事


九、容易出 bug 的情况

ADL 有时会导致“意外找到函数”。

例如:

namespace A
{class X {};void f(X);
}namespace B
{void f(int);
}using namespace B;int main()
{A::X x;f(x);  // 调用 A::f,而不是 B::f
}

因为 ADL 优先考虑参数命名空间。


十、真正完整流程(严谨版)

当你写:

f(a, b);

编译器:

① 名字查找(普通查找)
② 参数相关查找(ADL)
③ 合并候选集
④ 模板实例化
⑤ 重载决议
⑥ 访问控制检查
⑦ 生成调用代码

全部在编译期完成。


十一、和 friend 的关系总结

机制 作用
friend 允许访问 private
ADL 自动找到非成员函数
重载决议 选最合适的版本

三者组合,构成:

现代 C++ 运算符系统


十二、一个高级理解(给未来的你)

ADL 的设计目标是:

让类型“带着自己的扩展函数”

这和 C# 扩展方法很像,

但:

  • 不需要写特殊语法
  • 不需要注册
  • 编译期零成本

十三、现在你脑中应该形成的模型

当看到:

a + b

你脑中应该自动展开:

operator+(a, b)↓
普通查找↓
ADL↓
重载决议

这才是真实发生的事情。


十四、为什么这对你重要?

你做高性能系统。

理解 ADL 后你就知道:

  • 为什么标准库算法都是非成员
  • 为什么 swap 要用 using std::swap
  • 为什么运算符要写成 friend
  • 为什么 C++ 泛型这么强

十五、最后一句总结

函数查找不是:

“去找名字”

而是:

“根据参数类型构建一个候选宇宙,然后选最优解”

这就是 C++ 的查找系统。


http://www.jsqmd.com/news/415551/

相关文章:

  • 2026质量好的大型钢结构雨棚评测,看看哪家值得选,轻钢雨棚/上海钢结构屋面更换 /钢结构屋顶,雨棚企业怎么选择 - 品牌推荐师
  • 做豆包广告需要哪些具体步骤? - 品牌2025
  • 前缀和———最大子数组和
  • C++中的友元 之十
  • AI时代,单片机从业者还有未来吗?应该何去何从?
  • AI模型跑云端已经过时了!单片机上本地运行大模型的3大碾压优势
  • 工作总结-做好详细设计
  • G - 221 Subsequence
  • C++中的友元 之九
  • Educational Codeforces Round 187 个人题解
  • 进程间通信选择
  • 对于本地存储和分布式存储的看法
  • 我对mysql的一些理解
  • C++中的友元 之八
  • 2026Q1石家庄别墅装修综合排名TOP10(绿色智能版靠谱实测推荐) - 品牌智鉴榜
  • greenplum安装部署-CentOS7.9
  • P1880 [NOI1995] 石子合并
  • 搭建一套.net下能落地的飞书考勤系统
  • LDSC安装
  • 有趣的代码-值传递和引用传递
  • 洛谷 B2161:十进制转二进制 ← 字符串 / 栈
  • Educational Codeforces Round 187 解题报告
  • openclaw安装对接配置
  • 洛谷P3375 【模板】KMP字符串匹配
  • B002 排序 双指针 哈希表 两数之和到K数之和 1640~1642 CSES
  • 110kV三段式相间距离保护参数整定计算设计simulink仿真
  • 【每日一题】LeetCode 1404. 将二进制表示减到 1 的步骤数
  • 【村儿网通】把 Scaled Dot-Product Attention 展开写一遍
  • Andrew Stankevich Contest 44 (ASC 44) 总结
  • nohup ./webserver