C++ Primer CH1 Getting Started

最简单的 C++ 程序

int main()
{
    return 0;
}

所有 C++ 程序都包含一个以上的函数,其中最重要的 main 函数,操作系统通过调用 main 函数来运行程序。函数包含四个元素:返回类型、函数名字、形参列表、函数体。main 函数被指定返回 int 类型,int 类型是内置类型,也就是语言本身提供的类型。通常 main 函数返回 0 表示程序运行正常,返回非 0 值表示遇到错误。函数体是以 { 开头的语句块。函数体中的 return 语句将终止函数的执行,并返回一个值给调用者,返回的值的类型必须与函数的返回类型一致。对于返回类型为 void 的函数,return; 将直接将控制权返回调用者,而执行到函数末尾也将隐式的返回。

最开始学习 C++ 语言时闹出过一个笑话,当时写了一小段代码,怎么都编译不通过,当时就慌了。找了好几天都找不出问题,还认为自己根本学不好编程。后来问了素未相识的朋友,人家一下就指出是我的 main 写成了 mian 了。直到现在我还印象非常深刻,当时简直是要怀疑人生了。

C++ 中有需要地方需要用到分号,而分号也非常容易被忽略。可能仅仅因为缺少一个分号,编译器就会报出一大堆的错误。在这些细节上不能马虎,当然坑踩的多了自然就会小心。

上面所讲的函数的要素如今在任何类 C 的语言中都一样,只要了解过任何一门此类语言肯定能够知道我在将什么。

关键概念:类型

类型在任何一门语言中都具有极其重要的地位,你不会见过一门没有类型的语言。差异在于有的语言允许自定义类型,有的语言只能使用语言内置的类型,有的语言允许变量是动态类型,有的语言要求变量只能是固定的类型。类型定义了数据内容和作用于这些数据上的操作。我们定义的数据保存在变量中,而所有变量都必须要一个类型。

书中提到在学习语言时不应该过于使用的 IDE,因为 IDE 本身需要花大量的时间来学习如何使用。如果通过命令行去执行 C++ 的编译器来编译简单的程序,就可以将注意力集中在 C++ 语言本身。C++ 源文件的后缀名最常用的有:.cc.cxx.cpp.cp.C

如何编译程序请参考 GCC Basic ,为了开启 C++11 特性需要使用 -std=c++0x 编译选项。

输入输出

C++ 语言本身是不包含输入输出的。C++ 跟很多别的语言一样通过提供一个 IO 库来实现输入输出。本书中使用的输入输出库时 iostream 库,iostream 库有两个基本的输入输出类型,istream 用于输入,ostream 用于输出。流(stream)是从 IO 设备读取或写入的一连串字符。术语 stream 的含义就是字符以序列(sequence)的形式被产生和消费。C++ IO 库定义了 istream 类型的标准输入对象 cin,ostream 类型的标准输出对象 cout,另外两个 ostream 对象 cerr 表示标准误,clog 产生程序执行的通用信息。一般 clog 用的比较少。标准输入/输出从命令行窗口读取和写入,是最基本的输入输出方式。

#include 是预编译指令,通常用来包含程序需要的头文件,通常 #include 放在源文件的顶部。

std::cout << "Enter two numbers:" << std::endl;

C++ 中的表达式由至少一个操作数和一个操作符组成,通常会产生一个结果。表达式和表达式可以组成新的表达式,所以说表达式的定义是递归的。<< 操作符有两个操作数,左边的是 ostream 类型对象,右边的是需要输出的值,这个表达式的结果是左边的值,也就是 std::cout 对象本身。由于这样的设计多个 << 串行,这种操作也称之为流式操作(flow operation),操作返回操作对象本身。

std::endl 是一个特殊的值,称之为操纵器(manipulator),这种数据可以对流本身进行设置,会影响流的状态。写入 endl 将会插入一个换行符到流中,并且刷新流缓冲。刷新缓冲将保证写入的数据被真正写入到输出流中,而不是暂存在内存中。其实呢,刷新流只是将写的数据提交给操作系统,操作系统本身也有一个文件缓冲,只有当文件缓冲积累到一定程度时才会真正写入到文件中,这是对文件系统的优化,避免每次写入都对磁盘进行操作。要了解这些细节需要参考相关的书籍。

在写代码过程中我们经常会在关键点埋一些调试打印语句,这些打印语句应当经常刷新缓冲,因为一旦程序 crash 掉时,运行时几乎会立即停止,保留在缓冲中的数据和可能会缺失,这样就很可能根本找不到程序在哪里导致了异常。

C++ 有更令人头疼的地方,因为 C++ 强调了程序员的责任,并且将资源管理交给了程序员。因此很多 crash 的地方与引发错误的地方相距非常远,要发现这种 bug 非常困难。C++ 发展出了很多好的设计程序的方法来应付这种情况,比如防御性编程。虽然防御性编程对 Java 等其它高级语言来说很重要,但相对来说对于 C++ 语言更为重要。有时候你根本不知道错误出现在哪里,而重现有极其困难,此时才是最令人烦恼的时刻。

C++ 对 C 的一个重要改进就是名称空间(namespace),C 中所有的外部可见名字都在同一个名称空间中,所以 C 发展出了一个简单有效的方式来避免名称冲突,就在以 <lib>_ 为前缀,lib 就是你开发的库名字。名称空间像作用域一样将名字限制住,不同名称空间中的相同名字不会冲突。所有的标准库的名字都定义在 std 名称空间中。要使用名称空间中的名字必须加上名称限定符(:: operator),如 std::cout 引用标准库中的 cout 名字。

int v1 = 0, v2 = 0;

这条语句涉及到变量的定义初始化,C++ 中的初始化比 C 复杂不少。C++ 的一大目标就是使得自定义的类型与内置类型的操作方式统一,C++ 在这方面做了许多努力同时这些方面的确也是很复杂的。当定义一个变量时同时提供一个初始的值(这里是 0)就将变量给初始化了。

std::cin >> v1 >> v2;

>> 操作符与上面的 << 操作符很类似,这里以 istream 类型对象为左操作数,以 v1 和 v2 变量为右操作数,功能是读取用户的输入并将值保存在 v1 和 v2 变量中。这个操作符也返回左操作数,所以可以将多个 >> 串在一起组成一个单一语句。当然分开写也是可以的。

std::cout << "The sum of " << v1 << " and " << v2
          << " is " << v1+v2 << std::endl;

这个语句与输出提示的语句一样,唯一值得注意的是现在 << 处理了两种不同类型:字符串类型和 int 类型。C++ 的库函数定义很多重载操作符来应对不同类型的操作数。

1.3 注释

注释是程序员对程序语句的解释,注释会被编译器完全过滤掉,所以对程序运行本身没什么影响。注释的主要功能是提供给程序员看的,可能是别的 review code 的同事也可能就是作者自己。注释是人与人的一种沟通方式。注释通常用来解释一个复杂的算法,解释变量的含义以及解释某段晦涩代码的执行过程。但是注释不易过多,过度的注释反而妨碍阅读者的视线和思维,真正好的代码应该是自明的,从阅读代码中就可以清晰明白代码的意图,如果代码需要注释才能让人看懂通常意味着代码很可能隐藏着 bug 。而且必须保证注释的含义与代码真实的含义一致,否则就是一种误导,必然会招致阅读者的咒骂,所以在更新代码时务必更新注释。要么干脆就把代码写的足够简单,从而省略掉注释。

C++ 中有两种注释:单行(//) 注释和跨行注释(/**/)。单行注释将在行末结束,其中可以包含任何字符包括 // 符号本身。跨行注释是从 C 中继承过来的,这种跨行注释内容是 /* 和对应匹配的 */ 间的内容,内容中不能包含结束符,否则就是语法错误。这种注释最大的好处是可包含换行符,因而也成为块注释符。很多跨行注释都会在行首写一个前导 * 字符来标示多行注释。开始时 C 语言只支持这种块注释,后来受到了 C++ 的影响也开始支持单行注释。这里抠一个细节,块注释可以放在任何空白符可以放的地方,这些地方不包括字符串内部、变量名内部、别的注释内部。而单行注释将会导致行的后半段全部变成注释,所以通常放在行尾或占据一整行。

上面的细节并不重要,重要的领会注释的功能以及恰当使用注释的哲学。

1.4 控制流

一般代码的执行都是顺序的,而控制流可以改变程序执行的顺序。只有极少的程序不需要控制流。控制流包括循环、分支、返回、跳出等。下面会简单介绍几个控制流语句。

while (condition)
    statement

while 循环将在给定条件为 true 时重复执行,直到条件变为 false。所谓条件(condition)是一个表达式,这个表达式会产生一个 true 或 false 的值。while 循环会先测试条件是否为真,并在条件为真的情况下执行语句块,然后再测试条件,在决定是继续执行还是结束循环。语句块由一条或多条在大括号中的语句组成。语句块也是语句,可以放在任何语句可以存在的地方。通常,程序会在语句块中改变条件测试的变量的状态。

for (init-statement; condition; update-expresion)
    statement

for 循环将控制变量的初始化、测试和更新放在了同一个位置,方便程序员查看修改。for 循环也用来遍历容器对象,这是绝大部分应用程序都会用到的特性。因而,在实际运用中 for 循环会比 while 循环运用的更多。for 语句由控制部分和主体部分组成。其中控制部分有三部分:初始化语句、条件语句和更新表达式。初始化语句中定义的控制变量只存在于 for 循环中,出了循环这个控制变量将不会存在。初始化语句只会在开始执行 for 循环时执行一次,条件将在每次将要进入主体语句前测试,测试如果为 true 将执行语句,否则将直接退出 for 循环。每次执行完主体语句之后就执行更新表达式来更新控制变量。然后接着重复测试条件与执行主体语句,直到最后测试条件为 false 。C++ 并没有规定这三个部分需要包含什么特定语句,其实你可以放任何表达式在里边,甚至所有部分留空都可以,如果留空将执行 forever loop 。

while (std::cin >> value)

当我们将 istream 对象作为条件时,效果是测试流的状态。如果流没有遇到任何错误或者到达文件末尾(end-of-file),测试将返回 true 否则将返回 false 。以这种方式测试 istream 实质上是在 istream 类中设置到 bool 内置类型的转换函数。这个函数是隐式调用的,具体在后面章节会讲到。

编译器的工作

编译器的工作是将源文件代码重新生成为操作系统可以识别的格式,在 Linux 下是 ELF 格式。编译器不能识别程序作者的意图,但是可以发现程序的语法错误、类型匹配错误以及声明错误。

  • 语法错误:任何该有分号的地方没有分号,括号不匹配之类的错误;
  • 类型匹配错误:给 int 类型变量赋值字符串类型值之类的错误;
  • 声明错误:没有声明就使用变量或者重复声明变量;

编译很可能在程序中有错误的时候产生非常的错误信息,这些信息过多几乎可以肯定没办法定位到所有错误。正确的做法是从上到下进行修复,每次修复一个就重新编译一下,然后看看是否还有错误。这个循环叫 edit-compile-debug .

Warning: 在 C++ 中 = 号用于赋值,== 用于比较。两个操作符都可以放在条件表达式中。如果在条件语句中混淆使用了 === 我相信你会非常头疼的,而且这种 bug 还非常常见。

程序风格

程序风格是一种极具个人特色的东西,而且特别容易引起程序员之间的争执。程序员为了该用空格还是 Tab 字符来缩进都能争吵一上午。如果用空格的话还可以为用 2 个还是 4 个空格争吵。总之是其乐无穷。但是所有优秀的程序都必定具有统一而漂亮的程序风格。如果你还没有形成一种风格,那么模仿其中一种是一个很好的选择。书中给出几个建议。函数体的 { 可以单独一行。而云风的代码中返回值是单独一行的。C# 将所有 { 单独放一行。具体怎么做取决于你自己,选择一个比较通用的风格,然后一直坚持就行。也请包容其它的程序风格。

1.5 类的介绍

在 C++ 中通过定义 来定义用户自定义数据结构。类定义了一个类型以及与类型相关的操作,这是 C++ 的数据封装和抽象的核心。类是 C++ 最重要的特性之一,事实上设计 C++ 的一个主要目的就是使得类类型表现得跟内置类型一样自然,写法上一致并且行为上也一致。这个 Java 很不一样,Java 没有以表现一致为目的而是为了高效进行对象资源管理。相对来说 Java 会安全的更多。

类包括名字、结构和操作。通常结构会被放在头文件中,而操作定义在 cpp 文件中。通常头文件名字与类型一致。头文件后缀有以下一些选择:.h 、 .hpp 、 .hxx 。标准库的头文件通常没有任何后缀,这是编译器所不关注的,而 IDE 可能会关注头文件后缀。

为了使用类,不必关心内部实现细节,这是类的实现者需要关注的。用户要关注的是此类对象所能提供的操作。所有的类(class)定义了一个类型(type),并且类型名与类名一致。如本章的例子:Sales_item 类定义了 Sales_item 类型。当定义了类之后就可以像内置类一样使用。

Sales_item item;

以上语句声明了一个类型为 Sales_item 的对象 item ,在 C++ 和 C 语言倾向于认为所有的变量都是对象,包括内置类型的变量,而不仅限于自定义的类型变量。所有这些变量可以作为函数的参数,被输入输出符读写,被等号赋值,使用加号对两个对象相加,当然也可以用 += 符号进行操作。

关键概念:类定义行为

在阅读文本中的程序时需要注意的是 Sales_item 的作者定义了这个自定义类型对象的所有行为(action),Sales_item 类定了当对象定义时发生了什么,赋值时发生了什么,以及做加法、输入输出时发生了什么。总的来说,类的作者定义了自定义类型对象的所有操作,从而所有类的操作可以从类的结构看出。

#include <iostream>
#include "Sales_item.h"

int main()
{
    Sales_item item1, item2;
    std::cin >> item1 >> item2;
    std::cout << item1+item2 << std::endl;
    return 0;
}

来自标准库的头文件用尖括号(< >)包围,来自程序自定义的头文件用双引号(" ")包围。值得注意的是这里对两个 Sales_item 对象进行输入输出以及做加法。加法的实际意义由类的作者对 Sales_item 对象进行定义。

item1.isbn() == item2.isbn();

这里的成员函数是 isbn,成员函数(member function)是被定义为类一部分的函数,有时也被称为方法(methods)。通常用对象去调用成员函数,item1.isbn 中使用点号操作符来说明“取 item1 对象中的 isbn 成员”,点号操作符(.)只能作用于类的对象。其左操作数必须是类的对象,右操作数必须是类的一个成员名,结果就是取对象的一个成员。当用点号操作符访问一个成员函数时,通常是想调用该函数。我们使用调用运算符(())来调用函数,调用运算符是一对圆括号,里边放置实参(argument)列表(可能为空)。成员函数 isbn 并不接收参数。

因而 item1.isbn() 调用对象 item1 的成员函数 isbn,函数返回 item1 中保存的 ISBN 书号。

关键术语

  • buffer 缓冲: 用来存储数据的一段内存,IO 通常都会有缓冲用于输入和输出,对于应用程序来说缓冲是透明的。输出缓冲可以被刷新,从而强制写入到目的地。cin 的读入会刷新 cout 缓冲,cout 的缓冲也会在程序结束时刷新。
  • built-in type 内置类型:由语言定义的类型,比如 int 、long、double 等;
  • class 类:一种用于定义自己的数据结构及相关操作的机制。类是 C++ 中最基本的特性之一。
  • class type 类类型:类定义的类型,类名就是类型名;
  • data structure 数据结构:数据及其上所允许的操作的一种逻辑组合;
  • expression 表达式:计算的最小单元。一个表达式由一个或多个操作数以及一个或多个操作符组成。表达式通常会产生一个结果。
  • function 函数:命名的计算单元;
  • initialize 初始化:当一个对象创建时同时赋予值;
  • standrad library 标准库:每一个 C++ 编译器必须支持的类型和函数集合。
  • statement 语句:程序的一部分,指定了在当程序执行时进行什么动作。一个表达式接一个分号就是一条语句。其它类型的语句包括 if 、for 、 while 语句,所有这些语句又可以包含别的语句。
  • uninitialized variable 未初始化变量:没有给初始值的变量。当类类型没有给初始值时其初始化行为由类自己定义。函数内部的内置类型初始化在未显式初始化时其值是未定义的。尝试使用未初始化的值是错误的,并且是 bug 的常见原因。
  • variable 变量:具名对象;

Leave a Reply

Your email address will not be published. Required fields are marked *