Effective C++ 笔记 (1)

我的关于Effective C++的笔记。javascript

Note:

  • 未彻底根据 C++1114 进行修正,待更新html

01 关于C++

能够参考怎么样才算是精通 C++? - vzch的回答 - 知乎java

02 尽少使用 #define

  • 常量:用 const代替c++

  • 函数:用inline代替git

  • 类常量: 用constenum代替github

03 尽可能使用const

  • const的对象: 通常变量, 引用参数,指针,返回值、成员函数shell

  • 养成能写const就添加const关键字的习惯,严谨定义对象的性质。数据库

  • const 和 非const函数有相同实现时,在非const函数实现中复用const函数实现。后端

而在Java中,仅当成员方法、方法参数、lambda方法中引用的变量中须要特别强调不可变的性质时才须要声明final关键字,通常状况下不须要使用final对一个方法中的对象参数声明为final几乎没有意义数组

在Javascript 中,也尽可能使用constlet代替var

04 使用对象前确保对象已初始化

  • 每个类型的构造函数须要确保已对全部成员变量初始化,特别是,子类须要经过调用父类的对应函数确保父类部分的完整初始化行为。

  • 不一样于Java, 应使用成员初始化列表而不是在构造函数中使用赋值操做

  • 静态(全局)对象使用函数包裹,以函数方式(接口)提供静态对象的访问。

06 对每一个编写的类肯定是否容许复制行为

  • 复制行为包括copy ctoroperator=

  • 若肯定不容许/不须要复制行为时,应禁用copy ctoroperator=,(经过privatedelete )

  • 若肯定容许/须要复制行为时,仔细定义完整的复制行为,参见 12

07 基类的析构函数必须声明为virtual

  • 对基类的析构函数声明为virtual ~Clazz() = 0,能够防止基类被实例化。

  • 若是不是用做基类,则不该声明析构函数为virtual。

  • 不要继承没有声明为虚析构函数的类,特别是,不要继承任何标准库类请使用组合而非继承

(在ES6中也不要继承任何内置对象,包括Error)

08 不使用异常

  • 尽量不使用异常。

  • 不在析构函数上抛出异常,若是可能出现异常,必须在析构函数中进行捕获和处理。不然,析构数组时可能出现内存泄露和不一致数据。

  • 不使用异常规格(exception specification)

一些关于异常的使用观点 https://www.zhihu.com/questio...

09 不在ctordtor调用virtual方法

  • 因为在父类构造析构函数调用在子类构造析构以前,子类未完成初始化,virtual方法会调用父类行为,C++不会私自调用未定义的行为,所以不会调用子类virtual方法。

  • 子类初始化逻辑应在子类构造中明肯定义。

  • Java旨在维持继承概念的完整性,在父类构造函数能够调用到子类的virtual方法,但仍不推荐在构造函数中调用子类virtual方法,由于子类部分仍未初始化。

    // An example that the base class invoke the devired class in Java 
    class Base {
      public Base() { 
        System.out.println("Base::Base()"); 
        virt(); 
      }
      void virt() { System.out.println("Base::virt()"); }
    }
    
    class Derived extends Base {
      public Derived() { 
        System.out.println("Derived::Derived()"); 
        virt(); 
      }
      void virt() { System.out.println("Derived::virt()"); }
    }
    // Output
    Base::Base()
    Derived::virt()
    Derived::Derived()
    Derived::virt()

10 11 =operator 实现的正确姿式

  • 标准的函数声明,rhs means 'right hand side':

    Clazz& operator=(const Clazz& rhs);

    =operator 函数声明应返回赋值对象的引用,保持连续赋值的语义。返回语句:

    return *this;
  • 考虑参数rhs和自身对象是同一个引用的case

  • =operator正确行为应该是,先复制参数对象的数据,后删除本身对象的数据

  • 这属于一种新旧对象的处置过程思想,尤为当旧的对象须要作出必定处理时,(若是不须要处理就随便了)如:

    • 在缓存池中,缓存空间满时,新的待缓存对象须要选择剔除一个已缓存对象以腾出空间,待剔除的缓存对象由于可能在缓存期间进行了更新,须要写入这些更新到后端(如数据库)中以保持一致性。则这些对象的写入规则应该是:(1) 将待删除缓存对象,(2)持久化到后端,(3)等到持久化完成时才写入新的缓存对象,也即在持久化期间须要对这个缓存空间加锁。而不是先删除旧缓存,缓存新对象,再持久化,不然,将会致使旧的后端数据又在缓存中,致使数据不一致。

  • 适当调用父类的=operator函数,见12

12 定义完整的复制行为

  • 复制行为包括copy ctoroperator=

  • 完整行为包括:(1)经过调用父类的对应函数确保父类的完整复制行为。(2)完整处理当前每个成员变量

13 17 以对象定义对象的全部权(RAII)

  • C++ 和Java一个明显不一样点是须要明确对象的资源全部权,资源通常指所占内存,也包括文件、流、锁等,全部权决定了当对象结束使用时销毁其资源的义务

    std::vector<Fruit> fruits;

    vector中的fruit对象的全部权属于fruits,fruits负责对fruit的资源管理义务,即fruits被销毁时,fruits销毁全部数组中的元素

    std::vector<*Fruit> fruits
  • fruits仅对*Fruit变量(指针)所占资源负责,fruits不负责对fruit的资源管理义务,即fruits被销毁时,fruits不会销毁指向的元素。对应的,应是调用的fruits.push_back(&fruit)的对象(或函数)拥有对fruit的资源管理义务。

  • 解决对象全部权处理(内存管理)的基本思路是,设计一个在栈上的对象,并将该对象和动态内存的对象关联起来,因为栈上的对象总会在函数或做用域结束时被销毁(调用析构函数),所以只要在此对象中的析构函数实行对动态内存对象的管理操做,便可完成全部权的管理。这些处理全部权的对象即shared_ptr, unique_ptr

  • RAII的核心是,当对象被建立时,其生命周期也被准肯定义,一定存在一个肯定条件,使得对象资源在知足条件时必定会被回收处理, 且肯定条件一定可达。

  • 使用单独的语句声明建立管理资源对象(make_shared()等)。 不要与其余函数调用等语句复合。(如 getCat(make_shared<int>(42), init()), 若init发生异常抛出时将可能致使内存泄露)

  • 在管理资源对象中良好封装被管理资源的delete操做,不要暴露到外部,不然可能会出现兼容性问题。

  • 一个典型的RAII特性使用的例子是:不须要对流(istreamostream)显式调用close, stackoverflow

14 注意资源管理类的复制行为

  • 通常状况下应禁用operator=copy ctor,C++11以前使用private限定访问符,C++11以后使用delete关键字

  • 若能够复制,则明确复制的行为:(1)仅复制引用 (2)深度复制 (3)转移全部权

15 资源管理类须要提供给对资源的访问接口

  • 经过operator->访问对应被管理对象的公开属性和方法。

  • 经过get()得到原始对象的指针。

  • 经过隐式转换 operator T() (不建议任何隐式行为)

16 注意使用delete []对动态分配的数组进行析构

  • 给定一个指针p,系统没法知道p指向一个对象仍是指向一个对象数组,只能经过调用不一样的delete operatordeletedelete[])间接告诉系统须要释放的行为。

  • 只要向指针p调用delete[], 系统即知道须要释放数组空间,并且也知道分配的内存大小。主流编译器一般有两种方法记录数组的元数据。

    • over-allocation:大部分编译器采用此方法实现,使用即另外再分配一段空间专门存储数组的元信息。一般放置在对象分配空间的前面,注意此时传入operator delete[]的指针会指向元数据开始处(比第一个对象的分配地址更低的地址),由于元数据自己的空间也须要回收。

    • associative array:专门设置一个内置对象(如arrayLengthAssociation)存储全部动态分配数组的元信息。

  • 不要对数组对象使用typedef声明类型别名,这会在使用别名时掩盖了数组对象的实质。

  • 对于数组对象的动态分配,建议使用vector<T>代替。

18 设计良好的接口

  • 尽可能使用自定义的类型封装数据,限制合法输入,并提供可读的接口声明。

  • 接口的语义应该符合人的惯性思惟,特别地,要和语言内置的接口、类型声明风格保持一致。

话虽这样说,但我的认为准官方日期库date并无设计出易用的日期API,反而造出一堆须要理解的晦涩的学术概念,如time_point, duration, system_clock等(说明文档在此),并暴露在API层上,使用前必须得先了解这些概念才能上手。历来没用过一个简单需求的API能用得如此痛苦。严谨是一回事,易用是另外一回事。

例如假设须要将一个int整形看成Unix时间戳并转化为一个日期对象,获取年月日等的信息时,JavaScript 只须要:

let myDate = new Date(1487489218000);
myDate.getYear();

惟一注意的是时间戳的单位是毫秒,算是一个不足,可使用更好的第三方库moment

let day = moment.unix(1487489218);
day.year();

而C++中须要:

  1. uint64_t转成duration<microseconds>,告诉这个是以毫秒为单位的。

    microseconds{ timestamp }
  2. duration构造出时间点time_pointduration只是一个时间段的值,要设定基准点为Unix系统时钟system_clock

    const time_point<system_clock> datetime_point(microseconds{ timestamp });
  3. 这时候还不能拿出年月日的数据,须要转化为年月日,还要告诉如何处理时分秒的数据,这里把时分秒数据截断,只要拿到年月日就行。

    floor<days>(datetime_point)
  4. 须要转为一个日期对象,是Date类型吗?不是,是year_month_day这么一个名字:

    auto ymd = year_month_day(floor<days>(datetime_point));
  5. 这时候终于能够获取年月日了,文档还说明最好转换为unsigned类型,须要这么写:

    unsigned(ymd.month());
  6. 这是若是须要获取时分秒信息怎么办,很差意思,year_month_day就是年月日,没有其余信息,本身查文档吧。

这真的使人失去耐心。可能有提供更简洁的方法,但至少根据文档说明应该是这样写的。

另外,日期的构造声明方法也破坏了概念的一致性,为了实现声明日期的简洁化,擅自使用除号重载/做为日期属性分割符(date.h),又因为重载符只能在自定义类型中使用,不能写成2015/4/13,只能写成这样的半成品:

constexpr auto x1 = 2015_y/mar/22;

还增长使用者的记忆负担,年月日必须至少有一个须要以显式类型声明,而且分割符是/, 而不是. 或者 \ 。还不如好好地遵照C++构造函数的语法传递参数。

相关文章
相关标签/搜索