《Effective C++》笔记(二)
四、设计与声明
18.让接口更容易被正确使用,不易被误用
首先,必须考虑客户可能做出什么样的错误。
例如:
class Date{
public:
Date(int month, int day, int year);
};
Date d(30, 3, 1995);//不合理,应该(3, 30, 1995)
Date d(2, 30, 1995);//不合理,2月30号无效日期
考虑导入简单的外覆类型,具备了更高的类型安全性:
struct Day{
explicit Day(int d)
:val(d) { }
int val;
};
struct Month{
explicit Month(int m)
:val(m) { }
int val;
};
struct Year{
explicit Year(int y)
:val(y) { }
int val;
};
class Date{
public:
Date(const Month& m, const Day& d, const Year& y);
//...
};
Date d(30, 3, 1995);//错误!不正确类型
Date d(Day(30), Month(3), Year(1995));//错误!不正确的类型
Date d(Month(3), Day(30), Year(1995));//OK,类型正确
请记住:
- 好的接口很容易被正确使用,不容易被误用。应该在所有接口中努力达成这些性质。
- “促进正确使用”,要重视接口一致性,以及与内置类型的行为兼容。
- “阻止误用”,办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- tr1::shared_ptr支持定制删除器。这可防范DLL问题,可被用来自动解除互斥锁。
//std::shared_ptr的删除器使用示例
#include <iostream>
#include <memory>
struct Foo { int i; };
void foo_deleter(Foo * p)
{
std::cout << "foo_deleter called!\n";
delete p;
}
int main()
{
std::shared_ptr<int> aptr;
{
// 创建拥有一个 Foo 和删除器的 shared_ptr
auto foo_p = new Foo;
std::shared_ptr<Foo> r(foo_p, foo_deleter);
aptr = std::shared_ptr<int>(r, &r->i); // 别名使用构造函数
// aptr 现在指向 int ,但管理整个 Foo
} // r 被销毁(不调用删除器)
// 获得指向删除器的指针:
if(auto del_p = std::get_deleter<void(*)(Foo*)>(aptr))
{
std::cout << "shared_ptr<int> owns a deleter\n";
if(*del_p == foo_deleter)
std::cout << "...and it equals &foo_deleter\n";
} else
std::cout << "The deleter of shared_ptr<int> is null!\n";
} // 于此调用删除器
19.设计class犹如设计type
设计高效的class,需要面对的问题:
- 新type的对象应该如何被创建和销毁?影响的是class的构造函数、析构函数、内存分配函数和释放函数(operator new,operator new[], operator delete和operator delete[])
- 对象的初始化和对象的赋值该有什么样的差别?决定你的构造函数和赋值操作符的行为,以及两者之间的差异。
- 新type的对象如果被pass by value(值传递),意味着什么?记住,copy构造函数用来定义一个type的pass-by-value该如何实现。
- 什么是新type的“合法值”?对class的成员变量而言,通常只有某些数值集是有效的,那些数值集决定了你的class必须维护的约束条件,也就决定了你的成员函数必须进行的错误检查工作,它也影响函数抛出的异常。
- 你的新type需要配合某个继承图系吗?继承自某些既有的classes,则受到那些classes的设计的束缚,尤其是virtual火non-virtual影响。同理,如果允许其他classes继承你的class,那会影响你所声明的函数,尤其是virtual。
- 你的新type需要什么样的转换?需要提供类型转换函数。特别的,如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不得为类型转换操作符或non-explicit-one-argument构造函数。
- 什么样的操作符和函数对此新type而言是合理的?决定你将为你的class声明哪些函数。
- 什么样的标准函数应该被驳回?那些正是你必须声明为private者。
- 谁该取用新type的成员?决定成员的public,protected,private属性,和哪一个classes和函数应该是friends及其嵌套于另一个之内是否合理。
- 什么是新type的“未声明接口”?它对效率、异常安全性以及资源运用(例如多任务锁定和动态内存)提供何种保证?
- 你的新type有多么一般化?如果需要定义的是一整个types家族,则应该定义一个新的class template。
- 你真的需要一个新type吗?如果只是定义新的子类以便为既有的类添加新的功能,那么说不定单纯定义一个或多个non-member函数或templates,更能达到目标。
20.宁以pass-by-reference-to-const替换pass-by-value
c++编译器的底层,reference往往以指针实现出来,因此pass-by-value通常意味真正传递的指针。因此如果你有个对象属于内置类型,pass-by-value往往比pass-by-reference的效率更高。这一点也适用于STL的迭代器和函数对象,因此习惯上它们都被设计为pass-by-value。
内置类型都相当小,因此有人认为,所有小型types都是pass-by-values的合格候选人,甚至它们都是用户自定义的class亦然,这是个不可靠的推论。对象小并不意味着其copy构造函数并不昂贵。许多对象 - 包括大多数STL容器 - 内含的东西只比一个指针多一些,但复制这种对象却需承担“复制那些指针所指的每一样东西”。那将非常昂贵。另外还有某些编译器对待“内置类型”和“用户自定义类型”的态度截然不同。纵使两者拥有相同的底层描述。如编译器拒绝把只由一个double组成的对象放进缓存器内,却很乐意在一个正规基础上对光秃秃的doubles那么做。另外作为一个用户自定义类型,其大小可能会有所变化。
请记住:
- 尽量以pass-by-value-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。
21.必须返回对象时,别妄想返回其reference
在stack空间创建新对象,
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
这个函数返回一个reference指向result,但result是个local对象,而local对象在函数退出前被销毁了。
在heap空间创建对象,
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
//像下面这样就会有资源泄露问题
//例1.
Rationl w,x,y,z;
w = x * y * z; //与operator*(operator*(x, y), z)相同
除了依旧必须付出一个“构造函数调用”代价,还有一个新的问题,谁该对new出来的对象进行delete?
如代码块中例1,同一个语句内调用了两次operator*,因而两次使用new,也就需要两次delete。但却没有合理的办法让operator *使用者进行那些delete调用,因为没有合理的办法让他们去的operator *返回的reference背后隐藏的那个指针。
为了避免任何构造函数被调用,有可能想到这样的实现代码,
const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
static Rational result;
result = /*...*/;
return result;
}
//首先,显而易见这样不是多线程安全的
//此外,看如下这种情况,
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a,b,c,d;
if((a * b) == (c * d))
{
//...
}
else
{
//...
}
//这样表达式(a * b) == (c * d)总是被核算为true
//其等价函数形式(operator==(operator*(a, b), operator*(c, d))),
//这样子两个operator*返回的reference指向的是同一个static Rational对象
请记住:
- (1)不要返回指针或引用指向一个local stack对象,(2)不要返回引用指向heap-allocated对象,(3)不要返回指针或引用指向一个local static对象而有可能同时需要多个这样的对象。
22.将成员变量声明为private
为什么成员变量不该是public?
- 语法一致性。如果成员变量不是public,客户唯一能够访问对象的方法就是通过成员函数。使用函数可以让你对成员变量的处理有更精确的控制。
- 封装性。如果你通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化。将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。
请记住:
-
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
-
protected并不比public更具封装性。
23.宁以non-member-no-friend替换member函数
//一个网页浏览器类
class WebBrowser{
public:
void clearCache();//清除下载元素高速缓存区
void clearHistory();//清除访问过的URLs历史记录
void removeCookies();//移除系统中的所有cookies
};
如果想一整个执行所有这些动作,可以有两种方式,
//1.WebBrower也提供这样一个函数
class WebBrower{
public:
void clearEverything()
{
clearCache();
clearHistory();
removeCookies();
}
};
//2.提供一个普通的no-member函数
void clearBrower(WebBrower& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
哪一个比较好?
面向对象守则要求数据应该尽可能被封装,然而与直观相反的,member函数clearEverything带来的封装性比non-member函数clearBrowser低。此外,提供non-member函数可允许对WebBrowser相关机能有较大的包裹弹性,能有更低的编译相侬度,增加WebBrowser的可延伸性。
愈少代码可以看到数据,愈多的数据可被封装,而我们也就愈能自由地改变对象数据,例如改变成员变量的数量、类型等等。成员变量应该是private,如果不是,就有无限量的函数可以访问它们,也就毫无封装性。能够访问private成员变量的函数只有class的member函数和friend函数而已。如果在一个member函数和一个non-member-non-friend函数做抉择,且两者提供相同功能,那么,使用non-member-non-friend函数封装性更好,因为它并不增加能够访问class内之private成分的函数数量。
让clearBrowser成为一个non-member函数并且位于WebBrowser所在的同一个namespace内,比起在class内,能降低编译依存性。
24.若所有参数皆需类型转换,请为此采用non-member函数
- 如果你需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。(不一定是friend函数,无论何时如果你可以避免friend函数就该避免)
25.考虑写出一个不抛异常的swap函数
swap是个有趣的函数。原本它只是STL的一部分,而后成为异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。