目录
前言:
个人感觉把博客写的越细越好,因为自己也是总忘 所以为了不让自己去翻教学视频 博客记录的比较详细
类型
要使这种东西在C++程序里面有意义,名字 x
、y
和 y
就需要有合适的声明。这也就是说,程序员必须描述分别命名为x
、y
和 f
实体的存在性,而且它们的类型能使 =
(赋值)、+
(加)以及 ()
(函数调用)分别有意义。
在C++程序里,每个名字都有一个与之相关联的类型,这个类型决定了可以对这个名字应用什么操作(即应用于这个名字所指称的实体),并决定这些操作将如何做出解释。例如,声明
将能使上面的那个例子有意义。因为 y
被声明为 int
,所以它可以被赋值,可以用在算术表达式里,如此等等。另一方面,f
被声明为一个以 int
为参数的函数,所以可以用合适的参数去调用它。
这里的例子只是为了阐释各种语言特征,并不想做什么有意义的事情。范围更广阔且更实际的例子留在后面各章,留到描述了C++更多的东西之后。本章只提供一些最基本的元素,C++程序将从这些东西中构造出来。你必须知道这些东西,再加上伴随它们的术语和简单的语法,以便能够在C++里完成真正的项目,能够去阅读其他人写的代码。然而,要理解后面各章,并不需要彻底理解本章中所提到的每一个细节。因此,你可以按自己的喜好看我写的博客,只留意其中最主要的概念,等到以后根据需要再转回来理解进一步的细节。
基本类型
C++有一组基本类型,它们对应于计算机的基本存储单元和使用这些单元去保存数据的一些最常见方式:
除此之外,用户还可以定义
从这些类型出发,我们可以构造出其他类型:
我们将布尔量、字符和整数类型放到一起称为整型 。
整型和浮点类型一起称为算术类型。
枚举和类被称为用户定义类型,因为它们必须由用户定义出来,而不能事先没有声明就直接使用,而这正是那些基本类型的情况。与用户定义类型相对应,其他类型都被称为内部类型。
整型和浮点类型都提供了多种不同的大小,以便在所占用的存储量、表示精度和计算的可能范围等诸方面,给程序员提供一个选择的机会。这里的假设是,计算机提供了字节以存放字符,提供了机器字以存放并计算整数值,提供了某些最适合存放浮点数的实体,还有地址,通过它可以引用所有这些实体。C++的基本类型,再加上指针和数组,以一种与具体及其无关的方式,为程序员呈现了这些位于机器层面上的概念。
对大部分应用而言,你可以简单地用 bool
表示逻辑值,用 char
表示字符,用 int
表示整数,用 double
表示浮点值。其他基本类型都是为优化或特殊需要而提供的变化,在真正需要它们之前最好是忽略之。当然,必须知道它们,以便能够阅读已有的C和C++代码。
布尔量
一个布尔量,bool
,可以具有两个值 true
或 false
之一。布尔量用于表示逻辑运算的结果。
例如,
如果 a
和 b
具有相同的值,b1
将变成 true
;否则 b1
将变成 false
。
bool
最常见的使用是作为检查某些条件是否成立的函数(谓词)的结果类型。例如,
按照定义,true
具有值 1
,而 false
具有值 0
。与此相对应,整数可以隐式地转换到 bool
值:非零的整数转换为 true
,而 0
转换为 false
。例如,
在算术和逻辑表达式里,bool
都将被转为 int
,在这种转换之后得到的值上进行各种算术和逻辑运算。如果结果又被转回 bool
,那么 0
将转为 false
,所有非零值都转为 true
。
指针也可以隐式地转换到 bool
,非零指针转为 true
,具有零值的指针转为 false
。
1. 字符类型
类型为 char
的变量可以保存具体实现所用的字符集里的一个字符。例如,
实际情况中一个 char
类型几乎都包含 8
个二进制位,因此它可以保存 256
种不同的值。典型的情况是有关字符集采用ISO-646的某个变形,例如 ASCII,并由此提供了你键盘上的那些字符。这一字符集只是部分地标准化了,由此也引起了许多问题。
在支持不同自然语言的字符集之间存在着巨大的差异,以不同方式支持同一种自然语言的字符集之间也有类似情况。当然,我们在这里只对这些差异如何影响C++感兴趣,怎样在多语言、多字符集的环境中编写程序的事情已经超过了本书的范围,虽然这种事情也会在一些地方向我们招手。
假定实现所用的字符集包括数字、26个英文字母,以及某些基本标点符号是没问题的。而做出如下假定都是不安全的:在 8
位字符集中共有不超过 127
个字符(例如,有的字符集提供了 255
个字符),不存在超出英语的字母(大部分欧洲语言都提供了更多的字母),字母字符是连续排列的(EBCDIC在 'i
' 和 'j
' 之间留有空隙),写C++所需要的每个字符都是可用的(例如,有些国家的字符集中没有提供 {}[]|
。只要有可能,我们都应该避免做出有关对象表示方式的任何假定,这个普遍规则甚至也适用于字符。
每个字符常量有一个整数值。例如,在ASCII字符集里'b
'的值是 98
。这里是一个小程序,它可以告诉你,你仔细输入的任一个字符所对应的整数值是什么
记法 int(c)
将给出字符 c
的整数值。能够把 char
转为整数也引起了一个问题:一个 char
是有符号的还是没符号的?由8个二进制位表示的256个值可以解释为整数值0~255,或者解释为-128~127。不幸的是,关于普通 char
如何选择的问题是由实现决定的。C++提供了另外两个类型,它们都确切地回答了这个问题:signed char
保存的值是-128~127;而 unsigned char
保存的值是0~255。幸运的是,这方面的差异只出现在那些超出127的值,而最常用的字符都在127之内。
将超过上述范围的值存入普通的 char
将会导致微妙的移植性问题。如果你真的需要使用不止一种 char
类型,或者你需要将整数存储到 char
中。
这里还提供了另一个类型 wchar_t
,用于保存更大的字符集里的字符,例如 Unicode
的字符。这是另外一个独立的类型,wchar_t
的大小由实现确定,且保证足够存放具体实现所用的现场 所支持的最大的字符集。这个奇怪的名字来源于C。在C语言里,wchar_t
是一个 typedef
而不是一个内部类型。加上后缀 _t
就是为了区分标准类型和 typedef
。
请注意,字符类型都是整型,可以对它们使用算术和逻辑运算符。
1.1 字符文字量
字符文字量常被称做字符常量,其形式就是用单引号括起的一个字符,例如 'a
' 和 '0
'。字符文字量的类型就是 char
。这样的字符文字量实际上是一种符号常量,表示在C++程序运行的计算机上的字符集中该字符的整数值。例如,如果你在一台使用ASCII字符集的机器上运行程序,'0
' 的值就是 48
。采用字符文字量而不用十进制写法能使程序更具有可移植性。另有几个字符具有标准的名字,采用反斜杠字符作为换义字符。例如,\n
是换行符,\t
是水平制表符。参看C.3.2节里有关各转义字符的细节。
宽字符文字量的形式是 L'ab'
,这里放在引号间的字符个数及意义由实现根据 wchar_t
类型确定。宽字符文字量的类型是 wchar_t
。
2. 整数类型
与 char
一样,每个整数类型也有三种形式:“普通的” int
,signed int
和 unsigned int
。此外,整数还有三种大小:short int
,“普通的” int
和 long int
。long int
可以简单地写成 long
。类似地,short
也是 short int
的同义词。unsigned
和 signed
分别是 unsigned int
和 signed int
的同义词。
unsigned
整数类型对于将存储看做是二进制位数组的使用方式非常理想。采用 unsigned
而不用 int
以便多获得一个位去表示正整数,就不是什么好主意。通过将变量声明为 unsigned
而保证某些值始终为正的企图常常会被隐含的类型转换规则破坏。
与 char
不同的是,普通的 int
总是有符号的。因此,那些有符号的 int
类型只不过是所对应的普通 int
类型的一个同义词罢了。
2.1 整数文字量
整数文字量有四种表面形式:十进制
、八进制
、十六进制
和 字符文字量
(A.3节)。十进制文字量用得最多,其形式如你所预期的
编译器应该能够对文字量过长,无法表示的情况给出警告。
由 0
开头后跟 x(0x)
的文字量是十六进制(以16作为数的基数),以 0
开头的文字量后面没有 x
的是八进制数(以8为基数)。例如,
字符a、b、c、d、e、f与其大写形式等价,分别表示10、11、12、13、14、15。八进制和十六进制形式特别适合用于表示二进制位的模式。使用这些记法表示真正的数值则常常会使人感到意外。例如,在一台将 int
表示为16位二补码整数的机器上,0xffff
将是十进制数 -1
。如果表示整数所采用的位数更多,它就会是 65535
。
后缀 U
可以用于显式地写出 unsigned int
型。类似地,后缀 L
可用于显式地写 long
文字量。例如,3是一个 int
型,3U
是一个 unsigned int
型,而 3L
是一个 long int
型。如果没有后缀,编译器就会基于文字量值的大小以及实现中整数的大小,为整数文字量确定一个适当的类型。
限制使用那些意义不明显的常量,只将它们用在给几个 const
或者 枚举量
初始化的表示中,这是一种很好的做法。
3. 浮点类型
浮点类型表示浮点型。像整数一样,浮点数也有三种大小:float(单精度),double(双精度)和long double(扩展精度)。
单、双和扩展精度的确切意义由实现确定。要想为某些正确选择数的精度具有重要意义的问题选择一种正确的精度,就需要有对浮点计算的深入理解。如果你并没有这种理解,那么给你一个忠告是花时间去学习,或者就选择double并期待着能得到最好的结果。
3.1 浮点文字量
按默认规定,浮点文字量的类型是double。再说一次,编译器应该能对浮点文字量太大以至无法表示的问题提出警告。下面是一些浮点文字量:
请注意,在浮点文字量的中间不能出现空格。例如,65.43 e-21不是一个浮点文字量,而是下面这样的四个词法单词(且将引起语法错误):
如果需要写类型为float的浮点文字量,你可以通过后缀f或F定义它们:
void类型
类型void是一个语法上的基本类型。它可以作为更复杂的类型中的组成部分,但是没有类型为void的对象。void被用于刻画一个函数并不返回值,它还被用做指向不明类型的对象的指针的基础类型。例如,
在声明函数时,你必须刻画返回值的类型。逻辑上说,你可能希望通过忽略返回值类型来表示一个函数不返回值。但那样做将会减弱语法的规范性,而且也与C的使用方式冲突。因此这里将void用做一个“伪返回类型”,用于说明一个函数不返回值。
枚举
一个枚举是一个类型,它可以保存一组由用户刻画的值。一旦定义之后,枚举的使用就很像是一个整数类型。
命名的整数常量可以定义为枚举的成员。例如
这就定义了三个被称为枚举符的整数常量,并给他们赋了值。按默认方式,枚举符所赋的值从0开始递增,所以ASM == 0,AUTO == 1,BREAK == 2。枚举也可以命名,例如,
每个枚举都是一个独立的类型,枚举符的类型就是它所在的那个枚举。例如,AUTO的类型就是keyword。
将一个变量声明为keyword而不是一个普通的int,能够给用户和编译器一些有关该变量拟议中的用途的提示。例如,
编译器由可能提出一个警告,因为在三个keyword值中只有两个被处理了。
枚举符也可以用整型(4.1.1节)的constant-expression(常量表达式)(C.5节)进行初始化。如果某个枚举中所有枚举符的值均非负,该枚举的表示范围就是[0:2的k次方 - 1],其中的2的k次方是能使所有枚举符位于此范围内的最小的2的幂;如果存在负的枚举符值,该枚举的取值范围就是[-2的k次方 : 2的k次方-1]。这样就定义了一个最小的位段,其中能保存所有枚举符值的常规2补码表示,例如,
一个整型值可以显式地转换到一个枚举值。除非这种转换的结果位于该枚举的范围之内,否则就是无定义的。例如,
最后一个赋值说明了为什么不允许隐式地从整数转换到枚举:大部分整数值在特定的枚举里都没有对应的表示。
有关枚举的值范围的概念与Pascal一族语言中的枚举概念不同。无论如何,有关按位操作的各种例子在C和C++里已经有很长的历史了,其中都要求对超出枚举符集合的值有良好的定义。
一个枚举的sizeof就是某个能够容纳其范围的整型的sizeof,而且不会大于sizeof(int),除非有某个枚举符的值不能用int也不能用unsigned int表示。举例来说,在sizeof(int) == 4的机器上,sizeof(e1)可以是1,也可能是4,但绝对不会是8。
按照默认方式,枚举可以转换到整数去参加算术运算。一个枚举是一个用户定义类型,所以用户可以为枚举定义它自身的操作,例如定义++或<<。
4. 声明
一个名字(标识符)能够在C++程序里使用之前必须首先声明。也就是说,必须刻画清楚它的类型,以通知编译器这个名字所引用的哪一类的实体。这里有一些例子,展示了各种各样的声明:
正如在这些例子中所看到的,声明能做的事情可以比简单地为一个名字关联一个类型更多一些。这些声明中的大部分同时也是定义,也就是说,它们也定义了有关的名字所引用的那个实体。对于ch,它所对应实体就是适当数量的存储,以使它能够被用做一个变量---这块存储被分配。day被定义的东西就是这里所描述的函数。常数pi被定义为具有值3.1415926535897932385。Date被定义为一个新类型。Point被定义为是类型complex< short>,所以Point也就成为complex< short>的同义词。在上面这些声明中,只有
不是定义,也就是说,它们所引用的实体必须在其他地方定义。函数sqrt的代码(体)必须通过另外的某个声明描述,int变量error_number的存储必须由某个另外的error_number的声明去分配,必须有另外的某个对类型User的声明,定义出这个类型是什么样子。例如,
在一个C++程序里(关于#include的影响),每个命名实体必须有恰好一个定义。当然,它们可以有许多声明。一个实体的所有声明必须在所引用的类型上完全一致。所以,下面这个片段就包含两个错误:
下面这个片段则没有错误:
有些定义还为它们所定义的实体确定了一个“值”。例如,
对于类型、模版、函数和常数,这种所谓的“值”是持久不变的。而对那些不是常量的数据类型,这种初始值可以在以后改变。例如
在前面的定义里,只有
没有描述有关的值。对于如何以及何时给变量赋以默认值的问题,。任何描述了初始值的声明都是一个定义。
4.1 声明的结构
一个声明由四个部分组成:一个可选的“描述符”,一个基础类型,一个声明符,还有一个可选的初始式。除了函数和名字空间之外,其他声明都应该由分号结束。例如,
这里的基础类型是char,声明符是*kings[],而初始式是 = { ... }。
描述符是一个开始关键字,例如virtual和extern,它们说明了被声明事物的某些非类型的属性。
声明符由一给名字和可选的若干声明运算符组成。最常用的声明运算符是
如果这些东西都是前缀或者都是后缀,其使用就会很简单。但是,、[]和()被设计为模仿它们在表达式里的使用方式。这样,就是前缀的,而[]和()都是后缀的。而且后缀的声明运算符比前缀的那些声明运算符约束力更强。因此,*kings[] 就是一个指向什么东西的指针数组。还有,我们有时就必须使用括号,例如要表示“指向函数的指针”。
请注意,在一个声明中不能没有类型。例如,
在这里,标准C++与C和C++的早期版本有所不同,原来一直将前面两个例子看成以int作为没有明确说明的类型。这种“隐式的int”是许多微妙错误和混乱的一个根源。 声明多
4.2 声明多个名字
可以在单个的声明中声明多个名字。这种声明包含一个由逗号分隔的声明符的表列。例如,我们可以像下面这样声明两个整数:
int x, y; // intx; int y;
注意,运算符只作用于一个单独的名字---而不是同一声明中随后写的所有名字。例如,
int* p, y; // int* p; int y; ⚠️不是int* y;
int x, *q; // int x; int* q;
int v[10], *pv; // int v[10]; int* pv;
这种结构不利于阅读程序,因此应该避免。
4.3 名字
一个名字(标识符)由一系列字母和数字构成,其中的第一个字符必须是字母。下划线字符也作为字母看待。C++对一个名字里字符的个数并未强加任何限制。然后,一个具体实现中的某些部分可能不受写编辑器的人们控制(特别是连接器),而这部分有可能强加某些限制,这当然非常不幸。有些运行环境还对标识符中可以接受的字符做了某些扩充或者限制。扩充(例如,允许在名字中使用$)将产生不可移植的程序。C++的关键字,例如,new和int,不能用做用户定义实体的名字。名字的例子如下所示:
hello this_is_a_most_unusually_long_name
DEFINED foO bAr u_name HorseSense
var0 var1 CLASS _class ___
不能作为标识符的字符序列的例子如:
012 a fool $sys class 3var
pay.due foo~bar .name if
以下划线符开头的名字是保留给实现或者运行环境,用于特殊的目的,在应用程序里不要采用这样的名字。
编译器在读程序时,总是设法去寻找最长的能够做成一个名字的字符序列。这样,var10就是一个名字,而不是名字var后面跟着数10。还有,elseif是一个名字,而不是关键字else后跟关键字if。
大写和小写字母是区分的,所以Count和count是不同的名字,选择那些只是在大小写上有差异的名字可不是一种聪明饿做法。一般来说,最好能避免那些仅以某些微细的方式区分的不同的名字。举例来说,大写的o(O)和零(0)很难分辨,小写的L(l)和一(1)也是这样。因此,用lO、l0、l1、ll作为标识符名字是很糟糕的选择。
用于较大的作用域的名字应该是相对较长的更加明确的名字,例如vector、Window_with_border和Department_number。然而,如果在很小的作用域里只使用那些短小而熟悉的名字,如x、i和p,代码会显得更清晰些。类和名字空间,可用来保持较小的作用域。让那些频繁使用的名字相对比较短,将较长的名字留给不常用的实体,这种做法也很有价值。名字的选择应该反映一个实体的意义,而补水它的实现。例如,phone_book就比number_list好,即使这些电话号码实际存放在一个list里。选择好的名字也是一种艺术。
应设法保持一种统一的命名风格。例如,对非标准库的用户定义类型都用大写,非类型的开头用小写字母(例如,Shape和current_token)。还有,对宏用全部大写的名字(如果你真的要使用宏的话;例如,HACK),并用下划线分隔名字中的单词。当然,统一用法是很难做到的,因为程序常常由一些片段组成,它们有不同的来源,采用的是不同的也都合理的风格。你自己对缩写和首字母缩写应该保持某种一致性。
4.4 作用域
一个声明将一个名字引进一个作用域;也就是说,这个名字只能在程序正文的一个特定部分内使用。对于在函数里声明的名字(经常被称为局部名字),其作用域从它声明的那一点开始,直到这个声明所在的块结束为止。一个块就是由{}围起的一段代码。
一个名字称为是全局的,如果它是在所有函数、类和名字空间之外定义的。全局名字的作用域从声明的那一点开始,一直延伸到这个声明所在的文件的结束。在一个块里声明的名字可以屏蔽在其外围的块里所声明的名字或者全局的名字。也就是说,在一个块里可以重新定义一个名字,让它去引用另一个不同的实体。在退出这个块之后,该名字又恢复了它原来的意义。例如,
int x; // 全局的x
void f()
{
int x; // 局部的x屏蔽了全局的x
x = 1; // 给局部的x赋值
{
int x; // 屏蔽第一个局部的x
x = 2; // 给第二个局部的x赋值
}
x = 3; // 给第一个局部的x赋值
}
int* p = &x; // 取全局x的地址
屏蔽某些名字的情况在写大程序时是不可避免的。但是,读程序的人很容易没有注意某个名字已经被屏蔽了。由于这类错误相对不那么常见,它们反而很难被发现。因此,还是应该尽量避免名字遮蔽的情况。对全局变量或者很大的函数里的局部变量,使用像i或x一类的名字实际上就是自找麻烦。
被遮蔽的全局名字可以通过作用域解析运算符::去引用。例如,
int x;
void f2()
{
int x = 1; // 遮蔽全局的x
::x = 2; // 给全局的x赋值
x = 2; // 给局部的x赋值
// ...
}
没有办法去使用被遮蔽的局部名字。
一个名字的作用域从它被声明的那点开始;也就是说,在声明符结束之后,初始式的开始之前。这意味着一个名字甚至可以用于描述自己的初始值。例如,
int x;
void f3()
{
int x = x; // 不当:用x自己(未初始化)的值初始化x。
}
这样做并不非法,只是荒谬。好编译器能对变量在未设置之前就使用提出警告(5.9[9])。
在一个块里,也有可能同一个名字(不通过::运算符)实际引用的是两个不同的对象。看下面的例子:
int x = 11;
void f4() // 不当:
{
int y = x; // 使用全局的x: y = 11
int x = 22;
y = x; // 使用局部的x: y = 22
}
函数参数被当做在函数最外层的块中声明的名字。所以
void f5(int x)
{
int x; // 错误❌
}
是错误的,因为x在同一个作用域里定义了两次。将这种情况作为错误,将能捕捉到一种相当常见的微妙失误。
4.5 初始化
如果为一个对象提供了初始式,这个初始式将确定对象的初始值。如果没有提供初始式,全局的、名字空间的和局部静态的对象(统称为静态对象)将被自动初始化为适当类型的0。例如,
int a; // 意思是 "int a = 0;"
double d; // 意思是 "double d = 0.0;"
局部对象(有时称为自动对象)和在自由存储区里建立的对象(有时被称为动态对象或者堆对象)将不会用默认值做初始化。例如,
void f()
{
int x; // x没有定义良好的值
// ...
}
数组和结构的成员,也根据数组或结构是否为静态来确定是否默认地进行初始化。用户定义类型可以有自定义的默认初始化方式。
更复杂的对象需要以多于一个的值作为初始式。数组和结构的C风格初始化采用的是 { 和 } 括起的初始式列表描述。带有构造函数的用户定义类型采用的是函数风格的参数表形式。
请注意,在声明中写一对空的括号总意味着是“函数”。例如
4.6 对象的左值
我们可以分配和使用没有名字的“变量”,而且可能对一个看起来很奇怪的表达式赋值(例如,*p[a+10] = 7)。因此,对一个名字的某种需要就是它应该表示“存储器里的什么东西”。这也就是最简单最基本的对象概念。这样,一个对象就是存储中一片连续的区域;左值就是引用某个对象的表达式。术语左值原本是想说“某个可以放在赋值左边的东西”。然而,并不是每个左值都能够被用在赋值的左边,左值也可以是引用了某个常量。没有被声明为常量的左值常常被称做是可修改的左值。不要将对象的这种简单和低级的概念与类对象和多态性类型的对象概念弄混淆了。
除非程序员另有描述,在一个函数里声明的对象都在其定义被遇到之时建立,在它的名字离开作用域的时候被销毁。这种对象被称做自动对象。在全局和名字空间作用域里的对象,以及在函数和类里声明为static的对象只建立一次,它们一直“生存”到程序结束。这种对象被称为静态对象。数组成员、非静态结构和类的成员的生存期由它们作为其部分的那些对象决定。
通过new和delete运算符,你可以建立生存期可以直接控制的对象。
4.7 typedef
如果一个声明以typedef为前缀,它就是为类型声明了一个新名字,而不是声明一个指定类型的对象。例如,
typedef char* Pchar;
Pchar p1, p2; // p1和p2都是char*s
char* p3 = p1;
一个这样定义的名字称为一个“typedef”,可以方便地作为原来较笨拙的类型名的缩写。例如,unsigned char对于频繁使用而言显得太长。那么我们就可以为之定义一个同义词:
typedef unsigned char uchar;
typedef的另一类使用是将对某个类型的直接引用限制到一个地方。例如,
typedef int int32;
typedef int int16;
如果我们在所有需要可能较大的整数的地方都用int32,那么就很容易将我们的程序移植到一个sizeof(int)是2的机器上,因为只要在代码中重新定义int32的惟一一个出现
typedef long int32;
无论是好是坏,typedef都只是其他类型的同义词,而不是独立的类型。因此,typedef可以随意地与作为其同义词的类型混合使用。如果希望得到独立的类型,而又有着相同的语义或相同的表示,那么就应该去用枚举。
4.8 大小
C++基本类型的某些方面是由实现确定的,例如int的大小。我总要指出这种依赖性,并常常提出应该避免它们,或者通过某些方式尽可能减小其影响的建议。为什么需要为这些东西操心呢?在各种各样的系统中或使用多种编译器去编程序的人们很注意这些事情,因为如果他们不这样做,那他们就会被迫去花时间寻找和纠正很隐蔽的错误。那些自称不关心移植性的人也确实常常像自己所说的那样去做,因为他们只使用一种系统,并认为自己能承担如下看法:“这个语言不过就是我的编译器所实现的那种东西”。但是,这实际上是一种狭隘的短视的观点。如果你的程序是成功的,那么它就很可能需要移植,而这时某些人就必须去寻找并纠正那些依赖于实现的特征了。此外,程序经常需要为了同一个系统而用其他编译器来编译,甚至你最喜爱的编译器的未来版本在做某些事情时,也可能采用与目前的版本不同的方式。在写程序时,理解程序对实现的依赖性的影响并予以限制,比以后再去踩这个泥潭要容易得多。
限制依赖于实现的预言特征的影响并不太难,限制依赖于系统的库功能的影响就困难得多了。在所有可能之处都使用标准库的功能是一个办法。
提供了多种整数类型、多种无符号类型、多种浮点类型的原因就是希望使程序员能够利用各种硬件特性。在许多机器上,不同种类的基础类型之间,在对存储的需求、存储访问时间和计算速度方面存在着明显的差异。如果你了解一台机器,那么就很容易做出选择,例如,选择对某个变量所适用的整数类型。而写出真正可移植性的低级代码就要困难得多。
C++对象的大小是用char的大小的倍数表示的,所以,按照定义char的大小为1。一个对象或者类型的大小可以用sizeof运算符得到。下面是基本类型的大小所能够保证的性质:
1 = sizeof(char) ⩽ sizeof(short) ⩽ sizeof(int) ⩽ sizeof(long)
1 ⩽ sizeof(bool) ⩽ sizeof(long)
sizeof(char) ⩽ sizeof(wchar_t) ⩽ sizeof(long)
sizeof(float) ⩽ sizeof(double) ⩽ sizeof(long double)
sizeof(N) = sizeof(signed N) = sizeof(unsigned N)
其中的N可以是char、short int、int或long int。此外,这里还保证char至少有8位,short至少有16位,而long至少有32位。一个char能保存机器的字符集中的一个字符。
按照同样的尺度(一个字节0.2 in, 1 in = 0.0254m),一兆字节的存储器将向右延展大约3mile(5km, 1 mile = 1 609.344 m)。
char类型被假定是由实现选择的,在给定的计算机上选定最适合存储和操作字符的类型,典型情况下就是8位的字节。类似地,int类型也被假定是由实现选择的,采用在给定计算机上最适合存储和操作整数的类型,典型情况下是一个4字节(32位)的机器字。假定得更多就不明智了,因为确实存在着某些采用32字符的机器。
如果需要的话,有关某个具体实现的所有依赖于实现的特征都可以在< limits>里找到(22.2节)。例如,
#include <limits>
#include <iostream>
int main()
{
cout << "largest float == " << numeric_limits<float>::max()
<< ", char is signed == " << numeric_limits<char>::is_signed << '\n';
}
在赋值和表达式里,都可以随意地混合使用各种基本类型。只要可能,有关的值将会被转换,尽可能不损失信息。
如果一个值v可以在一个类型T的变量里确切地表示,那么从v到T的转换将是保值的,不会有任何问题。在那些转换不能保值的地方最好是避免有关的转换。
你需要在某种细节程度上理解隐式转换,以便能完成重要的项目,特别是去理解别人写的实际代码。当然,这种理解对于阅读下面的各章并不是必需的。
个人总结
1)、保持较小的作用域;。
2)、不要在一个作用域和它外围的作用域里采用同样的名字;
3)、在一个声明中(只)声明一个名字;
4)、让常用的和局部的名字比较短,让不常用的和全局的名字比较长;
5)、避免看起来类似的名字;
6)、维持某种统一的命名风格;
7)、仔细选择名字,反映其意义而不是反映实现方式;
8)、如果所用的内部类型表示某种可能变化的值,请用typedef为它定义一个有意义的名字
9)、用typedef为类型定义同义词,用枚举或类去定义新类型;
10)、切记每个声明中都必须描述一个类型(没有“隐式的int”);
11)、避免有关字符数值的不必要假设;
12)、避免有关整数大小的不必要假设;
13)、避免有关浮点类型表示范围的不必要假设;
14)、优先使用普通的int而不是short int或者long int;
15)、优先使用double而不是float或者long double;
16)、优先使用普通的char而不是signed char或者unsigned char;
17)、避免做出有关对象大小的不必要假设;
18)、避免无符号算术;
19)、应该带着疑问去看待从signed到unsigned,或者从unsigned到signed的转换;
20)、应该带着疑问去看待从浮点到整数的转换;
21)、应该带着疑问去看待向较小类型的转换,如将int转换到char;