《Effective C++》读书笔记(二)

来源:互联网 发布:誰も知らない 编辑:程序博客网 时间:2024/04/28 21:30

一.资源管理

资源管理就是我们申请的资源,不管是内存,互斥锁,文件等等,使用过后,都需要归还给系统。C++没有自带的垃圾回收机制,所以自己把握好资源管理是很重要的!

 

13.以对象管理资源:

a)      将一个对象所需要的所有资源放到对象内部,在对象初始化的时候分配资源,并且在对象被销毁的时候将资源释放。

b)      我们通常new一个对象,然后通过指针指向对象,在用过后delete掉。但是有时候会忘记delete,或者在delete前函数return了,这时候,这个指针没了,那这个对象的内存空间就泄露了。对于这种情况,可以使用auto_ptr来解决。

c)      atuo_ptr,智能指针。当auto_ptr被销毁的时候,会自动释放auto_ptr所指向的内容。换句话说,auto_ptr指针指向的内容不需要我们delete,当超出函数作用域,auto_ptr自动销毁,会直接使其所指的对象也销毁。但是auto_ptr的性质决定了其另一个特性,如果auto_ptr进行拷贝或者赋值的话,原来的auto_ptr会被置空。

d)      比auto_ptr更好的为shared_ptr,和auto_ptr用法相同,但是shared_ptr有引用计数机制,当引用计数为0才会清理资源。

e)      auto_ptr的机制比较蛋疼,所以常用的还是shared_ptr。不过注意两者都是进行delete,而不是delete[]操作,所以指向的对象是一个对象,对于一组对象,最好使用vector或者自己DIY一个资源管理类。

 

14.在资源管理类中小心coping行为:

(RAII—Resource Acquisition Is Initialization初始化时获得资源)

如果一个对象中管理了一系列资源,当这个对象被拷贝时,我们就得好好考虑一下了。

a)      禁止复制,这个最简单粗暴,直接拷贝构造函数私有化。

b)      对底层资源采用引用计数法,reference-count

c)      复制底部资源,直接拷贝一份资源给新的资源管理对象。

d)      转移资源拥有权,即原本的资源管理对象管理的资源管理权转移给对象,原本管理权清空。


15.在资源管理类中提供对原始资源的访问:

a)      虽然我们使用资源管理类将资源封装了起来,但是绝大多数的api是不会认得我们自己的资源管理类的,所以,要想使用这个资源,我们必须提供一个对外的接口,换句话说,需要提供资源的原始指针或者引用给外部。

b)      对于这种接口,可以提供显式的接口,比如get()方法,获得原始的指针或引用。而更方便的做法是提供一个隐式转化的函数(实现的话采用operator重载)。两者均可,显式转化比较安全,隐式转化比较方便。

 

16.成对使用new和delete时要采用相同的形式:

这条比较简单,就是new出一个的时候,delete一个,而new出一组的时候,delete也要释放一组。就是当new时使用[]时,delete也要加上[]。

 

17.以独立语句将newed对象置入智能指针:

这个也比较简单干脆。就是先new出来一个对象,然后再把它放入智能指针中,不要把这些操作放在一个语句中,不然,如果中途出现异常,new出来的对象为空,很难察觉。

 

二.设计与声明

针对接口编程,而不是实现编程。所以接口是灰常重要的。我们设计接口的目的为:让接口容易被正确使用,不容易被误用。保证正确性,高效性,封装性,维护性,延展性以及协议的一致性。使用接口容易出错的话,不只是因为用的人不会用,设计接口的人也要负有责任。


18.让接口容易被正确使用,不易被误用:

最简单的,如果一个函数接收一大串参数的话,如果类型不同,也许我们写错位置了编译会报错,如果类型相同,那么,即使写错了位置,编译时也不会报错,运行时就会出现意外的情况。所以个人感觉,能传结构体的还是用结构体传递参数吧,不然实在是太乱了。

关于资源管理的,接口内部是否会涉及到资源的申请以及释放,外部要怎么处理,这也是要注意的。

 

19.设计class犹如设计type:

如果把我们自己写的类当成一个内置的数据类型的话,好像有太多太多的东西我们没有考虑到。比如重载函数和操作符,控制内存分配,对象初始化和终结等。

a)对象如何被创建和释放

b)对象初始化和对象赋值有神马差别

c)如果对象被pass by value了,会发生什么

d)对象内容的合法值范围

e)是否需要考虑继承,如果继承了其他类,虚函数要怎么写,如果要给其他类继承,是否要声明虚函数,尤其是关于虚析构函数。

f)当面对转换类型时应该怎么办

g)什么样的操作符和函数是合理的

h)什么样的函数不应该暴露出去,什么样的系统自动生成的函数应该自己设为private防止暴露。是否需要友元函数

i)是否足够一般化,如果是一个type家族,考虑是否需要用模板

j)是否真的需要重新设计一个类,现有的类是否可以派生出现在的类

 

20.宁以pass-by-reference替换pass-by-value:

a)      通过值传递时,会调用拷贝构造函数构造一个对象的副本,当出了函数的作用域时,又会调用析构函数删除对象,比如一个复杂的对象,内部包含很多字段甚至还包含其他对象,那么,传递这样一个对象简直是灾难。

b)      而且,如果通过值传递参数,如果函数形参是一个基类对象,而实参是一个子类的对象,那么,这样传递进去的参数就会发生切割,即只保留了基类的副本(实参变成了一个基类的对象),而采用pass-by-reference则能够避免这种情况。

c)      pass-by-reference时,一般都是需要使用const关键字限定参数的,即在函数内部不能够对传递进去的参数进行修改。当然,如果需要通过引用来返回结果的话,就不必用const了。

d)      当然,虽然pass-by-reference这么好用,也不是什么时候都要用这个的。当我们要传递的参数是一个内置数据类型时,使用传值可能会效率更高。而且对于STL迭代器和函数对象也是直接传递较好。

 

21.必须返回对象时,别妄想返回其reference:

上面说了,传递参数用reference或者pointer比较好,但是返回的时候,要怎么办,试想一下,一个本地对象,我们返回其reference或者pointer,但是函数结束了,该对象就销毁了,那我们如果再使用这个reference或者pointer,那么程序必崩无疑!!!

看《Effective C++》的作者写了好几个“尝试返回本地reference的反面教材”,有直接返回的,肯定崩;本地new一个再返回的,那么这个对象不会被合理析构,而且如果有连续调用就更麻烦;还有使用static对象的,这样更惨,static对象虽然表面上看起来获得的值不等,但是如果用==判等的话,是必定相等的。

所以,凡事必有两面性,凡事也都有存在的意义。该用什么就用什么,哪个合适就用什么,不要过火。

 

22.将成员变量声明为private:

a)      最直接的就是,如果成员变量为private,那么唯一访问成员变量的接口就是函数,这样可以省去不少麻烦,即所谓的封装。封装最直接的意义就在于可以对变量进行访问控制,比如只读,读写等等。进一步看,有了封装,如果内部成员改变,外部不需要进行改变,减少了耦合性。

b)      封装进一步的保证了“针对接口编程”,同样的一个类,内部存储了一系列属性,但是暴露给外部的接口就是这些,里面可以进行多种实现,为我们提供了灵活性。

c)      再一个灰常有用的地方,当成员变量被修改时,我们可以很容易的知道变量被修改了,因为改变变量的唯一方法就是这个函数,那么只要在这个函数里面动一点手脚就可以了。

d)      如果不进行封装,那些经常变动的东西被外部调用了,那么如果想要修改,就麻烦得多了。

e)      protected类型并没有比public多多少封装性。虽然protected也可以提供封装,但是它对子类是public的,表面上看我们访问不到protected的内容,但是如果它改变了,那它所有的子类也都要遭殃。难怪有人说慎用protected呢!

 

23.宁以non-member&&non-friend替换member函数:

a)      虽然对象中的内容封装起来比较好,但是,对于变量的封装是一部分,而如果需要将多个成员函数一并调用时,再写一个成员函数包含这几个函数,效果却没有非成员函数接受对象,然后在非成员函数中一起调用多个函数效果好。

b)      这里所说的非成员函数指的是非本类的成员函数,可以是全局的函数,不属于任何一个类,也可以是另一个类的成员函数,注意不要是友元函数,友元函数破坏了封装性。

c)      为什么会这样:由于private已经把需要封装的东西封装起来了,暴露出去的接口应该都可以被使用,那么在内部再建立一个包含这些接口的函数并不能更好地再次封装,反而降低了灵活性。

d)      设计了一个类之后,可以用上述的方法,在同一个命名空间中加入一些配合类使用的工具函数,工具函数接受一个类的引用,然后组合调用类的一些方法,这些方法称为便利函数。这些便利函数可以和类在同一个文件中,这样比较方便,可以直接使用。也可以分别位于不同的头文件中,然后使用相同的命名空间即可,这样就可以简单引入一些头文件,而不是全部引入。

 

23.若所有参数都需要类型转换,请为此采用non-number函数:

a)      这个貌似关于运算符重载的时候发生得很多,还是一个例子比较容易看出来问题所在:

比如一个复数类支持*运算符重载,operator* (),我们调用的时候经常是这样用的,obj1 *obj2,但是实际上这个函数是这样调用的obj1.operator*(obj2)。乍一看没有什么问题,但是,如果这样呢obj1.operator*(非该类型的对象),这是,第二个对象会被转化成该对象(前提是支持这种转化),那么操作还是可以进行的。但是,如果反过来,非该类型的对象*obj1,就会变成这样:非该类型的对象.operator*(obj1),那么编译器就会找非该类型的对象是否有operator*操作符,没有的话,找全局操作符,也没有,那么,好的,崩了。

b)      解决这样的问题,就是提供一个non-member函数,因为上面的 obj.operator*(obj)等价于这种operator*(operator1, operator2),用这种函数就可以避免那种类型转换之后,找不到特定函数的特殊情况。其实个人感觉还是这种容易理解,上面那个抽象,而且容易出错。

 

 

24.考虑写出一个不抛异常的swap函数:

Swap函数,本来是stl里面的标准库函数,但是本人貌似基本木有用过。还是看一下这条吧,说实话,服了,这本书的作者真的是C++神级人物。

a)关于swap,简单来说就是交换两个对象(或者原生数据类型)。如果都是对象的话,那么直接赋值就好,但是,如果有指针或者引用的话,就要注意浅拷贝深拷贝的问题。但是,我们正常设计一个类的话,拷贝都设计成深拷贝的。而对于swap,并不需要深拷贝,他只要交换。

b)但是,所以按照深拷贝的方法,那会是新建两个对象,代价很大,最好的办法就是直接交换两个对象中的指针。我们可以考虑自己提供一个对象的swap函数。

c)直接用一个接受两个对象的non-member函数的话,由于要访问私有对象,没有办法实现。所以可以先设计一个member函数,然后用non-member函数调用这个函数。

d)做完这一步,并不算完,因为,你要确保调用swap调用的是我们写的这个版本的,而不是std的。用一个函数,将上面的non-member函数封起来,其中using std::swap。这样,直接调用swap的话,编译器会调用特化最好的swap,因为我们有定义这个函数,所以会调用我们自己写的那个swap,而如果没有这样的swap的话,就调用标准的那个swap,这个是STL里面那个模板定义的,不够特化,所以在有自己的swap时是不会调用这个的。注意调用swap时,不要加任何命名空间前缀。

f)不要对于std添加自定义的东东。

h)这个swap函数是为异常安全提供保障的,它本身绝对不要抛出异常。

0 0
原创粉丝点击