c++基础
在开始之前
大家一定都曾听说过“编程”,但是很大一部分人并不了解编程到底是在干什么。为了理解这个过程,我们简单举一个例子。
现在我们手头上有一个任务:给你一堆数字,要计算它们的和。你一定会觉得这是一个再简单不过的任务:只需要列出竖式,依次计算就可以了。这当然没有任何问题,但是假如我告诉你,有足足 \(10^8\) 个 \(10\) 位数呢?是的,这个任务对于人类来说是几乎不可能完成的,但是计算机不同,上面这个例子可以通过现代家用计算机在 \(1\) 秒之内算出答案(我们假定这 \(10^8\) 个数字不需要你手动输入到计算机中)。
具体的过程说起来也并不困难:首先我们需要站在巨人的肩膀上,通过已有的“地基”,也就是编程语言,以及其中内置的工具,将上面的自然语言“翻译”成计算机可以执行的程序。
在算法竞赛中,我们被允许使用c++作为我们的编程语言,并使用c++14标准执行(你可以认为这个标准就是随着时间的推移,它被一些管理者作出更新、加入新的更好用的内容,暂时没有理解也没有关系)。
编程语言当然不止有c++一种,事实上,它本身就来自c语言的扩展,现在人工智能领域常用python,知名游戏Minecraft就是通过java编写的(基岩版是c++)。
现在可能有人会认为,我们的任务就是将题目转化为代码,然后进行统一测试(判卷),看看大家能不能得出正确的结果。这个理解没有问题,但这样未免有些无聊:大家当然也明白计算机并不能解决目前人类遇到的所有计算难题,限制因素就是计算速度,我们称之为“算力”。\(10^8\) 个 \(10\) 位数的和对计算机来说是再简单不过的问题,但假如是 \(10^{10^8}\) 个 \(10^8\) 位数的和呢?这其中的计算难度差异是显而易见的。
再举个例子,假如我们要计算 \(1+2+3+\cdots +n\),一个只会加法的小学生只能一步一步计算(我们假定,这个小学生不是高斯),他就需要算 \(n-1\) 次加法。但是对于我们来说,这个问题就是最基础的等差数列求和,答案是 \(\frac{n(n+1)}{2}\),只需要算一次加法,一次乘法,一次除法就可以了,我们的“算力”没有发生变化,因为在计算的始终是我,但是通过第一种方式计算出结果是困难的,但是通过第二种方式计算是十分容易的。计算机也是如此,入门之后,我们要解决的问题几乎不会是简单地“将自然语言转化为代码”这样无趣的工作,而是想办法在有限的“算力”下,在限定的时间内(通常是 \(1\) 秒左右)解决一个问题,正如前面的等差数列求和公式一样。
于是我们可以这样解释我们接下来一段时间要做的工作:将所给问题转化为可以在一定算力下,限定时间、空间内,得出正确答案的代码。为了优化我们的程序(缩短运行时间或占用空间),我们将要学习很多算法,即解决问题的方式,或是数据结构,即高效存储、获取所需数据的方式,通过渐进时间复杂度理论估算我们写出程序的运行效率。
编写第一个c++程序!
前置知识
我们要做的第一个任务是编写一个程序,使其输出“Hello World”。
为了让代码能够输入、输出文字,我们需要先引用一个名为iostream
的“库”(也叫“头文件”),“库”指的就是前人写好的,你可以直接使用的框架,这个库的含义为“输入输出流”,i
指input
,o
指output
,stream
就是流。至于为什么叫做“流”,我们稍后再给出解释。
引用一个c++内部的库的方式是在代码中写这样一行:
1 |
|
通过将NAME
替换为iostream
,我们就引用了iostream
库,可以通过其中的东西方便地输入输出。
代码应当有一个执行起点,值得注意的是,这个起点并不是代码的第一行,而是被包裹在一个东西内部,我们称之为主函数,它是一个特殊的函数,我们目前不用考虑什么是函数,只需要知道主函数的写法以及代码从这个特殊的框架的第一行开始执行。
1 2 3 4 5 6 7 |
|
主函数内部用大括号{}包裹,大括号结尾应当有一行 return 0;
,注意行末的分号,这是不能省略的,同时不能使用中文分号,因为这是两个不同的字符。
现在该使用iostream
库里的东西了!,在主函数内部加入这样的一行代码:
1 |
|
提示
cout
可以理解为c
(我们使用的是c++)和output
同样注意行末的分号不能省略,于是我们就写出了第一个程序,下面是其完整代码。
1 2 3 4 5 |
|
为了让代码更加美观,我们可以在单词两侧任意添加空格,行和行之间可以随意添加空行,于是将代码写成更规范的形式:
1 2 3 4 5 6 |
|
同时每行开头的空格也是可以删去的,但强烈建议大家先按照上面的代码规范书写。
可以通过下面的方式连续输出许多内容:
1 |
|
程序会依次输出Hello
,空格和World
,和之前的效果相同。这里的<<
被形象地称作“流运算符”,像水流一样将待输出的内容依次流到输出区域。
细心的同学可能会发现,代码中的Hello World
被英文双引号所包裹,同时双引号没有被输出,这是基于这样一个设计逻辑:输出的“文字”应该可以方便地与代码的内容,如cout
,#include <iostream>
所区别,如果没有双引号包裹,代码将可能会变成
1 |
|
完全乱套了!于是c++做出了这样的规定,文本,或者说,字符串(一串字符),在语言中的表示方式为双引号内包裹的文字,例如我们要输出#include <iostream>
就可以这么写:
1 |
|
完全没有问题!这时有人就会有问题了:假如我要输出双引号呢?解决方法是规定\"
表示双引号这个字符,其中反斜杠\
被称作“转义字符”,相当于将"
的含义作了转换,从字符串的标志变作了一个字符。转移字符\
当然不只转义这一个内容,例如\n
就会被转义为换行,是的,换行也是一个字符,被表示为\n
,试着输出 Hello \n World
吧!
问题又出现了,我们怎么输出\
这个字符?解决方法很简单,就是再转义一次,让\\
表示\
这个字符,繁琐但有效,问题彻底被解决了。
数据的存储与计算
在本节的最开头,我们提出过计算一些数字和,这个任务在计算机中又该怎么完成呢?
数字的存储
在c++中,我们通过下面的方式定义一个整数:
1 |
|
int
指integer
,即英文整数。由于计算机的空间是有限的,自然而然地,这个 \(x\) 也必然存在一个存储的上下限,在这里,\(x\) 应当是一个 \(-2^{31}\) 到 \(2^{31}-1\) 内的整数,否则存储会出现问题。
可以通过=
给一个数赋值,例如可以这样写:
1 2 |
|
上面代码的前两行可以简写作int x = 7;
,同时可以像int x, y, z;
这样一下子定义多个值。同样注意行末分号,之后不再提醒。
现在运用我们学过的输出方式:
1 2 |
|
成功输出了7
!这里如果给x
包裹上双引号,输出的就不会是7
而会是x
,因为此时表示x
这个字符串,而非代号x
中存储的内容。
试试加法?
1 2 |
|
成功输出了15
,事实上可以不用这么麻烦:
1 |
|
效果是一样的。
问题来了,假如我需要写一个程序计算两个数的和,但这两个数不是确定的,而是我每次输入进去的,该怎么做?类似输出,我们可以使用cin
实现输入,在引用iostream
头文件后在主函数内写:
1 2 3 |
|
执行程序,输入1 3
,再输入回车,程序正确地输出了 4
,完整程序如下:
1 2 3 4 5 6 7 8 |
|
数据类型与运算符
有些同学可能会兴奋地尝试其他加法,例如输入了0.1 0.2
,但是输出了错误的结果!
这是怎么一回事呢?计算机出错了?并不是,计算机会忠实地执行计算任务,只可能是代码本身出现的问题。
问题出在哪里呢?在int
这里。我们说过,int
表示的是整数,将一个小数塞进去当然会出问题,我们通过double
来定义一个小数(计算机中称作浮点数),将代码换作
1 2 3 |
|
就可以输出正确的结果了,同样地,double
也存在其存储上限。double
和int
等被我们称作“数据类型”,“浮点数”和“整数”是不同类型的数据,它们在计算机中的存储形式也不同,即使它们都是数字。
下面给出其他各种数据类型:
待补充
待补充:数据类型表
数字的加、减、乘、除在c++中分别对应+
、-
、*
、/
,还有一种特殊的运算叫做取模,对应%
,取模是整数与非零整数之间的运算,用小学生的语言来讲,a % b
的结果就是 \(a\) 除以 \(b\) 的余数,例如 3 % 2
的结果为1
,用数学语言可以写作 \(a \bmod b\),这个 \(\bmod\) 就是c++中的%
符号。
我们说过,整数和浮点数是不同类型的数据,但是两个整数做除法可能得到一个非整数!这时候c++会如何处理呢?是否会自动将它转化为浮点数类型?答案是否定的,两个int
类型做除法得到的结果还是int
类型,int
之间的除法得到的结果是向下取整的除法,也有人叫地板除。同样用小学生的语言来描述,就是忽略余数之后做除法的结果,例如我们知道 \(7\) 除以 \(3\) 得 \(2\) 余 \(1\),于是在c++中,7 / 3 = 2
,7 % 3 = 1
,两个double
(或float
)之间做除法就没有这样的困扰,当然,它们不能进行取模运算。