Effective C++ - 实现

5. 实现

条款26: Postpone variable definitions as long as possible.

由于每定义一个变量都会带来构造和析构成本, 所以变量的定义应该尽可能的延后, 延后到不得不用的时候. 因为有时候一个变量可能并没有使用程序就已经离开了控制流. 并且, 在定义变量时如果已经具有初值并且该类型具有直接赋初值的构造函数, 那就应该赋初值. 因为调用default构造函数再进行赋值会带来性能损耗.

条款27: Minimize casting.

C++的四种新式转型:

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
  • const_cast通常用来将对象的常量性移除(cast away the constness), 唯一可以将const转换为non-const的转型.
  • dynamic_cast主要用来执行"安全向下转型"(safe downcasting), 也就是用来决定对象是否归属继承体系中的某个类型. 是唯一可能耗费重大运行成本的转型动作.
  • reinterpret_cast是很强大的一种转换, 可以从任何内置类型转换为另一种, 也可以从任意一种指针类型转换为另一种指针类型. 不在不得已的情况下不要使用.
  • static_cast用来强迫内置类型之间的隐式转换(implicit conversions), 例如将int强制转换为float, 尽管会带来精度损失. 此外, static_cast还可以在related pointers之间进行转换.

在C++中存在的一个令人震惊的事实是: C++中单一对象可能拥有一个以上的地址, 尤其是对于继承的对象来说. (例如以Base*指向它时和以Derived*指向它时.)

对象的布局方式和它们的地址计算方式随着编译器的不同而不同.

另一件关于转型的有趣的事情是很多框架在调用derived class的virtual函数之前会要求调用其base class的virtual函数. 通常会这么实现:
static_const<Base>(*this).func();

然而, 它调用的并不是当前对象的base class的成员函数, 而是转型过程中创造的关于当前对象的副本的成员函数. 如果该成员函数会对数据进行修改的话, 那么当前对象将不会得到修改, 修改的是当前对象的副本.

正确的做法是

Base::func();

条款28: Avoid returnning "handles" to object internals.

C++中类对于成员变量的权限保护仅限于该类型, 而该类型如果还有更具体的类型, 则不收到权限保护.

例如, 你在类中的一个成员变量为一个封装了两个坐标点的struct, 权限为private. 则外界对于该struct的访问受到限制. 但是如果你写一个成员函数, 直接返回了该struct的x坐标或者y坐标, 这两个变量不受到任何保护, 可以由外界任意修改, 等于该struct就没有任何保护. 这种情况就算标注了返回const都没有用, 因为别人修改只会修改该struct内的指针所指向的内存, 该struct从始至终都没有变, 不违反const的限制.

所以说不要返回一个界于该object内部的handle. 这类handle包括reference, pointer, iterator.

同时, handle所指向的资源一般都会在函数结束后被销毁, 导致返回的handle可能指向一个不再存在的对象, 变成一个dangling pointer.

条款29: Strive for exception-safe code.

所谓exception-safe(异常安全性), 在异常发生时需要满足两个条件:

  • 不允许泄露任何资源
  • 不允许数据败坏: 打个比方说, 一个壁纸程序, 需要定时在多张壁纸间更换. 然后发生了文件读取异常, 这时必须保证异常发生时屏幕依然是有数据显示壁纸的. 不然就会发生用户可察觉的bug. 一般写程序就算发生bug也要尽量不让用户察觉到, 否则就是比较严重的问题了.

有一种策略可以保证异常安全性, 称为copy and swap. 如果你要对某个对象进行某种改动的话, 就先建立一份副本, 在副本的基础上进行改动. 如果没有产生任何异常的话只要将指针改一下就好了.

条款30: Understand the ins and outs of inlining.

对于内联函数, 编译器会自动将该函数的代码插入到调用到该函数的地方. 看起来和宏有点类似, 但最起码比宏多了类型检查的优点.

但是, 内联函数也有增加代码内存, 以及在修改内联函数时, 所有使用到内联函数的地方都需要被重新编译.

inline函数对于编译器来说只是一个申请, 并非强制命令. 编译器有权决定不讲inline修饰的函数作为内联函数处理.

大部分编译器会拒绝将太过复杂(例如带有递归或循环)的函数作为内联函数处理. 而virtual函数也不会被作为内联函数处理, 因为编译器需要在编译期就知道内敛函数的本体, 而virtual函数需要在运行期间才能具体确定到底是哪个函数. 所以两者是相矛盾的.

构造函数和析构函数也是看起来不可能被内联的一类函数, 这类函数在实现者看起来没有什么代码, 但是会被编译器插入很多代码. 同时在很多场合都会被调用, 如果被内联的话就会显得太累赘了.

对于没有成功内联的函数, 编译器会给出警告.

条款31: Minimize compilation dependencies between files.

当我们改动了一个文件后重新编译时, 编译器只会重新编译该文件和与其相关的一些文件. 如果我们不想每次都编译很久, 则我们应该做好接口的分离.

在实际写C++时, 我们可以通过前置声明来避免头文件包含问题. 如果在一个类中, 只需要返回另一个类型, 就不需要知道它的大小. 我们这时候可以用前置声明.

比如下面的代码:

class Date;
class Task {
public:
    Date getDate();
};

getDate不需要知道Date具体的实现细节, 也不需要知道Date有多大, 所以使用前置声明. 这样当Date改变时, 就不需要重新编译Task类.

而如果在Task中需要创建一个Date类型的变量, 则必须包含Date的头文件. 这时我们可以用指针来代替, 因为指针的大小是固定的.

后面的真的有点看不懂了.

posted @ 2020-09-21 16:25  kaleidopink  阅读(156)  评论(0编辑  收藏  举报