前面学习的示例程序很简单,甚至会认为编写的程序无外乎就是一个“计算器”罢了。但是C++程序能完成的任务不仅仅计算几个表达式这么简单。有些复杂的问题需要经过多个表达式计算才能解决,这个时候需要想办法来存储计算的中间结果,使用变量能够很好应对这样的情况。
一、变量与赋值
小A在玩一个小游戏,玩游戏的过程如下:
- 游戏开始时,有5条命
- 第一关小A顺利闯过,没有损失命,获得2条命的奖励
- 第二关小A顺利闯过,没有损失命,因为耗时比前一关长只获得1条命的奖励
- 第三关小A顺利闯过,但太过大意导致损失了3条命,没有获得任何奖励
- 第四关小A顺利闯过,损失了2条命,也获得了1条命的奖励
- 最后一关较困难,小A损失了所有的命数也未能通关
现在编写程序输出游戏开始前和每关结束后小A的命数。你可能马上想到的是这样输出:
cout<<5<<endl<<7<<endl<<8<<endl<<5<<endl<<4<<endl<<0<<endl;
确实得出了正确的结果,但是这样编程没有体现出“让计算机来计算”,使用计算机就是要让计算机来计算解决问题的,这里却是人计算出了结果然后让计算机输出。借助变量可以这样编程:
#include<iostream> using namespace std; int main() { int n = 5; cout<<n<<endl; n = n+2; cout<<n<<endl; n += 1; cout<<n<<endl; n -= 3; cout<<n<<endl; n = n-2+1; cout<<n<<endl; n = 0; cout<<n<<endl; return 0; }
程序第5行 int n = 5;
定义了一个int类型的变量n,同时给变量n赋初值5。int是一种最基本的整数数据类型(后文会介绍),可以存储一个整数。这一句相当于给了一个专门存放整数的盒子,这个盒子的名称是n,最开始往里面存放了5这个整数。
第6行 cout<<n<<endl;
使用cout语句输出变量n的值,此时会输出5。这一句相当于取出了存放在名称为n的盒子的数拿来使用(输出这个数)。
第8行 n = n+2;
是一个赋值语句(第5行其实也是赋值语句,不过还先定义了变量n的数据类型为int)。注意这里的等号 \(=\) 不能认为是判断相等(如果是判断相等,n=n+2是不成立的),这里的 \(=\) 应该读作赋值成,\(=\) 是赋值符号。这一句赋值语句就相当于向名称为n的盒子里放新的东西,首先会计算赋值符号右边表达式 n+2
的结果(要计算这个结果,首先要使用变量n的值,就和第6行输出n值一样,要取出名称为n的盒子里的数来参加计算,也就是取出来名称为n的盒子里的数+2),然后将计算结果(7)仍然存放到名称为n的盒子里,原来盒子里存放的数据会被丢弃。
这里小结一下,可以通过赋值语句为变量赋值,赋值语句的作用是先计算赋值符号右边表达式的结果,然后将结果赋值给左边的变量。给一个变量重新赋值,变量之前的值会被丢弃(标准说法是被覆盖掉)。在cout输出语句或者计算表达式中可以通过变量名取出变量当前存储的数据。
第11行 n += 1;
也是赋值语句,确切地说是“自赋值”。这一句的效果同 n = n+1;
一致,这里可以简单认为 n += 1;
就是 n = n+1;
的简写形式。作用是将n的值取出来,然后加上1,最后将结果又赋值给变量n。除了+=,还能使用 -= 、*=、/=、%=来进行自赋值。读者可以尝试修改第8行和11行原来的赋值语句为自赋值语句。
其实 n += 1;
还可以简写成 n++;
或者 ++n;
(n++;
和++n;
两者在这里效果相同,但执行过程不同,后面会介绍);同样的n -= 1;
可以简写成 n--;
或者 --n;
统观整个程序代码,会发现在程序运行过程中,变量n的值发生了多次改变(都是通过赋值语句实现的),这也是变量这一程序名词中“变”的含义。可以通过赋值语句(当然不仅仅是赋值语句)随时改变变量的值。
再来看一个计算圆的周长和面积的例子:
#include<iostream> using namespace std; int main() { double r; r = 12.56; cout<<2*3.14*r<<endl; cout<<3.14*r*r<<endl; return 0; }
第5行 double r;
声明了一个double类型(浮点数)的变量r。第6行使用赋值语句为变量r赋值。第7、8行使用变量r参加了计算并使用cout输出计算结果。
#include<iostream> using namespace std; int main() { double r,C,S; r = 12.56; C = 2*3.14*r; S = 3.14*r*r; cout<<C<<endl<<S<<endl; return 0; }
第5行 double r,C,S;
声明了三个double类型(浮点数)的变量r、C、S。第6行使用赋值语句为变量r赋值。第7、8行使用赋值语句为变量C、S赋值(变量r参加了计算,出现在赋值符号右边的表达式中)。第9行使用cout输出变量C、S的值。
对前面的内容做一个小结:
- 变量在使用(为其赋值或者取出值参加计算)前必须先定义,定义变量的格式为:
数据类型 变量名;
可以在定义变量的同时为变量赋初值:数据类型 变量名 = 初始值;
还可以一次性定义多个相同数据类型的变量:数据类型 变量名1,变量名2,变量名3,...;
注意:变量名区分大小写!a和A是不同的变量。 - 变量取名规范:
- 只能由大小写英文字母、数字、下划线(_)组成;
- 不能以数字打头;
- 不能使用C++的关键字。C++有不少关键字(又称为保留字),例如using、namespace、int、double、if、for、while等,使用关键字作为变量名会出现编译错误;
- 在同一作用域不能定义相同名称的变量,作用域的概念后续会接触到。例如:
int n = 12;
int n = 0;
会导致编译错误; - 变量的取名不能太随意,建议做到“见命知义”,或者遵循一些普遍的做法(例如上面示例程序使用变量n来存储整数,后续还会接触到其它变量命名的普遍做法),这样不仅提高代码可读性方便他人阅读,也能使自己在编写程序时思路更加清晰。
- 使用变量的值前,需要确保变量已经存储有确定的值。例如下面的语句:
int n;
cout<<n<<endl;
定义了变量n,但是没有为变量n赋值,后面直接取出变量n的值用于输出,这个时候变量的值是不确定的。在使用变量的时候要注意避免使用未赋值的变量的情况。 - 赋值语句的格式为:
变量名 = 计算表达式;
其中的 \(=\) 是赋值符号,可以读作“赋值为”。赋值语句的作用是先计算赋值符号右边计算表达式的结果,然后将结果赋值给左边的变量。赋值后变量中存储的是新值,原来的旧值会被覆盖(丢弃)。 - 自赋值语句的格式为:
变量名 自赋值符号 计算表达式;
其中自赋值符号可以是 +=、-=、*=、/=和%=。自赋值语句的作用是先计算自赋值符号右边计算表达式的结果,然后将左边变量当前值与右边计算结果根据自赋值符号中的运算符进行计算,将最后结果赋值给左边的变量。例如n += 1;
相当于n = n+1;
。 n += 1;
还可以简写成n++;
或者++n;
;同样的n -= 1;
可以简写成n--;
或者--n;
。
二、数据类型
上面的讲解已经涉及到数据类型的概念。我们用“盒子存取东西”较为形象地说明变量的使用,那么数据类型可以理解成“盒子的类型”,不同的数据类型对应不同类型的“盒子”,就像现实生活中有用来装水、装油、装化学试剂的不同用途的盒子一样。要处理整数,那么应该用int类型的盒子,也就是要使用int类型的变量;要用变量存储圆的半径,为了提高程序的实用性,最好使用double类型的盒子(整数小数都能装下),也就是要使用double类型的变量。
在计算机内部,数据的处理采用的是二进制,二进制的最小单位是位bit(1个bit只能存放0或者1),最基本的存储单元是字节Byte(1个Byte是连续的8bit)。以存储整数的盒子(整数数据类型)为例,不同的数据类型还可以理解成“盒子的尺寸”,C++中就提供了1Byte、4Byte、8Byte这三个尺寸的盒子,分别对应char、int和long long三种存储整数的数据类型。不同的尺寸就意味着盒子的容量不同,对应地,不同数据类型的变量,能存储的数的范围不同。
以1个字节的char类型为例,其存储数据的方式和存储数据的范围如下图所示:
注意:计算机内部实际处理时,对于负数会采用“补码”的形式编码,char类型的负整数最小值是-128(\(-2^7\))。
同样的分析可知,都可以用来存储整数的char、int、long long三种数据类型的存储范围:
- 1Byte(8bit)的char类型的存储范围是:\(-2^{7}\) ~ \(2^{7}-1\);
- 4Byte(32bit)的int类型的存储范围是:\(-2^{31}\) ~ \(2^{31}-1\);
- 8Byte(64bit)的long long类型的存储范围是:\(-2^{63}\) ~ \(2^{63}-1\)
其实char、int、long long还有对应的无符号数据类型(只能存储非负整数)unsigned char、unsigned int、unsigned long long,也就是最高位不是符号位,所有的位都是数据位:
- 1Byte(8bit)的unsigned char类型的存储范围是:\(0\) ~ \(2^{8}-1\);
- 4Byte(32bit)的unsigned int类型的存储范围是:\(0\) ~ \(2^{32}-1\);
- 8Byte(64bit)的unsigned long long类型的存储范围是:\(0\) ~ \(2^{64}-1\)
常见数据类型:
数据类型 | 说明 | 占用空间 | 存储范围 |
char | 整型/字符型 | 1Byte(8bit) | -128 ~ 127 |
unsigned char | 无符号整型 | 1Byte(8bit) | 0~255 |
int 或者long | 整型 | 4Byte(32bit) | \(-2^{31}\) ~ \(2^{31}-1\),大约能表示绝对值不超过 \(2.1\times10^{9}\)的整数 |
unsigned int 或者unsigned long | 无符号整型 | 4Byte(32bit) | \(0\) ~ \(2^{32}-1\),大约能表示不超过 \(4.2\times10^{9}\)的非负整数 |
long long | 长整型 | 8Byte(64bit) | \(-2^{63}\) ~ \(2^{63}-1\),大约能表示绝对值不超过 \(9.2\times10^{18}\)的整数 |
unsigned long long | 无符号长整型 | 8Byte(64bit) | \(0\) ~ \(2^{64}-1\),大约能表示不超过 \(1.8\times10^{19}\)的非负整数 |
float | 单精度浮点型 | 4Byte(32bit) | 大约指数绝对值不超过37(\(10^{-37}\)、\(10^{37}\)) 6位有效数字 |
double | 双精度浮点型 | 8Byte(64bit) | 大约指数绝对值不超过307(\(10^{-307}\)、\(10^{307}\)) 15位有效数字 |
上表取值范围的“大约”表示保守估计,特别是float和double类型(char、unsigned char、int、unsigned int、long long、unsigned long long都有精确的取值范围,这几种数据类型表中给出的大约是为了给大家一个较为直观的感受)。这里也能看得出来,C++中对于浮点数,只能保证在一定的有效数字范围内是可靠的,不能保证浮点数是绝对精确的。
从表中可知存储整数的数据类型很多,但是long long的范围最大(只考虑有符号数),那是不是存储整数就都采用long long类型呢?答案是否定的。合适的才是最好的。编程时,对于要处理的数据大小范围是可以预估的,应该根据预估情况来选择合适的数据类型。计算机内存有限(竞赛时往往还会设定内存限制),如果都使用long long类型,那么在有限的总空间限制下,存储数据的数量就少了;并且一般来说,大范围的数据运算速度和小范围相比较还是有差异的。
一般情况下,使用int数据类型来存储整数,只有当int无法存储一些绝对值特别大的整数时,才使用long long数据类型;而浮点数一般都使用double,如果空间限制特别严格则使用float。至于long long都无法存储的超大整数,可以使用其它方法来模拟存储(算法部分的“高精度计算”会具体介绍)。
如果强行为变量赋值为超过其存储范围的数据,此时C++按照一定的规则处理,处理后实际存储到变量里的数据是不可靠的,这样的情况称为数据溢出,编程时要避免这样的情况。可以测试下面的程序:
#include<iostream> using namespace std; int main() { int n = 2147483647; cout<<n<<endl; //正常输出2147483647 n++; //n自加1,超出int存储范围,出现数据溢出(上溢) cout<<n<<endl; //输出-2147483648(不可靠) int m = -2147483648; cout<<m<<endl; //正常输出-2147483648 m--; //n自减1,超出int存储范围,出现数据溢出(下溢) cout<<m<<endl; //输出2147483647(不可靠) return 0; }
三、字符串类型string简介
除了上面介绍的常用的数值类的数据,程序中往往还要处理文本类型的数据,这些文本类型的数据更专业的称谓是“字符串”。C++中可以使用string类型来存储字符串,并且字符串还支持判断相等、加法(两个字符串拼接到一起)等运算:
#include<iostream> using namespace std; int main() { string s1,s2; s1 = "Hello"; cin>>s2; //这里的if语句后面会学习到 if(s1=="Hello") cout<<"YES"<<endl; else cout<<"NO"<<endl; cout<<s1<<endl<<s2<<endl; cout<<s1+s2<<endl; return 0; }
后面会详细介绍字符串的使用。
四、常量的使用
回到前面计算圆的周长和面积的程序:
#include<iostream> using namespace std; int main() { double r,C,S; r = 12.56; C = 2*3.14*r; S = 3.14*r*r; cout<<C<<endl<<S<<endl; return 0; }
程序中除了使用double类型的三个变量r,C,S外,还直接使用了一些数字:12.56、2、3.14159。像这样直接书写在程序中的量,称之为字面常量。
可以使用sizeof函数来计算常量或者变量所占存储空间的字节数,运行下面的测试程序:
#include<iostream> using namespace std; int main() { cout<<sizeof(5)<<endl; //输出4,整数字面常量默认是int类型 cout<<sizeof(5LL)<<endl; //输出8,整数字面常量后加上LL,则是long long类型 cout<<sizeof(5.0)<<endl; //输出8,浮点数字面常量默认是double类型 cout<<sizeof(5.0F)<<endl; //输出4,浮点数字面常量末尾加上F,则是float类型 return 0; }
再来看下面一段仍然是计算圆的周长和面积的程序:
#include<iostream> using namespace std; int main() { const double PI = 3.14; double r,C,S; r = 12.56; C = 2 * PI * r; S = PI * r * r; cout<<C<<endl<<S<<endl; return 0; }
第5行 const double PI = 3.14;
,是在普通的定义double变量PI并赋初值的语句 double PI = 3.14;
前面额外添加了一个修饰词 const。这样这个PI就成为了一个符号常量。这里的符号常量与变量用法一致,只是不能再修改它的值。一般约定符号常量名全部大写。
#include<iostream> using namespace std; int main() { const double PI = 3.14; PI = 3.14159; return 0; }
第6行使用赋值语句尝试修改符号常量PI的值,会出现编译错误。不能修改符号常量的值。
#include<iostream> using namespace std; int main() { const double PI; PI = 3.14; return 0; }
第5行定义了符号常量但是没有立即赋值,也会出现编译错误。定义符号常量的时候必须赋值。
为什么要使用符号常量呢?假设我们要提高计算精度,需要将程序中的圆周率3.14修改成3.14159。使用字面常量修改如下(需要修改两处):
#include<iostream> using namespace std; int main() { double r,C,S; r = 12.56; C = 2*3.14*r; S = 3.14*r*r; cout<<C<<endl<<S<<endl; return 0; }
#include<iostream> using namespace std; int main() { double r,C,S; r = 12.56; C = 2*3.14159*r; S = 3.14159*r*r; cout<<C<<endl<<S<<endl; return 0; }
如果不小心只修改了一处,那么程序就存在BUG(本例代码量少,排查时很容易发现BUG,如果代码量多,并且使用圆周率的语句也多,那就不好排查了。当然,遇到这样的情况是可以使用编辑器的替换功能来批量修改避免漏改导致的BUG)。
使用符号常量的修改如下(只需要修改一处,真正的“一改全改”):
#include<iostream> using namespace std; int main() { const double PI = 3.14; double r,C,S; r = 12.56; C = 2 * PI * r; S = PI * r * r; cout<<C<<endl<<S<<endl; return 0; }
#include<iostream> using namespace std; int main() { const double PI = 3.14159; double r,C,S; r = 12.56; C = 2 * PI * r; S = PI * r * r; cout<<C<<endl<<S<<endl; return 0; }
此外,还可以通过C风格的#define语句来定义符号常量,注意语法与const的不同:
#include<iostream> using namespace std; int main() { #define PI 3.14 double r,C,S; r = 12.56; C = 2 * PI * r; S = PI * r * r; cout<<C<<endl<<S<<endl; return 0; }
#define语句定义符号常量语法格式:#define 常量名 常量值
。注意#define语句没有体现常量的数据类型,也没有以分号;结束
#include<iostream> #define PI 3.14 using namespace std; int main() { double r,C,S; r = 12.56; C = 2 * PI * r; S = PI * r * r; cout<<C<<endl<<S<<endl; return 0; }
习惯性地将#define语句写到程序的头部
#define其实是宏定义,编译时宏定义之后的代码中所有的 PI 会被简单粗暴地全部替换成3.14,所以这里不需要指定数据类型。