3.3 数值数据类型
从每天早上睁开眼睛的那一刻开始,我们几乎每时每刻都在与数字打交道:从闹钟上的6点30分,到上班坐的216路公共汽车;从新闻中说的房价跌到了100元每平米到回家买菜时的西红柿3.5元一斤。我们生活在一个充满数字的世界。程序是对现实世界的描述与表达,自然也会有很多数字需要表达。为了做到这一点,C++提供了丰富多样的数值数据类型,从整数到小数、从单精度浮点数到双精度浮点数、从有符号数到无符号数。有了这些数值数据类型,我们就可以定义各种变量来表示现实世界中的各种数字了。
3.3.1 整型数值类型
在现实世界中,最常见的数字应该是各种大大小小的整数了,而在C++中,我们用整型数值类型来表示现实世界中的整数。根据这些数据类型所占用的内存资源多少和取值范围的不同,整型数值类型又被分为以下四种。
1. 基本型
其类型说明符为int(integer),在内存中占4个字节,其取值范围从-2147483648到2147483647,基本上包含了我们最常见的整数,能够满足我们在程序中对表达整数的需要。同时,又因为它是CPU原生支持的整型类型,所以处理起来最快。因此,int是我们最为常用的整型数值类型。
特别注意:数据类型的字节数在不同环境下可能有不同值
需要注意的是,我们在这里所讨论的各种数据类型所占用的内存字节数,只是在典型情况(32位操作系统,32位主流编译器)下的字节数。在一些特殊环境(64位操作系统、特殊的编译器)下,各种数据类型所占用的字节数可能有所不同。所以,要想获得某种数据类型在当前环境下所占用的字节数,最好的方法是使用sizeof关键字动态地计算其实际的字节数,而不是使用某个固定的字节数。具体方法,可以参看后文对sizeof的介绍。
2. 短整型
有时候,我们需要表达的整数只是在一个比较小的范围内,比如,我们要表达一个学生的成绩,最小可以到0,而最大也只能到100,如果这时仍旧使用int这种取值范围比较大的数据类型来表示,就显得有点资源浪费了。现在正在倡导创建节约型社会,而C++世界也不例外。为了表示这种取值范围相对较小的整数,C++提供了短整型数据类型,其类型说明符为short或short int,所占内存字节数只有int类型的一半,也就是两个字节。自然地,其取值范围也相应地缩小为从- 32768到32767,我们可以用它来表达生活中常见的整型数值。但是,相对于int类型,short类型的处理速度要慢一些,而内存又相对比较便宜,所以,我们往往为了性能而牺牲空间,更多的使用int类型来代替short类型。
3. 长整型
在某些情况下,我们并不知道我们需要表达的整数到底有多大,有可能是几千,也有可能是几千个亿。在这种情况下,我们总是希望得到一个当前平台支持的最大整型数值类型,以避免因取值范围太小而造成的错误。在C++中,我们用长整型来表示当前平台原生支持的最大整型数值类型。其类型说明符为long或long int。在32位平台上,它占用4个字节的内存,其取值范围与int相同。而在64位平台上,其占用的内存扩展为8个字节,取值范围也相应地增大为骇人听闻的从-9223372036854775808到9223372036854775807,绝对满足我们对整数取值范围的需要。
4. 长长整型
在C++中,为了表示现实世界中的某些特别大特别大的整数,比如某个星系的星球总数,我们还需要用到取值范围更大的长长整型。它的类型说明符是long long或long long int,在内存中占8个字节。其取值范围已经是个天文数字,所以它也只是更多地用于天文计算等特殊的科学计算中。
另外,还可以用关键字unsigned或者signed对这些整型数据类型进行修饰,构成无符号或有符号的整型数据类型。所谓的有无符号,是指这些数值在内存中的第一位是用来表示正负符号还是用来表示数值。在默认情况下,上面所介绍的整型数据类型都是有符号的,只要在其类型说明符之前加上unsiged关键字,就成为了对应的无符号整型数据类型。
int a; // 默认为有符号int类型 signed short b; // 有符号short类型 unsighed long c; // 无符号long类型
各种无符号类型所占用的内存空间与相应的有符号类型占用的内存空间是相同的。但由于无符号类型省去了符号位,故不能表示负数,而相应的,它所能够表示的正数范围会扩大到原来的一倍。当我们事先知道要表示的整数不会出现负数的时候,可以使用unsigned修饰某个整型数值类型,使其成为一个无符号的数据类型,扩展其在正数上的取值范围。表3-3列出了在典型的32位平台上,C++中各种整型数值数据类型、取值范围、位数和使用规则。
表3-3 整型数值类型
数 据 类 型 |
位数 |
取 值 范 围 |
使 用 规 则 |
int |
32 |
-2147483648 ~ 2147483647 |
最常用的,也是处理速度最快的整型数值类型,用来表示常见的整型数值 |
signed int |
32 |
-2147483648 ~ 2147483647 |
在使用上等同于int类型,所以很少使用,基本上都是直接使用int类型 |
unsigned int |
32 |
0 ~ 4294967295 |
无符号的int类型,只能表示正整数。当要表示的整数只有正数值的时候,可以使用无符号数据类型 |
short int |
16 |
-32768 ~ 32767 |
通常简写为short,用以表示范围较小的整数以节省内存资源,但处理速度有所减低 |
signed short int |
16 |
-32768 ~ 32767 |
— |
unsigned short int |
16 |
0 ~ 65535 |
— |
long int |
32 |
-2147483648 ~ 2147483647 |
通常简写为long,它代表了当前平台上CPU原生支持的最大整型数据类型。在32位平台上,long和int的取值范围是相同的,只是在64位操作系统上两者有差别,这一点在开发跨平台的应用程序时需要注意 |
signed long int |
32 |
-2147483648 ~ 2147483647 |
— |
unsigned long int |
32 |
0 ~ 4294967295 |
— |
long long int |
64 |
-9223372036854775808 ~ 9223372036854775807 |
比long还要long的整数,我们不用去数它到底有多少位,只需要知道,当要表示很大很大的整数时,就可以用它 |
知道更多:用sizeof牌体重计为数据类型量体重
在现实世界中的人们,无论高矮都有个体重,人们根据自己的体重来决定穿多大尺寸的衣服;而在C++世界中的各种数据类型,同样也都有一个体重,而数据类型的体重,决定了它需要占用多少字节的内存空间。一个数据类型所占用的内存字节数,也就是它的体重。
为什么我们需要知道数据类型的体重?在通过指针直接操作内存对数据进行读写访问的时候,我们往往需要知道这种数据类型所占用的内存字节数,这样我们才能为这些数据分配合适的内存资源,就像我们要为一个人做衣服,先要知道他的身高一样。例如,我们想动态地申请一段内存来保存1024个long类型的数据,这时我们就需要根据每个long类型数据占用的内存字节数来计算一共需要申请多少字节的内存资源来存放这些数据。虽然我们知道long类型的字节数大多数时候是4个字节,但是,我们不能在程序中直接使用这个从天而降的魔数(Magic Number),那样一个不知从何而来的数字会让程序的可维护性变得很差;同时,数据类型占用的字节数就像人的体重一样,是会根据环境的变化而发生变化的,并不是所有平台上的long类型都是4个字节。如果我们在程序中用固定的数字4来表示long数据类型的字节数,在某些平台上这个程序也许会运行正常,但当这个程序移植到另外一个平台时,将有可能因为long数据类型字节数的不同而产生错误。为了提高程序的可维护性,保证程序的可移植性,我们必须在代码中动态地获得数据类型在当前平台上的字节数,并最终计算出需要的内存资源总数。
所有这些关于数据类型字节数的问题,我们都可以使用C++提供给我们的sizeof牌数据类型体重计来一劳永逸地轻松解决。sizeof实际上是C++中的一个操作符,它可以对一个数据类型或者这个数据类型的一个变量或数组进行操作,从而获得这个类型或者变量(数组)实际占用的内存字节数。例如,我们在用memset()函数清零某个数组时,通常用它来计算这个数组的字节数:
#include <cstring> // 引入memset()函数所在的头文件 // … // 定义数组 int res[1024]; // 用sizeof计算res占用的字节数 memset(res,0,sizeof(res));
这里我们在用memset()函数对数组res进行清零操作时,第一个参数是数组名,也就是数组的首地址,第二个参数是初始值,通常是0,第三个参数就是用sizeof关键字计算出来的res数组所占用的字节数了。幸亏有sizeof这个体重计,要不然我们这里要写成更复杂的“1024*4”。这种复杂的形式虽然也同样能够达到计算数组体积的目的,但是,很容易出错,且毫无可移植性可言。而这,也正是sizeof体重计受到程序员们钟爱的原因。
sizeof牌数据类型体重计不仅可以获得内建数据类型(例如,int、long和double等)的体重,它同样也可以获得自定义的结构体或者类所占用的字节数(包含因为字节对齐而添加的字节数)。例如:
// 定义一个结构体 struct Human { char cName[3]; // 3个字节 // char pad; // 这里因字节对齐,编译器补齐一个字节 int nAge; // 4个字节 }; // 声明一个结构体变量 Human huZengmei; // 输出结构体的字节数为8,包含因字节对齐而添加的一个字节,即3+1+4=8 cout<<"Human结构体占用的字节数是:"<<sizeof(Human)<<endl; // 输出结构体变量的字节数,与结构体的字节数相同 cout<<"Human对象占用的字节数是:"<<sizeof(huZengmei)<<endl;
sizeof牌体重计是如此好用,它应该应用在任何需要知道某个数据类型或者变量(包括数组)所占用内存空间的地方,例如,用memset()函数对数组进行清零操作,根据某个基本数据类型占用的字节数来判断当前硬件平台是32位还是64位等,以此来避免人为地指定数据类型的字节数可能带来的可维护性和可移植性问题。
图3-2 sizeof牌体重计
3.3.2 浮点型数值类型
现实生活中的数字,除了表示公共汽车路数的216这种整数外,更多地是表示西红柿价格的3.5这种小数。而在C++中,我们使用浮点型数值类型来表示小数。根据取值范围的不同,C++中的浮点型数值类型可以分为单精度型、双精度型和长双精度型三种。
1. 单精度型
其类型说明符为float。单精度浮点型数值类型占4个字节的内存空间,其取值范围为-3.4E+38 ~ +3.4E+38。这里需要注意的是,因为浮点型数值类型(包括后面的double和long double类型)无法精确地表示零值,所以其取值范围实际上并不连续,在中间接近零值的地方,被分为了正负两部分。因为受到计算机存储浮点数机制的限制,使用float类型表示浮点数时,能够保证精确到小数点前后至少6位有效数字,最多可以达到7位有效数字。例如:
float fPrice = 3.5; // 用float类型的变量fPrice表示西红柿3.5元一斤
知道更多:为什么小数在C++中被称为浮点数?
在C++中,我们将小数称为浮点数,而将表示小数的数据类型称为浮点型数值类型。这里大家一定会问:为什么小数被称为浮点数?而其中的“浮”又是什么意思呢?
这一切,都与小数在C++中的表达方式有关系。所谓的浮点,是相对于定点而言。比如,我们要在C++中表达这样两个小数:
100000000000.0 0.000000000001
如果采用定点(小数点固定)的表达方式,我们需要保存成如下的形式:
100000000000.000000000000 000000000000.000000000001
采用这种方式,我们不得不将每一位上的数据都原原本本地保存下来,这其中的某些数据对于小数的数值和精度都毫无意义,反而却浪费了宝贵的存储资源。为了解决这个问题,C++采用了一种新的保存方式:将数字表示成指数形式,保存每个数字的有效数字和指数。按照这种方式,上面的两个数可以保存成如下的形式:
小数 1 指数 11(小数点往左移动了11位) 小数 1 指数 -12(小数点往右移动了12位)
通过小数点位置的移动,我们只需要保存小数的有效数字和小数点移动的位置,就可以以更加简洁的方式保存下整个数字。因为这种表达方式中的小数点是浮动(float)的,所以小数也被称为浮点数。
2. 双精度型
其类型说明符为double。双精度浮点型数值类型占8 个字节的内存空间,是单精度浮点数值类型的两(double)倍,所以双精度类型不仅取值范围更大,可以达到-1.7E+308~1.7E+308,同时其精度也更高,可以精确到小数点前后15位有效数字,最多可以达到16位。例如:
double fD = 0.0000003; // 用double类型的变量表示支原体细胞的直径
3. 长双精度型
其类型说明符为long double。长双精度浮点型数值类型占12个字节的内存空间,其数值范围可以达到天文数字级别的-1.2E+4932~1.2E+4932所以,这种类型更多地只是用于科学计算中,日常开发较少用到。
表3-4列出了在典型的32位平台上,浮点型数值的数据类型、位数、精确数字位数、取值范围和使用规则。
表3-4 浮点型数值类型
数 据 类 型 |
位数 |
有效数字 |
取 值 范 围 |
使 用 规 则 |
|
float |
32 |
6-7 |
-3.4E+38 ~ 3.4E+38 |
如果要表示的浮点数数值不是特别大,精度要求也不是很高,比如我们日常生活中遇到的各种小数,就可以使用float类型来表示,不仅可以满足需要还可以节省内存空间,提高性能 |
|
double |
64 |
15-16 |
-1.7E+308 ~ 1.7E+308 |
如果要表示的浮点数数值比较大,或精度要求比较高,可以使用double类型来表示,虽然占用了更多内存空间,但是可以保证取值范围和数据精度 |
|
long double |
96 |
18-19 |
-1.2E+4932 ~ 1.2E+4932 |
如果要表示天文数字,就用它 |
|
知道更多:如何产生随机数?
所谓随机数,通俗地讲,就是由计算机通过一定的随机数算法所产生的,按照某种规律分布(平均分布或正态分布)的某个大小范围内的数字。在程序设计中,随机数广泛地被应用在测试、游戏、仿真以及安全等领域。所以,掌握各种随机数的产生方式,也成为我们使用C++进行开发的一个必备技能。
在C++11中,一个随机数的产生需要由随机引擎(engine)对象和分布(distribution)对象两部分共同完成。其中,分布对象负责随机数的取值范围和分布。比如,使用uniform_int_distribution分布,表示将引擎产生的随机数字平均分布在某个范围内;而使用normal_distribution分布,则表示将这些随机数字正态分布在某个范围。相应地,引擎对象则负责根据分布对象确定的取值范围和分布产生相应的随机数字。当我们在程序中确定随机数产生所需要的引擎对象和分布对象后,就可以用引擎对象作为参数,调用分布对象这个函数对象,从而得到我们所需要的随机数了。例如,网站登录验证码的产生就需要用到随机数:
// 引入随机数引擎和分布所在的头文件 #include <random> #include <iostream> // 使用std名字空间 using namespace std; int main() { // 定义一个默认的随机数引擎 default_random_engine reng; // 构建一个从0到25之间的平均分布 uniform_int_distribution<int> uni_dist(0,25); // 使用random_device设置随机数引擎的种子, // 以防止每次运行都产生相同的伪随机数序列 random_device rnd_device; reng.seed(rnd_device()); // 验证码一共4位 const int n = 4; char code[n]; // 保存验证码的字符数组 // 提示输入验证码 cout<<"请输入下面的验证码:"<<endl; // 利用for循环产生4个验证码字母字符 for (int i = 0; i < n; ++i) { // uni_dist(reng)表示让reng引擎按照uni_dist分布, // 产生取值在0到25之间呈平均分布的随机数 // 然后在‘A’的基础上向后偏移,就得到了随机的验证码字母字符 code[i] = ‘A‘ + uni_dist(reng); // 输出验证码字母字符 cout<<code[i]; } // … return 0; }
在这段程序中,我们首先引入了C++标准库中关于随机数的头文件<random>,然后,我们就可以定义相应的随机数引擎对象(reng)和分布对象(uni_dist),在定义分布对象的同时,我们以构造函数参数的形式,确定了随机数的取值范围。有了它们,就可以用引擎对象reng作为参数,调用uni_dist分布对象这个函数对象,最后得到的就是我们需要的在0和25范围之内平均分布的随机数了。在这里,我们还利用了ASCII表中字母字符呈现连续分布的特性,在字符‘A’的基础上,加上一个随机数,就得到了我们最终想要的随机的字母字符。值得特别提醒的是,在产生随机数之前,我们必须用引擎对象的seed()函数设置随机种子,否则,每次运行所产生的随机数序列都是一样的,那样就失去了随机的意义了。
另外,这个程序只是利用随机数产生了验证码字符,接下来,我们还需要接收用户的输入,并与当前的验证码进行比较,以此来判断用户输入是否正确。这些工作,就留给大家在学习了后面的内容(各种控制结构、字符串处理等)之后,自己动手来完成了。相信大家很快就可以做到这一点。
------->
敬请期待下集:他到底爱不爱我?3.4 布尔类型