Effective C++ 笔记 第四部分 设计与声明
来源:互联网 发布:朋克 知乎 编辑:程序博客网 时间:2024/05/19 03:45
18.让接口容易被使用,不易被误用(Make onterfaces easy to use correctly and hard to use incorrectly)
好的接口容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。
若希望将对象使用智能指针管理,在工厂中要返回智能指针,这样可以使用户不会忘记使用智能指针而导致内存问题。
class Duck{};std::shared_ptr<Duck> createDuck(){}
使用shared_ptr避免cross-DLL-problem
cross-DLL-problem:对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁。这样会导致运行期错误。使用shared_ptr缺省使用原来(new出对象的)的delete.
19.设计class犹如设计type(Treat class design as type design)
Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。
设计高效classes需要考虑的问题:
1.新type的对象应如何被创建和销毁
2.对象的初始化和对象的赋值该有什么样的差别
3.新type的对象如果被passed by value,意味着什么?(copy构造函数定义了passed by value如何实现。)
4.什么是新type的合法值。(构造函数,赋值操作符setter函数需做错误检查。)
5.你的新type需要配合某个继承图系吗?(如果其他class继承你的class则你的析构函数需要声明为virtual,见条款7)
6.你的新type需要什么样的转换?(使用类型转换函数配合,见条款15 )
7.什么样的操作符和函数对此新type而言是合理的?
8.什么样的函数应该被驳回?(声明为private,见条款6)
9.谁该取用新type成员?(决定成员是public,private,protected。和哪个function或class是friend.)
10.什么是新type的”未声明接口”?
11.你的新type有多么一般化?(定义class template还是class)
12.你真的需要一个新的type吗?
20.宁以pass-by-reference-to-const替换pass-by-value(Prefer pass-by-reference-to-const to pass-by-value)
尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。
缺省情况下C++以by value方式传递对象至函数,调用端所获得的亦是函数返回值的一个附件,这样做会调用copy构造函数和析构函数,产生费时的操作。
使用by reference可防止调用copy构造函数和析构函数,使用const可防止被修改。
切割问题:
当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数就会被调用,造成derived所添加的功能被切割。
#include <iostream>class Base{public: Base(){ printf("Base constructor\n"); } Base(const Base& rhs){ printf("Base copy\n"); } virtual ~Base(){ printf("Base DEL\n"); } virtual void display() const{ printf("Base\n"); }};class Derived: public Base{public: Derived(){ printf("Derived constructor\n"); } Derived(const Derived& rhs) :Base(rhs) { printf("Derived copy\n"); } ~Derived(){ printf("Derived DEL\n"); } virtual void display() const{ printf("Derived\n"); }};void function(Base c){ //切割问题,pass-by-value的效率问题 //输出Base copy c.display();//输出Base,切割问题 //输出Base DEL}int main(int argc, const char * argv[]) { Derived d; //输出Base constructor、Derived constructor function(d); //输出Base copy、Base、Base DEL return 0; //输出Derived DEL、Base DEL}输出:Base constructorDerived constructorBase copyBaseBase DELDerived DELBase DEL
将function修改为pass-by-reference-to-const后
void function(const Base& c){ c.display();//输出Derived}输出Base constructor //main中Derived d;语句输出的Derived constructor //main中Derived d;语句输出的Derived //function输出的Derived DEL //main结束输出的Base DEL //main结束输出的
调用function并没有使用copy构造函数和析构函数,并解决了切割问题。
一般而言pass-by-value并不昂贵的唯一对象就是内置类型和STL的迭代器和函数对象。
21.必须返回对象时,别妄想返回其reference(Don’t try to return a reference when you must return an object)
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为”在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
返回一个指向stack空间的reference,当离开作用域后reference将指向一个被删除了的对象。造成未定义结果。
若函数使用new在heap上创建对象并以reference方式返回。则会造成内存泄露,因为谁new了对象谁就delete他,但是用这种方式我们并没有合理的办法delete该reference。
22.将成员变量声明为private(Declare data members private)
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。
为保持语法一致性。我们应该使public接口内每样东西都是函数,客户访问member data的唯一方式是通过public function。
使用private成员变量可以实现访问控制,例如读写访问,错误输入筛选。
使用private成员变量可以实现良好的封装性。
23.宁以non-member、non-friend替换member函数(Prefer non-member non-friend functions to member functions.)
宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。
若我们需要一个便利函数,他的作用是调用类中其他的函数来完成自己的任务,那么我们需要将该还是设计为non-member还是member?
例如下面的例子,我们需要在浏览器类中提供一个便利函数以清除数据。
class WebBrowser{public: void clearCache(); void clearHistory(); void removeCookies(); //方法一:使用member函数 void clearEverything(){ clearCache(); clearHistory(); removeCookies(); }};//方法二:使用non-member函数void clearBrowser(WebBrowser& wb){ wb.clearCache(); wb.clearHistory(); wb.removeCookies();}
结果是使用方法二更好,下面将论证:
面向对象的守则要求数据应该尽可能的封装,member函数带来的封装性比non-member函数低。在对象内的数据,越少的代码可以访问他,那么封装性将会越高。所以应该尽可能的降低访问对象内数据的代码。如member函数。
而使用很多non-member便利函数将会带来代码混乱,使我们不清楚哪个函数匹配哪一个类,解决方法是使用namespace。
//头文件WebBrowser.hnamespace WebBrowserStuff { class WebBrowser{ public: void clearCache(); void clearHistory(); void removeCookies(); }; void clearBrowser(WebBrowser& wb){ wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }}//头文件WebBrowserBookmarks.h namespace WebBrowserStuff { //与书签相关的便利函数}
使用namespace不仅可以清晰的使用non-member函数,还可以在不同的文件中声明同一namespace下的其他non-member函数。这也是c++标准程序库使用的组织方式,在数十个头文件(vector,memory等)中声明namespace std内的不同功能。从而降低编译依存性(客户只对他们所用的一小部分系统形成编译相依),并使客户可以自定义一些功能到namespace中, 而member函数并不能这样。
使用non-member函数可以提供封装性,包裹弹性,机能扩充性。
24.若所用参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters )
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member.
一个有理数类,有分子和分母两个成员变量,我们希望通过operator*实现一个有理数类对象与一个int类型相乘,得到正确的结果。我们需要将int转换为有理数类在调用operator*。现考虑使用类内声明的operator*。
#include <iostream>class Rational{public: //non-explicit的构造函数 Rational(int numerator = 0,int denominator = 1): _numerator(numerator), _denominator(denominator) {} //getter函数 int numerator() const{ return _numerator; } int denominator() const{ return _denominator; } //member方式的operator const Rational operator*(const Rational& rhs) const{ return Rational(this->numerator()*rhs.numerator(), this->denominator()*rhs.denominator() ); }private: int _numerator;//分子 int _denominator;//分母};int main(int argc, const char * argv[]) { Rational onHalf(1,2); Rational result = onHalf * 2;//OK result = 2 * onHalf;//ERREOR:Invalid operands to binary expression ('int' and 'Rational') return 0;}
我们通过测试可以看出onHalf * 2可以得到正确的结果。但是2 * onHalf缺导致了error。其原因是2 * onHalf将会被转换为:
2.operator*(oneHalf);
2并没转换为Rational类型,也没有operator*函数。
当函数中的参数需要进行类型转换时,声明为member函数会导致一些情况下的错误。解决方法是声明为non-member函数。
const Rational operator*(const Rational& lhs,const Rational& rhs){ return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator() );}int main(int argc, const char * argv[]) { Rational onHalf(1,2); Rational result = onHalf * 2;//OK result = 2 * onHalf;//OK return 0;}
25.考虑写出一个不抛异常的swap函数(Consider support for a a non-throwing swap)
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap
调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何”命名空间资格修饰”。
为”用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
如果swap缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。
如果swap缺省版本的效率不足,几乎因为使用了pimpl手法(pimpl:pointer to implementation),试着做以下事情:
1.提供一个public swap成员函数,让他高效的置换你的类型的两个对象值。
2.在你的class或template所在的命名空间内提供一个non-member swap,并令他调用上述swap成员函数。
3.如果你正编写一个class(非class temple),为你的class特化std::swap。并令它调用你的swap成员函数。
4.如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
并注意成员版swap绝不要抛出异常,因为许多线程安全是由此保证的。
#include <iostream>class Data{public: Data(int data): _data(data) {} void serData(int data){ _data = data; } int getData(){ return _data; }private: int _data;};namespace WidgetStuff { template<typename T> class Widget{ public: Widget(Data* data = new Data(100) ,T Tdata = nullptr): _data(data), _Tdata(Tdata) {} void swap(Widget& other){ printf("swap\n"); using std::swap; swap(_data, other._data); swap(_Tdata, other._Tdata); } T getTdata(){ return _Tdata; } int getData(){ return _data->getData(); } private: T _Tdata; Data* _data; }; //c++的名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap template<typename T> void swap(Widget<T>& a,Widget<T>& b){ a.swap(b); }}int main(int argc, const char * argv[]) { WidgetStuff::Widget<double> a(new Data(200),205.5); WidgetStuff::Widget<double> b(new Data(100),101.5); //使用std的swap,为T类型对象调用最佳swap版本。但是并不能以std::swap方式调用,这样会强迫使用std的swap using std::swap; swap(a, b); printf("a.data = %d,a.Tdata = %f\nb.data = %d,b.Tdata = %f\n",a.getData(),a.getTdata(),b.getData(),b.getTdata()); return 0;}输出:swapa.data = 100,a.Tdata = 101.500000b.data = 200,b.Tdata = 205.500000
- Effective C++ 笔记 第四部分 设计与声明
- 《Effective C++》第四章:设计与声明
- Effective C++读书笔记 第四部分 设计与声明
- (Effective C++)第四章 设计与声明(Design and declaration)
- Effective C++(四)设计与声明
- effective C++: 4.设计与声明
- Effective C++(四)接口设计与声明
- <<Effective C++>>读书笔记4: 设计与声明
- 《Effective C++》设计与声明章节
- Effective C++笔记: 设计与声明(一)
- Effective C++笔记: 设计与声明(二)
- Effective C++笔记: 设计与声明(三)
- Effective C++笔记: 设计与声明(四)
- 《Effective C++》读书笔记(五) 设计与声明(第一部分)
- 《Effective C++》读书笔记(六) 设计与声明(第二部分)
- Effective C++第四章-设计与声明-1
- Effective C++第四章-设计与声明-2
- 第四部分 设计与声明(1)(条款18-22)
- 字符串之字符数组种是否所有的字符都只出现过一次
- 黑马程序员-c语言学习笔记-运算符
- 任务调度
- 搭建golang开发环境
- 设计模式之桥接与工厂模式
- Effective C++ 笔记 第四部分 设计与声明
- 黑马程序员_JavaSE基础知识总结七:new、this、static关键字和单例模式初步
- Makefile编写执行问题,报错undefined reference to `main'
- Theano学习笔记(六)——载入与保存、条件
- 《Erlang 程序设计》练习答案 -- 第五章 记录与映射组
- 地图显示用户头像
- 《二分》hdoj acm 4.1.3
- UVA 10645Menu (dp)
- Android仿微信语音聊天功能