我的关于Effective C++的笔记。javascript
未彻底根据 C++1114 进行修正,待更新html
能够参考怎么样才算是精通 C++? - vzch的回答 - 知乎java
#define
常量:用 const
代替c++
函数:用inline
代替git
类常量: 用const
, enum
代替github
const
const
的对象: 通常变量, 引用参数,指针,返回值、成员函数shell
养成能写const
就添加const
关键字的习惯,严谨定义对象的性质。数据库
在const
和 非const
函数有相同实现时,在非const
函数实现中复用const
函数实现。后端
而在Java中,仅当成员方法、方法参数、lambda方法中引用的变量中须要特别强调不可变的性质时才须要声明
final
关键字,通常状况下不须要使用final
,对一个方法中的对象参数声明为final
几乎没有意义。数组在Javascript 中,也尽可能使用
const
和let
代替var
每个类型的构造函数须要确保已对全部成员变量初始化,特别是,子类须要经过调用父类的对应函数确保父类部分的完整初始化行为。
不一样于Java, 应使用成员初始化列表而不是在构造函数中使用赋值操做。
静态(全局)对象使用函数包裹,以函数方式(接口)提供静态对象的访问。
复制行为包括copy ctor
, operator=
若肯定不容许/不须要复制行为时,应禁用copy ctor
, operator=
,(经过private
或 delete
)
若肯定容许/须要复制行为时,仔细定义完整的复制行为,参见 12
对基类的析构函数声明为virtual ~Clazz() = 0
,能够防止基类被实例化。
若是不是用做基类,则不该声明析构函数为virtual。
不要继承没有声明为虚析构函数的类,特别是,不要继承任何标准库类,请使用组合而非继承。
(在ES6中也不要继承任何内置对象,包括Error)
尽量不使用异常。
不在析构函数上抛出异常,若是可能出现异常,必须在析构函数中进行捕获和处理。不然,析构数组时可能出现内存泄露和不一致数据。
不使用异常规格(exception specification)
一些关于异常的使用观点 https://www.zhihu.com/questio...
ctor
、dtor
调用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()
=operator
实现的正确姿式标准的函数声明,rhs
means 'right hand side':
Clazz& operator=(const Clazz& rhs);
=operator
函数声明应返回赋值对象的引用,保持连续赋值的语义。返回语句:
return *this;
考虑参数rhs
和自身对象是同一个引用的case
=operator
正确行为应该是,先复制参数对象的数据,后删除本身对象的数据。
这属于一种新旧对象的处置过程思想,尤为当旧的对象须要作出必定处理时,(若是不须要处理就随便了)如:
在缓存池中,缓存空间满时,新的待缓存对象须要选择剔除一个已缓存对象以腾出空间,待剔除的缓存对象由于可能在缓存期间进行了更新,须要写入这些更新到后端(如数据库)中以保持一致性。则这些对象的写入规则应该是:(1) 将待删除缓存对象,(2)持久化到后端,(3)等到持久化完成时才写入新的缓存对象,也即在持久化期间须要对这个缓存空间加锁。而不是先删除旧缓存,缓存新对象,再持久化,不然,将会致使旧的后端数据又在缓存中,致使数据不一致。
适当调用父类的=operator
函数,见12
复制行为包括copy ctor
, operator=
完整行为包括:(1)经过调用父类的对应函数确保父类的完整复制行为。(2)完整处理当前每个成员变量
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特性使用的例子是:不须要对流(istream
,ostream
)显式调用close
, stackoverflow
通常状况下应禁用operator=
和copy ctor
,C++11以前使用private
限定访问符,C++11以后使用delete
关键字
若能够复制,则明确复制的行为:(1)仅复制引用 (2)深度复制 (3)转移全部权
经过operator->
访问对应被管理对象的公开属性和方法。
经过get()
得到原始对象的指针。
经过隐式转换 operator T()
(不建议任何隐式行为)
delete []
对动态分配的数组进行析构给定一个指针p,系统没法知道p指向一个对象仍是指向一个对象数组,只能经过调用不一样的delete operator
(delete
和delete[]
)间接告诉系统须要释放的行为。
只要向指针p调用delete[]
, 系统即知道须要释放数组空间,并且也知道分配的内存大小。主流编译器一般有两种方法记录数组的元数据。
over-allocation:大部分编译器采用此方法实现,使用即另外再分配一段空间专门存储数组的元信息。一般放置在对象分配空间的前面,注意此时传入operator delete[]
的指针会指向元数据开始处(比第一个对象的分配地址更低的地址),由于元数据自己的空间也须要回收。
associative array:专门设置一个内置对象(如arrayLengthAssociation
)存储全部动态分配数组的元信息。
不要对数组对象使用typedef
声明类型别名,这会在使用别名时掩盖了数组对象的实质。
对于数组对象的动态分配,建议使用vector<T>
代替。
尽可能使用自定义的类型封装数据,限制合法输入,并提供可读的接口声明。
接口的语义应该符合人的惯性思惟,特别地,要和语言内置的接口、类型声明风格保持一致。
话虽这样说,但我的认为准官方日期库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++中须要:
将
uint64_t
转成duration<microseconds>
,告诉这个是以毫秒为单位的。microseconds{ timestamp }从
duration
构造出时间点time_point
,duration
只是一个时间段的值,要设定基准点为Unix系统时钟system_clock
。const time_point<system_clock> datetime_point(microseconds{ timestamp });这时候还不能拿出年月日的数据,须要转化为年月日,还要告诉如何处理时分秒的数据,这里把时分秒数据截断,只要拿到年月日就行。
floor<days>(datetime_point)须要转为一个日期对象,是
Date
类型吗?不是,是year_month_day
这么一个名字:auto ymd = year_month_day(floor<days>(datetime_point));这时候终于能够获取年月日了,文档还说明最好转换为
unsigned
类型,须要这么写:unsigned(ymd.month());这是若是须要获取时分秒信息怎么办,很差意思,
year_month_day
就是年月日,没有其余信息,本身查文档吧。这真的使人失去耐心。可能有提供更简洁的方法,但至少根据文档说明应该是这样写的。
另外,日期的构造声明方法也破坏了概念的一致性,为了实现声明日期的简洁化,擅自使用除号重载
/
做为日期属性分割符(date.h),又因为重载符只能在自定义类型中使用,不能写成2015/4/13
,只能写成这样的半成品:constexpr auto x1 = 2015_y/mar/22;还增长使用者的记忆负担,年月日必须至少有一个须要以显式类型声明,而且分割符是
/
, 而不是.
或者\
。还不如好好地遵照C++构造函数的语法传递参数。