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

Item23--宁以 non-member、non-friend 替换 member 函数

1. 直觉 vs. 现实

我们的直觉(通常是错的)

在面向对象设计中,我们习惯把数据和操作数据的函数打包在一起。 这就导致一种思维定势:“如果有一个函数是专门用来操作类 A 的,那它就应该被写在类 A 里面,成为一个成员函数。” 我们通常认为这样封装性更好。

Item 23 的现实

Scott Meyers 告诉你:错了。 如果一个函数可以通过调用类的 public 接口来实现其功能,那么将它写成非成员函数(Non-member function),其实比写成成员函数具有更好的封装性。


2. 核心案例:WebBrowser

让我们用书中的经典例子来说明。假设你正在写一个浏览器类:

class WebBrowser {
public:void clearCache();void clearHistory();void removeCookies();// ... 还有很多其他核心功能
};

现在,用户想要一个方便的函数 clearEverything(),它可以一次性清除这三样东西。你有两个选择:

选项 A:作为成员函数 (Member Function)

class WebBrowser {
public:// ...void clearEverything() { // 添加进类定义clearCache();clearHistory();removeCookies();}
};
// 调用: wb.clearEverything();

选项 B:作为非成员函数 (Non-member Function)

// 定义在类外面
void clearBrowser(WebBrowser& wb) {wb.clearCache();wb.clearHistory();wb.removeCookies();
}
// 调用: clearBrowser(wb);

问题来了:哪一个封装性更好? 答案是 选项 B (非成员函数)


3. 为什么非成员函数封装性更好?

要理解这点,我们需要重新定义如何衡量封装性

  1. 封装的定义:封装就是把东西(数据)藏起来。
  2. 封装的好处:藏得越好,我们将来修改这些数据(比如改变底层实现)时,受影响的代码就越少。
  3. 量化指标有多少代码可以访问这些私有数据?
    • 如果有 N 个函数可以访问 private 数据,那么当 private 数据改变时,这 N 个函数都可能需要重写。
    • 显然,N 越小,封装性越好

回到比较

  • 成员函数 (Option A):不仅仅可以调用 clearCache 等公有接口,它还有权访问 WebBrowser 内部所有的 private 成员变量。即使它现在没用,它在语法上也是“特权阶级”。这增加了能访问私有数据的函数数量 (N 变大了)。
  • 非成员函数 (Option B):它只能使用 public 接口。它完全接触不到类的 private 成员。它和普通的客户端代码没有区别。这没有增加能访问私有数据的函数数量 (N 保持不变)。

结论:在功能相同的前提下,非成员函数(Non-member)因为不增加“特权阶级”的数量,所以提供了更好的封装性。

注意:这里特指 Non-member Non-friend。如果是 Friend(友元)函数,它和成员函数一样能访问私有数据,封装性优势就没了。


4. 更好的组织方式:Namespace (命名空间)

既然不放在类里面,那这些工具函数应该放哪呢?散落在全局作用域显得很乱。 Item 23 建议使用 namespace

namespace WebBrowserStuff {class WebBrowser { ... };// 工具函数放在同一个命名空间void clearBrowser(WebBrowser& wb) {wb.clearCache();wb.clearHistory();wb.removeCookies();}
}

这带来了巨大的扩展性优势

假设 WebBrowser 有很多不同种类的工具函数:有些是关于书签的,有些是关于打印的,有些是关于 Cookie 管理的。

如果它们都是成员函数,你的 WebBrowser 类定义文件(.h)会变得无比巨大,任何用户只要包含这个头文件,就必须编译所有这些依赖。

如果使用 Namespace + Non-member 策略,你可以像 C++ 标准库(STL)那样组织代码:

  • WebBrowser.h: 包含类定义本身。
  • WebBrowserBookmarks.h: 包含 namespace WebBrowserStuff 下关于书签的工具函数。
  • WebBrowserCookies.h: 包含 namespace WebBrowserStuff 下关于 Cookie 的工具函数。

用户体验

  • 如果我只在乎书签,我只需要 #include "WebBrowserBookmarks.h"
  • 我也完全可以在自己的文件里,向 WebBrowserStuff 命名空间里添加我自己的便利函数。这是类(Class)做不到的,因为类定义一旦结束就不能再往里塞东西了,但命名空间可以跨文件扩展。

这不仅是理论,更是C++ 标准库(STL)的构建方式。 你想想看:

  • std::vector 定义在 <vector> 头文件中。
  • std::sort 定义在 <algorithm> 头文件中。
  • 它们都在 std 命名空间下。
  • 如果你只想用 vector 存数据,你不需要为 sort 的代码付出编译代价。

为了让你对这个架构理解得更透彻,我补充一个非常重要但常被忽略的底层机制,正是这个机制让这种“分散定义”的策略变得如此好用。

这就是 ADL (Argument-Dependent Lookup),也叫 Koenig Lookup


为什么 Namespace + Non-member 用起来很舒服?

你可能会担心: “把函数放在 namespace 里,那我每次调用岂不是都要写很长的名字?比如 WebBrowserStuff::clearBrowser(wb)?”

答案是:通常不需要。

正是因为 ADL 的存在,编译器非常聪明。当你调用一个函数,并把某个对象作为参数传进去时,编译器会自动去该参数所在的命名空间里寻找这个函数。

代码演示

1. 核心定义 (WebBrowser.h)

namespace WebBrowserStuff {class WebBrowser { ... }; // 核心类
}

2. 扩展工具 (WebBrowserBookmarks.h)

#include "WebBrowser.h"
namespace WebBrowserStuff {// 注意:这是非成员函数,但放在同一个 namespace 下void clearBrowser(WebBrowser& wb) { ... } 
}

3. 用户代码 (main.cpp)

#include "WebBrowserBookmarks.h"int main() {WebBrowserStuff::WebBrowser wb;// 重点来了!// 你不需要写 WebBrowserStuff::clearBrowser(wb);clearBrowser(wb); return 0;
}
发生了什么?

编译器看到 clearBrowser(wb) 时:

  1. 它发现 wb 的类型是 WebBrowserStuff::WebBrowser
  2. ADL 触发:编译器会自动进入 WebBrowserStuff 这个命名空间去查找有没有叫 clearBrowser 的函数。
  3. 它找到了!于是编译通过。

这种架构的示意图

这种组织方式让我们可以构建出非常清晰的依赖关系图:

  • 核心层WebBrowser.h(只包含类定义,体积小,改动少)。
  • 扩展层WebBrowserBookmarks.h, WebBrowserCookies.h 等(包含大量非成员便利函数)。
  • 用户层:用户根据需要包含扩展层的头文件,或者创建自己的扩展文件

真正的高级用法:用户扩展

最妙的是,用户可以在自己的文件里,给你的类“添加方法”,且用法和原生的一模一样。

UserExtensions.h (用户自己写的)

namespace WebBrowserStuff { // 打开你的命名空间// 用户定义了一个针对自己业务的清理函数void clearForMyCompany(WebBrowser& wb) {wb.removeCookies();// 做一些公司特定的清理...}
}

main.cpp

WebBrowserStuff::WebBrowser wb;
clearForMyCompany(wb); // ADL 再次生效!

在调用者看来,clearForMyCompanyclearBrowser 用起来没有任何区别。非成员函数 + 命名空间 使得“官方扩展”和“用户扩展”在地位上是平等的。


总结 (Summary)

  1. 原则:如果一个函数可以通过调用类的 Public 接口来实现,不需要直接访问 Private 成员,那就不要把它写成成员函数。
  2. 原因(封装性):成员函数越多,能访问私有数据的人就越多,封装性就越差(修改私有数据时的破坏力越大)。非成员函数无法访问私有数据,因此封装性更强。
  3. 原因(扩展性):非成员函数可以定义在不同的头文件中,允许用户按需引用,减少编译依赖。同时也允许用户通过命名空间扩展功能。
  4. 例外:如果该函数必须访问 Private 成员,或者它是虚函数(Virtual Function),那它必须是成员函数(或友元)。
http://www.jsqmd.com/news/115985/

相关文章:

  • 无金融背景想入行?2025年靠这几张AI证书实现转行突破
  • 【Memory协议栈】AUTOSAR架构下NvM_ReadAll时间优化的实用方案
  • 实习期忙到飞?2025职场新人低成本学习AI生存指南
  • 电池管理系统BMS
  • 今天,终于进博客园了
  • 今天,终于进博客园了
  • 基于java的SpringBoot/SSM+Vue+uniapp的心理咨询预约管理的详细设计和实现(源码+lw+部署文档+讲解等)
  • Nginx负载均衡策略详解与Session一致性解决方案
  • Item34--区分接口继承和实现继承
  • 【2025避坑指南】10款常见降AI率工具大汇总(含真实有效的免费降AI版本)
  • Item24--若所有参数皆需类型转换,请为此采用 non-member 函数
  • 基于java的SpringBoot/SSM+Vue+uniapp的赛车爱好者交流平台的详细设计和实现(源码+lw+部署文档+讲解等)
  • Item18--让接口容易被正确使用,不易被误用
  • 算法日记专题:位运算II( 只出现一次的数字I II III 面试题:消失的两个数字 比特位计数)
  • PyTorch - 指南
  • 【2025红黑榜】10款常见降AI率工具大汇总(含不限次数免费降AI版本)
  • Item25--考虑写出一个不抛异常的 swap 函数
  • Item25--考虑写出一个不抛异常的 swap 函数
  • 3562. 折扣价交易股票的最大利润
  • 【2025终极测评】10款常见降AI率工具大汇总(含0元免费降AI版本)
  • 算法日记专题:位运算I(汉明距离I II 面试题:判断是不是唯一的字符 丢失的数字 两个整数相加)
  • Item21--必须返回对象时,别妄想返回其 reference
  • Item15--在资源管理类中提供对原始资源的访问
  • 1985-2024年中国绿色专利数据库(绿色技术专利分类)
  • Item22--将成员变量声明为 private
  • Item16--`new` 与 `delete` 的对应规则
  • 3777. 使子字符串变交替的最少删除次数
  • item11--在 operator= 中处理“自我赋值
  • 预见2026:家居新品首秀平台选择战略——五大核心展会深度评估与推荐 - 匠子网络
  • Item20--宁以 pass-by-reference-to-const 替换 pass-by-value