《C++ Primer Plus》第三章

第三章 数据处理(Dealing with data)

摘要:

  • 变量命名规则
  • C++基本整数类型:unsigned long, long, unsigned int, int, unsigned short, short, char, unsigned char, signed char, bool
  • C++11新增:unsigned long long, long long
  • climits文件,定义了每种数字类型的上限
  • 每种整数类型的数字常量
  • 用const定义符来创建一个常量
  • C++基本浮点数类型:float, double and long double
  • cfloat文件,定义了每种浮点数的上限
  • 每种浮点数的常量
  • C++的计算符
  • 自动类型转换
  • 强制类型转换

OOP编程的核心的就是设计和扩展你自己的数据类型。设计自己的数据类型代表你试图让类型去符合你的数据。

C++基本类型分两种,基础数据类型和符合数据类型。这一章将会讲到前一种:整数与浮点数。到了第四章就会讲到在这些基础类型上定义的复合类型:数组,字符串,指针和结构


简单变量


要存储一个数据,程序需要知道三件事:

  1. 存在哪
  2. 存的值是几
  3. 存了什么类型

比如以下这两句:

int braincount;

braincount = 5;

就说的要存一个int类型,值是5的数据。程序就会去找一块足够能放下一个int的存储空间,记下地址,然后把5存进去。然后你就可以用braincount这个变量名去访问这个存储地址。当然,在这个程序里你是看不到存储地址的,而是程序自己来记住那个地址。不过你也可以用&操作符来获得这个地址,这个会在下一章讲指针的时候讲到。


变量名

C++是鼓励你创建有意义的变量名的。my_name, my_id这种变量名肯定比a, b要更合理。另外还有必须几条遵守命名规则:

  • 变量名只能由字母、数字和下划线(_)组成
  • 第一个字符不能是数字
  • 大小写是不同的
  • 不能把C++关键字当名字
  • 以两个下划线或者一个下划线加一个大写字母开头的名字是给接口(也就是编译器和它用到的资源文件)用的。一个下划线开头的是给接口用来当做全局标识(global identifier)的
  • C++不限制名字的长度,名字里每个字符都是必要的。但有些平台可能会限制长度。

倒数第二点和其他不同,你写了一个类似__time或_Time的变量不会导致编译错误,但会让程序产生未定义的行为,也就是你不知道这样会发生什么。不触发编译错误是因为下划线开头的命名并不是违规的,只是被预留给接口了。而全局名(global name)是指变量被声明的地方,第四章将会讲到这个问题。


整数类型(Integer type)

整数就是没有小数的数,比如2,98,-256和0之类的。整数当然是无限的,但计算机不可能有足够内存来存储无限大的整数,所以一门程序语言只能表示出整数的一部分。有些程序只提供一种整数类型,但C++提供了好几种选择。这让你可以从中选择更符合你需求的类型。

这些不同的整数类型其实就是他们占据了不同大小的存储空间。一个占据更大存储空间的数据类型自然可以表示更大范围的整数。而有些类型(signed type)可以表示正和负,而其他的(unsigned type)则不行。通常我们用宽度(width)来表示一个整数类型使用了多少的存储空间。C++的基础整数类型,宽度从小到大排分别是char, short, int, long和C++11才有的long long。每一种都分为signed和unsigned两种,所以你就有多达十种整数类型可选。现在我们就来讲讲这几个类型,因为char有着些和其他类型不同的特性(毕竟它更多是用来表示字符而不是数字),下面我们会先讲另外的几种。

short,int,long还有long long

计算机存储单位是bits,C++的类型short,int,long和long long就是使用了不同数量的bits。当然,如果在所有系统里面,这四个类型使用的bits个数都一样就最好了。但事实却并非如此,并不是每个计算机的设计都是相同的。C++提供了一个灵活的标准,采用了C语言的下限标准:

  • short至少要16bits长
  • int至少要和short一样长
  • long至少要32bits长且至少要和int一样长
  • long long至少要64bits长且至少要和long一样长

许多系统现在使用的都是这个最小值标准,就是short16bits,long32bits。这样就让int比较灵活,它可以是16,24,32bits。它甚至可以是64bits,当然这样long和long long就也要至少长64bits了。通常来说,int在老式的IBM PC接口里是16bits(和short一样)而在WindowsXP到Windows7还有Macintosh OS X和VAX还有许多迷你计算机接口里是32bits(和long一样)。有些接口甚至允许你选择怎么处理int。这些对于int的不同定义让C++在移植时很容易发生问题。不过接下来我们会提到一些降低这些问题出现的方法。

提一下bits和bytes的区别。bit是计算机存储的单元。你可以吧一个bit当成一个开关。它开就代表1,关就代表0.一个8-bit的空间就有2的8次方也就是256种组合。也就是,8-bit的空间可以表示从0到255或者-128到127这个范围的数。往上推就可以知道16-bit可以有65536种可能,32-bit可以有4294672296种,64-bit有18446744073709551616种。也就是说,一个unsigned long没法存下现在地球的人口数,但long long可以。

byte通常指一个8-bit的空间。它是指计算机存储空间的一个计量单位。平时我们看的kb,mb,gb就是kilobyte(1024 byte),megabyte(1024 kb),gigabyte(1024 mb).然而C++不是这么定义的。C++的byte是由足够能存储接口的一个基本字符的bits组成。也就是一个byte要能存下一个独立的字符。在美国,计算机一般用的是ASCII和EBCDIC字符集,一个字符8bits就足够了。但在一些国际程序会使用更大的字符集比如Unicode,所以有些接口会使用16-bit甚至32-bit的byte。

sizeof操作符和climits头文件

sizeof操作符会告诉你一个int在基本系统里是4byte长(一个byte是8bits)。你可以对类型名字使用sizeof,你要用括号把类型名字括起来。但如果你是用在一个变量上面,括号就不是必须的了。举个例子:

sizeof(int);

sizeof a;

climits头文件定义了符号化常量(symbolic constants)来表示每个类型的上限。INT_MAX就是最大的int。在Windows7就会得到2147483647,而另外的16-bit的int的编译器就会得到32767。这个编译器制造方会提供和他们的编译器相符合的climits头文件。

关于符号化常量(symbolic constants),在climits头文件会有类似这样的定义语句:

#define INT_MAX 32767

我们还记得C++的编译器会首先处理头文件。这个#define和#Include一样是个预处理指令,它的意思是:在整个程序中找到所有INT_MAX,然后将它们全部替换成32767.所以#define基本上和文档编辑器里的全局查找替换功能差不多。

初始化Initialization

初始化包括了声明和赋值。比如:

int n_int = INT_MAX;

你也可以用数字比如255来初始化,也可以使用另一个变量(当然是要在这之前就初始化了的变量)。你甚至可以用一个表达式来初始化。比如:

int uncles = 25;

int aunts = uncles;

int chairs = aunts + uncles + 4;

如果你把uncles的初始化移到了这三句的最后,这程序就是无效的了。因为这样aunts和chairs在初始化时会没法知道uncles是什么。

上面这种初始化格式是来自C语言的,C++有自己新的初始化格式:

int mother(1); //mother赋值为1

当然如果你在初始化变量的时候还不清楚它的值,你可以暂时不给它赋值:

int year;

year = 2018;

不过在初始化时就赋值可以防止你放了给它赋值。

C++11的初始化

有一种初始化格式是用于数组(array)和结构(structure)的,但在C++98里单值变量也可以这么用:

int pizza = {30};

用大括号初始化之前并不常见,但C++11扩展了这种用法。首先,它可以省略等号:

int pizza{30};

其次,大括号里面可以为空,变量将会被赋值为0:

int pizza{};

第三,这样会针对类型转换起到更好的保护作用,这个会在本章晚些提到。


unsigned类型

先前介绍的4种整数类型都是unsigned类型,是无法表示负数的。这样的优势是它们可以表示更大的数。比如signed类型的short可以表示-32768到32767,而unsigned的short可以表示0到65535.当然,你只应该在没有负数的情况下使用unsigned类型。

#include <iostream>
#define ZERO 0
#include <climits>

int main()
{
    using namespace std;
    short sht = SHRT_MAX;
    unsigned short u_sht = sht;
    cout << "short:" << sht << endl;
    cout << "unsigned short:" << u_sht <<endl;
    sht = sht + 1;
    u_sht = u_sht + 1;
    cout << "short:" << sht << endl;
    cout << "unsigned short:" << u_sht << endl;
    sht = u_sht = ZERO;
    sht = sht - 1;
    u_sht = u_sht - 1;
    cout << "short:" << sht << endl;
    cout << "unsigned short:" << u_sht << endl;
    return 0;
}

以上程序输出是:

short:32767

unsigned short:32767

short:-32768

unsigned short:32768

short:-1

unsigned short:65535

可以看到signed类型的最大值加一会得到它表示范围内的最小值。而unsigned类型的0减一会得到它的最大值。这其实涉及到计算机组成原理讲到的机器表示数的知识。在这里就不细写了。


如何选择整数类型?

有这么多整数类型,怎么选?自然,int是最“自然”的类型。最“自然”的类型意味着计算机处理它是最有效率的。如果没什么特别的理由,int应该就是最佳的选择。

但为什么你会需要其他的类型呢?如果一个数据不可能为负,比如人数之类的,你就应该使用unsigned类型,这样变量可以容纳更大的值。

如果变量的取值可能会超过16bits,你应该使用long,即便你的系统里int是32bits的。因为你要考虑到你的程序在int只有16bits的系统上运行的情况。

当short比int小时,使用short可以节省一些存储空间。一般来说,只有你的数组非常非常大的时候这个选择才有意义。(数组就是一种将多个同类型数据顺序存储的数据结构)如果节省存储空间对你来说很重要,那么即便你的系统里short和int一样长,你也应该采用short,理由和为什么要使用long一样。


常数

常数或常量就是你明确写出来的数,比如225和1024.C++和C一样允许你用三种进制来写常数:十进制(大众喜好),八进制(老式Unix喜好)和十六进制(硬件骇客喜好)。C++使用数字的第一或第一二位来区分进制。如果数字开头是1-9,那就是个十进制数。如果开头是0,第二位是1-7,那就是个八进制数。如果前二位是0x或0X,那就是个十六进制数。对于十六进制数,a-f或者A-F表示十六进制下10-15的取值。比如15的十六进制是0xf,0xA5的十进制是165.


C++如何识别常数的类型?

声明语句会明确告诉编译器标量的类型,但常量呢?比如你在语句里写一个1024,程序会把它存为一个int还是long还是short?答案是C++会把它默认存为int除非有必要存成别的类型,比如你用特殊的后缀指明了常数的类型又或者常数大于int的可表示上限。

首先我们来看看后缀。你可以放一些字母在数字末尾来表明它的类型。l或者L代表long,u或者U代表unsigned int,ul(大小写任意组合)代表unsigned long。(不过因为l和1看起来很像,所以还是尽量用L比较好)

还有就是C++11下当然还会有ll(或LL)代表long long,和ull(大小写任意,但两个l的大小写要一致)代表unsigned long long。

然后再来看看大小问题。C++在十进制整数处理上和十六进制和八进制不太一样。一个没有后缀的十进制数会以int,long和long long中最小的又足够大的一种类型存储。而十六进制还有八进制则会以int,unsigned int,long,unsigned long,long long和unsigned long long中最小又足够大的一种类型存储。


char:字符和小整数

终于我们说到了最后一种整数类型:char。就像你猜的那样,char是设计用来存储字符的,比如字母和数字。我们都清楚存数字对计算机来说没什么难的,但存储字符可是另一回事。所以char是一种特殊的整数类型,它一定足够大来表示字符集中的每一个字符。实际应用中,很多系统仅支持少于128个字符,所以一个byte就够用了。所以,虽然char是用来存储字符的,但你同样可以把它当做比short更小的一个整数类型来使用。

美国最常用的字符集是ASCII。一个ASCII码代表一个字符,比如65代表A,77代表M。为了方便,本书中默认程序使用的是ASCII码。但C++的接口使用的字符集是取决于它的原初系统,比如在IBM的主框架下使用的是EBCDIC。无论是ASCII和EBCDIC都无法满足国际程序的需要,所以C++支持一种宽字符(wide character)类型来存储更大的字符码,比如用于字符集Unicode。本章晚些就会讲到这个w_chat类型。

注意

char是一个整数类型,在内存里面它存储的就是一个整数,你可以对它进行任何的整数适用的操作,比如给它加一。之前便提到过,是类型让cout决定如何输出这个值——又一个智能对象的表现。

C++将字符当做整数处理是非常方便的一个做法,这样你就不需要使用麻烦的转换方法来让值在整数和ASCII码之间反复转换。

就算你通过键盘输入数字也可以被识别为字符,比如:

char ch;

cin >> ch;

如果你输入一个7,那么程序将读入为‘7’这个数字,将‘7’的ASCII码55存入ch。而下面这两行:

int n;

cin >> n;

同样输入一个7,程序会读入7这个数值,然后将7存入n。

成员方法:cout.put()

这个cout.put()是什么?这是你第一个关于C++ OOP的例子:成员方法。先前说过类定义了如何表示数据以及如何操作它们。比如ostream这个类,它就有一个put方法,设计用于输出。

要通过一个对象比如cout来使用一个方法比如put,你要用一个点(.)来吧cout和put()链接起来。这个小数点叫成员操作符(membership operator)。cout.put()表示通过对象cout来调用成员方法put。第十章会讲到这方面更详细的细节。

cout.put()这个成员方法还提供了一个另外的选择:用<<操作符来输出。也就是先前我们一直在用的那种。你可能会奇怪那cout.put()这个方法还有什么必要。这个涉及到一些历史性的因素。在C++2.0发布之前,cout会把字符变量输出为字符,但会把字符常量比如'M'输出为数字。这是因为早期的C++和C一样,将字符常量存为int。也就是说'M'在内存里是16bits或32bits长的值为77的int。而以下这一句:

char ch = 'M';

被声明的ch是一个长为8bits的char。也就是说,对于cout,'M'和ch差别挺大的,尽管他们理论上应该是一样的值。所以以下这两句:

cout << 'M';

cout.put('M');

第一句会输出77,第二句会输出M。

现在C++2.0发布了,C++会将字符常量存储为char。所以这个问题就解决了。

cin对象读入字符有好几种不同的方式,你可以写一个循环读入再输出的程序反复输入不同的字符来研究一下。详细的会在第五章讲循环的时候讲到。

char常量

虽然我们说字符在程序中都是存储为整数的,数值和字符一一对应。但在写字符常量的时候,还是写字符的好。一来这样更清楚不容易犯错,二来不同字符集中字符和整数的对应是不一样的。ASCII中65是A,但EBCDIC中65可就不是A了。

另外,有些符号你是不能直接通过键盘写入程序的。比如你不能用回车键让字符串换行,因为回车是用来让源代码换行的。其他字符同样有这个问题因为C++将它们用作重要的标识。比如双引号是用来标识字符串常量的首尾的,所以你没法在字符串中间插一个双引号。C++有一个特殊的符号叫转义序列,专门为这种字符服务。下表列出了它们:

C++转义序列
名字 ASCII符号 C++代码 ASCII码(十进制) ASCII码(十六进制)
换行 NL(NF) \n 10 0xA
水平缩进 HT \t 9 0x9
垂直缩进 VT \v 11 0xB
后退 BS \b 8 0x8
回车 CR \r 13 0xD
提醒(响一声) BEL \a 7 0x7
反斜杠 \ \\ 92 0x5C
问号 ? \? 63 0x3F
单引号 ' \' 39 0x27
双引号 " \" 34 0x22

signed和unsigned char

不像int,char并不是默认为signed,也不默认为unsigned,而是让C++接口来决定。这样让编译器开发者可以更好地作出符合它们的硬件情况的决定。如果signed和unsigned char的区别对你很重要,你可以直接写明:

unsigned char u_ch;

signed char s_ch;

不过这只有在你把它们当做整数类型来用的时候才有意义。unsigned类型的范围是0到255而signed类型是-128到127。

当你想要更多:wchar_t

当你的字符集太大,8bits的char没法满足你了,C++提供了两种处理办法。一,如果这个巨型字符集是系统的基础字符集,那么C++接口可以将char定义为16bits长或更长。二,如果接口可以同时支持一个小的字符集和一个扩展字符集,那么基本的8bits的char可以用来表示小的字符集,另一个类型:wchar_t,可以用来表示扩展字符集。wchar_t是一个更长的char,wchar_t的大小和大小和sign类型和另一个叫做底层类型(underlying type)是一样的。而底层类型的选择是由接口决定的,所以有些系统里是unsigned short,有些系统里是int。

cin和cout是用于处理char流的,所以它们并不合适用于处理wchar_t类型。iostream头文件提供了类似的一个工具:wcin和wcout。还有你可以使用L作为前缀来指明宽字符常量或字符串。以下是一个例子:

wchar_t w_ch = L'p';

wcout << L"tall" << endl;

这本书里不会用到wchar_t,不过你还是需要了解一下。

C++11:char16_t, char32_t

就是unsigned,一个16bits一个32bits长的char。u前缀代表char16_t,U前缀代表char32_t。


bool类型

ANSI/ISO C++标准添加了一个新的类型:bool。这个名字是为了纪念发明了数学逻辑学的数学家George Boole。在计算机中,boolean变量的值可以是true或者false。以前C++和C一样,没有Boolean类型。关于这一点的细节会在第五第六章讲到,C++将非0的值当做true,而0处理为false。现在你可以用bool这个类型来表示true和false了,而true和false被预定义为了对应的值。也就是说你可以这样写:

bool is_empty = true;

true和false常量可以被转成int类型,true会转成1,false转成0:

int a = true;  // a = 1

同样地,数值或指针值可以被隐式地转为Boolean。非0的是true,0是false:

bool b = -100; // b = true


const标识符


现在我们说回符号化常量。当你在程序的多处使用一个常量的时候,你可以通过将其定义为符号化常量并可以通过改变它的定义来简单更改它的值。本章早些讲到#define时说过,C++有一个更好的方法来处理符号化常量。那就是使用const来标识变量的声明和初始化。举个例子:

const int Months = 12;

现在你可以在程序里写Months而不用写12.(在程序里简单写一个数字12,这个12可能是长度可能是人数可能是很多东西。但在程序里写一个Months就很清楚是月份数。)在你初始化了这样一个常量后,这个常量就是固定的了。编译器不会允许你在之后的程序中更改这个Months的值。

比较常见的做法是将常量的第一个字母大写来提醒自己这个是不可更改的常量。不过这不是约定俗成的,也有很多其他的比如将常量名全部大写,这常见于#define定义的常量。也有一种是在常量名之前加k,比如kmonths。也有许许多多其他的做法。

注意的是你要在常量的声明语句中将它初始化。以下的做法是不行的:

const int a;

a = 1;

如果你在常量声明语句中没有初始化,那么这个常量就会保持未定义,并且也无法更改。

如果你学过C,你会觉得#define就已经够用了。但const是更好的做法。首先,它让你明确写出了常量的类型。第二,你可以用C++的范围规则来让常量的作用范围局限于某个方法或文件内。(范围规则描述了对于不同模型一个名称的作用范围。这个会在第九章中讲到。)第三,你可以用const来定义更多种类型的常量,比如会在第四章讲到数组和结构。

ANSI C也有const标识符,这是从C++借来的。如果你对ANSI C很熟,你要注意C++的版本是不太一样的。其中一个不同是关于范围规则,第九章会讲到。另一个主要不同是在C++里,你可以用const常量来定义数组的大小。这个会在第四章看到例子。


浮点数(Floating-Point Numbers)


浮点数是C++的第二种基础类型,用于表示带小数的数,或者过于大的数。计算机把小数存储为两部分,一部分是数值,另一部分是数值扩展方向。比如34.125和3.4125的第一部分是一样的。因为它们都可以写成0.34125*100和0.34125*10.C++是采用类似这样的方式来存储浮点数,只不过是二进制形式的,所以第二部分的单位是*2而不是*10.但我们不需要记住这种内部的表示,只要知道浮点数这个类型就可以了。


浮点数的使用

C++有两种浮点数写法,第一种就是日常的用法,8.0,3.25这样的。第二种写法是用E,比如这样:3.1E6.这个数代表3.1乘10的六次方,也就是3100000.E后面也可以是负数:2.67E-3,这个数的值是0.00267.


浮点数类型

C++有三种浮点数类型:float,double和long double。它们区别在于它们可表示的关键数的数量和可允许的指数的最小范围。关键数是指一个数中有意义的数字。比如14265有五个关键数,而14000则只有两个。关键数个数和小数点的位置没有关系

C++和C要求float的关键数不能少于32bits,double不能少于48bits且不能少于float,还有long double至少要和double一样。这三个实际上是可以一样大小的,但一般来说,float是32bits,double是64bits,long double是80,96甚至128bits。而指数范围对于三者都是至少是-37到37.你可以在cfloat或float.h头文件中查看你的系统的设定。

以下这段程序验证了float和double在关键数方面的精度差别。程序中使用了17章将会讲到的ostream中的setf方法。这个强制输出不以E符号形式输出小数以更好地对比精度,同时它强制输出小数点后六位。参数ios_base::fixed和ios_base::floatfield是iostream中的常量。

#include <iostream>
int main()
{
    using namespace std;
    cout.setf(ios_base::fixed, ios_base::floatfield);
    float f = 10.0/3.0;
    double d = 10.0/3.0;
    const float million = 1.0e6;

    cout << "f = " << f << endl;
    cout << "f * million = " << f * million << endl;
    cout << "f * million * 10 = " << f * million * 10 << endl;
    cout << "d = " << d << endl;
    cout << "d * million = " << d * million << endl;
    return 0;
}

输出:

f = 3.333333
f * million = 3333333.250000
f * million * 10 = 33333332.000000
d = 3.333333
d * million = 3333333.333333

注意

一般来说,cout会把多于的0去掉。比如33333.250000会输出为33333.25.调用cout.setf()改写了这个行为。

更主要的是上面的程序展示了float的精度比double低了多少。f和d都是用10.0/3.0初始化的。它们的值应为3.3333333(无限循环)。因为cout输出小数点的后六位,你可以看到f和d都保持准确。但当它们乘以1000000,你发现f在第七个3之后的值就不对了。f在7个关键数内是准确的。而double类型的变量显示了13个3,所以它可以保证13个关键数的精度。因为系统其实可以保证double有15个关键数精度。另外你发现f*1000000再乘以10后,结果进一步出错,这再一次指出了float的精度问题。


浮点数常量

浮点数常量会默认存储为double,你可以用后缀来指明常量的类型。f或F代表float,l或L代表long double(l看起来和1差不多,所以还是尽量用L)。比如

1.234f   //float

2.65e8 //double

2.2L   //long double


浮点数优缺点

浮点数对于整数来说有两个有点:一、它可以表示整数之间的数。二、因为科学计数法,它可以表示大很多很多的数。另一方面,浮点数运算通常比整数运算稍慢一点点,且你会损失精度。

#include <iostream>
int main()
{
    using namespace std;
    float a = 2.34e+22f;
    float b = a + 1.0f;

    cout << "a = " << a << endl;
    cout << "b - a = " << b - a << endl;
    return 0;
}

输出:

a = 2.34e+022
b - a = 0

问题出在了,2.34e+22表示了一个小数点左边有23位长的数,也就是23400000000000000000000.你给这个数加一,得到23400000000000000000001.但float只能存储六七个关键数,也就是2340000,最后那个1无法保留。


C++ 算符


C++拥有以下的基本算符:

  • + - * / 对应加减乘除
  • %是模,这个符号会算出左边数除以右边数得到的余数。比如19%6=1.

算符顺序:算符优先级与关联

你可以在C++里做复合运算,比如:

int a  = 3 + 5 * 4;

但你要搞清楚运算的优先级。C++的优先级和普通代数学的优先级是一样的,先算乘除和模,再算加减。也就是上面那个复合运算应该是3 + (5 * 4)。但有时还有优先级解释不了的情况,比如:

float f = 120 / 4 * 5;

乘除是没有优先级之分的。C++采用的是从左到右的规则。先算左边,再算右边。但还有另一种情况:

int b = 20 * 5 + 4 * 3;

根据优先级,在做加法之前,程序要先算出20*5和4*3.那先算哪个呢?从左到右吗?并不是,因为这两个运算没有共享一个参数,所以从左到右规则不适用。这里C++让接口来决定运算顺序。在这里,先算哪个并没有关系,但在有些情况下是有关系的,第五章将会讲到这个问题。


除法分歧

除号的运算取决于参数的类型的。如果两个参数都是整数类型,C++会进行整数除法。也就是算出来的结果的小数部分会被丢掉,得到的结果是个整数。如果有一个或者两个参数都是浮点数,那么结果的小数部分就会保留,得到的结果是个浮点数。


类型转换

C++定义了许多自动的类型转换:

  • 当你将一种算术类型的值赋值给另一种算术类型的变量
  • 当你在表达式中使用多种类型
  • 当你向方法传入参数

初始化与赋值时的转换

将一个类型的值赋给更大类型的变量(比如将int类型的值赋给long类型变量)通常不会导致问题。但将一个很大的long比如21556547赋给float变量就会损失精度了,因为float只能存下6个关键数。得到的float值大概是2.15565e+7。所以有些类型转换是安全的,有些就会导致问题了,看下图:

数值类型转换潜在问题
转换类型 潜在问题
大浮点数类型->小浮点数类型 如 double->float 损失精度(关键数个数);值可能会超出目标类型的范围,其转换结果是未定义的
浮点数->整数 失去小数部分;原数值可能超出目标数值范围,其转换结果是未定义的
大整数类型->小整数类型 如long->int 原数值可能超出目标数值范围;一般来说只有低位的byte会被复制

初始化时的类型转换与赋值一样。

使用{}初始化时的类型转换(C++11)

大括号初始化不允许进行变窄的类型转换,比如浮点数转整数。但如果是用常量对小类型初始化,如果值是小类型变量可以存储的,那么初始化也是允许的。比如:

const int code = 66;

int X = 66;

char c1 {21212};  //这是变窄,不行

char c2 {66};        //可以,因为char可以存下66

char c3 {code};     //同上

char c4 {X};          //不行,X不是常量

X = 21212;

char c5 = X;          //这种形式初始化时允许的

对于c4的初始化,我们知道X的值是66.但由于X是个变量,它的值是可变的。程序并不会对这个变量的值得变化保持追踪,所以编译器无法确定X在初始化和赋值之间值有没有发生变化。

表达式内的类型转换

在一个算术表达式中使用两个不同类型的参数,C++会进行两种自动类型转换。一,有些类型只要出现就会被转换。二,有些类型在和其他类型出现在同一表达式中时会被转换。

首先来看看自动转换。在评估一个表达式时,C++会将bool,char,unsigned char,signed char,short都转成int。true会转成1,false转成0.这叫整体升级(integral promotion)看如下几行:

short pigs = 11;

short dogs = 13;

short all = pigs + dogs;

执行第三行时,C++首先将pigs和dogs的值转为int,算出结果后,再把结果从int转回short,因为结果被赋给了一个short变量。这是作为最自然的类型,int的运算比其他类型运算更快一些的原因。

还有更多的自动转换。如果short比int小,那么unsigned short会被转为int。而如果二者相同,unsigned short 会被转成unsigned int。这个规则保证了转换中不会出现数据损失。类似的,wchar_t会被转成这几种类型中最小又足够大的类型:int,unsigned int,long,unsigned long.

然后是多类型组合时的类型转换,比如用int加float。当运算涉及两个类型时,较小的那个类型会被转成较大的那个类型。比如9.0/3,9.0是个double,3是个int。那么3会被转为一个double。一般来说,C++会通过查表来确定哪一个类型是较小的。下表是C++11稍作修改后的表,编译器会顺序逐条查看:

  1. 如果有一个long double,那么另一个就会被转成long double
  2. 当以上不成立,如果有一个是double,那么另一个会被转为double
  3. 当以上不成立,如果有一个是float,那么另一个会被转为float
  4. 当以上不成立,也就是参数都是整数,进行整体升级(integral promotion)
  5. 在第四条中,如果两个参数都是signed或都是unsigned,且一个比另一个小,小的会被转成大的
  6. 如果第五条不成立,也就是一个是signed一个unsigned。如果unsigned那个是更大的类型,signed那个会被转成unsigned的那个类型
  7. 如果第六条不成立,如果signed的那个类型可以表示unsigned那个类型的所有值,unsigned的这个值会被转成signed的那个类型
  8. 如果第七条不成立,两个值都会被转成signed那个类型对应的unsigned类型

传递参数时的类型转换

一般来说,C++方法原型会控制传参时的类型转换,这个第七章会学到。但你也可以,虽然不太明智,放弃原型对传参的控制。在这个情况下,C++对char和short进行整体升级(integral promotion)同时为了保证对于经典C中大量代码的适应性,在向放弃了方法原型的方法传参时,C++会将所有float参数转为double。

强制转换

C++允许使用通过cast机制强制进行类型转换。强转有两种格式,比如你要将一个名为juice的int类型变量强转为long,你可以这么写:

(long) juice

long (juice)

强转并不会改变juice变量本身,而是将其值强转后作为新值返回。以上格式第一种是来自C语言,第二种则是C++才会采用的。

C++还有另一种更严格的强转操作符

static_cast<typeName> (value)


C++11的自动声明

C++11新添了让编译器根据初始化值推测类型的功能。通过auto关键字来使用这个功能。就在声明语句中用auto代替类型,编译器就会让变量类型和初始化的值得类型一致。

auto n = 100;  //n是int

auto x = 1.5;    //x是double

auto y = 1.3e2L   //y是long double

但auto并不是用在这种简单的地方的。甚至你可能会因为这个出错,比如假设x,y,z都应该是double,然后你写了以下语句:

auto x = 0.0;    //可以,x是double因为0.0是double

double y = 0;    //可以,0被转成了double

auto z = 0;       //不行,z是int因为0是int

auto更多还是用在复杂类型的处理,比如C++98种可能有这两句:

std::vector<double> scores;

std::vector<double>::iterator pv = scores.begin();

在C++11,你就可以这么写上面两句:

std::vector<double> scores;

auto pv = scores.begin();


总结


  • C++有两种基本类型,整数和浮点数。整数类型之间区别在于不同的存储空间还有它们是signed还是unsigned。整数类型从小到大排是:bool,char,signed char,unsigned char,short,signed short,unsigned short,int,unsigned int,long,unsigned long还有C++11的long long和unsigned long long。另外还有宽字符wchar_t以及C++11添加的char16_t和char32_t。
  • 字符是用其数码表示的,I/O系统决定了这些码应该被翻译成字符还是数字。
  • 浮点数有三种类型,float,double和long double。C++规定float不能比double大,而double不能比long double大。一般来说,float是32bits,double是64bits,long double在80到128bits之间。
  • C++提供了基本的算符:加减乘除和模。当两个算符共享了一个参数,C++的优先级和关联性决定了哪个算符先执行。
  • 当你赋值,在运算中使用多种类型还有使用cast进行强转,C++就会进行类型转换。一些转换是安全的,而另一些则需要注意一点。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章