条款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)
{
return
const_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:让接口容易被正确使用,不易被误用
用户可能传错数字或者传入无效的数字
条款19:设计 class 犹如设计 type
- 新type的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值该有什么样的差别?
- 新type 的对象如何被 pased by vlaue,意味着什么?
- 什么是新 type 的“合法值”?
- 你的新 type 需要配合某个继承图系吗?
- 你的新 type 需要什么样的转换?
- 什么样的操作符和函数对此新 type 而言是合理的?
- 什么样的操作符和函数对此新 type 而言是合理的?
- 什么样的标准函数应该驳回?
- 谁该取用新 type 的成员?
- 什么是新 type 的“未声明接口”
- 你的新 type 有多么一般化?
- 你真的需要一个新 type 吗?
条款20:宁以 pass-by-reference-to-const 替换 pass-by-value
pass-by-value 有时会不必要的多次调用构造函数和析构函数(类本身调用,类中的成员调用)费时费力,可以用 pass-by-reference-to-const 来替代
class Window{
public:
...
std::string name() const; //返回窗口名称
virtual void display() const; //显示窗口和其内容
};
class WindowWithScrollBars: public Window{
public:
...
virtual void display() const;
};
//现在假设希望写一个函数打印窗口名称,然后显示该窗口。下面是错误示范:
void printNameAndDisplay(Window w) //不正确,参数会被切割
{
std::cout << w.name();
w.display();
}
WindowWithScrolBars wwsb;
printNameAndDisplay(wwsb);
//当调用上述函数并交给它一个 windowWithScrollBars 对象时,参数w会被构造成一个window对象,
//因为它是passed by value,造成wwsb“之所以是个 WindowWithScrollBars对象”的
//所有特化信息会被切除,所以在函数内调用的总是 Window::display绝不会是 //WindowWithScrollBars::display
解决切割问题的方法,就是 by reference-to-const 的方式传递 w
void printNameAndDisplay(const Window& w)
{
std::cout<<w.name();
w.display();
}
现在传进来的窗口是什么类型,w就表现出哪种类型
references 往往以指针实现出来,因此 pass by reference 通常意味着真正传递的是指针。因此如果传递的对象是内置类型(例如 int),pass by value 往往比 pass by reference 效率高些
条款21:必须返回对象时,别妄想返回其 reference
周所周知,return 返回的是一个浅拷贝副本,返回一个对象是没有问题的,但如果返回一个引用,原对象被销毁之后,引用的指向也被销毁了,也就是引用指空,出错。
我们当然可以用 new 解决这个问题,但是当变量数目多的时候,程序员往往不知道怎么使用 delete
总结:虽然返回一个对象需要构造,析构等操作而产生一些代价,但是如果我们不想改变已有的值,就最好不要返回一个引用,而是支付这些代价。(这在时间上会多一点,但创建的对象会随运算符的结束而被销毁。这比“未定义行为(返回一个新建对象的引用)”,“资源泄漏”,“结果出错”要好得多了。)
条款22:将成员变量声明为 private
条款23:宁以 non-member,non-friend 替换 member 函数
条款24:若所有参数皆需类型转换,请为此采用 non-member 函数
class Rational
{
public:
Rational(int numerator = 0, int denominator = 1)//分子与分母
...
const Rational operator*(const Rational& right_number) const;
...
};
Rational oneEighth(1, 8);
Rational onehalf(1, 2);
Rational result1 = onehalf * oneEighth;
Rational result2 = onehalf * 2;
Rational result3 = 2 * onehalf;//error!
onehalf*2 相当于 onehaf.operator*(2)
首先创建了一个临时对象 const Rational temp(2);
再让两个 Rational 对象运算。
2*onehalf 是 2 调用了operator*。因为 2 是需要被转换的参数,而 2 的位置和 this(调用operator *) 对象的位置是一样的,所以无法将 2 转换成 Rational 类型,也就无法调用 operator* 函数。
解决办法:使用 non-member 函数,让左右参数的地位平等:
const Rational operator*(const Rational& left_number, const Rational& right_number)
{...}
条款25:考虑写出一个不抛异常的 swap 函数
周所周知,swap 可以交换两个数的值,标准库的 swap 函数是通过拷贝完成这种运算的。想想,如果是交换两个类对象的值,如果类中变量的个数很少,那么 swap 是有一定效率的,但如果变量个数很多呢?
你一定联想到了之前提过的,引用传递替换值传递。没错,交换两个类对象的地址就可以很有效率地完成大量变量的 swap 操作。不幸的是,标准库的 swap 并无交换对象地址的行为,所以我们需要自己写 swap 函数。
class person{...};
void my_swap(person& p1, person& p2)
{
swap(p1.ptr, p2.ptr);
}
这个函数无法通过编译,因为类变量是 private,无法通过对象访问。所以要把它变成成员函数。
class person
{
public:
void my_swap(person& p)
{
swap(this->ptr, p.ptr);
}
...
};
如果你觉得 p1.my_swap(p2) 的调用形式太low了,你可以设计一个non-member 函数(如果是在同一个命名空间那就再好不过了),实现swap(p1, p2),这里不做演示。你还可以特化 std 里的 swap 函数:
namespace std
{
template<>
void swap<person> (person& p1, person& p2)
{
p1.my_swap(p2);
}
}
C++允许对模板类进行偏特化,不允许对模板函数进行偏特化。
条款26:尽可能延后变量定义式的出现时间
- 尽可能延后变量定义式的出现时间。这样可增加程序的清晰度并改善程序效率
- 你不应该只延后变量的定义,指导非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
- 如果一个变量只在循环内使用,那么把它定义于循环外并每次给他赋值比较好,还是把它定义于循环内较好呢?这个问题值得思考。
通过 default 构造函数构建一个对象然后对它赋值比直接在构造时指定初值效率差
条款27:尽量少做转型动作
请记住:
- 如果可以,尽量避免转型,特别是在注重效率的代码中使用dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
- 如果转型是必要的,试着将它隐藏域某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。
- 宁可使用C++-style转型,不要使用旧式转型。前者很容易辨别出来,而且也比较有着分门别类的职掌。
1,旧式类型转换
(T) expression
T(expression)
2,新式类型转换
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
3,const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++-style转型操作符;reinterpret_cast意图执行低级转型,实际动作由编译器决定,这也就意味着它不可移植,很少使用;static_cast用来强迫隐式转换,一般用在内置类型的一些转换。
4,dynamic_cast主要用于“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型,它是唯一无法由旧式语法执行的动作。通常一个继承体系下,如果有虚函数,一般不用dynamic_cast。一般用在那种不能设置为虚函数的函数调用。dynamic_cast执行速度慢,是因为他会使用RTTI机制,使用type_id找类型,耗时。
virtual void onResize() {
static_cast<Window>(*this).onResize();
}
这段代码,static_cast中传值方法是值传递,值传递肯定要一个copy的动作,所以会产生一个副本,所以,修改这个副本,不会影响this指针。
应该这样使用:
virtual void onResize() {
Window::onResize();
}
条款28:避免返回 handles 指向对象内部成分
reference,指针,迭代器都是所谓的 handle ,返回一个代表对象内部数据的 handle 会降低对象封装性,还可能导致”虽然调用 const 成员函数却造成对象状态被更改“
请记住:
避免使用 handles(包括 references,指针,迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const ,并将发生”虚吊的号码牌“可能性降至最低
条款29:为“异常安全”而努力是值得的
当异常被抛出时,带有异常安全性的函数会:
- 不泄露任何资源
- 不允许数据败坏
异常安全函数(Exception-safe functions) 提供以下三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
- 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态
- 不抛掷保证,承诺绝不抛出异常。因为它们总是能够完成它们原先承诺的功能。
copy-and-swap:为你打算修改的对象做出一份副本,然后在副本身上进行一切必要修改,然后将修改后的数据和原件置换。
请记住:
- 异常安全函数即使发生异常也不会泄露资源或者允许任何数据额结构破坏。这样的函数区分为三种可能得保证:基本型、强烈型、不抛异常型
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者,短板效应
条款30:透彻了解 inline 的里里外外
请记住;
- 将大多数inline限制在小型、被频繁调用的函数身上。这可使得日后的调试过程和二进制升级更容易,也可使潜在代码膨胀问题最小化,使程序的速度提升机会最大化
- 不要只因为function template出现在头文件,就将他声明为inline
- 编译器如果要对函数做inline,那么它必须知道inline函数长什么样子。这句话说明,inline得在头文件中,发生在编译期。
- 虚函数、构造、析构函数不适合inline。
- inline会引发的问题有:代码膨胀,修改inline导致全重编译。
- 记住中第二点说:不要只因为function template出现在头文件,就将他声明为inline。因为,template本身就会代码膨胀,inline也会导致这个问题,如果两个在一起,那就是膨胀+膨胀。
参考文章:EFFECTIVE C++ (万字详解)(一)-CSDN博客
《Effective C++》笔记_effectivec++ 笔记-CSDN博客