第2章 变量和基本类型
导读
本章介绍了C++的几种典型数据类型,它们分别是:
●基本内置类型
●复合类型
●自定义数据结构
其中,char、int、long、float、double、bool是最常见的基本内置类型;引用和指针是两种最重要的复合类型;struct关键字和class关键字则常用于声明用户自定义的数据结构。
除此之外,本章的重要知识点还包括:变量的声明、定义和作用域,顶层const和底层const。C++11新标准引入了auto关键字和decltype关键字,利用这两个关键字,程序员就可以不必指定变量的数据类型,而把这项任务交给编译器自动执行。
练习2.1:类型int、long、longlong和short的区别是什么?无符号类型和带符号类型的区别是什么?float和double的区别是什么?
【出题思路】
本题旨在考查C++语言中几种主要算术类型的区别以及符号的表示方法和意义,读者需要重点理解几种算术类型在内存中的存储方式。
【解答】
在C++语言中,int、long、longlong和short都属于整型,区别是C++标准规定的尺寸的最小值(即该类型在内存中所占的比特数)不同。其中,short是短整型,占16位;int是整型,占16位;long和long long均为长整型,分别占32位和64位。C++标准允许不同的编译器赋予这些类型更大的尺寸。某一类型占的比特数不同,它所能表示的数据范围也不一样。
大多数整型都可以划分为无符号类型和带符号类型,在无符号类型中所有比特都用来存储数值,但是仅能表示大于等于0的值;带符号类型则可以表示正数、负数或0。
float和double分别是单精度浮点数和双精度浮点数,区别主要是在内存中所占的比特数不同,以及默认规定的有效位数不同。
练习2.2:计算按揭贷款时,对于利率、本金和付款分别应选择何种数据类型?说明你的理由。
【出题思路】
本题旨在考查C++语言中选择数据类型的方法。
【解答】
在实际应用中,利率、本金和付款既有可能是整数,也有可能是普通的实数。因此应该选择一种浮点类型来表示。在三种可供选择的浮点类型float、double和long double中,double和float的计算代价比较接近且表示范围更广,long double的计算代价则相对较大,一般情况下没有选择的必要。综合以上分析,选择double是比较恰当的。
练习2.3:读程序写结果。
【出题思路】
本题考查的知识点包括:无符号数的计算、带符号数的计算以及混合计算。
【解答】
程序的运行结果是:
u和u2都是无符号整数,因此u2-u得到了正确的结果(42-10=32);u-u2也能正确计算,但是因为直接计算的结果是-32,所以在表示为无符号整数时自动加上了模,在作者的编译环境中int占32位,因此加模的结果是4294967264。
i和i2都是带符号整数,因此中间两个式子的结果比较直观,42-10=32,10-42=-32。
在最后两个式子中,u和i分别是无符号整数和带符号整数,计算时编译器先把带符号数转换为无符号数,幸运的是,i本身是一个正数,因此转换后不会出现异常情况,两个式子的计算结果都是0。
不过需要提醒读者注意的是,一般情况下请不要在同一个表达式中混合使用无符号类型和带符号类型。因为计算前带符号类型会自动转换成无符号类型,当带符号类型取值为负时就会出现异常结果。
练习2.4:编写程序检查你的估计是否正确,如果不正确,请仔细研读本节直到弄明白问题所在。
【出题思路】
本题旨在考查同时含有无符号类型和带符号类型的表达式的计算问题。
【解答】
对于读程序写结果的题目,读者首先应该根据本节知识点独立思考,在脑海中勾勒出题目的考查角度和关键点,进而写出结果并在编程环境中加以验证。
通过练习本题,尤其是观察u-u2的异常结果,进一步理解无符号类型和带符号类型的区别。读者不妨思考,你是否能用上述4个变量组合出其他产生异常结果的式子?应该如何避免这种情况?
练习2.5:指出下述字面值的数据类型并说明每一组内几种字面值的区别:
【出题思路】
本题考查的知识点是利用特殊的前缀和后缀指定字面值的类型。
【解答】
(a)'a'表示字符a,L'a'表示宽字符型字面值a且类型是wchar_t,"a"表示字符串a,L"a"表示宽字符型字符串a。
(b)10是一个普通的整数类型字面值,10u表示一个无符号数,10L表示一个长整型数,10uL表示一个无符号长整型数,012是一个八进制数(对应的十进制数是10),0xC是一个十六进制数(对应的十进制数是12)。
(c)3.14是一个普通的浮点类型字面值,3.14f表示一个float类型的单精度浮点数,3.14L表示一个long double类型的扩展精度浮点数。
(d)10是一个整数,10u是一个无符号整数,10.是一个浮点数,10e-2是一个科学计数法表示的浮点数,大小为10*10-2=0.1。
练习2.6:下面两组定义是否有区别,如果有,请叙述之:
【出题思路】
本题旨在考查十进制数字与八进制数字的表示方法。
【解答】
第一组定义是正确的,定义了两个十进制数9和7。
第二组定义是错误的,编译时将报错。因为以0开头的数是八进制数,而数字9显然超出了八进制数能表示的范围,所以第二组定义无法被编译通过。
练习2.7:下述字面值表示何种含义?它们各自的数据类型是什么?
【出题思路】
本题考查的知识点是转义字符及利用特殊的后缀指定字面值的类型。
【解答】
(a)是一个字符串,包含两个转义字符,其中\145表示字符e,\012表示一个换行符,因此该字符串的输出结果是Who goes with Fergus?
(b)是一个科学计数法表示的扩展精度浮点数,大小为3.14*101=31.4。
(c)试图表示一个单精度浮点数,但是该形式在某些编译器中将报错,因为后缀f直接跟在了整数1024后面;改写成1024.f就可以了。
(d)是一个扩展精度浮点数,类型是long double,大小为3.14。
练习2.8:请利用转义字符编写一段程序,要求先输出2M,然后转到新一行。修改程序使其先输出2,然后输出制表符,再输出M,最后转到新一行。
【出题思路】
本题旨在考查转义字符的使用。
【解答】
主函数的前两行分别实现了题目中要求的两种输出形式。
其中,字符串"2\x4d\012"先输出字符2,紧接着利用转义字符\x4d输出字符M,最后利用转义字符\012转到新一行。
字符串"2\tM\n"先输出字符2,然后利用转义字符\t输出一个制表符,接着输出字符M,最后利用转义字符\n转到新一行。
读者可以发现,输出同一个字符有多种方式可供选择。例如,可以直接输出字符M,也可以通过转义字符\x4d输出字符M;可以用转义字符\012换行,也可以用转义字符\n换行。
练习2.9:解释下列定义的含义。对于非法的定义,请说明错在何处并将其改正。
【出题思路】
本题旨在考查变量定义与初始化,其中列表初始化是重点也是难点。
【解答】
(a)是错误的,输入运算符的右侧需要一个明确的变量名称,而非定义变量的语句,改正后的结果是:
(b)引发警告,该语句定义了一个整型变量i,但是试图通过列表初始化的方式把浮点数3.14赋值给i,这样做将造成小数部分丢失,是一种不被建议的窄化操作。
(c)是错误的,该语句试图将9999.99分别赋值给salary和wage,但是在声明语句中声明多个变量时需要用逗号将变量名隔开,而不能直接用赋值运算符连接,改正后的结果是:
(d)引发警告,该语句定义了一个整型变量i,但是试图把浮点数3.14赋值给i,这样做将造成小数部分丢失,与(b)一样是不被建议的窄化操作。
练习2.10:下列变量的初值分别是什么?
【出题思路】
本题旨在考查默认初始化的几种不同情况,如全局变量和局部变量的区别、内置类型和复合类型的区别。
【解答】
对于string类型的变量来说,因为string类型本身接受无参数的初始化方式,所以不论变量定义在函数内还是函数外都被默认初始化为空串。
对于内置类型int来说,变量global_int定义在所有函数体之外,根据C++的规定,global_int默认初始化为0;而变量local_int定义在main函数的内部,将不被初始化,如果程序试图拷贝或输出未初始化的变量,将遇到一个未定义的奇异值。
练习2.11:指出下面的语句是声明还是定义:
【出题思路】
本题旨在考查变量声明和定义的关系。
【解答】
声明与定义的关系是:声明使得名字为程序所知,而定义负责创建与名字关联的实体。(a)定义了变量ix,(b)声明并定义了变量iy,(c)声明了变量iz。
练习2.12:请指出下面的名字哪个是非法的?
【出题思路】
本题旨在考查C++标识符的命名规则。
【解答】
(a)是非法的,因为double是C++关键字,代表一种数据类型,不能作为变量的名字。
(c)是非法的,在标识符中只能出现字母、数字和下画线,不能出现符号-,如果改成“int catch_22;”就是合法的了。
(d)是非法的,因为标识符必须以字母或下画线开头,不能以数字开头。
(b)和(e)是合法的命名。
练习2.13:下面程序中j的值是多少?
【出题思路】
本题旨在考查全局作用域与局部作用域的关系。
【解答】
j的值是100。C++允许内层作用域重新定义外层作用域中已有的名字,在本题中,int i=42;位于外层作用域,但是变量i在内层作用域被重新定义了,因此真正赋予j的值是定义在内层作用域中的i的值,即100。
练习2.14:下面的程序合法吗?如果合法,它将输出什么?
【出题思路】
本题旨在考查嵌套作用域中变量的定义和使用。
【解答】
该程序是合法的,输出结果是100 45。
该程序存在嵌套的作用域,其中for循环之外是外层作用域,for循环内部是内层作用域。首先在外层作用域中定义了i和sum,但是在for循环内部i被重新定义了,因此for循环实际上是从i=0循环到了i=9,内层作用域中没有重新定义sum,因此sum的初始值是0并在此基础上依次累加。最后一句输出语句位于外层作用域中,此时在for循环内部重新定义的i已经失效,因此实际输出的仍然是外层作用域的i,值为100;而sum经由循环累加,值变为了45。
练习2.15:下面的哪个定义是不合法的?为什么?
【出题思路】
本题旨在考查引用的含义,应该明确引用与对象的关系。
【解答】
(b)是非法的,引用必须指向一个实际存在的对象而非字面值常量。
(d)是非法的,因为我们无法令引用重新绑定到另外一个对象,所以引用必须初始化。
(a)和(c)是合法的。
练习2.16:考查下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了什么样的操作?
【出题思路】
本题旨在考查引用的含义及用法,应该明确引用与对象的关系。
【解答】
(a)是合法的,为引用赋值实际上是把值赋给了与引用绑定的对象,在这里是把3.14159赋给了变量d。
(b)是合法的,以引用作为初始值实际上是以引用绑定的对象作为初始值,在这里是把i的值赋给了变量d。
(c)是合法的,把d的值赋给了变量i,因为d是双精度浮点数而i是整数,所以该语句实际上执行了窄化操作。
(d)是合法的,把d的值赋给了变量i,与上一条语句一样执行了窄化操作。
练习2.17:执行下面的代码段将输出什么结果?
【出题思路】
本题旨在考查引用的含义及用法,应该明确引用与对象的关系。
【解答】
程序的输出结果是10 10。
引用不是对象,它只是为已经存在的对象起了另外一个名字,因此ri实际上是i的别名。在上述程序中,首先将i赋值为5,然后把这个值更新为10。因为ri是i的引用,所以它们的输出结果是一样的。
练习2.18:编写代码分别更改指针的值以及指针所指对象的值。
【出题思路】
本题旨在考查指针的含义,以及指针本身的值与指针所指对象值的区别,重点掌握解引用运算符的使用方法。
【解答】
一个满足要求的简单示例如下所示:
程序的输出结果是:
在上述示例中,首先定义了两个整型变量i和j以及一个整型指针p,初始情况下令指针p指向变量i,此时分别输出p的值(即p所指对象的内存地址)以及p所指对象的值,得到0x28fef8和5。
随后依次更改指针的值以及指针所指对象的值。p=&j;更改了指针的值,令指针p指向另外一个整数对象j。*p=20;和j=30是两种更改指针所指对象值的方式,前者显式地更改指针p所指的内容,后者则通过更改变量j的值实现同样的目的。
练习2.19:说明指针和引用的主要区别。
【出题思路】
指针和引用同为复合类型,都与内存中实际存在的对象有联系。本题旨在考查二者的主要区别。
【解答】
指针“指向”内存中的某个对象,而引用“绑定到”内存中的某个对象,它们都实现了对其他对象的间接访问,二者的区别主要有两方面:
第一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以指向几个不同的对象;引用不是一个对象,无法令引用重新绑定到另外一个对象。
第二,指针无须在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值;引用则必须在定义时赋初值。
练习2.20:请叙述下面这段代码的作用。
【出题思路】
本题旨在考查运算符*的两种含义:一是指针声明符,二是解引用运算符。
【解答】
这段代码首先定义了一个整型变量i并设其初值为42;接着定义了一个整型指针pl,令其指向变量i;最后取出pl所指的当前值,计算平方后重新赋给pl所指的变量i。
第二行的*表示声明一个指针,第三行的*表示解引用运算,即取出指针pl所指对象的值。
练习2.21:请解释下述定义。在这些定义中有非法的吗?如果有,为什么?
【出题思路】
本题旨在考查指针的声明和定义。
【解答】
(a)是非法的,dp是一个double指针,而i是一个int变量,类型不匹配。
(b)是非法的,不能直接把int变量赋给int指针,正确的做法是通过取地址运算&i得到变量i在内存中的地址,然后再将该地址赋给指针。
(c)是合法的。
练习2.22:假设p是一个int型指针,请说明下述代码的含义。
【出题思路】
本题旨在考查指针的值与指针所指对象的值的区别,巧妙之处是把p和*p作为if语句的条件,通过判断其是否为真帮助读者加深理解。
【解答】
指针p作为if语句的条件时,实际检验的是指针本身的值,即指针所指的地址值。如果指针指向一个真实存在的变量,则其值必不为0,此时条件为真;如果指针没有指向任何对象或者是无效指针,则对p的使用将引发不可预计的结果。
解引用运算符*p作为if语句的条件时,实际检验的是指针所指的对象内容,在上面的示例中是指针p所指的int值。如果该int值为0,则条件为假;否则,如果该int值不为0,对应条件为真。
一个简单的示例如下所示:
在这段程序中,p和p1是两个整型指针,其中p1被定义为空指针(nullptr),p则指向整数i。在3个判断条件中,p1指向为空,意即指向地址为0,条件不满足;p指向i,在内存中有一个实际的地址值且不为0,因此该条件满足;*p表示p所指对象的内容,即整数i的值,因为程序开始处i被赋予了初值0,所以这个条件不满足。
综合以上分析,程序的输出结果是p pass。
练习2.23:给定指针p,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断的思路;如果不能,说明原因。
【出题思路】
本题旨在考查指针初始化,读者应该熟悉并掌握C++11的新语法特征nullptr。
【解答】
在C++程序中,应该尽量初始化所有指针,并且尽可能等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它有没有指向一个具体的对象了。其中,nullptr是C++11新标准刚刚引入的一个特殊字面值,它可以转换成任意其他的指针类型。在此前提下,判断p是否指向合法的对象,只需把p作为if语句的条件即可,如果p的值是nullptr,则条件为假;反之,条件为真。
如果不注意初始化所有指针而贸然判断指针的值,则有可能引发不可预知的结果。一种处理的办法是把if(p)置于try结构中,当程序块顺利执行时,表示p指向了合法的对象;当程序块出错跳转到catch语句时,表示p没有指向合法的对象。
练习2.24:在下面这段代码中为什么p合法而lp非法?
【出题思路】
本题主要考查void*指针的含义和用法。
【解答】
p是合法的,因为void*是一种特殊的指针类型,可用于存放任意对象的地址。
lp是非法的,因为lp是一个长整型指针,而i只是一个普通整型数,二者的类型不匹配。
练习2.25:说明下列变量的类型和值。
【出题思路】
本题旨在考查指针和引用这两种复合类型的声明。
【解答】
(a)ip是一个整型指针,指向一个整型数,它的值是所指整型数在内存中的地址;i是一个整型数;r是一个引用,它绑定了i,可以看作是i的别名,r的值就是i的值。
(b)i是一个整型数;ip是一个整型指针,但是它不指向任何具体的对象,它的值被初始化为0。
(c)ip是一个整型指针,指向一个整型数,它的值是所指整型数在内存中的地址;ip2是一个整型数。
练习2.26:下面哪些句子是合法的?如果有不合法的句子,请说明为什么?
【出题思路】
本题旨在考查const限定符的用法,尤其是const对象的定义、初始化和运算。
【解答】
本题的所有语句应该被看作是顺序执行的,即形如:
(a)是非法的,const对象一旦创建后其值就不能改变,所以const对象必须初始化。该句应修改为const int buf = 10。
(b)和(c)是合法的。
(d)是非法的,sz是一个const对象,其值不能被改变,当然不能执行自增操作。
练习2.27:下面的哪些初始化是合法的?请说明原因。
【出题思路】
本题旨在考查常量引用、常量指针和指向常量的指针的初始化方法。
【解答】
(a)是非法的,非常量引用r不能引用字面值常量0。
(b)是合法的,p2是一个常量指针,p2的值永不改变,即p2永远指向变量i2。
(c)是合法的,i是一个常量,r是一个常量引用,此时r可以绑定到字面值常量0。
(d)是合法的,p3是一个常量指针,p3的值永不改变,即p3永远指向变量i2;同时p3指向的是常量,即我们不能通过p3改变所指对象的值。
(e)是合法的,p1指向一个常量,即我们不能通过p1改变所指对象的值。
(f)是非法的,引用本身不是对象,因此不能让引用恒定不变。
(g)是合法的,i2是一个常量,r是一个常量引用。
练习2.28:说明下面的这些定义是什么意思,挑出其中不合法的。
【出题思路】
本题旨在考查常量引用、常量指针和指向常量的指针的定义及区别。
【解答】
(a)是非法的,cp是一个常量指针,因其值不能被改变,所以必须初始化。
(b)是非法的,cp2是一个常量指针,因其值不能被改变,所以必须初始化。
(c)是非法的,ic是一个常量,因其值不能被改变,所以必须初始化。
(d)是非法的,p3是一个常量指针,因其值不能被改变,所以必须初始化;同时p3指向的是常量,即我们不能通过p3改变所指对象的值。
(e)是合法的,但是p没有指向任何实际的对象。
练习2.29:假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?请说明原因。
【出题思路】
本题旨在考查常量引用、常量指针和指向常量的指针的赋值方法。
【解答】
(a)是合法的,常量ic的值赋给了非常量i。
(b)是非法的,普通指针p1指向了一个常量,从语法上说,p1的值可以随意改变,显然是不合理的。
(c)是非法的,普通指针p1指向了一个常量,错误情况与上一条类似。
(d)是非法的,p3是一个常量指针,不能被赋值。
(e)是非法的,p2是一个常量指针,不能被赋值。
(f)是非法的,ic是一个常量,不能被赋值。
练习2.30:对于下面的这些语句,请说明对象被声明成了顶层const还是底层const?
【出题思路】
本题旨在考查顶层const和底层const的区别,读者应明确顶层const表示任意的对象是常量,而底层const与指针和引用等复合类型的基本类型部分有关。
【解答】
v2和p3是顶层const,分别表示一个整型常量和一个整型常量指针;p2和r2是底层const,分别表示它们所指(所引用)的对象是常量。
练习2.31:假设已有上一个练习中所做的那些声明,则下面的哪些语句是合法的?请说明顶层const和底层const在每个例子中有何体现。
【出题思路】
本题旨在考查顶层const和底层const对于拷贝操作的影响。
【解答】
在执行拷贝操作时,顶层const和底层const区别明显。其中,顶层const不受影响,这是因为拷贝操作并不会改变被拷贝对象的值。底层const的限制则不容忽视,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行。
r1=v2;是合法的,r1是一个非常量引用,v2是一个常量(顶层const),把v2的值拷贝给r1不会对v2有任何影响。
p1=p2;是非法的,p1是普通指针,指向的对象可以是任意值,p2是指向常量的指针(底层const),令p1指向p2所指的内容,有可能错误地改变常量的值。
p2=p1;是合法的,与上一条语句相反,p2可以指向一个非常量,只不过我们不会通过p2更改它所指的值。
p1=p3;是非法的,p3包含底层const定义(p3所指的对象是常量),不能把p3的值赋给普通指针。
p2=p3;是合法的,p2和p3包含相同的底层const,p3的顶层const则可以忽略不计。
练习2.32:下面的代码是否合法?如果非法,请设法将其修改正确。
【出题思路】
本题旨在考查指针的使用。
【解答】
上述代码是非法的,null是一个int变量,p是一个int指针,二者不能直接绑定。仅从语法角度来说,可以将代码修改为:
显然,这种改法与代码的原意不一定相符。另一种改法是使用nullptr:
练习2.33:利用本节定义的变量,判断下列语句的运行结果。
【出题思路】
本题旨在考查auto说明符与复合类型、常量混合使用时的各种情形。首先,使用引用其实是使用引用的对象,所以当引用被用作初始值时,真正参与初始化的其实是引用对象的值,编译器以引用对象的类型作为auto的推断类型。其次,auto一般会忽略掉顶层const,同时保留底层const。
【解答】
前3条赋值语句是合法的,原因如下:
r是i的别名,而i是一个整数,所以a的类型推断结果是一个整数;ci是一个整型常量,在类型推断时顶层const被忽略掉了,所以b是一个整数;cr是ci的别名,而ci是一个整型常量,所以c的类型推断结果是一个整数。因为a、b、c都是整数,所以为其赋值42是合法的。
后3条赋值语句是非法的,原因如下:
i是一个整数,&i是i的地址,所以d的类型推断结果是一个整型指针;ci是一个整型常量,&ci是一个整型常量的地址,所以e的类型推断结果是一个指向整型常量的指针;ci是一个整型常量,所以g的类型推断结果是一个整型常量引用。因为d和e都是指针,所以不能直接用字面值常量为其赋值;g绑定到了整型常量,所以不能修改它的值。
练习2.34:基于上一个练习中的变量和语句编写一段程序,输出赋值前后变量的内容,你刚才的推断正确吗?如果不对,请反复研读本节的示例直到你明白错在何处为止。
【出题思路】
本题旨在考查auto说明符与复合类型、常量混合使用时的各种情形。
【解答】
基于上一个练习中的变量和语句编写的程序如下所示:
程序的输出结果是:
练习2.35:判断下列定义推断出的类型是什么,然后编写程序进行验证。
【出题思路】
本题旨在考查auto说明符与复合类型、常量混合使用时的各种情形。
【解答】
由题意可知,i是一个整型常量,j的类型推断结果是整数,k的类型推断结果是整型常量,p的类型推断结果是指向整型常量的指针,j2的类型推断结果是整数,k2的类型推断结果是整数。
用于验证的程序是:
练习2.36:关于下面的代码,请指出每一个变量的类型以及程序结束时它们各自的值。
【出题思路】
本题旨在考查decltype与引用的关系。对于decltype所用的表达式来说,如果变量名加上一对括号,得到的类型与不加括号时会有不同。具体来说,如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式,从而推断得到引用类型。
【解答】
在本题的程序中,初始情况下a的值是3、b的值是4。decltype(a) c=a;使用的是一个不加括号的变量,因此c的类型就是a的类型,即该语句等同于int c=a;,此时c是一个新整型变量,值为3。decltype((b)) d=a;使用的是一个加了括号的变量,因此d的类型是引用,即该语句等同于int &d=a;,此时d是变量a的别名。
执行++c; ++d;时,变量c的值自增为4,因为d是a的别名,所以d自增1意味着a的值变成了4。当程序结束时,a、b、c、d的值都是4。
练习2.37:赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果i是int,则表达式i=x的类型是int&。根据这一特点,请指出下面的代码中每一个变量的类型和值。
【出题思路】
decltype的参数既可以是普通变量,也可以是一个表达式。当参数是普通变量时,推断出的类型就是该变量的类型;当参数是表达式时,推断出的类型是引用。
【解答】
根据decltype的上述性质可知,c的类型是int,值为3;表达式a=b作为decltype的参数,编译器分析表达式并得到它的类型作为d的推断类型,但是不实际计算该表达式,所以a的值不发生改变,仍然是3;d的类型是int&,d是a的别名,值是3;b的值一直没有发生改变,为4。
练习2.38:说明由decltype指定类型和由auto指定类型有何区别。请举出一个例子,decltype指定的类型与auto指定的类型一样;再举一个例子,decltype指定的类型与auto指定的类型不一样。
【出题思路】
auto和decltype是两种类型推断的方式,本题旨在考查二者的区别和联系。
【解答】
auto和decltype的区别主要有三个方面:
第一,auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
第二,编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto一般会忽略掉顶层const,而把底层const保留下来。与之相反,decltype会保留变量的顶层const。
第三,与auto不同,decltype的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。
一个用以说明的示例如下所示:
对于第一组类型推断来说,a是一个非常量整数,c1的推断结果是整数,c2的推断结果也是整数,c3的推断结果由于变量a额外加了一对括号所以是整数引用。c1、c2、c3依次执行自增操作,因为c3是变量a的别名,所以c3自增等同于a自增,最终a、c1、c2、c3的值都变为4。
对于第二组类型推断来说,d是一个常量整数,含有顶层const,使用auto推断类型自动忽略掉顶层const,因此f1的推断结果是整数;decltype则保留顶层const,所以f2的推断结果是整数常量。f1可以正常执行自增操作,而常量f2的值不能被改变,所以无法自增。
练习2.39:编译下面的程序观察其运行结果,注意,如果忘记写类定义体后面的分号会发生什么情况?记录下相关信息,以后可能会有用。
【出题思路】
本题旨在考查类定义的语法规范,尤其要注意类体结束之后的分号必不可少。
【解答】
该程序无法编译通过,原因是缺少了一个分号。因为类体后面可以紧跟变量名以示对该类型对象的定义,所以在类体右侧表示结束的花括号之后必须写一个分号。稍作修改,该程序就可以编译通过了。
练习2.40:根据自己的理解写出Sales_data类,最好与书中的例子有所区别。
【出题思路】
类的设计源于实际应用,设计Sales_data类的关键是理解在销售过程中应该包含哪些数据元素,同时为每个元素设定合理的数据类型。
【解答】
原书中的程序包含3个数据成员,分别是bookNo(书籍编号)、units_sold(销售量)、revenue(销售收入),新设计的Sales_data类细化了销售收入的计算方式,在保留bookNo和units_sold的基础上,新增了sellingprice(零售价、原价)、saleprice(实售价、折扣价)、discount(折扣),其中discount=saleprice/sellingprice。
练习2.41:使用你自己的Sales_data类重写1.5.1节(第20页)、1.5.2节(第21页)和1.6节(第22页)的练习。眼下先把Sales_data类的定义和main函数放在同一个文件里。
【出题思路】
本题旨在考查如何利用自定义类创建对象并执行基本操作。
【解答】
练习2.42:根据你自己的理解重写一个Sales_data.h头文件,并以此为基础重做2.6.2节(第67页)的练习。
【出题思路】
本题旨在考查自定义头文件并基于头文件编写程序的方法。
【解答】
将上题答案中类的定义放置于Sales_data.h头文件中,具体的使用方法与第1章练习1.20~练习1.25类似。
Sales_data.h头文件的内容是: