您的位置:首页 > 编程语言 > C语言/C++

《Effective C++》读书笔记(六) 设计与声明(第二部分)

2013-10-21 18:39 344 查看
设计与声明
Designs and Declarations

条款22:将成员变量声明为private

Declare data members private.

将成员变量声明为public、protected和private有什么区别?乍一看反正都声明了,应该无所谓的。但是,面向对象编程(OOP)是一种特殊的、设计程序的概念性方法。最重要的OOP特性有:抽象、封装和数据隐藏、多态、继承、代码可重用性。

public成员变量,只要使用类对象的程序,都能直接访问。也就是说,将public成员变量与公有接口放在一起,public成员变量就能被随意修改,也就谈不上封装,在未来编写大程序的时候,只要随便一个不小心,修改了public成员变量,就会造成数据混乱,不可预知的大量代码受到破坏,太多代码就会因此需要重写、重新测试、重新编写文档、重新编译。

而protected成员变量,在《C++ Primer Plus》上说:“关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。”因此,对于外部世界来说,protected与private相似,但对于派生类来说,protected与public相似。使用了protected,就难以避免设计缺陷,只要与基类和派生类相关。原因同public成员变量:缺乏封装性。

private成员变量,外界只能通过公有成员函数或友元函数来访问。从而避免了随意修改,体现了封装性,这样对程序的维护以及数据保护都大有裨益。

总之,从封装的角度来说,只有两种访问权限:private(提供封装)和其他(不提供封装)。

☆切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。

☆protected并不比public更具封装性。

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

Prefer non-member non-friend functions to member functions.

最重要的OOP特性有:抽象、封装和数据隐藏、多态、继承、代码可重用性。

先讨论封装:如果某些东西被封装,它就不再可见(不能被访问)。越多东西被封装,越少人就能看到他。而越少人看到它,就有越大的弹性去改变它,因此那些改变仅仅直接影响看到改变的那些人事物。然而,如果越多的函数可以访问它,数据的封装性就越低。当然了,条款22说过,要想有封装性,成员变量只能声明为private。而private成员变量只能通过member函数以及friend函数访问。从而non-friend函数的封装性比friend函数的封装性强,因为两种提供相同机能,但是non-friend函数访问不了private,从而比后者的封装性强;同理可证non-member与member函数。

有两件事值得注意:第一,这个论述只适用于non-member non-friend函数。friend函数对class private成员的访问权力和member函数相同。第二,只因在意封装性而让函数“成为class的non-member函数”并不意味着它“不可以是另一个class的member函数”

可以多多留意C++标准程序库的组织方式。C++标准程序库并不是拥有单一、整体、庞大的头文件,而是有若干头文件,只要需要某些功能,引用某个头文件,并且编写时声明std::的某些机能即可。就没必要写成一个大的class并写入各种member函数或friend函数了。

也就是说,将所有便利函数放在多个头文件内但隶属于同一个名称空间,意味客户可以轻松扩展这一组便利函数。他们需要做的就是添加更多non-member non-friend函数到此名称空间内。这样就能增加包裹弹性(packaging flexibility)和机能扩充性。

☆宁可靠non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

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

Declare non-member functions when type conversions should apply to all parameters.

令classes支持隐式类型转换通常是个糟糕的主意,不过有例外。比如建立数值类型的时候。条款22曾经用一个class的例子用来表现有理数:

class Rational
{
public:
Rational(int numerator=0,int denominator=1);  //构造函数刻意不为explicit
int numerator() const;      //分子numerator
int denominator() const;    //分母denominator
private:
...
};

然后将member函数放入public内,要注意返回的是by-value:

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

如果采用混合式算术,会发现只有一半行得通,而另一半不行:

result=oneHalf*2; //OK

result=2*oneHalf; //ERROR

归根结底是这个原因:

result=oneHalf.operator*(2) //OK

result=2.operator*(oneHalf); //ERROR

由于这个member函数并没有找到const Rational operator*(_____, const Rational& rhs)const{ } ,也调用不了non-member版本的operator*,因此member函数不适合参数之间进行类型转换。也就是说,只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。所以第一个表达式的第二个参数能用隐式类型转换,第二个的就不可以。

解决问题的方法很简单:让operator*成为一个non-member函数,使允许编译器在每一个实参身上执行隐式类型转换:

class Rational
{
...
};
const Rational operator* (const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs.denominator());
}

还要注意一点是,member函数的反面是non-member函数,而不是friend函数。不能够只因为函数不该成为member,就自动让它成为friend。当然了,本条款适用于Object-Oriented C++,但Template C++仍需斟酌。可见原书条款46

☆如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member

条款25:考虑一个不抛异常的swap函数

Consider support for a non-throwing swap.

swap用于置换两对象值,就是把两对象的值彼此赋予对象。一般情况下的swap函数由STL提供的算法完成。由于swap在太多太多的应用中需要。所以很有必要在不同的情况下讨论该怎么写出而不会抛出异常:

namespace std
{
template<typename T>       //std::swap的典型实现
void swap(T& a,T& b)
{
T temp(a);
a=b;
b=temp;
}
}

缺省的swap实现版本十分普通:a复制到temp,b复制到a,temp复制到b。但是对某些类型而言,这些复制动作无一必要;对它们而言swap缺省行为等于是把高速铁路铺设在慢速小巷弄内。

有一个非常好用的手法,“以指针指向一个对象,内含真正数据”那种类型。这种设计的常见表现形式是所谓“pimpl手法”(pimpl是"pointer to implementation"的缩写),如果用这种手法设计Widget class,就像这样:

class WidgetImpl
{
public:
...
private:
int a,b,c;
std::vector<double> v;
...
};
class Widget            //这个class使用pimpl手法
{
public:
Widget(const Widget& rhs);             //复制Widget时,令它
Widget& operator=(const Widget& rhs)   //复制其WidgetImpl对象
{
...
*pImpl=*(rhs.pImpl);
...
}
...
private:
WidgetImpl* pImpl;           //指针,所指对象内含Widget数据
};

在这个class中,一旦要置换两个Widget对象值,唯一需要做的就是置换其pimpl指针。但是缺省的swap算法不只是复制三个Widgets,还复制三个Widgetimpl对象,非常缺乏效率。

要弄一个non-member函数利用pimpl手法来swap,可以令Widget声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用该member函数:

class Widget            //这个class使用pimpl手法
{
public:
...
void swap(Widget& other)
{
using std::swap;              //这个声明很重要
swap(pImpl,other.pImpl);      //若要置换,直接置换其pImpl指针
}
...
private:
WidgetImpl* pImpl;           //指针,所指对象内含Widget数据
};
namespace std
{
template<>
void swap<Widget>(Widget& a,Widget& b)
{
a.swap(b);          //调用其成员函数
}
}

这种做法不只能够通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)

以上的Widget和WidgetImpl都是class,而class templates的Widget与WidgetImpl会有所区别。可以试试将两个类转换成class templates:

template<typename T>

class WidgetImpl{...};

template<typename T>

class Widget{...};

但是在特化std::swap时会遇到麻烦:

namespace std

{

template<typename T>

void swap(Widget<T>& a,Widget<T>& b)

{a.swap(b); }

}

看起来合情合理,却不合法。究其原因,std是个特殊的命名空间。std的内容完全由C++标准委员会决定,会禁止我们膨胀那些已经声明好的东西。如果强行在自己的<std>中加入自己写的东西,很可能会带来不可预期的行为。毕竟使用std::的情况太多了。

解决办法是这样的:还是声明一个non-member swap让它调用member swap,但不再是那个non-member swap声明为std::swap的特化版本或重载版本。而是将Widget及其相关机能放入namespace WidgetStuff中:

namespace WidgetStuff
{
...                      //模板化的WidgetImpl等等
template<typename T>     //同前,内含swap成员函数
class Widget{...};
...
template<typename T>
void swap(Widget<T>& a,Widget<T>& b)
{
a.swap(b);
}
}

接下来,讨论的就是以function template来交换两个对象值:

template<typename T>
void doSomething(T& obj1,T& obj2)
{
using std::swap;    //令std::swap在此函数内可用
...
swap(obj1,obj2);    //为T型对象调用最佳swap版本
...
}

那应该调用哪一个swap呢?C++的名称查找法则(name lookup rules)确保将找到global作用域或T所在之名称空间内的任何T专属swap。如果T是Widget并位于名称空间WidgetStuff内,编译器会使用“实参取决之查找规则”(argument-dependent lookup)找出WidgetStuff内的swap。如果没有T专属之swap存在,编译器就使用std内的swap,这得感谢using声明式让std::swap在函数内曝光。但需要小心的是,别把这一调用添加额外修饰符,因为那会影响C++挑选适当函数。比如std::swap(obj1,obj2);就会强迫编译器只认std内的swap(包括其任何template特化),因而不再可能调用一个定义于它处的较适当T专属版本。

最后有一个忠告:成员版swap绝不可抛出异常。那是因为swap的一个最好的应用是帮助classes(和class templatess)提供强烈的异常安全性保障,有关叙述在条款29。而且这一约束不适用于非成员版。因为swap缺省版本以copy构造函数和copy assignment操作符为基础,而一般情况下两者都允许抛出异常。因此当写下一个自定义swap,往往提供的不只是高效置换对象值的办法,而且不抛出异常。一般而言这两个swap特性是连在一起的,因为高效率的swaps几乎总是基于对内置类型的操作(例如pimpl手法的底层指针),而内置类型上的操作绝不会抛出异常。

☆当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常

☆如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap

☆调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“名称空间资格修饰”。

☆为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

参考文献:

1.《Effective C++》3rd Scott Meyers著,侯捷译

2.《C++ Primer Plus》5ed Stephen Prata著,孙建春、韦强译
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: