SamJ's blog

EffectiveC艹笔记(五)

Word count: 2kReading time: 7 min
2019/11/24 Share

条款二十一:不要总想着返回对象的引用而不返回对象本身

我们上一条了解到pass-by-value会带来效率上的问题,但不要什么都pass-by-reference。特别是当一些reference会指向不存在的对象的时候。

考虑下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Rational{
public:
Rational(int numerator = 0,
int denominator = 1);
private:
int n, d;
friend const Rational
operator* (const Rational &lhs,
const Rational &rhs);
};
//这时可能有人会想,把operator*的返回值改为reference这样可以减少return value 的代价。
//于是有了下面的代码
const Rational& operator*(const Rational &lhs,
const Rational &rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
//这时返回的是一个引用,指向了局部变量result,而result会在函数结束时销毁
//所以此时返回会发生错误,不是想要的结果。

所以,像operator*这样必须返回一个对象的函数就让他返回一个新对象。

  • 不要返回指向local变量的指针或者引用。单线程中,只有一个local static对象的情况是例外(单例模式)。

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

首先是为了语法一致性,如果成员变量不是public,那么用户只能访问成员函数,用户可以很容易地记住使用方法。

第二是可以堆成员变量的访问进行有效的控制。

如:

1
2
3
4
5
6
7
8
9
10
11
12
class AccessLevels{
public:
int getReadOnly() const { return _readOnly;}
void setReadWrite(int value) { _readWrite = value;}
int getReadWrite() const { return _readWrite;}
void setWriteOnly(int value) { _writeOnly = value;}
private:
int _noAccess; //不允许进行访问
int _readOnly; //只读
int _readWrite; //读写
int _writeOnly; //只写
};

第三,为了更好的封装性,将成员变量生命为private可以对用户隐藏处理他们的细节,防止后续对这些处理过程改变时引起的大量的代码重写。

第四,protected不必public封装性强,因为如果基类的protected成员改变时,其派生类的所有相关函数都要进行修改。

  • 将成员变量声明为private,这可以赋予用户访问数据的一致性,可细粒度地划分访问控制权限,并在修改class时堆用户造成较小的麻烦。
  • protected封装性并不比public强。

条款二十三:以非成员,非友元替换成员函数

举的是浏览器类的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();//一些清理不同数据的函数
};

//有些用户会想用一个函数完成这三个函数的功能。
//考虑下面那种方式会更好
//1,以成员函数提供
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();//一些清理不同数据的函数
void clearEverythin();//调用上面的三个函数
};
//2,以非成员函数提供
void clearBrowser(WebBrowser& wb)
{
wb.cleraCache();
wb.clearHistory();
wb.removeCookies();
}

从题目可以知道,肯定是非成员函数的版本号(笑)。那么为什么呢?

这一条款只适用于在 成员函数 和 非成员函数,非友元函数之间进行选择的时候。

首先是封装性,越少的函数可以直接访问数据,封装性就越强,非成员函数形式并不能直接访问数据,可以知道分成原函数的版本封装性更强。

还有一个要注意的是clearBrowser可以是其他类的成员函数。

C++中比较普遍的做法是让clearBrowser称为一个non-member函数并且位于和WebBrowser相同的命名空间内。

1
2
3
4
namespace WebBrowserStuff{
class WebBrowser{..};
void clearBrowser(WebBrowser& wb);
}

namespace是可以跨文件的,所以我们可以把对一种数据的操作都放入一个头文件中,并且以WebBrowserStuff这一命名空间将其包裹。

这样的写法类似于使用标准库时,使用vector时写上#include <vector>用到哪些功能,就包含那个头文件,降低了编译依赖。同时,用户还可以扩展如cleraBrowser这样的函数,灵活地实现想要的功能。

  • 可以实现相同的功能时,用非成员函数,非友元函数替换成员函数,可以增加封装性,可扩展性。

条款二十四:若函数的所有参数都需要类型转换,要使用non-member函数

需要class支持隐式类型转换时:如定义了一个有理数类,允许其与int类型转换是一个合理的操作。

1
2
3
4
5
6
7
8
9
10
class Rational{
public:
Rational(int _numerator= 0, int _denominator = 1);//不为explicit
int numerator() const { return _numerator;}
int denominator() const { return _denominator;}

private:
int _numerator;
int _denominator;
};

当你想要实现一些算术操作时,首先会想到以成员函数来实现他们。

1
2
3
4
5
6
7
8
9
class Rational{
public:
...
const Rational operator* (const Rational& rhs) const; //可以实现有理数和有理数的相乘
};
//但当出现了下面的情况
Rational oneHalf(1, 2);
Rational result = oneHalf * 2; //可以调用成功
Rational result = 2 * oneHalf; //会出现错误

这不符合我们乘法应该满足交换律的认知。会出现错误的原因是2*oneHalf可以看成operator*(2, oneHalf)。但是并没有这样接收一个int类型和一个Rational为参数的乘法操作符。调用成功的是因为将2默认转换成了Rational。而当我们把operator*定义为一个非成员函数的时候,就可以解决这个问题。

1
2
3
4
5
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}

如果一个与某个class相关的函数不能成为成员函数,首先考虑的是用非成员函数解决它,而不是友元函数。

  • 如果某个函数的所有参数都要进行类型转换,那么这个函数必须是一个非成员函数。

条款二十五:写出一个不抛出异常的swap函数

swap是STL的一部分,缺省情况下swap的定义是这样的:

1
2
3
4
5
6
7
8
9
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}

只要T支持复制构造函数和赋值运算符swap就能顺利完成。而在一些情况下我们并不需要这样全盘赋值来实现swap 。举一个典型的例子是pImpl设计模式。以指针指向一个对象,指向的对象内有真正的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class WidgetImpl{
public:
...
private:
int a, b, c;
std::vector<double> v;//.....有很多数据
};

class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl);
...
}
private:
WidgetImpl* pImpl;
};

这时如果我们还调用std::swap的话,这个函数会赋值三个Widget和三个WidgetImpl对象,这大大影响了效率,我们完全可以通过直接交换pImpl指针来完成swap的工作。

正确的做法:在Widget内声明一个swappublic成员函数来做真正的置换工作,然后实现std::swap的特化版本来调用这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget{
public:
...
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl)
}
};

namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}

如果上面提到的WidgetWidgetImplclass template而非class ,我们如果想对其实现swap函数,应该采用类似的做法,同时也体现了要合理运用命名空间这一点:

1
2
3
4
5
6
7
8
9
10
namespace WidgetStuff{
template<typename T>
class Widget{...};

template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b); //在上面的Widget模板类中已经实现
}
}
  • 自己实现效率更高swap的时候,提供一个成员函数版本的swap 并提供一个非成员函数的swap来调用前者,对于非模板类,要特化std::swap
  • 调用swap时应针对std::swap使用using声明式,然后不带任何命令空间修饰地调用swap
  • 不要在std内加入对std而言全新的东西
CATALOG
  1. 1. 条款二十一:不要总想着返回对象的引用而不返回对象本身
  2. 2. 条款二十二:将成员变量声明为private
  3. 3. 条款二十三:以非成员,非友元替换成员函数
  4. 4. 条款二十四:若函数的所有参数都需要类型转换,要使用non-member函数
  5. 5. 条款二十五:写出一个不抛出异常的swap函数