2.1 基本内置类型
- 算术类型(arithmetictype)和空类型(void)在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
2.1.1算术类型
- 算术类型分为两类:整型(integraltype,包括字符和布尔类型在内)和浮点型。算术类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差别。表2.1列出了C++标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。某一类型所占的比特数不同,它所能表示的数据范围也不一样
- 类型char和类型signed char并不一样。尽管字符型有三种,但 是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两 种形式中的一种,具体是哪种由编译器决定。
- 无符号类型中所有比特都用来存储值,例如,8比特的unsigned char可以表示0至255区间内的值。C++标准并没有规定带符号类型应如何表示,但是约定了在表示范围内正值和负值的
量应该平衡。因此,8比特的signed char理论上应该可以表示-127至127区间内的值,大多数现代计算机将实际的表示范围定为-128至127。
如何选择类型
- 当明确知晓数值不可能为负时,选用无符号类型。
- 使用int执行整数运算。在实际应用中,short常常显得太小而long-般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用longlong。
- 在算术表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char。
- 执行浮点数运算选用double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。
类型转化
- 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true
- 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1
- 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
- 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
- 我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
- 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
2.1.3字而值常量
- 一个形如42的值被称作字面值常量(literal),这样的值一望而知。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
- 注意,如果反斜线'后面跟着的八进制数字超过3个,只有前3个数字与'构成转义序列。
- 例如,"\1234"表示2个字符,即八进制数123对应的字符以及字符4。相反,\x要用到后面跟着的所有数字,例如,"\xl234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。一般来说,超过8位的十六进制字符都是与表2.2中某个前缀作为开头的扩展字符集一起使用的。
- 对于一个整型字面值来说,我们能分别指定它是否带符号以及占用多少空间。如果后缀中有U,则该字面值属于无符号类型,也就是说,以U为后缀的十进制数、八进制数或十六进缶I擞都将从unsigned int、unsigned long和unsigned long long中选择能匹配的空间最小的一个作为其数据类型。如果后缀中有L,则字面值的类型至少是long;
- 如果后缀中有LL,则字面值的类型将是long long和unsigned long long中的一种。
- 显然我们可以将U与L或LL合在一起使用。例如,以UL为后缀的字面值的数据类型将根据具体数值情况或者取unsigned long,或者取unsigned long long
2.2 变量
- 变量定义的基本形式是:首先是类型说明符(typespecifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值:
- 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含篇点义是把对象的当前值擦除,而以一个新值来替代
列表初始化
- C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性体现。例如,要想定义一个名为units_sold的int变量并初始化为0,以下的4条语句都可以做到这一点:
- int units_sold = 0;
- int units_sold = {0};
- int units_sold{0};
- int units_sold(0);
- 用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的形式仅在某些受限的场合下才能使用,这种方式叫做列表初始化
- 无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值
默认初始化
- 如果定义变量时没有指定初值,则变量被默认初始化(defaultinitialized),此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
- 定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定:
2 .2 .2 变量声明和定义的关系
- 声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
- 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
- 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:
- extern int i; / / 声 明 i 而非定义i
- int j; / / 声明并定义j
- 任何包含了显式初始化的声明即成为定义。我们能给由extern 关键字标记的变量赋 一个初始值,但是这么做也就抵消了extern 的作用。extern语句如果包含初始值就不再是声明,而变成定义了:
- extern double pi = 3.1416; // 定义 在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误
- 变量能且只能被定义一次,但是可以被多次声明
- 声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
2 .2 .3 标识符
- C++的标识符(identifier)由字母、数字和下画线组成,其中必须以字母或下画线开 头。标识符的长度没有限制,但是对大小写字母敏感:
- 用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。
- 此外,定义在函数体外的标识符不能以下画线开头。
变量命名规范
2 .2 .4 名字的作用域
- 不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体:变量、函数类型等。然而,同一个名字如果出现在程序的不同位置,也可能指向的是不同实体
- 作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作 用域都以花括号分隔。
- 同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
- 作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(innerscope), 包含着别的作用域的作用域称为外层作用域 (outer scope)
- 使用作用域操作符(参见1.2节,第7页)来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。结果是,第三条输出语句使用全局变量reused,输出420。
2.3 复合类型
- 复合类型(compound type)是指基于其他类型定义的类型。C++语言有几种复合类型, 本章将介绍其中的两种:引用和指针
- 一条声明语句由一个基本数据类型(basetype)和紧随其后的一个声明符(declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
2 .3 .1 引用
- 引 用 (reference)为对象起了另外一个名字,引用类型引用(refers to )另外一种类型。 通过将声明符写成&d的形式来定义引用类型,其中d 是声明的变量名:
- int ival = 1024;
- int &refVal = ival; // refVal指 向 ival (是 ival的另一个名字)
- int &refVal2; / / 报错:引用必须被初始化
- 在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将 和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
- 引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字
2.3.2 指针
- 指针(pointer)是“指向(pointto)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。
- 其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
- 指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&)
空指针
- 空指针(null pointer) 不指向任何对象,在试图使用一个指针之前代码可以首先检查 它是否为空。以下列出几个生成空指针的方法:
- int *pl = nullptr; / / 等 价 于 int *pl =0;
- int *p2 =0; / / 直接将p2 初始化为字面常量0
- int *p3 = NULL; / / 等 价 于 int *p3 = o; //需 要 首 先 #include cstdlib
- 得到空指针最直接的办法就是用字面值nullptor来初始化指针,这也是C++11新标准刚 刚引入的一种方法。nullptor是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。另一种办法就如对p2 的定义一样,也可以通过将指针初始化为字面值0 来生成空指针
- 预处理器是运行于编译过程之前的一段程序就可以了。预处理变量不属于命名空间s td ,它由预处理 器负责管理,因此我们可以直接使用预处理变量而无须在前面加上s td ::
- 当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0 初始化指针是一样的。在新标准下,现在的C++程序最好使用nullptor, 同时尽量避免使用NULL。把 i n t 变量直接赋给指针是错误的操作,即使i n t 变量的值恰好等于0 也不行
- 有时候要想搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象。当写出如下语句时,
- pi = &ival; // pi的值被改变,现在 pi指向了 ival,意思是为pi赋一个新的值,也就是改变了那个存放在pi内的地址值。相反的,如果写出如下语句
- *pi = 0; // ival的值被改变,指 针 pi并没有改变,则*pi(也就是指针pi指向的那个对象)发生改变。
void* 指针
- void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着 一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
- 利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或 者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道 这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
2.3.3理解复合类型的声明击
- 如前所述,变量的定义包括一个基本数据类型(base type)和一组声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量;
- int i=1024,*p=&i,&r=i; //i是一个int型的数,p是一个int型指针,r是一个int型引用
指向指针的指针
- 一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
指向指针的引用
- 引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
- 要理解r 的类型到底是什么,最简单的办法是从右向左阅读r 的定义。离变量名最近的符 号(此例中是& r 的符号&)对变量的类型有最直接的影响,因此r 是一个引用。声明符的 其余部分用以确定r 引用的类型是什么,此例中的符号*说明r 引用的是一个指针。最后, 声明的基本数据类型部分指出r 引用的是一个int指针
2.4 const限定符
- 有时我们希望定义这样一种变量,它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时警惕防止程序一不小心改变了这个值。为了满足这一要求,可以用关键字const对变量的类型加以限定:
- const int bufSize = 512; // 输入缓冲区大小
- 这样就把bufSize定义成了一个常量。任何试图为bufSize赋值的行为都将引发错误:
- bufSize = 512; / / 错误:试 图 向 const对象写值,因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。一如既往, 初始值可以是任意复杂的表达式:
初始化和const
- 正如之前反复提到的,对象的类型决定了其上的操作。与非const类型所能参与的操作相比,const类型的对象能完成其中大部分,但也不是所有的操作都适合。主要的限制就是只能在const类型的对象上执行不改变其内容的操作。例如,const int和普通的int一样都能参与算术运算,也都能转换成一个布尔值,等等。在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要:
- int i=42;const int ci=i;int j=ci;//正确:i的值被拷贝给了ci//正确:ci的值被拷贝给了j
- 尽管c i 是整型常量,但无论如何c i 中的值还是一个整型数。c i 的常量特征仅仅在执行 改变c i 的操作时才会发挥作用。当用c i 去初始化j 时,根本无须在意ci 是不是一个常 量。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。
- 默认状态下,const对象仅在文件内有效,当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:
- const对象被设定为仅在文件内有效。当 多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
- 某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必 要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中 定义const.而在其他多个文件中声明并使用它。 解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样 只需定义一次就可以了:
2.4.1 const 的引用
- 可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
- const int ci = 1024;
- const int &r 1 = ci; / / 正确:引用及其对应的对象都是常量
- rl = 42; / / 错误:rl是对常量的引用
- int &r2 = ci;/ 错误:试图让一个非常量引用指向一个常量对象
- 因为不允许直接为c i 赋值,当然也就不能通过引用去改变c i。因此,对 r2 的初始化是错误的。假设该初始化合法,则可以通过r2 来改变它引用对象的值,这显然是不正确的。
初始化和对const的引用
- 2.3.1节(第46页)提到,引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成(参见2.1.2节,第32页)引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
- int i=42;
- const int &r1 = i;//允许将const int&绑定到一个普通int对象上
- const int &r2 = 42;//正确:r2是一个常量引用
- const int &r3 = rl*2;://正确 r3是一个常量引用
- in t&r4 = rl*2; //错误:r4是一个普通的非常量引用
- 要想理解这种例外情况的原因,最简单的办法是弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:
- double dval=3.14; const int &ri=dval;此处ri引用了一个int型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:const int temp=dval;//由双精度浮点数生成一个临时的整型常量 const int &ri=temp;//让ri绑定这个临时量
- 在这种情况下,ri绑定了一个临时量(temporary)对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。
- C++程序员们常常把临时量对象简称为临时量。
- 接下来探讨当ri不是常量时,如果执行了类似于上面的初始化过程将带来什么样的后果。如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值。注意,此时绑定的对象是一个临时量而非dval。程序员既然让ri引用dval,就肯定想通过ri改变dval的值,否则干什么要给ri赋值呢?如此看来,既然大家基本上不会想着把引用绑定到临时量上,C++语言也就把这种行为归为非法。
对const的引用可能引用一个并非const的对象
- 必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
- r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改。
2.4.2指针和const
- 与引用一样,也可以令指针指向常量或非常量。类似于常量引用(参见2.4.1节,第54页),指向常量的指针(pointertoconst)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:
- 2.3.2节 (第 47页)提到,指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象:
- double dval = 3.14; // dval是一个双精度浮点数,它的值可以改变
- cptr = &dval; / / 正确:但是不能通过cptr改变 dval的值
- 和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。所谓指向常量的指针或引用,不过是指针或引用“自作多情”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。
const指针
- 指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(constpointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:也就是我指向了你,一辈子就认定了你,但是你会变,变得很陌生
- 如同2.3.3节(第52页)所讲的,要想弄清楚这些声明的含义最行之有效的办法是从右向左阅读。此例中,离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是*,意思是curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。与之相似,我们也能推断出,pip是一个常量指针,它指向的对象是一个双精度浮点型常量。
- 指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。例如,pip是一个指向常量的常量指针,则不论是pip所指的对象值还是pip自己存储的那个地址都不能改变。相反的,curErr指向的是一个一般的非常量整数,那么就完全可以用curErr去修改errNumb的值
2.4.3顶层const
- 如前所述,指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层const表示指针本身是个常量,而用名词底层const表示指针所指的对象是一个常量。
- 更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显:
- const int *p2 = &ci; const int 表示我要存储的变量的类型是const int类型的,因此可以指向ci,ci是const int类型的
- const int * const p3 = p2; / / 靠右的 const 是顶层 const,靠左的是底层
- const const int &r = ci; / / 用于声明引用的const都是底层const
- 当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶 层const不受什么影响:
- i = ci; / / 正确:拷贝 ci的值,ci是一个顶层const, 对此操作无影响 单纯拷贝数值
- p2 = p3; / / 正确:p2和 p3指向的对象类型相同,p3顶层 const的部分不影响
- 执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。
- 另一方面,底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
- p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响
2.4.4 constexpr和常量表达式
- 常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。
- 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如
- const int max_files = 20; // max_files 是常量表达式
- const int limit = max_files + 1; // limit 是常量表达式
- int staff_size = 27; // staff_size 不是常量表达式
- const int sz = get_size () ; // sz 不是常量表达式
- 尽管staff_size的初始值是个字面值常量,但由于它的数据类型只是一个普通int而非const int,所以它不属于常量表达式。另一方面,尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
constexpr变量
- 在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。
- 当然可以定义一个const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事儿。
- C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:
- constexpr int mf = 20; // 20 是常量表达式
- constexpr int limit = mf + 1; // mf + 1 是常量表达式
- constexpr int sz = size () ; // 只有当 size 是一个 constexpr 函数时,才是一条正确的声明语句
- 尽管不能使用普通函数作为constexpr变量的初始值,但是正如6.5.2节(第214页)将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
- 一般来说, 如果你认定变量是一个常量表达式,那就把它声明成constexpr类型
字面值类型
- 常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型
- 到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr。其他一些字面值类型将在7.5.6节(第267页)和19.3节(第736页)介绍。尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。6.1.1节(第184页)将要提到,函数体内定义的变量一般来说并非存放在固定地址中,
- 因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。
指针和constexpr
- 必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关
- const int *p = nullptr; // p 是一个指向整型常量的指针
- constexpr int *q = nullptr; // q 是一个指向整数的常量指针
- p 和 q 的类型相差甚远,p 是一个指向常量的指针,而 q 是一个常量指针,其中的关键在 于 constexpr把它所定义的对象置为了顶层const (参见2.4.3节,第 57页)。 与其他常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量
2 . 5 处理类型
- 随着程序越来越复杂,程序中用到的类型也越来越复杂,这种复杂性体现在两个方面。一是一些类型难于“拼写",它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。二是有时候根本搞不清到底需要的类型是什么,程序员不得不回过头去从程序的上下文中寻求帮助。
2 .5 .1 类型别名
- 类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
- 有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:
- type def double wages; //wages 是 double 的同义词
- typedef wages base, *p; //base 是 double 的同义词,p 是 double*的同义词
- 其中,关键字typedef作为声明语句中的基本数据类型(参见2.3节,第 45页)的一部分出现。含有typedef的声明语句定义的不再是变量而是类型别名。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。
- 新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
- using SI = Sales_item; // SI 是 Sales_item的同义词
- 这种方法用关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号 左侧的名字规定成等号右侧类型的别名。类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名:
- wages hourly, weekly; // 等价于 double hourly、weekly;
- SI item; // 等价于 Sales_item item
指针、常量和类型别名
- 如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring ,它实际上是类型char*的别名:
- typedef char *pstring; const pstring cstr = 0; // cstr是指向 char 的常量指针
- const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
- 上述两条声明语句的基本数据类型都是const pstring ,和过去一样,const是对给定类型的修饰。pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。 遇到一条使用了类型别名的声明语句时,人们往往会错误地尝试把类型别名替换成它本来的样子,以理解该语句的含义:
- const char *cstr = 0; // 是对 const pstring cstr 的错误理解
- 再强调一遍:这种理解是错误的。声明语句中用到pstring时,其基本数据类型是指针。 可是用char*重写了声明语句后,数据类型就变成了 char, *成为了声明符的一部分。 这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
2.5.2 auto类型说明符
- 编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double) 不同,auto 让编译器通过初始值来推算变量的类型。显然,auto 定义的变量必须有初始值:
- / / 由 vail和 val2 相加的结果可以推断出item的类型
- auto item = vail + val2; // item初始化为vail和 val2 相加的结果
- 此处编译器将根据vail和val2相加的结果来推断item的类型。如果vail和val2是类Sales_ item (参 见 1.5节,第 17页)的对象,则 ite m 的类型就是Sales_ item ;
- 如果这两个变量的类型是double ,则item的类型就是double ,以此类推。 使用auto。也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
- auto i = 0, *p = &i; / / 正确:i 是整数、p 是整型指针 auto sz = 0, pi = 3.14; // 错误:sz 和 pi 的类型不一致
复合类型、常量和auto
- 编译器推断出来的auto。类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。首先,正如我们所熟知的,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto 的类型:
2.5.3 decltype类型指示符
- 有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符
- decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式 并得到它的类型,却不实际计算表达式的值:
- decltype(f()) sum // sum的类型就是函数f 的返回类型
- 编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。换句话说,编译器为sum指定的类型是什么呢?就是假如f 被调用的话将会返回的那个类型。
- decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
- 因为cj是一个引用,decltype (cj)的结果就是引用类型,因此作为引用的z必须被初始化。
- 需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
decltype和引用
- 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。如 4.1.1节 (第 120页)将要介绍的,有些表达式将向decltype返回一个引用类型。 -般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:
- 因为r是一个引用,因此decltype (r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如 r+0,显然这个表达式的结果将是一个具体值而非一个引用。
- 另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype (*p)的结果类型就是int &,而非int。
- decltype和 auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关。有一种情况需要特别注意:对于 decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果 decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型:如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:
补充知识
预处理器概述
- 确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前己经用到了一项预处理功能#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include。C++程序还会用到的一项预处理功能是头文件保护符(headerguard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义。
- #define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。
- 使用这些功能就能有效地防止重复包含的发生:
- 第一次包含Sales_data. h 时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data . h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分
- 预处理变量无视C++语言中关于作用域的规则" .
- 还可以使用 #pragma once 参考链接
- 整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。
小 结
- 类型是C++编程的基础。
- 类型规定了其对象的存储要求和所能执行的操作。C+ +语言提供了一套基础内置类型,如 int和 char等,这些类型与实现它们的机器硬件密切相关。类型分为非常量和常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外,还可以定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。
- C++语言允许用户以类的形式自定义类型。C++库通过类提供了一套高级抽象类型,如输入输出和string等。
术语表
- 地址(address)是一个数字,根据它可以找到内存中的一个字节。
- 别名声明(aliasdeclaration)为另外一种类型定义一个同义词:使用“名字=类型”的格式将名字作为该类型的同义词。
- 算术类型(arithmetictype)布尔值、字符、整数、浮点数等内置类型。
- 数组(array)是一种数据结构,存放着一组未命名的对象,可以通过索引来访问这些对象。3.5节将详细介绍数组的知识。
- auto是一个类型说明符,通过变量的初始值来推断变量的类型。
- 基本类型(basetype)是类型说明符,可用const修饰,在声明语句中位于声明符之前。基本类型提供了最常见的数据类型,以此为基础构建声明符。
- 绑定(bind)令某个名字与给定的实体关联在一起,使用该名字也就是使用该实体。例如,引用就是将某个名字与某个对象绑定在一起。参考链接
- 字节(byte)内存中可寻址的最小单元,大多数机器的字节占8位。
- 类成员(classmember)类的组成部分。复合类型(compoundtype)是一种类型,它的定义以其他类型为基础。
- const是一种类型修饰符,用于说明永不改变的对象。const对象一旦定义就无法再赋新值,所以必须初始化。
- 常量指针(constpointer)是一种指针,它的值永不改变。
- 常量引用(constreference)是一种习惯叫法,含义是指向常量的引用。
- 常量表达式(constexpression)能在编译时计算并获取结果的表达式。constexpr是一种函数,用于代表一条常量表达式。6.5.2节(第214页)将介绍constexpr函数。
- 转换(conversion)-种类型的值转变成另外一种类型值的过程。C++语言支持内置类型之间的转换。
- 数据成员(datamember)组成对象的数据元素,类的每个对象都有类的数据成员的一份拷贝。数据成员可以在类内部声明的同时初始化。
- 声明(declaration)声称存在一个变量、函数或是别处定义的类型。名字必须在定义或声明之后才能使用。
- 声明符(declarator)是声明的一部分,包括被定义的名字和类型修饰符,其中类型修饰符可以有也可以没有。
- decltype是一个类型说明符,从变量或表达式推断得到类型。默认初始化(defaultinitialization)当对象未被显式地赋予初始值时执行的初始化行为。由类本身负责执行的类对象的初始化
行为。全局作用域的内置类型对象初始化为 0;局部作用域的对象未被初始化即拥有未定义的值。 - 定义 (definition ) 为某一特定类型的变量申请存储空间,可以选择初始化该变量。名字必须在定义或声明之后才能使用。
- 转义序列 (escape sequence) 字符特别是那些不可打印字符的替代形式。转义以反斜线开头,后面紧跟个字符,或者不多于3 个八进制数字,或者字母x 加上 1 个 I-六进制数。
- 全局作用域(global scope)位于其他所有作用域之外的作用域。
- 头文件保护符(header guard)使用顶处理变量以防止头文件被某个文件重复包含。
- 标识符 (identifier)组成名字的字符序列, 标识符对大小写敏感。
- 类内初始值(in-class initializer)在声明类的数据成员时同时提供的初始值,必须置等号右侧或花括号内"
- 在作用域内(in scope) 名字在当前作用域内可见。
- 被初始化 (initialized) 变量在定义的同时被赋予初始值,变量一般都应该被初始化
- 内层作用域(inner scope)嵌套在其他作用域之内的作用域。
- 整 型 (integral type) 参见算术类型。 列表初始化(listinitialization)利用花括号把一个或多个初始值放在一起的初始化形式。
- 字 面 值 (literal)是一个不能改变的值,如 数字、字符、字符串等。单引号内的是字符字知值,双引号内的是字符串字面值。
- 局部作用域(local scope) 是块作用域的习惯叫法。
- 底层 const (low-level const) —个不属顶层的const,类型如果由底层常量定义,则不能被忽略。
- 成 员 (member)类的组成部分。
- 不可打印字符(nonprintable character)不具有可见形式的字符,如控制符、退格、换行符等。
- 空 指 针 (null pointer)值为0 的指针,空指针合法但是不指向任何对象。nullptr是表示空指针的字面值常星”
- 对 象 (object)是内存的块区域,具有某种类型,变量是命名了的对象。
- 外层作用域( outer scope) 嵌套着别的作用域的作用域。
- 指 针 (pointer)是一个对象,存放着某个对象的地址,或皆某个对象存储区域之后的下一地址,或者0。
- 指向常量的指针(pointer to const)是一个指针,存放着某个常量对象的地址。指向常量的指针不能用来改变它所指对象的值。
- 预 处 理 器 (preprocessor)在 C++编译过程中执行的-段程序。
- 预处理变量(preprocessor variable) 由预处理器管理的变量。在程序编译之前,预处理器负责将程序的预处理变量替换成它的真实值
- 引 用 (reference)是某个对象的别名。
- 对常量的引用(reference to const)是个引用,不能用来改变它所绑定对象的值。对常量的引用可以绑定常量对象,或者非常量对象,或者表达式的结果。
- 作用域(scope) 是程序的-部分,在其中某些名字有意义。C++有凡级作用域:
- 全 局 (global)----- 名字定义在所有其他作用域之外。
- 类 (class)-----名字定义在类内部。
- 命 名空间(namespace)----- 名字定义在命名空间内部。
- 块 (block)----- 名字定义在块内部。名字从声叫位置开始育至声明语句所在的作用域末端为止都是可用的。
- 分离式编译< separate compilation) 把程序分割为多个单独文件的能力。
- 带符号类型(signed)保存正数、负数或0的整型。
- 字符串(string)是一种库类型,表示可变长字符序列。
- struct是个关键宇,用于定义类。
- 临时值(temporary)编译器在计算表.达式结果时创建的无名对象。为某表达式创建了 -个临时值,则此临时值将一直存在直到包含何该表达式的最大的表达式计算完成为止。
- 顶层 const ( top-level const) 是一个const,规定某对象的值不能改变。
- 类型别名(type alias)是 个名字,是另外个类型的同义词 ,通过关键字typedef或别名声明语句来定义。
- 类型检查(type checking )是一个过程. 编译器检查给定类型对象的方式与该类型的定义是否一致。
- 类型说明符< type specifier) 类型的名字。
- typedef为某类型足义一个别名。当关键字 typedef作为声明的基本类型出现时,声明中定义的名字就是类型名。
- 未 定 义 (undefined) 即 C ++没有明确规定的情况。不论是否有意为之,未定义行为都能引发难以追踪的运行时错误、安全问题利可移植性问题。
- 未初始化(uninitialized)变量己定义但没被赋予初始值.一般来说,试图访问未初始化变量的值将引发未定义行为
- 无符号类型(unsigned)保存大于等于0的整型.
- 变量(variable)命名的对象或引用.C++语言要求变量要先声明后使用。
- void* 以指向任以非常量的指针类型,不能执行解引用操作。
- void类型是-种有特殊用处的类型,既无操作也无值.不能定义-个void类型的变量
- 字(word)在指定机器上进行整数运算:的自然单位。一般来说,字的空间足够字放地址.32位机器上的字通常占据4个字节
- &运算符(&operator)取地址运算符。
- *运算符(*operator)解引用运算符。解引用-个指针将返回该指针所指的对象,为解引用的结果赋值也就是为指针所指的对象赋值。
- #define是-条预处理指令,用于定义一个预处理变量
- #endif是一条预处理指令,用于结束一个#ifdef或#ifndef区域。
- #ifdef是-条预处理指令,用于判断给定的变量是否已经定义.
- #ifndef是-条预处理指令,用于判断给定的变量是否尚未定义’