扬州市城市建设投资公司网站,手机触屏网站开发教程,正规的百度快排seo,品牌营销的概念2.1 基本内置类型
算术类型#xff08;arithmetictype#xff09;和空类型#xff08;void#xff09;在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值#xff0c;仅用于一些特殊的场合#xff0c;例如最常见的是#xff0…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 而非定义iint 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 初始化为字面常量0int *p3 NULL; / / 等 价 于 int *p3 o //需 要 首 先 #include cstdlib得到空指针最直接的办法就是用字面值nullptor来初始化指针这也是C11新标准刚 刚引入的一种方法。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 i1024,*pi,ri; //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 i42;const int cii;int jci;//正确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 i42;const int r1 i//允许将const int绑定到一个普通int对象上const int r2 42;//正确r2是一个常量引用const int r3 rl*2;//正确 r3是一个常量引用in tr4 rl*2; //错误r4是一个普通的非常量引用要想理解这种例外情况的原因最简单的办法是弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么double dval3.14; const int ridval;此处ri引用了一个int型的数。对ri的操作应该是整数运算但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数编译器把上述代码变成了如下形式const int tempdval;//由双精度浮点数生成一个临时的整型常量 const int ritemp;//让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类型的因此可以指向cici是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变量并把它的初始值设为我们认为的某个常量表达式但在实际使用时尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说在此种情况下对象的定义和使用根本就是两回事儿。C11新标准规定允许将变量声明为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)是一个名字它是某种类型的同义词。使用类型别名有很多好处它让复杂的类型名字变得简单明了、易于理解和使用还有助于程序员清楚地知道使用该类型的真实目的。有两种方法可用于定义类型别名。传统的方法是使用关键字typedeftype 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类型说明符
编程时常常需要把表达式的值赋给变量这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易有时甚至根本做不到。为了解决这个问题C11新标准引入了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类型指示符
有时会遇到这种情况希望从表达式的类型推断出要定义的变量的类型但是不想用该表达式的值初始化变量。为了满足这一要求C11新标准引入了第二种类型说明符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作为表达式的一部分如 r0,显然这个表达式的结果将是一个具体值而非一个引用。另一方面如果表达式的内容是解引用操作则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是-条预处理指令,用于判断给定的变量是否尚未定义’