变量和基本类型
C++是一种静态数据类型语言,它的类型检查发生在编译时。因此,编译器必须知道程序中每一个变量对应的数据类型。
数据类型是程序的基础:它告诉我们数据的意义以及我们能在数据上执行的操作。
基本内置类型
算术类型
算术类型分为两类:整型(integral type,包括字符和布尔类型在内)和浮点型。
算术类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差别。
布尔类型( bool)的取值是真(true)或者假(false)。
基本的字符类型是char,一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值。也就是说,一个 char的大小和一个机器字节一样。
其他字符类型用于扩展字符集,如wchar_t、char16_t、char32_t。wchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符,类型 char16_t和char32_t则为Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)。
内置类型的机器实现
大多数计算机以2的整数次幂个比特作为块来处理内存,可寻址的最小内存块称为“字节(byte”,存储的基本单元称为“字(word”,它通常由几个字节组成。在C++语言中,一个字节要至少能容纳机器基本字符集中的字符。大多数机器的字节由8比特构成,字则由32或64比特构成,也就是4或8字节。
如果位置736424处的对象类型是float,并且该机器中float 以32比特存储,那么我们就能知道这个对象的内容占满了整个字。这个float数的实际值依赖于该机器是如何存储浮点数的。或者如果位置736424处的对象类型是unsigned char,并且该机器使用ISO-Latin-1字符集,则该位置处的字节表示一个分号。
带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值。
类型int、short、long和 long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。类型unsigned int可以缩写为unsigned。
与其他整型不同,字符型被分为了三种:char、 signed char和unsigned char.特别需要注意的是:类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。
C++标准并没有规定带符号类型应如何表示,但是约定了在表示范围内正值和负值的量应该平衡。因此,8比特的signed char理论上应该可以表示-127至127区间内的值,大多数现代计算机将实际的表示范围定为-128至127。
如何选择类型
当明确知晓数值不可能为负时,选用无符号类型。
在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型 char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned ch
类型转换
对象的类型定义了对象能包含的数据和能参与的运算,其中一种运算被大多数类型支持,就是将对象从一种给定的类型转换(convert)为另一种相关类型。
当在程序的某处我们使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换
1 | bool b = 42; // b为真 |
当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。
当我们把一个布尔值赋给非布尔类型时,初始值为false 则结果为0,初始值为true则结果为1。
当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小unsigned char所得的结果是255。(负数就从该符号的范围倒着数)
当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
避免无法预知和依赖于实现环境的行为
程序也应该尽量避免依赖于实现环境的行为。如果我们把int的尺寸看成是一个确定不变的已知值,那么这样的程序就称作不可移植(nonportable)。当程序移植到别的机器上后,依赖于实现环境的程序就可能发生错误。要从过去的代码中定位这类错误可不是一件轻松愉快的工作。
在第一个输出表达式里,两个(负)整数相加并得到了期望的结果。在第二个输出表达式里,相加前首先把整数-42转换成无符号数。把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。
1 | unsigned u = 10;int i = -42; |
当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值
1 | for (unsigned u = 10; u >= 0; --u) |
练习
1 | unsigned u = 10, u2 = 42; |
字面值常量
整型和浮点型字面值
我们可以将整型字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0x开头的代表十六进制数。例如,我们能用下面的任意一种形式来表示数值20:
20/* 十进制*/
024/八进制/
0x14/十六进制/
浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识:3.14159
3.14159EO
0.0e0
.001
默认的,浮点型字面值是一个double
字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
‘a’//字符字面值
“Hello world! “//字符串字面值
字符串字面值的类型实际上是由常量字符构成的数组( array)。编译器在每个字符串的结尾处添加一个空字符(( ‘\0’),因此,字符串字面值的实际长度要比它的内容多1。例如,字面值’A’表示的就是单独的字符A,而字符串”A”则代表了一个字符的数组,该数组包含两个字符:一个是字母A、另一个是空字符。
指定字面值的类型
1 | L' a' |
练习
1 | 指出下述字面值的数据类型并说明每一组内几种字面值的区别: |
变量
变量提供一个具名的、可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。对C++程序员来说,“变量 (variable)”和“对象(obiect)”一般可以百换使用。
变量定义
变量定义的基本形式是:首先是类型说明符(type specifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值:
1 | int sum=0,value, |
初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)了。用于初始化变量的值可以是任意复杂的表达式。当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量。
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
列表初始化
C++语言定义了初始化的好几种不同形式
1 | int unitssold=0; |
作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用,这种初始化的形式被称为列表初始化 (list initialization)。现在,无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值了。
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:
1 | long doubleld=3.1415926536; |
使用 long double 的值初始化 int 变量时可能丢失数据,所以编译器拒绝了 a和b的初始化请求。其中,至少ld 的小数部分会丢失掉,而且 nt 也可能存不下d 的整数部分。
默认初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。
定义于任何函数体之外的变量被初始化为 0。定义在函数体内部的内置类型变量将不被初始化 (uninitialized)。
一个未被初始化的内置类型变量的值是未定义的,如果试图贝或以其他形式访问此类值将引发错误。
每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也由类自己决定。如果类允许这种行为,它将决定对象的初始值到底是什么。绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值。
定义于任何函数体之外的变量被初始化为 0。
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。
类的对象如果没有显式地初始化,则其值由类确定
练习
解释下列定义的含义,对于非法的定义,请说明错在何处并将其改正。
(a) std::cin >> int input_value;
(b) int i = { 3.14 };
(c)double salary = wage = 9999.99;
(d) int i = 3.14;
(编译时记得使用C++11标准编译”-std=c++11”)
(a)非法,>>运算符后不能定义;
(b)非法,不能执行强制转换;
©非法,同一语句的初始化应该分别进行;
(d)合法,已强制转换。
下列变量的初值分别是什么?
std::string global_str;
int global_int;
int main()
{
int local_int;
std::string local_str;
}global_str,local_str为空字符串;
global_int为0;
local_int未初始化,没有初始值
变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译。
如果将程序分为多个文件,则需要有在文件间共享代码的方法。例如,一个文件的代码可能需要使用另一个文件中定义的变量。一个实际的例子是 std::cout和std::cin,它们定义于标准库,却能被我们写的程序使用。
为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
1 | //声明i而非定义i |
任何包含了显式初始化的声明即成为定义。我们能给由extern 关键字标记的变量赋一个初始值,但是这么做也就抵消了extern 的作用。extern 语句如果包含初始值就不再是声明,而变成定义了:
1 | extern doublepi=3.1416;//定义 |
在函数体内部,如果试图初始化一个由extern 关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以被多次声明
练习
指出下面的语句是声明还是定义:
(a) extern int ix = 1024;
(b) int iy;
(c)extern int iz;
(a)定义;
(b)定义;
(c)声明。
标识符
C++的标识符(identifier)由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感:
同时,C++也为标准库保留了一些名字。用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
练习
请指出下面的名字中哪些是非法的?
(a) int double = 3.14;
(b) int _;
(c)int catch-22;
(d) int 1_or_2 = 1;
(e) double Double = 3.14;
(a)非法,关键词;
(b)合法;
(c)非法;
(d)非法,字母、下划线开头;
(e)合法。
名字的作用域
作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字:
1 | //输出#3:显式地访问全局变量reused; |
练习
下面程序中 j 的值是多少?
int i = 42;
int main()
{
int i = 100;
int j = i;
}100
下面的程序合法吗?如果合法,它将输出什么?
int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
sum += i;
std::cout << i << “ “ << sum << std::endl;
复合类型
复合类型(compoundtype)是指基于其他类型定义的类型.
引用
引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:
1 | int ival=1024; |
在初始化变量时,初始值会被拷贝到新建的对象中。
然而定义引用时,程序把引用和它的初始值绑定 (bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
1 | refVal=2;//把2赋给 refVal指向的对象,此处即是赋给了ival |
为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值:
1 | //正确:refVal3绑定到了那个与refVal绑定的对象上,这里就是绑定到ival上 |
因为引用本身不是一个对象,所以不能定义引用的引用。
引用的定义
1 | int i=1024,i2=2048;//i和12都是int |
引用的类型都要和与之绑定的对象严格匹配。而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起
1 | int &refVal4=10; //错误:引用类型的初始值必须是一个对象 |
除了初始化要注意一些细节。不能换引用值,其他都和普通变量无异。
练习
下面的哪个定义是不合法的?为什么?
(a) int ival = 1.01;
(b) int &rval1 = 1.01;
(c)int &rval2 = ival;
(d) int &rval3;
(a)合法;隐式类型转换(b)不合法,引用类型的初始值必须是一个对象;
(c)合法;(d)不合法,引用类型必须初始化。
考察下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了哪些操作?
int i = 0, &r1 = i; double d = 0, &r2 = d;
(a) r2 = 3.14159;
(b) r2 = r1;
(c) i = r2;
(d) r1 = d;
(a)合法,将3.14159赋值给r2所引用的对象d。
(b)合法,将r1所引用的对象i的值赋值给r2所引用的对象d。
(c)合法,将r2所引用的对象d的值赋值给i。
(d)合法,将d的值赋值给r1所引用的对象i。
执行下面的代码段将输出什么结果?
int i, &ri = i;
std::cout << i << “ “ << ri << std::endl;
i = 5; ri = 10;
std::cout << i << “ “ << ri << std::endl;
随机 随机 10 10
函数体外初始化为0,函数体内不初始化,i和ri指向同一对象。
指针
指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&):
1 | int ival=42; |
第二条语句把p定义为一个指向int 的指针,随后初始化p令其指向名为ival的int对象。因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
1 | double dval; |
因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。
利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象
1 | int ival=42; |
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值:
1 | *p=0;//由符号*得到指针p所指的对象,即可经由p为变量ival赋值 |
如上述程序所示,为*p 赋值实际上是为 p 所指的对象赋值
某些符号有多重含义
像&和*这样的符号,既能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义:
1 | int i=42; |
空指针
空指针(null pointer)不指向任何对象,在试图使用个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法:
1 | int *pl = nullptr; //等价于int *pl =0; |
得到空指针最直接的办法就是用字面值nullptr来初始化指针
过去的程序还会用到一个名为NULL的预处理变量(preprocessor variable)来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0.
预处理变量不属于命名空间std,它由预处理器负责管理,因此我们可以直接使用预处理变量而无须在前面加上std::,当用到一个预处理变量时,预处理器会自动地将它替换为实际值
把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。
1 | int zero = 0; |
赋值和指针
指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象:
1 | int i=42; |
其他指针操作
只要指针拥有一个合法值,就能将它用在条件表达式中。和采用算术值作为条件遵循的规则类似,如果指针的值是0,条件取false,任何非О指针对应的条件值都是true。
对于两个类型相同的合法指针,可以用相等操作符(==)或不相等操作符(!=)来比较它们,比较的结果是布尔类型。如果两个指针存放的地址值相同,则它们相等;反之它们不相等。
void*指针
void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
1 | double obj = 3.14, *pd = &obj; //正确:void*能存放任意类型对象的地址 |
以 void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象
练习
说明指针和引用的主要区别
1.指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象;2.指针无须在定义时赋初值。
请叙述下面这段代码的作用。
1 | int i = 42; |
p指向i,i最后的值为1746(42*42)
请解释下述定义。在这些定义中有非法的吗?如果有,为什么?
1 | int i = 0; |
假设 p 是一个 int 型指针,请说明下述代码的含义。
1 | if (p) // ... |
在下面这段代码中为什么 p 合法而 lp 非法?
1 | int i = 42; |
理解复合类型的声明
在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量:
1 | // i是一个int型的数,p是一个int型指针,r是一个int型引用 |
int* p基本数据类型是int而非int。仅仅是修饰了p而已,对该声明语句中的其他变量,它并不产生任何作用:
指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
1 | int ival =1024; |
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
1 | int i = 42; |
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。
练习
1 | 说明下列变量的类型和值。 |
const 限定符
此书中指针常量一类概念和我们常说的是相反的。
本书推崇从右往左看等号左边的符号。
指针常量此书中叫做常量指针。 const pointer
常量指针称为指向常量的指针 pointer to const
有时我们希望定义这样一种变量,它的值不能被改变。
使用关键字const对变量的类型加以限定:
1 | const int bufsize = 512; |
这样就把bufSize定义成了一个常量。任何试图为buffSize赋值的行为都将引发错误:
1 | bufSize = 512; //错误:试图向const对象写值 |
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。一如既往,初始值可以是任意复杂的表达式:
1 | const int i = get_size(); |
在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是 const都无关紧要:
1 | int i =42; |
尽管ci是整型常量,但无论如何ci中的值还是一个整型数。ci的常量特征仅仅在执行改变ci的操作时才会发挥作用。当用ci去初始化j时,根本无须在意ci是不是一个常量。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。
当以编译时初始化的方式定义一个const对象时,就如对 bufSize的定义一样:
1 | const int bufSize = 512;//输入缓冲区大小` |
编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
练习
下面哪些语句是合法的?如果不合法,请说明为什么?
const int buf;
int cnt = 0;
const int sz = cnt;
++cnt; ++sz;
(a)不合法,const int必须初始化;
(b)合法;
(c)合法;
(d)++cnt,合法;++sz,不合法,const int不能改变。
const 的引用
此书中指针常量与常量指针和网上相反
可以把引用绑定到 const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
1 | const int ci = 1024; |
初始化和对const的引用
普通引用不允许表达式,常引用可以。普通引用不能绑定常量,常引用可以绑定普通变量,他自己不能改此变量但别人可以改。
引用的类型必须与其所引用对象的类型一致,但是有两个例外。
第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
1 | int i =42;· |
此处ri引用了一个int型的数。对 ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:
1 | double dval = 3.14; |
1 | //由双精度浮点数生成一个临时的整型常量 |
对const的引用可能引用一个并非const的对象
1 | int i=42; |
r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改。
指针和const
与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针( pointer to const)不能用于改变其所指对象的值。这就是我们俗称的常量指针
要想存放常量对象的地址,只能使用指向常量的指针:
1 | const double pi = 3.14; |
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象:
1 | double dval = 3.14; |
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
试试这样想吧:所谓指向常量的指针或引用,不过是指针或引用目以为定罢了,它们觉得自已己指向了常量,所以自觉地不去改变所指对象的值。
const指针
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,此处的常量指针就是我们俗称的指针常量
而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
1 | int errNumb = 0; |
练习
下面的哪些初始化是合法的?请说明原因。
1 | int i = -1, &r = 0; |
说明下面的这些定义是什么意思,挑出其中不合法的。
1 | int i, *const cp; |
假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?请说明原因。
1 | i = ic; |
顶层const
用名词顶层const( top-level const)表示指针本身是个常量,而用名词底层const (low-level const)表示指针所指的对象是一个常量。
1 | int i = 0; |
作用后不能改变本身的值是顶层,不能改变所指对象的值是底层
当执行对象的铂贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响,
底层 const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
1 | int *p= p3; |
练习
对于下面的这些语句,请说明对象被声明成了顶层const还是底层const?
1 | const int v2 = 0; int v1 = v2; |
假设已有上一个练习中所做的那些声明,则下面的哪些语句是合法的?请说明顶层const和底层const在每个例子中有何体现。
1 | r1 = v2; |
constexpr和常量表达式
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:
1 | const int max__files = 20; |
尽管staff _size的初始值是个字面值常量,但由于它的数据类型只是一个普通int而非const int,所以它不属于常量表达式。另一方面,
尽管sz本身是一个常量,但它
的具体值直到运行时才能获取到,所以也不是常量表达式。
constexpr变量
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:
1 | constexpr int mf = 20; |
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”(literal type)。
到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。自定义类sales_item、IO 库、string 类型则不属于字面值类型,也就不能被定义成constexpr。
处理类型
随着程序越来越复杂,程序中用到的类型也越来越复杂,它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。有时候根本搞不清到底需要的类型是什么。
类型别名
有两种方法可用于定义类型别名。传统的方法是使用关键字
1 | typedef:typedef double wages;// wages是double的同义词 |
新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
1 | using ST = Sales_item; |
auto类型说明符
编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如 double)不同,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值:
1 | //由val1和val2相加的结果可以推断出item的类型 |
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
1 | auto i = 0, *p = &i;/正确:i是整数、p是整型指针 |
复合类型、常量和auto
其次,auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
1 | const int ci = i, &cr = ci; |
对常量对象取地址是一种底层const
如果希望推断出的auto类型是一个顶层const,需要明确指出:
1 | // ci的推演类型是int,f是const int |
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。
1 | auto &g = ci; |
如果初始值是一个顶层常量,则定义出来的引用也是一个顶层常量引用。
如果初始值是一个非常量,则定义出来的引用也是一个非常量引用。
在C++中,整数值不能直接赋值给指针变量,因为它们的数据类型不同。指针变量必须存储一个有效的内存地址,而整数值只是一个数值,它没有内存地址。
练习
判断下列定义推断出的类型是什么,然后编写程序进行验证。
1 | const int i = 42; |
decltype类型指示符
decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
1 | decltype(f() ) sum = x; / / sum的类型就是函数f的返回类型 |
decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
1 | const int ci = 0, &cj- ci;decltype(ci) x = 0; |
因为r是一个引用,因此 decltype (r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则decltype 将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
1 | // decltype的表达式如果是加上了括号的变量,结果将是引用decltype((i))d; |
decltype中表达式产生左值就返回引用
切记:decltype ((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。
练习
关于下面的代码,请指出每一个变量的类型以及程序结束时它们各自的值。
1 | int a = 3, b = 4; |
赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。根据这一特点,请指出下面的代码中每一个变量的类型和值。
1 | int a = 3, b = 4; |
说明由decltype 指定类型和由auto指定类型有何区别。请举一个例子,decltype指定的类型与auto指定的类型一样;再举一个例子,decltype指定的类型与auto指定的类型不一样。
1 | 如果使用引用类型,auto会识别为其引用对象的类型; |