首页 > 程序开发 > 软件开发 > C++ >

Effective C++读书笔记(14)

2012-02-04

条款23:宁以non-member、non-friend替换member函数Prefer non-member non-friend functions tomember functions想象一个用来表示网页浏览器浏览器的类。这样一个类可能提供的大量函数中,有一些用来清空下...

条款23:宁以non-member、non-friend替换member函数

Prefer non-member non-friend functions tomember functions

想象一个用来表示网页浏览器浏览器的类。这样一个类可能提供的大量函数中,有一些用来清空下载元素高速缓存区、清空访问过的URLs历史,以及从系统移除所有cookies的功能:

class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};

很多用户希望能一起执行全部这些动作,所以WebBrowser可能也会提供一个函数去这样做:

class WebBrowser {
public:
...
void clearEverything();
// calls clearCache, clearHistory, and removeCookies
...
};

当然,这个功能也能通过非成员函数调用适当的成员函数来提供:

void clearBrowser(WebBrowser& wb)
{wb.clearCache();wb.clearHistory();wb.removeCookies();}

那么哪个更好呢,成员函数clearEverything还是非成员函数clearBrowser?

面向对象原则指出:数据和对它们进行操作的函数应该被绑定到一起,而且建议成员函数是更好的选择。不幸的是,这个建议是不正确的。面向对象原则指出数据应该尽可能被封装,与直觉不符的是,成员函数clearEverything居然会造成比非成员函数clearBrowser更差的封装性。此外,提供非成员函数允许WebBrowser相关功能的更大的包装弹性,可以获得更少的编译依赖和增加WebBrowser的扩展性。因而,在很多方面非成员函数比成员函数更好。

我们将从封装开始。封装为我们提供一种改变事情的弹性,而仅仅影响有限的客户。结合对象内的数据考虑,越少有代码可以看到数据(访问它),数据的封装性就越强,我们改变对象数据的的自由也就越大,比如,数据成员的数量、类型,等等。如何度量有多少代码能看到数据呢?我们可以计算能访问数据的函数数量:越多函数能访问它,数据的封装性就越弱。

我们说过数据成员应该是private,否则它们根本就没有封装。对于private数据成员,能访问他们的函数数量就是类的成员函数加上友元函数,因为只有成员和友元函数能访问 private成员。假设在一个成员函数(能访问的不只是一个类的private数据,还有 private 函数,枚举,typedefs等等)和一个提供同样功能的非成员非友元函数(不能访问上述那些东西)之间选择,能获得更强封装性是非成员非友元函数。这就解释了为什么clearBrowser(非成员非友元函数)比clearEverything(成员函数)更可取:它能为WebBrowser获得更强的封装性。

在这一点,有两件事值得注意。首先,这个论证只适用于非成员非友元函数。友元能像成员函数一样访问一个类的private成员,因此同样影响封装。从封装的观点看,选择不是在成员和非成员函数之间,而是在成员函数和非成员非友元函数之间。

第二,只因关注封装而让函数成为类的非成员并不意味着它不可以是另一个类的成员。这对于习惯了所有函数必须属于类的语言(例如,Eiffel,Java,C#等等)的程序员是一个适度的安慰。例如,我们可以使clearBrowser成为某工具类的static成员函数,只要它不是WebBrowser的一部分(或友元),它就不会影响WebBrowser的private成员的封装。

在C++中,比较自然的做法是使clearBrowser成为与 WebBrowser在同一个namespace中的非成员函数:

namespace WebBrowserStuff {

classWebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}

namespace能跨越多个源文件而类不能。这是很重要的,因为类似clearBrowser的函数是提供便利的函数。如果既不是成员也不是友元,他们就没有对WebBrowser的特殊访问权力,所以不能提供任何一种WebBrowser客户无法以其它方法得到的机能。例如,如果clearBrowser不存在,客户可以直接调用clearCache,clearHistory和 removeCookies本身。

一个类似WebBrowser的类可以有大量的方便性函数,一些是书签相关的,另一些打印相关的,还有一些是cookie管理相关的,等等。通常多数客户仅对其中一些感兴趣。没有理由让一个只对书签相关便利函数感兴趣的客户在编译时依赖其它函数。分隔它们直截了当的方法就是将头文件分开声明:

// header "webbrowser.h" – 针对WebBrowser自身及其核心机能
namespace WebBrowserStuff {
class WebBrowser { ... };
... // 核心机能,如人人都会用到的non-member函数
}

// header "webbrowserbookmarks.h"
namespace WebBrowserStuff {
... // 书签相关的便利函数
}

// header "webbrowsercookies.h"
namespace WebBrowserStuff {
... // cookie相关的便利函数
}

...

这正是C++标准程序库的组织方式。标准程序库并不是拥有单一整体而庞大的<C++StandardLibrary>头文件并内含std namespace中的所有东西,它们在许多头文件中(例如,<vector>,<algorithm>,<memory>等等),每一个都声明了std中的一些机能。这就允许客户在编译时仅仅依赖他们实际使用的那部分系统。当机能来自一个类的成员函数时,用这种方法分割它是不可能的,因为一个类必须作为一个整体来定义,它不能四分五裂。

将所有方便性函数放入多个头文件中,但隶属于一个namespace中,意味着客户能容易地扩充便利函数的集合,要做的只是在namespace中加入更多的非成员非友元函数。例如,如果一个 WebBrowser的客户决定写一个关于下载图像的便利函数,仅仅需要新建一个头文件,包含那些函数在WebBrowserStuff namespace中的声明,这个新函数现在就像其它便利函数一样可用并被集成。这是类不能提供的另一个特性,因为类定义对于客户是不能扩展。当然,客户可以派生新类,但是派生类不能访问基类中被封装的(private)成员,所以这样的“扩充机能”只是次等身份。

· 用非成员非友元函数取代成员函数。这样做可以提高封装性,包装弹性,和机能扩充性。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

Declare non-member functions when typeconversions should apply to all parameters

让一个类支持隐式类型转换通常是一个不好的主意。当然,这条规则有一些例外,最普通的一种就是在创建数值类型时。例如,如果你设计一个用来表现有理数的类,允许从整数到有理数的隐式转换看上去并非不合理:

class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
// 非explicit,允许int-to-Rational隐式转换int-to-Rational
int numerator() const; // 分子和分母的访问函数
int denominator() const;

private:
...
};

应该支持算术运算,比如加法,乘法等等,但不能确定是通过成员函数、非成员函数、还是非成员的友元函数来实现它们。当你摇摆不定的时候,你应该坚持面向对象的原则。于是有理数的乘法与Rational类相关,所以在Rational类的内部实现有理数的operator*似乎更加正常,我们先让operator*成为Rational的一个成员函数:

class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};

Rational oneEighth(1, 8);

Rational oneHalf(1, 2);

Rational result = oneHalf * oneEighth; // fine

result = result * oneEighth; // fine

这个设计让你在有理数相乘时不费吹灰之力,但你还希望支持混合模式的操作,以便让 Rational能够和其它类型(如int)相乘。毕竟两个数相乘很正常,即使它们碰巧是不同类型的数值。

result = oneHalf * 2; // fine

result = 2 * oneHalf; // error!

只有一半行得通,但乘法必须是可交换的。当以对应的函数形式重写上述两个式子,问题所在便一目了然:

result = oneHalf.operator*(2);

result = 2.operator*(oneHalf);

对象oneHalf是一个包含 operator* 的类实例,所以编译器调用那个函数。然而整数2没有operator*成员函数。编译器同样要寻找可被如下调用的非成员operator*(也就是说,在 namespace 或全局范围内的operator*):

result = operator*(2, oneHalf);

但在本例中,没有非成员的接受int和Rational的operator*函数,所以搜索失败。再看那个成功的调用,它的第二个参数是整数2,而Rational::operator*却持有一个 Rational对象作为它的参数。这里发生了隐式类型转换。编译器知道你传递一个int而那个函数需要一个Rational,通过用你提供的int调用Rational的构造函数,它们能做出一个相配的Rational。换句话说,它们将那个调用或多或少看成如下这样:

const Rational temp(2); // 根据2建立一个临时Rational对象

result = oneHalf * temp; // 等同于oneHalf.operator*(temp);

当然,编译器这样做仅仅是因为提供了一个非explicit构造函数。如果Rational的构造函数是explicit,那两句语句都将无法编译,但至少语句的行为保持一致。

这两个语句一个可以编译而另一个不行的原因在于,当参数列在参数列表中的时候,才有资格进行隐式类型转换。现在支持混合运算的方法或许很清楚了:让operator*作为非成员函数,因此就允许将隐式类型转换应用于所有参数:

class Rational {... // 不包括operator*};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{return Rational(lhs.numerator() * rhs.numerator(),

lhs.denominator()* rhs.denominator());}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // fine
result = 2 * oneFourth; // it works!

另外,仅仅因为函数不应该作为成员并不自动意味着它应该作为友元。

· 如果你需要在一个函数的所有参数(包括被 this 指针所指向的那个)上使用类型转换,这个函数必须是一个非成员。



摘自 pandawuwyj的专栏

相关文章
最新文章
热点推荐