《effective c++》学习笔记(四)
# 让接口容易被正确使用,不易被误用
好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质 “促进正确使用”的办法包括接口上的一致性,以及与内置类型的行为兼容 “阻止误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任 shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等设计class犹如设计type
在设计一个class时,应该考虑以下几个问题:
新type的对象应该如何被创建和销毁 对象的初始化和对象的赋值有什么样的差别? 新type的对象如果被pass by value,意味着什么? 什么是新type的“合法值” 你的新type需要配合某个继承图系吗 你的新type需要什么样的转换 什么样的操作符和函数对此新type而言是合理的? 什么样的标准函数应该驳回? 谁该取用新type的成员? 什么是新type的“未声明接口” 你的新type多么一般化 你真的需要一个新type吗Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题
宁以pass-by-reference-to-const替换pass-by-value
考虑下面这份代码:
class Foo { // something private: string str; }; void bar(Foo f) { // something }
当我们调用bar时,并且在函数中只是对f做一些访问操作(即并不需要修改f的值)时,会引起f的拷贝构造的析构操作,原因是我们以pass-by-value方式进行传递值。所以当str很大时,会造成性能上的影响。
但如果以pass-by-reference方式调用bar,则不会造成任何额外的拷贝和析构操作:
class Foo { // something private: string str; }; void bar(Foo &f) { // something }
而当在bar里对f并没有修改,或者是不想改变实参的值,我们可以使用const修饰符将f保护起来:
class Foo {
// something
private:
string str;
};
void bar(const Foo &f) {
// something
}
而对于C++的内置类型(int、指针等等),或者迭代器等(内部实际是一个指针),则以pass-by-value类型传递较为高效。
尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当
必须返回对象时,别妄想返回其reference
必须返回对象时,别妄想返回其reference
当返回一个reference时,需要考虑是否返回了一个临时的reference,例如下面这份代码:
Bar &foo() {
Bar b;
return b;
}
当调用foo得到的Bar对象实际上是一个未定义的值
而就算使用new在堆上申请空间,也需要用户来主动释放,实际上这并不是一个很好的设计,因为有时候还是会产生未释放的指针,考虑下面这份代码:
Bar &operator*(const Bar &lhs, const Bar &rhs) { Bar *b = new Bar; return *b; } Bar a; Bar b; Bar c; Bar d = a * b * c;
实际上
Bar d = a * b * c;会得到两个Bar对象,但最后我们只会得到一份,另一份没有办法得到,所以也就没有办法去释放它
如果声明为static变量,也不是一个很好的方法,考虑下面这份代码:
Bar &operator*(const Bar &lhs, const Bar &rhs) { static Bar b; return b; } Bar a; Bar b; Bar c; Bar d; if (a * b == c * d) { // something }
这里的条件则会永真
实际上正确的写法应该是,直接返回一个Bar对象,而不是引用,这样就不会再出现上述的问题。
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象
将成员变量声明为private
将成员变量声明为private
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供class作者以充分的实现弹性 protected并不比public更具封装性
宁以non-member、non-freiend替换member函数
宁以non-member、non-freiend替换member函数
宁以non-memboer non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性
若所有参数皆需要类型转换,请为此采用non-member函数
若所有参数皆需要类型转换,请为此采用non-member函数
class Integer { friend Integer operator*(const Integer &lhs, const Integer &rhs); public: Integer() = default; Integer(int i) : x(i) { } private: int x; }; Integer operator*(const Integer &lhs, const Integer &rhs) { Integer res; res.x = lhs.x * rhs.x; return res; } int main() { Integer a; Integer b = 5 * a; return 0; }
为了实现int和Integer的互相转换,如果要把operator*声明为non-member,如果operator*为member函数,那么上述代码就不能通过编译
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member
考虑写一个不抛异常的swap
考虑写一个不抛异常的swap
STL库中的swap大致是这样:
template void swap(const T &lhs, const T &rhs) { T temp(lhs); lhs = rhs; rhs = temp; }
对于一般情况,这样的swap就可以满足要求,但对于类中有vector、string这种存储大量数据的成员、或者使用ptimpl手法时,这样拷贝实际上会消耗大量的时间,而实际上只需要置换两个对象的指针即可
所以需要做以下几个事情:
提供一个public swap成员函数,让它高效的置换你的类型的两个对象值,并且绝不应该抛出异常 在你的class或者template所在的命名空间提供一个non-member swap,并令它调用上述swap成员函数 如果你正在编写一个class,为你的class特化std::swap。并令它调用你的swap成员函数 如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸的使用swap
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes,也请特化std::swap 调用swap时应针对std::swap使用using声明式,然后调用swap并不带任何“命名空间资格修饰” 为”用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西