ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

现代c++第2.3章 友元函数

现代c++第2.3章 友元函数 先澄清一个容易混淆的点C 里没有「static class」这个语法。你在 C# 里写的static class MathHelper或者 Java 里的static class Inner在 C 里都不存在对应的关键字。C 的static放在不同位置含义完全不同写法位置含义class X { static int n; }类的静态成员属于类不属于对象class X { static void f(); }静态成员函数没有 this 指针static int g;全局/命名空间作用域内部链接只在本翻译单元可见void f() { static int c; }局部静态变量程序生命周期、首次执行时初始化static class X {};编译错误C 不允许下面按这几类分别说规则。一、类内的 static 数据成员1. 声明和定义要分开C17 之前classCounter{staticinttotal;// 声明在类体内};intCounter::total0;// 定义必须在且只能在一个 .cpp 里类体内的只是声明不算定义除了下面第 3 条的特例。漏掉类外定义链接时会报 undefined reference。C17 起可以用inline直接在类体内定义不用写.cppclassCounter{inlinestaticinttotal0;// C17等价于 inline 定义};2. 不属于任何对象所有实例共享一份sizeof(Counter)不包含它。即使一个对象都没创建它也存在程序启动时完成静态初始化。访问推荐用Counter::total虽然obj.total也合法但不推荐容易误以为是实例成员。3. 可以在类体内直接给初始值的例外const或constexpr的整型/枚举静态成员C98 起。constexpr静态成员C11 起隐含 inline。任何类型的inline static成员C17 起。classBuf{staticconstexprintSIZE1024;// OKinlinestaticstd::string namex;// C17 OK};注意const static成员如果被odr-use比如取地址Buf::SIZE、绑定到引用仍然需要一个类外的定义。4. 可以是自身类的不完整类型classNode{staticNode instance;// OK静态成员不计入类大小// Node child; // 错误非静态成员不能是不完整类型};5. 访问控制照样生效。private static成员只能在类内和友元里访问这点和实例成员一样。6. 继承与模板静态成员会被派生类继承也可以被同名成员隐藏但不能被 override重写是 virtual 的事。类模板的静态成员每个实例化版本各有一份Fooint::count和Foodouble::count是两个变量。二、static 成员函数核心规则没有 this 指针。因此不能直接访问非静态成员变量/函数必须通过对象或指针obj.x、p-f()。反过来它可以访问私有的非静态成员因为它仍是类的成员受友元规则保护。不能加 cv 限定和引用限定static void f() const是错的。不能是 virtualvirtual static互斥静态调用在编译期就绑定了谈不上多态。类型是普通函数指针void(*)()可以直接当 C 风格回调传进去而非静态成员函数指针类型是void(Class::*)()两者不兼容——这是静态成员函数做回调的最大优势。classTimer{public:staticvoidonTick(void*self);// 可传给 C APIprivate:intinterval;// 靠 self 参数反取};三、类外的 static文件作用域全局或命名空间里的static变量/函数 内部链接只在当前.cpp可见不会和其他翻译单元冲突。staticinthelper();// 只有本文件能看见staticconstintBUF64;两个注意点C17 起更推荐用匿名命名空间代替static全局量namespace { ... }语义更清晰还能放类型定义。头文件里的static const/static constexpr每个包含它的.cpp都会生成一份独立副本。constexpr通常没问题值一样优化后无开销但static的全局对象会有多份实例可能踩坑。四、函数内的局部 staticLoggergetLogger(){staticLogger inst;// 第一次走到这里才构造returninst;}生命周期到程序结束只初始化一次。这就是著名的Meyers Singleton写法。C11 起保证线程安全编译器会插入 once 机制可以放心在多线程里用。C11 之前不行。析构顺序与构造顺序相反且发生在main退出之后。如果某个静态对象的析构函数去用另一个可能已被销毁的静态对象就会崩溃——这是静态对象最常见的线上坑。对策故意不析构new出来不 delete、用指针/std::optional或者避免在析构函数里依赖其他全局状态。五、最麻烦的一条静态初始化顺序同一个翻译单元内按定义顺序初始化。跨翻译单元顺序未定义。// a.cppintxf();// f() 用到了 b.cpp 里的 y// b.cppinty42;// 谁先跑不知道这就是Static Initialization Order Fiasco。通用解法是Construct On First Use把全局量改成返回局部 static 引用的函数见第四节代码把“何时初始化”从链接期推迟到运行期第一次调用。六、那「静态类」在 C 里怎么做如果你想要的是“一组工具函数不需要实例化”C 的惯用做法是命名空间 自由函数而不是硬凑一个类namespacemath{intgcd(inta,intb);}好处没有多余的类名限定、支持 ADL 查找、可以被重载、不会诱导别人去继承或实例化。如果确实要写成类比如要配合模板参数、要封装一堆相关常量常见套路是禁用实例化classMathfinal{Math()delete;// 不能创建对象Math(constMath)delete;Mathoperator(constMath)delete;public:staticintgcd(inta,intb);inlinestaticconstexprdoublePI3.14159265358979;};顺带一提C 的嵌套类天生就相当于 Java 的 static nested class——它不持有外部类的引用也不需要任何static修饰。所以如果你在找“Java static 内部类”的对应物直接用嵌套类即可。七、速查清单容易写错的地方✅ 非 inline 的 static 数据成员必须在一个.cpp里定义别放头文件。✅ C17 优先用inline static省去.cpp的定义。✅ 静态成员函数里不能用this也不能直接碰非静态成员。✅static和virtual、const修饰互斥别一起写。✅ 静态成员函数指针 普通函数指针适合做回调非静态的不行。✅ 跨文件的静态对象初始化顺序未定义用“函数内局部 static”化解。✅ 局部 static 在 C11 后线程安全可放心做单例。✅ 别在静态对象的析构函数里依赖其他全局/静态对象。✅ 想做工具类优先考虑namespace其次才是删掉构造函数的全静态类。在 C 中友元friend的核心作用是打破封装让某个函数或类能够访问另一个类的private和protected成员。虽然它破坏了面向对象编程的封装性但在运算符重载、类之间紧密协作等场景下非常实用。下面为你详细拆解友元函数和友元类的使用方法 友元函数 (Friend Function)友元函数不是该类的成员函数但它可以像成员函数一样访问该类的所有成员。它通常在类内部用friend关键字声明但定义在类的外部。常见用途运算符重载如或、两个不同类之间的数据交换。代码示例#includeiostreamusingnamespacestd;classBox{private:doublewidth;public:Box(doublew):width(w){}// 声明友元函数// 注意友元函数不属于 Box 类所以没有 Box:: 作用域限定符friendvoidprintWidth(Box box);};// 友元函数的定义可以直接访问 Box 的私有成员 widthvoidprintWidth(Box box){coutBox 的宽度是: box.widthendl;}intmain(){Boxbox(10.0);// 像调用普通函数一样调用它printWidth(box);return0;} 友元类 (Friend Class)如果类 A 是类 B 的友元类那么类 A 中的所有成员函数都可以访问类 B 的私有和保护成员。常见用途当一个类需要为另一个类提供底层支持或管理功能时例如链表类访问节点类的私有指针。代码示例#includeiostreamusingnamespacestd;classA;// 前向声明classB{private:intsecret42;public:// 声明类 A 是类 B 的友元friendclassA;};classA{public:voiddisplay(Bb){// 因为 A 是 B 的友元所以可以直接访问 b 的私有成员 secretcoutB 的秘密是: b.secretendl;}};intmain(){A a;B b;a.display(b);return0;} 核心注意事项单向性友元关系是单向的。如果 A 是 B 的友元B 不一定是 A 的友元。不传递友元关系不能继承。如果 A 是 B 的友元B 是 C 的友元A 不一定是 C 的友元。声明位置friend关键字可以放在public、protected或private任何区域效果是一样的。谨慎使用除非确实需要比如重载operator否则尽量少用友元因为它会降低代码的模块化和安全性。需要我帮你写一个用友元函数重载operator让类对象能直接cout输出的完整例子吗
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进