【书摘】C++ Primer

时隔这么多年又拿起了这本书,简单挑选一些内容作为温故知新的笔记。


更新历史

  • 2017.01.29: 完成初稿

C++ 并没有直接定义进行输入或输出的任何语句,这种功能是由标准库提供的。

在写 C++ 程序时,大部分出现空格符的地方可用换行符代替。这条规则的一个例外是字符串字面值中的空格符不能用换行符代替。另一个例外是空格符不允许出现在预处理指示中。

类机制是 C++ 最重要的特征之一。

标准库的头文件用尖括号,非标准库的头文件用双引号括起来。

字符类型有两种:charwchar_t,其中 wchar_t 用于扩展字符集,比如汉字和日语,这些字符集中的一些字符不能用单个 char 表示。

程序不应该依赖未定义行为。

在 C++ 中理解『初始化不是赋值』是必要的。初始化值指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。

直接初始化语法更灵活且效率更高。

通常把一个对象定义在它首次使用的地方是一个很好的方法。

非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须显式地制定它为 extern。

当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。

非 const 引用只能绑定到与该引用同类型的对象。const 引用则可以绑定到不同但相关的类型的对象或绑定到右值。

typedef 通常被用于以下三种目的:

  • 为了隐藏特定类型的实现,强调使用
  • 简化负责的类型定义,使其更易理解
  • 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确

编程新手经常会忘记类定义后面的分号,这是个很普遍的错误!

用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而 class 的成员为 private。

头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。

通常,头文件中应该只定义确实必要的东西。请养成这个好习惯。

任何存储 string 的 size 操作结果的变量必须为 string::size_type 类型。特别重要的是,不要把 size 的返回值赋给一个 int 变量。

vector 不是一种数据类型,而只是一个类模板,可以用来定义任意多种数据类型。vector 类型的每一种都指定了其保存元素的类型。因此,vector<int>vector<string> 都是数据类型。

现代 C++ 程序应尽量使用 vector 和迭代器类型,而避免使用低级的数组和指针。设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。

除非显式地提供元素初值,否则内置类型的局部数组的元素没有初始化。此时,除了给元素赋值外,其他使用这些元素的操作没有定义。

一些编译器允许将数组赋值作为编译器扩展,但是如果希望编写的程序能在不同的编译器上运行,则应该避免使用像数组赋值这类依赖于编译器的非标准功能。

指针和数组容易产生不可预料的错误。其中一部分是概念上的问题:指针用于低级操作,容易产生与繁琐细节相关的错误。其他错误则源于使用指针的语法规则,特别是声明指针的语法。

很多运行时错误都源于使用了未初始化的指针。

如果可能的话,除非所指向的对象已经存在,否则不要先定义指针,这样可避免定义一个未初始化的指针。如果必须分开定义指针和其所指向的对象,则将指针初始化为 0,因为编译器可检测出 0 值的指针,程序可判断该指针并未指向一个对象。

指针的算术操作只有在原指针和计算出来的新指针都指向同一个数组的元素,或指向该数组存储空间的下一单元才是合法的。如果指针指向一对象,我们还可以在指针上加 1 从而获取指向相邻的下一个对象的指针。

C++ 允许计算数组或对象的超出末端的地址,但不允许对此地址进行解引用操作。而计算数组超出末端位置之后或数组首地址之前的地址都是不合法的。

不能使用指向 const 对象的指针修改基础对象,然而如果该指针指向的是一个非 const 对象,可用其他方法修改其所指的对象。

尽管 C++ 支持 C 风格字符串,但不应该在 C++ 程序中使用这个类型。C 风格字符串常常带来许多错误,是导致大量安全问题的根源。

对于动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。

对于位操作符,由于系统不能确保如何处理其操作数的符号位,所以强烈建议使用 unsigned 整型操作数。

一般而言,标准库提供的 bitset 操作更直接、更容易阅读和书写、正确使用的可能性更高。而且,bitset 对象的大小不受 unsigned 数的位数限制。通常来说,bitset 优于整型数据的低级直接位操作。

一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清除地表明指针不再指向任何对象。

虽然有时候确实需要强制类型转换,但是它们本质上是非常危险的。

使用空语句时应该加上注释,以便任何读这段代码的人都知道该语句是有意省略的。

无关的空语句并非总是无害的。

故意省略 case 后面的 break 语句是很罕见的,因此应该提供一些注释说明其逻辑。

在循环条件中定义的变量在每次循环里都要经历创建和撤销的过程。

如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为 const 引用。

通常,函数不应该有 vector 或其他标准库容器类型的形参。调用含有普通的非引用 vector 形参的函数将会复制 vector 的每一个元素。C++ 程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器。

理解返回引用至关重要的是:千万不能返回局部变量的引用。

定义函数的源文件应该包含声明该函数的头文件。

在 C++ 中,名字查找发生在类型检查之前。

如果指向函数的指针没有初始化,或者具有 0 值,则该指针不能在函数调用中使用。

为了使程序更清晰、简短,容器类型最常用的构造函数是默认构造函数。在大多数的程序中,使用默认构造函数能达到最佳运行时性能,并且使容器更容易使用。

使用迭代器编写程序时,必须留意哪些操作会式迭代器失效。使用无效迭代器将会导致严重的运行时错误。

任何 insert 或 push 操作都可能导致迭代器失效。当编写循环将元素插入到 vector 或 deque 容器中时,程序必须确保迭代器在每次循环后都得到更新。

不要存储 end 操作返回的迭代器。添加或删除 deque 或 vector 容器内的元素都会导致存储的迭代器失效。

最简单地来说,类就是定义了一个新的类型和一个新的作用域。

按照与成员声明一致的次序编写构造函数初始化列表十个好主意。此外、尽可能避免使用成员来初始化其他成员。

我们更喜欢使用默认实参,因为它减少代码重复。

如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。

通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。

定义和使用构造函数几乎总是较好的。当我们为自己定义的类型提供一个默认构造函数时,允许编译器自动运行那个构造函数,以保证每个类对象在初次使用之前正确地初始化。

通常,将友元声明成组地放在类定义的开始或结尾是个好主意。

保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类的非内联成员函数定义的文件中。

为了防止复制,类必须显式声明其复制构造函数为 private。

一般来说,最好显式或隐式定义默认构造函数和复制构造函数。只有不存在其他构造函数时才合成默认构造函数。如果定义类复制构造函数。也必须定义默认构造函数。

当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。

如果类需要析构函数,则它也需要赋值操作符和复制构造函数,这是一个有用的经验法则。

基类通常应将派生类需要重定义的任意函数定义为虚函数。

一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,但不是必须这样做。

只有成员函数中的代码才应该使用作用域操作符覆盖虚函数机制。

通过异常我们能够将问题的检测和问题的解决分离,这样程序的问题检测部分可以不必了解如何处理问题。

可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源。

定义多个不相关类型的命名空间应该使用分离的文件,表示该命名空间定义的每个类型。

using 指示不声明命名空间成员名字的别名,相反,它具有将命名空间成员提升到包含命名空间本身和 using 指示的最近作用域的效果。

using 指示有用的一种情况是,用在命名空间本身的实现文件中。