了解一座城市最好的办法就是走进其中;学习一门编程语言最好的方式就是亲自动手编码。
一、第一个C++程序
今天,我们就来走近C++,当然,不是深入其中的细节,而是从空中俯瞰,观察它大致的形状。下面的内容会引入很多概念,但是不必担心,不理解是很正常的,在后续的学习过程中你会慢慢掌握它们的。
下面我们就来看看一段经典的入门代码,这段代码的功能非常简单,它输出一行文字“你好,我是C++。”然后退出:
1 |
|
这虽然是一个很小的程序,但它已经具备了一个C++程序所必须的很多构造了。看到第一行:
1 |
在C++中,以井号#
开头,并且单独占一行的语言构造称为预处理指令(Preproccessor Directive)。在正式开始编译之前,预处理器会识别这些指令,并且对源代码进行预处理。就像炒菜之前,要将食材洗净切碎。预处理就是对源代码的第一步粗加工。
#include
指令也叫做包含指令,它的作用其实很简单:就是将后面尖括号中指定的那个文件复制粘贴到当前位置。
C++可以分为两个部分,一个是核心语言,它规定了C++的语法和语义;另一个就是标准库,它为我们提供了很多便利的工具。iostream
是标准库里的一个头文件。这个头文件里记录了std::cout
这个对象的声明(Declaration)。如果我们不引入这个头文件,那么编译器就会拒绝编译:我不认识std::cout
,请你提供它的声明。 除了关键字(Keyword)之外,使用任何标识符之前,都必须给编译器提供一个声明,否则编译器就不认识你写的是什么。
第二行是一条注释:
1 | // Say hello to C++ |
源代码除了给编译器看,更多的还是给程序员看。因此,在有些情况下使用自然语言描述你的代码干了什么事情也是很有必要的,这就是注释的作用,编译器在编译的过程中会忽略注释。在这个例子中,从双斜线//
开始一直到这一行结束的部分都是注释。
第三行是一个函数定义的开头:
1 | int main() |
函数定义用来向程序中引入函数(Function)。所谓函数就是完成某项具体功能的指令序列,一个C++程序会包含很多函数。函数这个名字应该会让你联想到数学里的函数,某种程度上来说,它们确实有一些相似之处。数学上的函数是从定义域到值域的一个映射,例如x -> x²
将自变量x
的值映射到它的平方。而C++的函数可以接受一些参数,让经过一系列的运算与操作给出一个返回值作为结果。当然C++的函数可以没有参数,也可以没有返回值,它们仅仅用来完成某种特定操作。
在所有C++函数中,main
函数是其中最特殊的存在。它是C++程序的入口,每一个C++程序都必须有一个main
函数。当计算机运行一个C++程序时,它就会去寻找这个入口,然后从这里开始运行。
前面的int
表示函数的返回值类型,它是一个C++关键字,意思是整数类型,来源于integer
一词的缩写。
然后main
就是这个函数的名字。后面一对括号()
是函数的参数列表,由于这个简单的例子中main
函数不需要参数,因此可以留空。在后续的学习中,我们还会见到带参数的main
函数。
第四行和第七行有一对相互匹配的花括号,这对花括号以及它们之间的内容就是main
函数的函数体。
1 | { |
函数体是由一条条语句(Statement)组成的。在调用main
函数的时候,就是在执行它函数体里面的语句。实际上,函数体本身就是一条复合语句,意思是由很多条语句复合而成的语句。
第五行是一条表达式语句:
1 | std::cout << "你好,我是C++。\n"; |
表达式语句是C++最常见的语句,顾名思义,表达式语句由表达式(expression)构成。这可能会让你再次联想到数学表达式。是的,C++的表达式和数学表达式有相似之处,但二者还是有很大区别的。我们在后续的学习过程中还会详细讲到C++表达式的语义。
在这条表达式语句里,std::cout
是标准库定义的一个全局对象。std
是标准库名称空间(namespace)的名字,源自standard
一词的缩写。紧接着由两个冒号::
组成的符号叫做作用域解析运算符,它明确了我们要访问的是std
这个作用域里的cout
,而不是其他地方的。cout
源自Charactor Output
的缩写,通常读作“see-out”。它表示的是标准字符输出流。C++将程序的输入和输出抽象为“流(Stream)”这个概念,在后续的教程中还会详细地介绍它们。
"你好,我是C++。\n"
这一串由双引号引起来的语言构造叫做字符串字面量(String Literal)。字符串就是由很多个字符构成的序列,我们用字符串来表示各种人能看懂的文字。而字面量就是直接写在源代码中的量,后面我们还会见到其他种类的字面量。这其中还有一个特殊的字符\n
。没错,这里只有一个字符,它表示一个换行符。诸如换行符之类的特殊字符无法直接写在字符串里,因此C++使用一些特殊的字符序列来代表一个字符,这就叫做转义序列。
在中间,由两个左尖括号或者说小于号组成的符号<<
叫做左移运算符,左右两侧是它的两个操作数std::cout
和"你好,我是C++。\n"
。如果你了解一点二进制运算的规则,那你可能会好奇这两个东西之间到底要如何进行左移运算,其实这里并不是真正的左移,而是有运算符重载,C++可以给运算符赋予自定义的含义。这里就是重载了左移运算符,用来表示将它右边的值输出到左边的输出流里。箭头的方向直观地指示了数据流的方向。正如前文所述,std::cout
背后连接的是标准输出流,这是操作系统中的一个概念,当我们在控制台运行一个程序的时候,程序写入标准输出流的东西就会显示在控制台上,于是我们的第一个C++程序发出的问候就显示在屏幕上了。
最后,还有一个很重要的符号,就是语句末尾的分号;
。C++用分号来表示语句的结束,就像我们写文章用句号来表示一个句子结束一样。
第六行是一条return
语句:
1 | return 0; |
return
也是一个C++关键字,它表示函数已经执行完毕,并且给调用者返回一个计算结果。前面我们知道,main
函数的返回值类型是int
,即一个整数。于是我们在最后返回了一个0
。main
函数的返回值的具体含义是由你,程序员决定的。但惯例是返回0表示程序执行得很完美,正常退出。而返回其他的值通常意味着执行过程中出了差错。
二、不要害怕犯错
这就是一个简单的C++程序,我强烈建议你亲自输入这些代码并且观察程序的输出。今天的教程不是为了让你掌握上面提到的数量爆炸的概念,而是另一个重要的主题——不要害怕犯错,编译器始终是你的好帮手。
C++作为一门形式语言,具有严格且明确的语法规则。代码中的每一个符号都会影响其语义。对于初学者,往往还没有完全掌握这些语法知识,因此学会抄写代码和阅读编译器的错误提示就是很重要的技能。
例如,新手最常犯的错误是拼写错误:
1 |
iostream
误写为iosteam
。如上文所说,#include
指令的作用是将某个文件的内容复制粘贴到当前位置,但是编译器找了半天找不到iosteam
这个文件,于是它说:No such file or directory。并且贴心地指出了错误的位置,第一行的第十个字符。
1 | <source>:1:10: fatal error: iosteam: No such file or directory |
再比如经典的mian
函数,编译器通常会给出这样的提示:
1 | /opt/compiler-explorer/gcc-13.2.0/bin/../lib/gcc/x86_64-linux-gnu/13.2.0/../../../../x86_64-linux-gnu/bin/ld: /lib/x86_64-linux-gnu/crt1.o: in function `_start': |
重点在第二行的undefined reference to `main'
,意思是找不到main
的定义。这次编译器并没有指出错误的具体位置,因为在程序中定义一个名字叫做mian
的函数是完全没问题的,编译器没法判断这是笔误还是真的想定义一个叫做mian
的函数。但是它在寻找程序的入口时发现找不到main
函数,于是只好提示你找不到main
的定义。
所以,不要害怕语法错误。相比程序中的逻辑错误,语法错误是最容易定位并解决的。在新手阶段,你可以完全相信编译器。编译器如果报错,那肯定是你违反了某项语法规定。学会观察编译器的错误信息。如果你看不懂,可以善用搜索引擎,直接将编译器的错误提示扔进搜索框通常都能找到解决方案。如果找不到,再向他人提问的时候提供编译器的错误提示也有助于别人快速定位出错的原因。
三、更多示例代码
下面是一些简单的C++程序,你可以尝试运行一下,观察它们的输出。猜一猜每一行代码的意思,根据自己的理解修改其中的代码,看看输出会有什么变化,验证自己的猜想是否正确。再次强调,不要害怕犯错,编译器会是你的好帮手。你甚至可以故意犯一些错误,看看编译器会作何反应。
程序一
1 |
|
程序二
1 |
|
程序三
1 |
|
程序四
这段代码稍微有些特殊,它涉及了一些C++23才有的新特性,目前只能在最新版的Visual Studio 2022上才能编译。其中println
是print line的缩写,是不是比std::cout
再加重载的左移运算符更加容易理解一点呢?
1 | import std; |