笔记:《C++ Primer》(第5版)

好习惯

1.   通常用cerr来输出警告和错误消息。

2.   打印语句应保证“一直”刷新流。

3.   注释掉代码的最好的方式是用单行注释方式注释掉代码段的每一行(所以用自动注释时最好连用两次)。

4.   按照报告的顺序来逐个修正错误。(但有时前面没有错,后面错了能导致前面也标错。)

5.   一旦选择了一种格式风格,就要坚持使用。

6.   当明确知晓数值不可能为负时,选用无符号类型。

7.   使用int执行整数运算。如果你的数值超过了int的表示范围,选用long long。

8.   在算数表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char。

9.   执行浮点数运算选用double。

10. 避免无法预知和依赖于实现环境的行为。

11. 切勿混用带符号类型和无符号类型。

12. 当使用一个长整型字面值时,请使用大写字母L来标记,因为小写字母l和数字1太容易混淆了。

13. 建议初始化每一个内置类型的变量。虽然并非必须这么做,但如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。

14. 标识符要能体现实际含义。

15. 变量名一般用小写字母。

16. 用户自定义的类名一般以大写字母开头。

17. 如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan。

18. 当你第一次使用变量时再定义它。

19. 如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

20. 建议初始化所有指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0。

21. 将*(或是&)与变量名连在一起。

22. 不要在程序中使用goto语句,因为它使得程序既难理解又难修改。

23. 最好不要把对象的定义和类的定义放在一起。

24. 类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。

25. 使用C++版本的C标准库头文件。

26. 下标必须大于等于0而小于字符串的size()的值。一种简便易行的方法是,总是设下标的类型为string::size_type。此时,代码只需保证下标小于size()的值就可以了。

27. 在定义vector对象的时候设定其大小没什么必要。只有一种例外情况,就是所有元素的值都一样。

28. 确保下标合法的一种有效手段就是尽可能使用范围for语句。

29. 只要我们养成使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。

30. 一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他的编译器上无法正常工作。

31. 尽量使用标准库类型而非数组:现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;尽量使用string,避免使用C风格的基于数组的字符串。

32. 以下两条经验准则对书写复合表达式有益:

(1)  拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑的要求。

(2)  如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。

第2条规则有一个重要例外,当改变运算对象的子表达式本身就是另外一个子表达式的运算对象时该规则无效。

33. 除非必须,否则不用递增递减运算符的后置版本(而应该用前置版本)。

34. 简洁。如书写cout << *iter++ << endl;而不是cout << *iter << endl;++iter;。

35. 随着条件运算嵌套层数的增加,代码的可读性急剧下降。条件运算的嵌套最好别超过两到三层。

36. 强烈建议仅将位运算符用于处理无符号类型,因为关于符号位如何处理没有明确的规定。

37. 建议:避免强制类型转换。这个建议对于reinterpret_cast尤其适用。在有重载函数的上下文中const_cast无可厚非,但是在其他情况下使用const_cast也就意味着程序存在某种设计缺陷。其他强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换值的作用域,并且记录对相关类型的所有假定。

38. 与命名的强制类型转换相比,旧式的强制类型转换从表现形式上来说不那么清晰明了,容易被看漏,所以一旦转换过程出现问题,追踪起来也更加困难。

39. 使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。

40. 有些编码风格要求在if或else之后必须写上花括号(对while和for语句的循环体两端也有同样的要求)。

41. 为了安全起见,一般不要省略case分支最后的break语句。

42. 即使不准备在default标签下做任何工作,定义一个default标签也是有用的。其目的在于告诉读者,我们已经考虑到了默认的情况,只是目前什么也没做。

43. 事实上,在函数的声明中经常省略形参的名字。尽管如此,写上形参的名字还是有用处的,它可以帮助使用者更好地理解函数的功能。

44. 我们建议变量在头文件中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义(否则函数可能会被重复定义而出错;但是函数可以定义在头文件的类内部,因为会隐式内联;如果非要在头文件内、类外定义函数,就定义成内联函数)。

45. 把函数的声明直接放在使用该函数的源文件中可能会很烦琐而且容易出错。相反,如果把函数声明放在头文件中,就能确保同一函数的所有声明保持一致。而且一旦我们想改变函数的接口,只需改变一条声明即可。

46. 熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参替代指针。

47. 参数传递:使用引用避免拷贝。拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。

48. 如果函数无须改变引用形参的值,最好将其声明为常量引用。

49. 尽管函数重载能在一定程度上减轻我们为函数起名字、记名字的负担,但是最好只重载那些确实非常相似的操作。有些情况下,给函数起不同的名字能使得程序更易理解。

50. 一般来说,将函数声明置于局部作用域内不是一个明智的选择。

51. 现在的C++程序最好使用nullprt,同时尽量避免使用NULL。

52. 在一些简单的应用程序中,类的用户和类的设计者常常是同一个人。尽管如此,还是最好把角色区分开来。当我们设计类的接口时,应该考虑如何才能使得类易于使用;当我们使用类时,不应该顾及类的实现机理。

53. 把this设置为指向常量的指针有助于提高函数的灵活性。

54. 构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。

55. 出于统一编程风格的考虑,当我们希望定义的类的所有成员是public的时,使用struct;反之,如果希望成员是private的,使用class。

56. 一般来说,最好在类定义的开始或结束前的位置集中声明友元。

57. 虽然我们无须在声明和定义的地方同时说明inline,但这么做其实是合法的。不过,最好只在类外部定义的地方说明inline,这样可以使类更容易理解。

58. 和我们在头文件中定义inline函数的原因一样,inline成员函数也应该与相应的类定义在同一个头文件中。

59. 对于公共代码使用私有功能函数。

60. 类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

61. 建议的写法:不要把成员名字作为参数或其他局部变量使用。

62. 建议读者养成使用构造函数初始值(列表而不是通过函数体赋值)的习惯,这样能避免某些意想不到的编译错误。

63. 最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。

64. 使用explicit的优点是避免因隐式类型转换而带来意想不到的错误,缺点是当用户的确需要这样的类类型转换时,不得不使用略显烦琐的方式来实现。

65. 要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。

66. 文件输入输出:因为调用打开文件可能失败,进行打开文件是否成功的检测通常是一个好习惯。

67. 现代C++程序应该使用标准库容器,而不是更原始的数据结构,如内置数组。

68. 确定使用哪种顺序容器:通常,使用vector是最好的选择,除非你有很好的理由选择其他容器。

69. 当不需要写访问时,应使用cbegin和cend。

70. 统一使用非成员版本的swap是一个好习惯。

71. 由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确重新定位迭代器。这个建议对vector、string和deque尤为重要。

72. 如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器。

73. 对于只读取而不改变元素的泛型算法,通常最好使用cbegin()和cend()。但是,如果你计划使用算法返回的迭代器来改变元素的值,就需要使用begin()和end()的结果作为参数。

74. 建议:尽量保存lambda的变量捕获简单化。一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针、迭代器或引用。

75. 新的C++程序应该使用bind而不是bind1st和bind2nd。

76. 对于list,应该优先使用成员函数版本的算法而不是通用算法。

77. 我们通常不对关联容器使用泛型算法(关联容器有专用算法)。在实际编程中,如果我们真要对一个关联容器使用算法,要么是将它当作一个源序列,要么当作一个目的位置。

78. 出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意(12.1.2)。

79. 坚持只使用智能指针,就可以避免使用new和delete管理动态内存的三个常见问题:忘记delete内存,使用已经释放掉的对象,同一块内存释放两次。

80. 不要混合使用普通指针和智能指针:当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该使用内置指针来访问shared_ptr所指向的内存了。

81. get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

82. 希望阻止拷贝的类应该使用=delete来定义它们自己的拷贝构造函数和拷贝赋值运算符(说明赋值要先拷贝),而不应该将它们声明为private的。

83. 尽管从语法上来说我们可以在派生类的构造函数体内给它的公有或受保护的基类成员赋值,但是最好不要这么做。和使用基类的其他场合一样,派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员。

84. 如果我们希望能覆盖基类中的虚函数,就使用override关键字。这样,如果实际上并未覆盖,编译器就会报错。

85. 如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

86. 一个私有派生的类最好显式地将private声明出来,而不要仅仅依赖于默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会。

87. 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

88. 看起来用关键字typename来指定模板类型参数比用class更为直观,而且,typename更清楚地指出随后的名字是一个类型名。

89. C++程序员喜欢用!=而不喜欢用<。

 

 

 

常规笔记

1.   对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。雷声大,如果一个操作需要很多语句才能完成,通常使用函数更好。

2.   通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。

3.   通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

4.   输入运算符必须处理输入可能失败的情况(为了确保输入失败时对象处于正确状态),而输出运算符不需要。

5.   当读取操作发生错误时,输入运算符应该负责从错误中恢复。

6.   拷贝构造函数永远都会合成默认的(但可能合成的默认的是删除的)。

7.   lanmada是通过匿名的函数对象(如果类定义了调用运算符,则该类的对象称作函数对象(function object))来实现的,因此我们可以把lamada看作是对函数对象在使用方式上进行的简化。

当代码需要一个简单的函数,并且这个函数并不会在其他地方使用时,就可以使用lambda来实现。但如果这个函数需要多次使用,并且它需要保存某些状态的话,使用函数对象更合适一些。

8.   如果在调用重载函数时我们需要使用构造函数或者强制类型转换来改变实参的类型,则这通常意味着程序的设计存在不足。

9.   一条声明语句的目的时令程序知晓某个名字的存在以及改名字表示一个什么样的实体。一个文件如果像使用别处定义的名字则必须包含对那个名字的声明。

PS:定义负责创建与名字关联的实体。

10. 之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分,而基类的引用或指针可以绑定到该基类部分上(从而绑定到相应的派生类对象)。当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

11. 只有当派生类公有地继承基类时,用户代码才能使用派生类向基类的转换。无论派生类以什么方式继承基类,派生类的成员函数和友元都能使用派生类向基类的转换。

已错的易错点

1.   const auto 声明的是顶层const,而c.cbegin()返回的是不能更改所指对象的迭代器。

2.   函数的返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

3.   声明函数指针的时候,*和函数名两端的括号必不可少。如果不写这对括号,则*修饰的是返回类型。

4.   如果定义类的非成员成员函数时用到类的数据成员,或定义类的成员函数时用到别的类的数据成员,就要记得在数据成员前加“类类型的变量.”

5.   那些令人摸不着头脑的错误,造成它们的原因往往时低级错误:比如括号少了半边,关键字拼写错误。

6.   this,指向调用者。

7.   需要自定义的拷贝构造函数的类也需要自定义的拷贝赋值运算符,反之亦然(PS:拷贝构造函数(构造函数:控制类的对象(对象:内存的一块区域,具有某种类型,变量是命名了的对象)的初始化过程的类的特殊的成员函数)控制拷贝初始化,拷贝初始化在用=定义变量时、在将一个对象作为实参传递给一个非引用类型的形参时、在从一个返回类型为非引用类型的函数返回一个对象、在初始化标准库容器或是调用其insert或push(此时还会发生析构)成员时会发生(书上所谓的直接初始化,至少像Employee a(b)这种,也会发生);拷贝赋值运算符控制赋值(赋值:抹去一个对象的当前值,用一个新值取代之)。

8.   类声明,解决C++两个类互相包含问题:

在构造自己的类时,有可能会碰到两个类之间的相互引用问题,例如:定义了类A类B,A中使用了B定义的类型,B中也使用了A定义的类型。

在这种情况下,想想可以有A.B.A.B.A.B.…………,很有点子子孙孙无穷尽之状,那么我们的机器也无法承受。最主要的还是这种关系很难存在,也很难管理。这种定义方式类同程序中的死循环。所以,一般来说,两者的定义,至少有一方是使用指针,或者两者都使用指针,但是决不能两者都定义实体对象。

言归正传,那么,在定义时因为相互引用肯定会需要相互包含头文件,如果仅仅只是在各自的头文件中包含对方的头文件,是会链接失败的。这样的包含方式可能会造成编译器有错误提示:A.h文件中使用了未知类型B。

怎么办?

一般的做法是:两个类的头文件之中,选一个包含另一个类的头文件,但另一个头文件中只能采用前向声明,而在实现文件(*.cpp)中包含头文件。或者(我发现的)把两个类写在一个头文件中(写在前面的类,需要使用后面的类的时候要声明,即前向声明,并承担相应的限制,见PS1)(相当于只是把一个头文件中的#include “*.h”换成了另一个头文件的内容而得到的头文件),然后把以不完全类型作为参数或者返回类型的函数的定义放在两个类的定义之后就可以了。综上所述,以不完全类型作为参数或者返回类型的函数要么,定义在头文件中,要么定义在相应的类的定义之后。注意:内置类型、复合类型和标准库类型当然都是完全类型。

PS1:不完全类型(声明之后定义之前的类类型)只能在非常有限的情景下使用:只可以定义指向这种类型的指针或引用,也可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数(因为编译器无法了解不完全类型的对象需要多少存储空间)。

PS2:预处理器是在编译之前执行的一段程序,预处理功能之一是#include(当预处理器看到#include标记时就会用指定的头文件的内容代替#include),即先执行头文件后执行源文件。因此,虽然不能在头文件中定义以不完全类型作为参数或者返回类型的函数,但是,如果在头文件中只声明而在源文件中定义就没问题。

PS3:编译(compile):利用编译程序从源语言编写的源程序产生目标程序(即*.obj文件,是二进制的文件)。

链接(link):将生成的*.obj文件与库文件*.lib等文件链接成可执行文件(*.exe文件)。

一个现代编译器的主要工作流程如下:

源程序(source code)→ 预处理器(preprocessor)→ 编译器(compiler)→ 汇编程序(assembler)→ 目标程序(object code)→链接器(Linker)→ 可执行程序(executables)

9.   内联的构造函数(包括拷贝构造函数)、拷贝赋值运算符和析构函数(至少这4个)都不能定义在源文件。为了避免麻烦,把内联的都定义在头文件里,定义在源文件的就都不内联(只用inline修饰声明也不行)。

错误提示:无法解析的外部命令/符号。

10.  以下代码会出错,因为(我猜)删除了元素导致迭代器失效,进而导致不能遍历了(遍历实际上时通过迭代器)。

set<int> is{ 1,2,3 };

for (auto a : is)

{

    is.erase(a);

}

    PS:错误提示是map/set iterator not incrementable

11. 重载下标运算符时注意返回类型,不是相应的类类型。

12. 括号,两边同时写。

13. 类的某个成员函数可直接调用其他成员函数而不用指出是谁调用(this指针隐式调用),就像访问类的数据成员一样。

14. 把一个类声明成友元之前要声明这个类,把一个函数声明成友元之前似乎不要声明这个函数。

15. if条件语句:如果前面的分支可能改变判断语句的结果,则必须用if-else结构而不能用两个并列的if。

技巧

1.   如果把变量定义为全局变量,就便于在函数间共享。当然也可以定义为局部变量,通过函数参数传递。

2.   关系运算符直接就能返回bool值了,不必画蛇添足加上条件运算符。

程序设计

1.   开始一个程序的设计的一种好方法时列出程序的操作,即从需求入手。了解需要哪些操作会帮助我们分析处需要什么样的数据结构。

2.   当我们设计一个类时,在真正实现成员之前先编写程序使用这个类,是一种非常有用的方法。通过这种方法,可以看到类是否具有我们所需要的操作。

3.   当你开始设计一个类时,首先应该考虑的是这个类将提供哪些操作。在确定类需要哪些操作之后,才能思考到底应该把每个类操作设成普通函数还是重载的运算符。如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符。我们的建议是:只有当操作的含义对于用户来说清晰明了时才使用运算符。如果用户对运算符可能有几种不同的理解,则使用这样的运算符将产生二义性。

OOP

1.   当我们令一个类公有地继承另一个类时,派生类应当反映与基类的“是一种(Is A)”关系。在设计良好的类体系当中,公有派生类的对象应该可以用在任何需要基类对象的地方。

泛型编程

1.   模板程序应该尽量减少对实参类型的要求。编写泛型代码的两个重要原则:

l  模板中的函数参数是const的引用。

l  函数体中的条件判断仅使用<比较运算。

2.   模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用的所有名字的声明。模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

结构化程序设计

1.   基本思想是采用"自顶向下,逐步求精"的程序设计方法和"单入口单出口"(即只用顺序、选择和循环三种基本程序结构,不使用goto)的控制结构。

2.   模块化。

猜你喜欢

转载自www.cnblogs.com/huyue/p/9164092.html
今日推荐