条款01:把 C++ 看成一个语言联邦
C++由几个重要的次语言构成
C语言:区块,语句,预处理器,数组,指针等等。
类:class,封装,继承,多态......(动态绑定等等)
模板:涉及泛型编程,内置数种可供套用的函数或者类。
STL:STL是个模板库,主要涉及容器,算法和迭代器
在不同情况下使用适合的部分,可以使 C++ 实现高效编程
条款02:尽量以const,enum,inline替换 #define
因为或许 #define 不被视为语言的一部分,这经常会产生让人捉摸不透的bug
1,#define 修饰的记号,在预处理的时候可能就替换成了对应的数值,当代码出错的时候会提到具体的数值,但是我们不知道这个数值是干什么的
解决之道:以一个常量替换上述的宏(#define)
const double AspectRatio = 1.653 //大写名称通常用于宏,因此这里改变名称写法
2.#define 无法创建一个 class 专属常量,因为 #define 不重视作用域。一旦宏被定义,那么它就在其后的编译过程中有效,它不提供任何封装性
//enum hack 补偿做法:
enum 枚举量{para1 = value1, para2 = value2,......}
//将一个枚举类型的数值当作 int 类型使用
//和 #define 很像,都不能取地址,但它没有 #define 的缺点
3.看看这个神奇的宏
template<typename T>
inline void callWithMax(const T& a,const T& b) //由于不知道T是什么,所以采用 pass by reference-to-const
{f(a > b? a:b);
}
- 对于单纯常量,最好以 const 对象或 enums 替换 #defines
- 对于形似函数的宏(macros),最好改用 inline 函数替换#define
条款03:尽可能使用 const
const 允许你指定一个于一约束,使一个值不被改动
如果关键字 const 出现在星号左边,表示被指物是常量,如果出现在星号右边,表示指针自身是常量。如果出现在两边,表示都是常量
const 修饰迭代器
const 修饰成员函数
如果两个成员函数只是常量性不同(其他相同)则可以发生重载
const 类对象调用 const 成员函数
non-const 类对象调用普通成员函数
bitwise:
const 成员函数不能改变(除 static)成员变量的值,因为常函数里 this 指针指向的值不可改变。同理,const 对象不可以调用 non-const 函数,因为函数有能力更改成员属性的值。
但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址(地址是 this 指向的值),则不算是 const 函数 ,但能够通过 bitwise 测试。
使用 mutable 可以消除 non-static 成员变量的 bitwise constness 约束。
3、当 const 和 non-const 成员函数有实质的等价实现时,利用两次转型,令 non-const 调用 const 可以避免代码重复。
const char& operator[](int pos) const
{//...//...return name[pos];
}char& operator[](int pos)
{returnconst_cast<char&>//移除第一次转型添加的 const(static_cast<const classname>(*this)[pos]//把 classname 类型数据转换为 const classname//使得能够调用 const operator[]);
}
条款04:确定对象被使用前已先被初始化
具体规则比较复杂,最佳处理办法就是:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,必须手工完成此事:
内置类型以外的任何其他类型,初始化责任落在构造函数身上
不要混淆了赋值和初始化
这会使对象带有你指定的值,并不是最佳的做法
如果成员变量是 const 或 reference,它们就一定需要初值,不能被赋值。为了避免需要记住成员变量合适必须在成员初值列中初始化,何时不需要,最简单的做法是:总是使用成员初值列。
class 的成员变量总是以其声明次序被初始化,即使更改初始值列表的次序也不影响
使用 local-static 对象替换 non-local-static 对象:
函数内 static 对象是 local-static 对象,函数外 static 对象是 non-local-static 对象。
C++ 对 “定义于不同编译单元内的 non-local static 对象”的初始化次序并无明确定义
解决办法:利用一个函数,定义并初始化本 static 对象,并返回它的引用。类似于 singleton 设计模式,调用这个函数获得想要的对象引用,而不是直接获取这个对象引用
条款05:了解C++默默编写并调用哪些函数
惟有当这些函数被需要,它们才会被编译器创建出来。下面代码造成上述每一个函数被编译器产出:
拷贝运算符注意事项:
若成员变量中有引用,或者被 const 修饰等等,拷贝运算符不可被调用。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
编译器自动生成的函数都是 public 函数,所以我们将 public 改为 private,就可以防止对象调用拷贝构造。
注:private 只有成员函数和友元函数可以调用。
同时也产生了一个问题,如何防止拷贝在成员函数或友元函数中被调用?
答案是建立一个父类,在父类中定义 private 拷贝函数,子类( person 等等)继承父类。因为子类不可以调用父类的 private 函数:
条款07:为多态基类声明 virtual 析构函数
如果一个基类没有声明 virtual 析构函数,那么当销毁一个指向该基类的派生类的指针时就会造成诡异的局部销毁,派生的部分还残留着
给 base class 一个 virtual 析构函数,以后删除 derived class 对象就会如我们希望的那样销毁整个对象
当 class 不企图被当作 base class ,令其析构函数为 virtual 往往是个馊主意
如果类中含有 virtual 函数,其体积会增加
条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励你这样做
在C++程序中,若是同时存在两个异常,则要么结束程序,要么导致不确定行为。结束程序,剩余的操作就无法完成,这对于程序员来说是一个麻烦。
异常处理方法:
try
{...}
//try 内部写可能产生异常的语句,没有产生异常,则catch语句不执行,产生则一一匹配
//catch 用于捕获并处理异常,和 case 有异曲同工之妙
catch(...)
{1、可以使用 abort(); 函数终止程序2、可以吞下这个异常,在 catch 内部做一些处理
}
条款09:绝不在构造和析构过程中调用 virtual 函数
众所周知,在类的操作中,父类比子类先构造,而子类也比父类先析构(多态也是如此,多态先通过 virtual 找到子类析构,再析构父类),所以在构造父类的时候,子类对象还未进行初始化,在析构父类的时候,子类已经被销毁。
此时,如果父类的构造和析构函数中有 virtual ,则该函数无法找到子类的地址(或者说无视子类,因为子类被销毁/未被初始化),使程序发生不明确的行为。
所以 virtual 函数的调用无法下降至子类,但是子类可以将必要的构造信息向上传递到父类:
条款10:令 operator= 返回一个 reference to *this
为了实现以上效果
条款11:在 operator= 中处理“自我赋值”
a[ i ] = a[ j ] //如果 i == j 那么也是自我赋值
*px = *py //潜在的自我赋值
一般而言如果某段代码操作 pointers 或 references 而被它们用来“指向多个相同类型的对象”,就需要考虑这些对象是否为同一个了。实际上只要来自同一个继承体系,就可能造成“别名”
条款12:复制对象时勿忘每一个成分
当你编写一个 copying 函数,请确保(1)复制所有的 local 成员变量 (2)调用所有 base classes 内的适当的 copying 函数
不应该令 copy assignment 操作符调用 copy 构造函数,反过来也一样。构造函数用来初始化新对象,而 assignment 操作符只施行于已初始化对象身上
条款13:以对象管理资源
为确保返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开负责析构该资源的函数时,该对象的析构函数会自动释放那些资源
auto_ptr 是个“类指针(pointer-like)对象”也就是所谓的“智能指针”,其析构函数自动对所指对象调用 delete
注意不要让多个 auto_ptrs 同时指向同一个对象。如果那样的话,对象会被删除一次以上,会导致未定义的错误行为
为了预防这个问题,auto_ptrs 有一个特别的性质--若通过 copy 构造函数或 copy assignment 操作符复制它们,它们就会变成 null,而复制所得的指针将取得资源的唯一拥有权
auto_ptr 和 tr1::shared_ptr 两者都在其析构函数内做 delete 而不是 delete[] 动作,那意味着在动态分配而得到的 array 身上使用它们是不合适的,但是这种行为还是可以通过编译
条款14:在资源管理类中小心 copying 行为
禁止复制 可以将copy设置为父类的私有函数
对底层资源祭出“引用计数法”
复制底部资源 进行深拷贝
转移底部资源的拥有权 拷贝之后删除被拷贝物
条款15:在资源管理类中提供对原始资源的访问
//使用智能指针如 auto_ptr 或 tr1::shared_ptr 保存 factory 函数如 createInvestment 的调用结果:
std::tr1::shared_ptr<Investment> pInv(createInvestment());
//假如希望以某个函数处理 Investment 对象,像这样:
int daysHeld(const Investment* pi) //返回投资天数
int days = daysHeld(pInv); //错误!需要的是一个Investment*指针,传给它的却是个类型为 //tr1::shared_ptr<Investment> 的对象
条款16:成对使用 new 和 delete 时要采取相同形式
delete 最大的问题在于:即将被删除的内存之内有多少对象?这个问题的答案决定了有多少个析构函数必须被调用
如果调用 new 时使用 [ ] ,必须在对应调用 delete 时也使用 [ ].如果调用 new 时没有使用 [ ] ,那么也不该在对应调用 delete 时使用 [ ].
条款17:以独立语句将 newed 对象置入智能指针
processWidegt(std::tr1::shared_ptr<widget>(new Widget),priority());
虽然我们在此使用"对象资源管理式资源",上述调用却可能泄露资源
万一对 priority 的调用导致异常,在此情况下"new Widget" 返回的指针将会遗失,因为它尚未被置入 tr1:shared_ptr 内
条款18:让接口容易被正确使用,不易被误用
用户可能传错数字或者传入无效的数字
参考文章:EFFECTIVE C++ (万字详解)(一)-CSDN博客