条款1: 仔细区分 pointers 和 references
pointers 是一个变量,其本身存放实际内容的地址
references 是一个引用,其就是实际内容的别名
两者都支持多态但是还是有一定区别的
pointer 在进行创建的时,不一定要立即给定一个准确值,虽然发生这种情况时,通常会赋予一个 null,来区别当前指针未进行初始化,杜绝由此产生的野指针,而 pointer 也是可以进行后续修改的,如果后续有该种需求,一个 pointer 可能会指向不同的对象,并利用多态来实现接口实现的切换,如设计模式中的 策略模式 ,就该用 pointer
//如果在某处定义了一个指针,在使用前必须要先进行检测才进行使用
string* _s = nullptr;
/*
...
*/
if(_s){
/*dosth*/
}
reference 是一个引用,相对于 pointer ,其在声明时就必须被指向具体的内容,也就是必须要有一个初始值,且在后续逻辑中不能进行改变, 当实现某些操作符的时,也需要使用 reference 否则语法会让人产生歧义 如 operator[]
char* _c_p = nullptr;
char& _c_ref = *_c_p;
这种写法是不允许存在的, reference 不能指向 null ,会产生未定义的结果
所以从两者中进行选择不会有太大的困难,如果后续有切换指向内容的需要就使用指针 pointer ,否则使用引用 reference
条款2: 最好使用 C++ 转型操作符
程序中很难避免发生转型操作,而转型操作通从用小括号加上对象类型来进行转换,
(type) expression
很难在外部统计的时候知晓当前程序是否有转型操作,所以有了以下几种转型操作符
static_cast<type> expression
该操作符可以进行类型的转换
//如果需要将一个 double 转换成 int
double _d = 12.3;
//C形式转换
int _i_old = (int)_d
//新转型操作符转换
int _i_new = static_cast<int> (_d)
const_cast<type> expression
该操作符可以改变变量的常量性或易变性
int _val = 10;
const int& _i_const = _val;
int& _i_non_const = const_cast<int&>(_i_const);
_i_non_const = 20;
std::cout << _val << std::endl;
类似于上述例子,该操作符可以改变 const 的属性,添加或删除,但是也仅限于此了,不能进行类型的转换
dynamic_coast<type> expression
动态类型转换,如果转换不成功,指针会为0,引用会抛出异常.
用该转型操作符,由于可以知晓是否转换成功,所以可以安全的进行转换.
reinterpret_cast<type> expression
强制类型转换,其会无视一切规则强行转换,所以很危险,在非绝境时不要进行这种转换.
条款3: 绝对不要以多态方式处理数组
class BST {};
class Derived : public BST {};
BST _b_arr[10];
for (int i = 0; i < 10; i++) {
_b_arr[i];
}
上述情景中,_b_arr是很危险的,当对其进行遍历时,编译器会按照父类BST 的大小进行遍历,但是如果数组中存在一个子类 Derived ,那就会出现未定义的行为.
通常认为,子类的大小是大于父类的,在编译器遍历的时候,将不知晓当前对象的实际类型,产生错误的跨度,导致未定义的结果
条款4:非必要不提供 default constructor
如非必要不需要提供 default constructor
default constructor 指的是默认构造函数,也就是一个对象即使不提供任何参数也可以被构造出来
SClass _c();
但是这么做实际上会造成很多实际逻辑中的困扰,如果一个对象必须有一个 唯一 id 来进行 初始化,那么进行了默认构造初始化,在某一个时刻,将会允许没有 id 的对象产生,这样明显是危险的,但是如果不提供 default constructor ,那么在进行 stl 使用时无法进行方便的使用,因为 stl 往往需要一个 def ctor 来进行初始构造,且在一个继承链中,所有的子类也必须要向上进行跟踪了解构造函数的意义,保证不会出现差错.
根据书上对于 stl 的解决方案可以知道即使不提供 def ctor ,我们也是有很多办法来解决上述问题,并且减少了很多的复杂度
操作符
条款5: 对定制的"类型转换函数"保持警觉
class Rational{
public:
operator double(){}
}
类型转换函数虽然很方便,但有些时候会造成编译器的强制匹配造成一些无法预见的结果,所以如果真的有转换的需求,最好是通过明显的方式来进行转换
class Rational{
public:
double asDouble(){}
}
这样的行为明显是通过大众认可的,就比如string 类型需要转换成 const char *时,并不是提供类型转换函数,而是通过 c_str 进行的显示转换
条款6:区别 increment/decrement 操作符的前置(refix)和后置(postfix)的形式
//refix
Int& Int::operator++(){
*this += 1
return *this;
}
//postfix
const Int Int::operator++(int ){
Int _old_value = *this;
++*this;
return _old_value;
}
关于前置和后置的问题可以通过源码进行分析,包括一些奇妙用法,
通过源码可以看出, 后置由于一个临时变量,书面上来说,效率就会低于前置操作符,所以在编写代码时候尽量使用前置操作符
再说复杂的操作符使用方式
Int _i = 10;
++++_i
_i++++
从代码角度分析,上述两种写法只有++++_i可能会达到我们的编写目的,其返回的自身的引用,而_i++++后一个后置操作符操作对象其实是一个临时变量,且 operator 是一个 none const member function ,也不能执行该种操作
条款7:千万不要重载 && || , 三个操作符
当操作符号被修改的,假如以下形式:
operator&&(expression1,expression2)
那么 expression1,expression2 的其内容的先后顺序将是未定义的,取决服编译器
i++&&i--
会发生未定义的结果, 可能i++先执行也可能i–先执行
并且如果重载了操作符,那么对其他维护人员来说将是一场灾难,并不会有人有人想到会有人去重载,即使可以进行重载,并且当重载内容完全颠覆了缘由的含义,那么会造成更严重的后果
条款8:了解各种不同意义的 new 和delete
通常我们使用的 new 都是new operator 操作符,在声请内存的同时,调用构造函数进行初始化,如果我们希望只申请而不调用构造函数,可以使用 operator new