读书笔记 effective c++ Item 23 宁可使用非成员非友元函数函数也不使用成员函数

简介: 1. 非成员非友元好还是成员函数好? 想象一个表示web浏览器的类。这样一个类提供了清除下载缓存,清除URL访问历史,从系统中移除所有cookies等接口:   1 class WebBrowser { 2 3 public: 4 5 .

1. 非成员非友元好还是成员函数好?

想象一个表示web浏览器的类。这样一个类提供了清除下载缓存,清除URL访问历史,从系统中移除所有cookies等接口:

 

 1 class WebBrowser {
 2 
 3 public:
 4 
 5 ...
 6 
 7 void clearCache();
 8 
 9 void clearHistory();
10 
11 void removeCookies();
12 
13 ...
14 
15 };

 

许多用户想将这些动作一块执行,所以web浏览器为此可以提供一个函数:

 1 class WebBrowser {
 2 
 3 public:
 4 
 5 ...
 6 
 7 void clearEverything(); // calls clearCache, clearHistory,
 8 
 9 // and removeCookies
10 
11 ...
12 
13 };

 

当然,这个功能也可以通过非成员函数来提供,让它调用合适的成员函数就可以了:

 1 void clearBrowser(WebBrowser& wb)
 2 
 3 {
 4 
 5 wb.clearCache();
 6 
 7 wb.clearHistory();
 8 
 9 wb.removeCookies();
10 
11 }

 

哪种方法才是更好的呢?是成员函数clearEverying还是非成员函数clearBrower?

2. 为什么非成员非友元函数好?

面向对象准则指出数据以及操作数据的函数应该被捆绑到一起,这就表明它建议成员函数是更好的选择。不幸的是,这个建议是不正确的。它曲解了面向对象的含义。面向对象准则指出数据应该尽可能的被封装。违反直觉的是,成员函数clearEverything实际上并没有比非成员函数clearBrower有更好的封装性。并且提供非成员函数能够为web浏览器的相关功能提供更大的包装(packaging)灵活性,相应的,就可以产生更少的编译依赖和更好的可扩展性。因此非成员函数比成员函数在许多方面都要好。明白为什么很重要。

2.1 用非成员非友元能产生更具封装性的类

以封装开始。如果一些东西被封装了,就意味着被隐藏起来了。封装的东西越多,就有更少的客户能看到它们。更少的客户能看到它们就意味着我们有更大的灵活性来进行对它们进行修改,因为我们的修改直接影响的是能看到这些修改的客户。因此封装性越好,就赋予我们更大的能力来对其进行修改。这也是我们将封装摆在第一位的原因:它以一种只影响有限数量的客户的方式为我们修改东西提供了灵活性

 

考虑同一个对象相关联的数据。看到这些数据的代码越少(也就是可访问它),数据就被封装的越好,我们就有更大的自由来修改这个对象的数据的一些特征,像数据成员的数量,类型等等。通过确认有多少代码能够看到数据来判断数据的封装性是粗粒度的方法,我们可以计算出能够访问数据的函数的数量,能访问数据的函数越多,封装性越差。

 

Item 22解释了数据成员应该是private的,因为如果不是,未限定数量的成员函数就能够访问它们。这样就根本没有封装性可言。对于private的数据成员,能够访问它们的函数的数量为所在类的成员函数的数量加上友元函数的数量,因为只有成员函数和友元函数能够访问private成员。考虑在一个成员函数(不仅能访问类的private数据,也能访问private函数,enums,typedef等等)和一个非成员非友元函数(私有的数据和函数都不能访问)之间做一个选择,它们提供了相同的功能,能够产生更大封装性的选择是非成员非友元函数,因为他们没有增加能够访问类私有部分的函数的数量。这就解释了为什么clearBrower(非成员非友元函数)要优于clearEverything:在WebBrowser类中,它产生了更大的封装。

在这点上有两件事情需要注意。第一,这个论述只适用于非成员非友元函数。友元同成员函数对类的私有成员有相同的访问权,因此对封装有相同的影响。从封装的观点来看,不是在成员和非成员函数之间进行选择,而是在成员和非成员非友元函数之间进行选择。(封装当然不是仅有的选择视角,Item 24中解释了在隐式类型转换中,需要在成员和非成员函数之间做出选择。)

第二件需要注意的事情恰恰是因为封装性指明类的函数为非成员函数这个观点,这并不意味着这个函数不能是别的类的成员函数。我们可以将clearBrower声明成一个utility类的静态成员函数。只要它不是WebBrowser的一部分(或者一个友元),它就不会影响WebBrower的private成员的封装性。

2.2 用非成员非友元可以减少编译依赖

在c++中,一个更加自然的方法是使clearBrower成为同WebBrowser有相同命名空间的非成员函数

1 namespace WebBrowserStuff {
2 
3 class WebBrowser { ... };
4 
5 void clearBrowser(WebBrowser& wb);
6 
7 ...
8 
9 }

 

不仅仅是更加自然,因为命名空间不像类,它是可以跨文件的。这是很重要的,因为像clearBrower这样的函数是很便利的函数。既不是成员也不是友元,对WebBrower类没有特殊访问权,因此它不能提供WebBrowser客户没有获取到的其他任何功能。举个例子,如果clearBrower这个函数不存在,客户只好自己调用clearCache,clearHistory,和removeCookies。

像webBrower这样的类可以有大量的便利函数,一些和标签相关,另一些和打印相关还有一些和cookie管理相关等等。通常大多数客户只对其中的一部分有兴趣。没有理由让只对标签便利函数感兴趣的客户编译依赖于cookie相关的便利函数。将它们分开的直接的方法是将它们声明在不同的头文件中。

 1 // header “webbrowser.h” — header for class WebBrowser itself
 2 
 3 // as well as “core” WebBrowser-related functionality
 4 
 5 namespace WebBrowserStuff {
 6 
 7 class WebBrowser { ... };
 8 
 9 ... // “core” related functionality, e.g.
10 
11 // non-member functions almost
12 
13 // all clients need
14 
15 }
16 
17 // header “webbrowserbookmarks.h”
18 
19 namespace WebBrowserStuff {
20 
21 ... // bookmark-related convenience
22 
23 } // functions
24 
25 // header “webbrowsercookies.h”
26 
27 namespace WebBrowserStuff {
28 
29 ... // cookie-related convenience
30 
31 } // functions

 

注意标准C++库就是这么组织的。它并没有在std命名空间中将所有东西包含在一个单一的<C++ Stand Library>头文件中,而是有许多头文件(<vector>,<algorithm>,<memory>等等),每个头文件声明了std命名空间中的一部分功能。只使用vector相关功能客户不需要#include <memory>;不需要使用list的客户不必#include <list>。这就允许客户只编译依赖于它们实际用到的部分。(Item 31中讨论了减少编译依赖的其他方法)。当一个功能来源于一个类的成员函数,那么将其分割就是不可能的,因为一个类必须被定义在一个整体中。它不能再分了。

2.2 用非成员非友元可以更好的提供扩展性

 将所有的便利函数放在不同的头文件中——但放在一个命名空间中——同样意味着客户可以很容易的对便利函数进行扩展。他们需要做的是向命名空间中添加更多的非成员非友元函数。举个例子,如果一个WebBrower客户决定实现图片下载相关的便利函数,他只需要创建一个头文件,在命名空间WebBrowserStuff中将这些函数进行声明。新函数能像旧的函数一样同它们整合在一起。这也是类不能提供的另外一个性质,因为客户是不能对类定义进行扩展的。当然,客户可以派生出新类,但派生类没有权限访问基类的封装成员(像private成员),这样的“扩展功能”就是二等身份。此外,正如Item 7中解释的,并不是所有类都被设计成基类。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
10月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
412 12
|
8月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
220 0
|
11月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
653 6
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
12月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
8月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
349 0
|
11月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
211 16
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
11月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
11月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。