Effective c++ 笔记1
名词解释:size_t 是c++中的一个代表计数的类型。其实际就是一个typedef 。 但是很有用。在容器的下标操作中,下标就会被转换为size_t 类型。接口interface 。不同于java中的接口,这里指可以访问的函数等资源。比如一个函数的signature(签名,也就是返回类型和参数类型,相当于这个函数名的类型),一个类的成员函数等。client。代表客户,也就是使用你所编写的代码的人
-
名词解释:
- size_t 是c++中的一个代表计数的类型。其实际就是一个typedef 。 但是很有用。在容器的下标操作中,下标就会被转换为size_t 类型。
- 接口interface 。不同于java中的接口,这里指可以访问的函数等资源。比如一个函数的signature(签名,也就是返回类型和参数类型,相当于这个函数名的类型),一个类的成员函数等。
- client。代表客户,也就是
使用
你所编写的代码的人或者程序。我们写代码要关注的是把client放在心上,让其可以更加便捷地去用我们写地代码
-
使用const替代宏
- define是一个宏指令。但是其替换是在预处理的时候进行替换的。这就会导致替换的数值没有进入可执行文件的
符号表
里面。导致如果报错,无法联系到宏。因此,建议使用const来替换define。因为const也是固定的,可以起到替换作用的。而且const可以封装,define没有作用域的概念。在define之后,在之后的编译中可以使用这个宏,除非遇到undef。(此时define一般向define一样放在头文件里) - 可以在类里面设置一个const变量来起到一个类内的define作用。并且只是类内。一般设置为static,为了保证只有一份这个const。此时可以在声明的时候设置初始值,一般的类成员变量是不允许的。不过,也可以在实现里面来定义初始值。(定义里面不需要写static了)
- enum hack是一种类似于define的方法。并且可以取代define的符号定义,并且可以有作用域
- 宏,也就是define的函数形式。容易导致一些问题。而template inline可以完美代替宏函数。都可以直接展开,避免函数的损失
- define是一个宏指令。但是其替换是在预处理的时候进行替换的。这就会导致替换的数值没有进入可执行文件的
-
要经常使用const
- const可以加在函数的返回值上,这样可以避免一些错误。也可以用于函数的参数列表上,类似于local const。如果说在函数中不需要进行修改的话,应该声明为const,这样可以避免一些错误,以及更加安全。比如,一个错误就是将 == 打成了 = ,如果没有const修饰的话,就不会报错。
- const成员函数。const成员函数的作用就是给那些 const的 类对象用的。说明this指向的这个对象是const的。因为this指针式隐式的,我们没有办法体现在参数列表里面。const指针类似于将this指针参数前面加上一个const。表示在这个函数中const对象没办法被修改。一般这样的成员函数也会返回一个const返回值。
- const成员函数一般不可以直接替代同名的non-const成员函数。因为普通的non-const对象可能想通过这个函数来获得一个可以改变的内容。而const对象可能不应该获得一个可以改变的内容。
- const成员的含义就是
bit不变
。如果改变指向的内容的话,但是只有指针才是const修饰的,那么不会报错。我们有时候需要logical const,也就是逻辑上是不变地。也就是需要在const对象时仍然可以改变某一个成员变量地数值。我们将这样地成员变量使用mutable来修饰
-
在使用变量之前一定要进行初始化
- 在一般的内置变量使用之前要进行初始化。当然初始化的方法也很多,赋值,cin输入等等。
- 对于一个类来说。初始化要在构造函数中完成。构造函数的写法很重要。(1)要使用初始化列表,而不要使用函数体内赋值的方法。因为前者的效率更高。其会调用一个default构造函数来进行初始化。然后执行函数体中的赋值语句。(2)在初始化列表中要列出所有的成员变量,避免有的被遗漏而没有初始化。比如内置类型。
- 构造函数往往有多个。如果每一个都全部列出全部成员变量,会显得比较麻烦。我们可以将那些赋值和初始化差不多的变量编程为一个函数,供所有的构造函数调用。
- 初始化次序不确定性的时候(比如不同编译单元的non-local static成员的初始化),但是又需要初始化次序按照一定的次序进行,可以将其被函数包裹,成为一个local static ,返回其引用(retference-returnning)
-
了解c++默默编写并调用了哪些函数
- 编译器可以暗自为一个类创建一个default构造函数,copy构造函数,copy assigment操作符,以及析构函数。
- default构造函数的作用就是调用non-static 成员以及base class的构造函数。析构函数就是调用non-sattic成员以及base class的析构函数。默认生成的析构函数不是 虚函数。除非其base class的析构函数为虚函数。
- copy构造函数是将另外一个对象的成员的简单拷贝.如果是对象成员,那么调用其copy构造函数。如果是内置类型,则追求bit一致。copy assignment操作符的操作符也是类似的。
- 当含有引用成员或者const成员的时候,不会生成默认的copy构造函数和copy assignment操作符。因为有时候,这些操作是不合法的。
- 这些默认生成的函数是在使用的时候才由编译器生成的。(也就是在编译的过程中)
-
若不想使用编译器自动生成的函数,就明确拒接
- 有时候,我们的类对象在逻辑上是独一无二的,比如某一个人,其是独一无二的。因此,我们不希望出现一个另外一个对象和它一摸一样。我们通过拒绝拷贝构造函数和copy assignment运算符来实现。拒绝这两个函数的方法有多种(1)将这两个函数的定义写到private修饰的里面。因为默认的生成函数是public的。写在private里面的话,可以防止定义默认的函数。但是又不可用。为了防止友元函数使用,或者自身的函数使用,我们往往不实现这两个函数。这样,在使用的时候,即使符合访问规则,也会报链接时错误。(2)定义一个基类,叫做Uncopyable。其copy构造函数和copy assignment操作符是private的。没有实现。这样,子类的编译器在生成默认的拷贝构造函数的时候会调用父类的拷贝构造函数,如果其不可用,那么会直接报错。因此,导致子类也没有拷贝构造函数。
-
为多态基类(会有子类的基类,并且会用父类指针指向子类对象的基类)声明virtual析构函数
- 多态基类是指:会有子类的基类,并且会用父类指针指向子类对象的基类。这样的基类的析构函数必须要加virtual。否则的话,会导致当用delete来释放一个指向这个 基类的派生类对象 的基类指针 的空间的时候,不会调用子类的析构函数。这样的话,子类成员(只属于子类的成员)就不会得到释放。就会导致半销毁 的状态。通常这样需要将析构函数设置为virtual的类还有其他的virtual成员函数。
- 如果不是多态基类的话,不要用virtual。因为加virtual会导致类带上 vptr 以及 vtbl。这会导致空间浪费32-128bit。
- 有些类不是多态基类(其析构函数没有virtual),但是也会被继承。这就会导致上述的危险。比如string 以及其他的stl对象,都为非多态基类
- pure virtual 函数(也就是在函数声明后面加上=0)会导致这个类变成 abstract class,也就是不可以被实例化。我们可以通过pure virtual的析构函数,来让这个类变成一个只能被继承的抽象类,并且此时析构函数为virtual也保证了其安全性。此时要注意,析构函数需要定义。否则,其类型的析构函数在执行父类的析构函数的时候会发生错误。
-
被让异常逃离析构函数
- 析构函数吐出异常是危险的。如果真地发生,应该捕获并且
吞下
它们。不要让其传播,造成不明确地行为。因为其是隐式调用的,没有办法被外围所捕获。 - 解决的办法就是将析构函数那些可能引发异常的操作独立出来,成为一个成员函数,让用户自己去调用,使其显式地发生,这样方便进行异常处理。比如,在数据库连接管理类中地关闭连接操作。
- 析构函数吐出异常是危险的。如果真地发生,应该捕获并且
-
不在构造和析构过程中调用virtual函数
- 子类的构建过程中会首先调用父类的构造函数。如果父类的构造函数中使用了虚函数,那也不会导致在初始化的时候去调用子类的函数。因为在执行父类的构造函数的时候,子类的成员还没初始化,不可以调用子类的函数。如果在子类中执行的虚函数是一个纯虚函数pure virtual,那么在执行的时候会导致错误。
- 子类析构的时候,会在调用子类的析构函数之后调用父类的析构函数。父类析构的时候,虚函数也不起作用。因为子类的成员已经视为不存在了。
- 如果子类的不同意味着父类也要不同的话,那么可以在子类调用父类构造函数的时候传递一些信息给父类构造函数
-
operator= 需要返回左侧类型引用。
- 因为这样方便进行连环赋值。是一个约定,一般都这样做。
-
在定义operator=的时候考虑自我赋值安全性
- 有时候当赋值对象是自己的时候,会导致一些问题发生。一般不会考虑这种特殊情况。比如,我们在赋值之前要把自身的成员毁掉,那么在自我赋值的时候,可能会把赋值对象的也毁掉,就无法完成后续赋值。也可能自身的已经毁掉了,但是申请空间出现了错误,这叫异常安全性,也就是发生异常的时候,是不是安全的。
- 解决的一个办法就是: 自我判定检查,也就是先判定一下是不是自己,可以通过指针比较,如果不是,然后再进行赋值。
- 解决办法2:先把自身的成员备份一下,先不要急着毁掉。然后进行更新。最后毁掉。可以同时解决异常安全性和自我赋值安全性。
-
operator=的时候需要将所有成员赋值
- operator= 运算符一定要将所有的成员都复制过来。如果不全部复制过来的话,编译器也不会报错。但是这才是可怕的问题。
- 子类的copying函数中必须要调用相应的父类的copying函数代码,这样才可以完成父类的复制。
- 复制的对象:所有的local成员 以及 父类成员
- 不要再copy构造函数里调用 赋值操作运算符。也不要早赋值操作运算符里面调用copy构造函数。
-
以对象管理资源
- 所谓
资源
,就是一旦用了它,一定要归还给系统。否则会发生坏事。动态内存,文件描述符,socket,数据库连接,锁等都是资源 - 以对象管理资源的一个好处就是保证资源的释放。这种思想往往就是”获得资源之后立即放进管理对象“,资源获得时就是初始化之时RAII。一个典型的例子就是对于 动态内存资源。我们通过 factory函数或者其他方法得到了一个指向动态内存的指针,应该将其放入 智能指针 对象中。这样不用手动delete,也不用担心在delete之前就退出或异常而导致没有执行delete。
- 对于管理动态内存资源:使用auto_ptr ,其只是适用于指针指向的资源只可以有一个拥有者的情况。如果有两个auto_ptr指向同一个对象,对出现两次delete的危险情况。其赋值和拷贝构造函数 在copy的同时会把右端的指针设置为 null。为了保证其唯一性。新标准的unique_ptr与之类似。shared_ptr的copying 函数就正常得多了。其属于reference-counting smart pointer 的一种。但无法打破环状引用。 weak_ptr可以解决这个弊端。这些智能指针的析构函数都是delete,而不是delete [] 因此,不要用其来管理一个对象数组资源。
- 所谓
-
在资源管理类中小心copying行为
- 对于一个RAII的对象,很多时候,其copy行为是不合理的。因为资源往往是独一无二的,不允许复制的。很少时候,可以合理得拥有两个相同地资源。我们采用 6 的方法来禁止复制。
- 或者我们允许资源有多个使用者。我们使用reference count,类似于shared_ptr 在有没有引用的时候,我们进行处理。有时可以借助shared_ptr 并且 定制化 删除器 来实现。
- 或者复制底层资源。也就是资源允许被随意复制。此时应该是deep copy,也就是将资源复制一份。
- 或者转移底部资源的所有权。保证只有一个使用者,并且允许转移。就像是房子,只有一个户主,但是允许转移。像是unique_ptr的方式。
其实这也说明了资源管理类有控制 资源的 所属权 的功能
-
在资源管理类中提供对原始资源的访问
- 很多api是需要使用原始资源的访问的。因此,资源访问类应该提供对原始资源的访问路径。比如 显式转换,也就是写一个函数,返回原始资源。(安全,不容易吴用,但是不够简洁)或者隐式转换,写一个隐式转换函数。(简洁,但是容易误用)
-
成对使用new和delete时要采取相同形式
- new的行为就是 1 分配内存 2 调用一个或者多个构造函数 (new [])。delete的行为 1 调用一个或者多个析构函数 2 回收内存(delete [])
- 对象数组内存结构有数组大小的记录。而单个对象,没有。
- 正是因为 new 和 new[] 导致不同的内存结构。其回收的动作,只能对应分别由 delete 和 delete[] 来完成
-
以独立语句将newed对象置入智能指针
-
让接口容易被使用,不易被误用
- 需要客户端错误可以因为导入新类型而获得预防。通过设置特定类型,来让用户填错类型(或者逻辑类型,比如同样的int类型数字,一个代表月份,一个代表年份。设计为两种不同类型Month和Year)的时候,得到提示。
- 有些类型的取值是固定的,离散的。比如Month,只能是1-12 。 使用enum来实现的好处是确实限制了范围。坏处就是没有类型安全性,而且容易出现non-local static对象的初始化时机是不确定的。可以在对应类里面根据static函数来返回相应的对象。
- 自定义的type最好和内置的type(如int)的行为保持一致。自定义的type之间如果有相似之处,也应该保持一致。
- 好的接口更容易被使用。如前面的factory函数,我们原来主张的策略就是返回一个指针之后立刻用shared_ptr来管理之。但是实际上,用户可能会忘记。那么我们直接返回一个shared_ptr即可。用户必须要用一个shared_ptr来进行接收。用户如果希望在释放这个资源的时候还有其他的动作,不仅仅是delete。那么可以自己定义一个删除器。这个删除器也可以绑定到shared_ptr上面,让其自动执行。从而避免了在不该删除的时候删除了。
-
设计class犹如设计type
- 想要设计出好用的class很难的,需要问自己一些问题。
-
使用pass by reference-to-const 替代 pass by value
- 缺省情况下,是用pass by value的方式来 传递给一个 函数的形参的。函数的返回值也是以pass by value的形式来返回的。这种情况如果数据结构比较复杂的话,是很浪费时间的。
- pass by value 就意味者我们不希望对 实参 做出任何改变。因为其操作的是副本,也就没有任何改变原值的机会。正因如此,我们将reference用const来修饰。起到同样保证不会有任何改变的效果。
- pass-by-reference-to-const还可以避免频繁调用 拷贝构造函数,析构函数。同样还可以避免 切割 问题slicing。
- 由于reference本质上是指针实现的。因此,对于内置类型来说,refrence返回不如value来的效率高。对于迭代器和函数对象来说,使用reference效率也不如by value
-
必须返回对象时,别妄想返回其reference
- 返回一个对象的refrence时候要很谨慎,这个reference指向的对象必须是在调用函数之前已经存在过的。否则,如果是在函数中定义的local stack对象,那么会在结束函数的时候销毁这个实际对象。如果返回的是函数中定义的heap-allocated对象,那么也会造成无法获得指针来销毁的内存错误。
- 比如拷贝赋值运算符,其返回赋值号左侧的引用。也就是 *this. 这是没问题的。因为其在调用函数前已经存在。
-
将成员变量声明为private
- 成员变量声明为private可以实现统一化,也就是所有的访问都需要加 () 通过函数访问。不必去记忆哪些可以直接访问
- 成员变量为private,访问通过getter和setter来实现。可以实现更好的控制。可以控制只读(只设置get函数,并且为const的),只写(只设置set函数),可读写。
- 封装的需求。封装其实就是隐藏,就是不给
客户
看。只通过某种途径(函数名等)来让客户用。我们将成员变量设置为public就是不封装。设置为private就是封装。不封装意味着不可改变。因为如果你想改变的话,可能需要改变客户代码
。也就是客户使用的代码。(假如说我们给一个成员变量换一个名字,如果不封装,那么客户端可能已经有直接使用的代码。需要改客户代码。如果封装,那么可以通过只修改函数实现来做到。) - 一个东西的封装性,和其改变时可能造成的代码破坏量成反比。封装地越好,也就意味着看见它地代码越少,那么直接依赖的代码就越少,我们就可以在不改变客户代码的情况下,有更大的操作空间。
- protected其实和public 一样都不具备封装性。protected对于其derived class来说是没有封装性的。
-
当可以实现相同功能的时候,使用non-member 和 non-friend函数来替换member函数
- 这其实是为了封装性考虑。在member函数中,其可以直接访问我们在类中封装的成员变量。那么可能会导致,如果成员变量改变了,那么其代码也可能收到影响。而non-member 和 non-friend函数就没有这样的担忧。
- 一个系统可以占据一个命名空间。这个系统中的所有内容,类,函数等都定在这个 命名空间内。但是,这样的话,文件会太大了。由于同一个命名空间可以在不同的文件中定义,在逻辑上属于同一个命名空间。因此,经常将不同的系统模块 制作为 不同的文件中。这样,用户可以根据需求使用相应部分,不必全部导入。也可以快速完成扩充,只要在自己定义的文件中使用同样的命名空间即可。
-
如果你需要为某一个函数的所有参数进行隐式类型转换,那么这个函数一定是一个non-member
- 比如operator* 运算符。其可以设计为一个类的成员函数,也可以设计为一个有两个参数的函数。
- 当我们设计为一个类的成员函数的时候, obj1*obj2 实际上调用的是obj1的成员函数。
- 当设计为non-member函数的时候,可以利用非explicit构造函数来实现,如果某内置类型可以转换为obj(构造函数只有这一个内置类型),那么 内置类型 * obj2 和 obj1 * 内置类型 都有用。内置类型因为在参数列表列表里,所以可以实现隐式转换。
- 因为只有在 参数列表里面 ,才有隐式类型转换的可能。
-
自己实现swap函数
- swap函数是一个很重要的函数。默认的std里面的swap函数是利用 拷贝构造函数,拷贝赋值运算符的一个template函数。但是有时候,想要交换的话,只要交换两个指针即可。使用swap效率太低了。
- 我们可以为std里的swap函数构造一个特化版本。也就是template <> 然后,后面的swap可以直接我们的自定义类型。这样,在调用到std里面的一般化swap版本的时候,如果类型符合,就会调用特化版本。但是如果我们的类型是一个template,那么由于在std里面添加这样的template< T >内容是不合理的,所以我们不可以这么做
- 我们还可以选择在类所在的命名空间中定义这样的swap函数,由于会优先搜类所在的命名空间内容。因此可以保证在 std swap之前执行。并且可以适用于自定义类 和 template class都行得通。只是,由于我们不建议申请友元函数(其会降低 封闭性),但是其很可能会需要访问 私有成员变量。因此我们定义一个成员函数swap,然后在外围的non-member swap中调用成员函数swap。std很多就是这么做的。
- 搜索名字的顺序 就是 global作用域 类的命名空间作用域 std作用域
-
尽可能延后变量定义式的出现,在不得不使用的时候才去使用
- 防止还没用就直接错误退出,就需要白白负担其创建和销毁成本。
-
尽量少做转型动作
- 旧式转型可以分为两种 (T)exp 和函数式 T(exp)。其一般可以被新式转型替代。可以起到类似于构造函数的作用。Widget(10) 可以将10转换为Widget,如果其构造函数只需要一个数字的时候。
- 转型并不是只是告诉编译器怎么去重新看待这个东西。其往往是有对应的转换代码的。否则也无法改变其内存结构。
- 类类型转换(不是类指针类型)会重新创建一个副本,也就是对象。在这个对象上的操作和原来的对象没啥关系。这可能在某些场景下造成错误。
- 也尽量不要使用dynamic_cast 因为其很耗费资源。当确实想要利用base class指针来调用 derived class的函数的时候,应该通过某些方法来绕过去,比如将virtual函数 在继承体系中向上移,在base class中也定义同名的虚函数,但是不定义内容。
-
避免返回handles指向对象内部成分
- handle表示一个取出一个对应对象的内容。比如一个对象的指针,reference,迭代器。
- 成员函数返回一个handle会面临handle比对象更长寿,从而handle指向为空的情况。
- 成员函数返回一个非const handle,还可能导致对象的封装新被破坏,也就是获得了本身被隐藏,被封装的内容。
-
为 异常安全 而努力是值得的
更多推荐
所有评论(0)