第二章 变量和基本类型
内容
2.1 基本的内置类型
2.2 变量
2.3 复合类型
2.4 const限定符
2.5 类型处理
2.6 自定义数据结构 小结 专业术语
类型是一门语言的基础:他表示数据的意义,以及作用在这些数据上面的操作。
C++有广泛的类型支持。它定义了几种基本内置类型(字符,整型,浮点数等)并且提供了自定义类型的机制。一些库使用这种机制定义了更加复杂的类型,例如可变长度的字符串-----string,vector等等。本章主要介绍基本内置类型,并开始展示,c++是如何支持复合类型的。
类型决定了数据的意义和操作。i = i + j;这个语句的意义依赖于i和j的类型。
如果i和j都是整型,则这个语句就是常见的算数加法。如果i和j是Sales_item类型,这个语句就是将两个对象的各个组成成员相加。
2.1 基本内置类型
c++定义了一些列的基本内置类型(本书中也叫做基本类型,或者内置类型),包括算数类型和一个特殊的void类型。算数类型有:字符型,整型,布尔型,浮点型。void类型没有对应的值,他只能被使用在一些少数情况下。最常见的情况就是一个函数的返回类型,这种void返回类型代表这个函数不返回值。
2.1.1 算数类型
算数类型分为两类:整数类型和浮点类型.算数类型的大小(即bit位的数量)依据平台的不同而不同.c++标准只保证类型的最少比特位,如下表.但是编译器也被允许使用比标准大的比特位的类型.某一种类型的bit位不同,它所表示的最大值或者最小值也不一样.
bool类型(又叫布尔类型),代表了true或false.
c++提供了几种字符类型.他们中的一部分是为了支持国际化而设计的.主要的字符类型是char.char的设计是为了:有足够的空间存放机器的基本字符集.因此.char和一个机器字节大小相同.
剩下的字符类型有:wchar_t,char16_t,char32_t.这些字符类型用于扩展字符集.wchar_t类型的设计是为了:有足够的空间来保存机器的最大字符集.而char16_t和char32_t用来保存unicode字符集(Unicode是所有自然语言的一种表示标准)
剩下的整型,代表了不同大小的整数.c++语言规定:int至少和short大小一样.long至少和int大小一样,long long至少和long大小一样.long long类型是c++11新标准中引入的一种新类型.
内置类型的机器级表示 计算机存储一些列的bit位,每个bit位只能保存0或者1.例如:
00011011011100010110010000111011 …
大多数的计算机,将内存按块来处理,块的大小是2的n次方.最小可寻址的内存块被称作字节(byte).最小的存储单元(通常为几个字节),被称为字(word).在c++里面,一个字节至少要能保存一个基本字符.在大多数的机器中,一个字节包含8位.一个字为32位,或者64位,即,4个或者8个字节.对于大多数的电脑,在内存中的每个字节都有一个数字,这个数字叫做地址.在一台8位为一字节,32位为一字的机器中,我们可以看到下面的内存,这个内存表示的是一个字的大小:
此处,字节的地址在左边.我们可以使用地址来表示,从这个地址开始的,但是不相同的几个数据.例如,我们可以说地址736424的那个字或者地址736424的那个字节.为了给内存中的某个地址赋予一个含义.我们必须知道存储在这个地址的类型.类型决定了有多少bit位被使用,以及怎么来解释这些bit位.
如果一个对象在736424地址处,并且类型为float,如果float在这个机器上以32位保存.那么我们就知道这个对象从736424地址开始,跨越了整整一个字的长度.float类型的值,依赖于机器如何存储浮点数.
如果,在736424地址处的对象为unsinged
char类型.并且这台机器使用了ISO-Latine-1字符集,那么在这个地址处的字节就是一个分号.
浮点类型可以表示:单精度,双精度,高精度.c++标准制定了浮点类型的最小有效数,然而大多数的编译器都提供了比标准更高精度的浮点类型.通常,float类型大小为一个字(32位),double类型为两个字(64位).long double 类型为三个字或者四个字(96位或者128位).float和double类型分别有7和16个有效位.long double常用在有特殊用途的浮点硬件上,它的精度依赖于它的具体实现.
有符号类型和无符号类型
除了bool类型和扩展的字符类型以外,其他类型还区分:有符号和无符号.有符号类型可以代表正数,负数,零。无符号类型只能表示大于等于0的数。
类型int,short,long和long long都是有符号的。如果我们要得到对应的无符号的类型,可以在前面加上unsigned.例如:unsigned long.另外,对于类型unsigned int可以缩写为unsigned.
与其他的整数类型不同的是,c++还定义了三个不同的字符类型:char,signed char和unsigned char.尤其的是:char类型。它和signed char类型不一样。尽管此处有三种char类型,但是只有两种表现形式:有符号和无符号。char类型到底是有符号还是无符号,取决于编译器的实现。
对于一个无符号的类型来说,所有的bit位都用来存储值。例如,一个8位的unsigned char类型,它可以保存0-255.
c++标准并没有规定有符号类型应该怎么表示,但是它规定了在表示范围内的正负值应该平均分配。因此,一个8位的signed char类型应该保证存储-127到127的数。大多数的现代机器都使用了一种-128到127的表示法。
建议:决定使用哪种类型
像c一样,c++被设计得更接近硬件。算数类型被设计来满足各种各样的硬件,因此算数类型的种类多得让人有点困惑。大多数的程序员可以通过限制使用的类型来忽略这种复杂性。下面的一些经验对于决定使用哪种类型有一定的帮助:
1. 当你知道值不会为负数的时候,可以使用一个无符号的类型。
2. 对于整数的操作使用int。在实际使用中,short太小,而 long的大小一般和int一样。如果你的值超出了int的表示范围,那么就使用long long
3. 不要在算数表达式中使用char类型或者bool类型。这两种类型只有在保存字符和真假值的时候使用。计算机使用char类型特别容易出错,因为char类型在一些机器上面是有符号的,而在两外一些机器上面是无符号的。如果你需要一个最小的整数,应该明确的声明为有符号或者无符号。
4. 对于浮点运算使用double类型,因为float类型通常精度不够,并且double类型的浮点精度提高产生的性能消耗可以忽略不计。事实上,在某些机器上,双精度浮点会更快一些。long
double的精度通常没有必要,并且带来的运行时消耗是不可忽略的。
2.1.2 类型转换
一个对象的类型包含了这个对象的值和这个对象的可做的操作。许多类型支持将一种类型转变成另外一种类型。
当我们使用了一种类型,而此处更希望是另外一种类型,那么类型转换将会自动发生。在4.11小节,我们还会讨论更多的类型转换。本节主要是讨论:当我们给一个类型赋值另外一种类型时的类型转换。
当我们给一种算数类型赋值其他算数类型时,如下:
bool b =42; //b is true
int i = b ; //i 得值为1
i = 3.14; //i的值为3
double pi = i; //pi的值为3.0
unsigned char c = -1 //假设为8位的char类型,c的值为255
signed char c2 = 256 //假设为8位的char类型,c2的值不能确定
对于上面的具体细节,依赖于类型能够保存的值的范围:
1. 当我们给一个bool类型赋值一个非bool类型的时候,结果就是:如果是0,就是false,非0,就是true。
2. 当我们将一个bool类型赋值给其他的算数类型的时候,结果就是:如果bool是true,那么就是1;如果是false,就是0。
3. 当我们将一个浮点数赋值给一个整型类型的时候,浮点值将会被截断。被存储的值仅仅只有小数点前面的整数部分。
4. 当我们将一个整型值赋值给一个浮点类型的时候,小数部分将会是0.如果整数的bit位已经超过了浮点数的容纳能力,那么可能会造成精度丢失。
5. 如果我们给一个无符号类型,赋值一个超出了它的范围的值,结果就是:对目标类型可以保存的个数取模之后的余数(也将这种情况成为值环绕)。例如,一个8位的unsigned
char类型,可以保存0-255的值。如果我们赋值超出了这个范围,那么就是对256取模,然后用余数。因此将-1赋值给一个8位的unsigned
char类型,将会得到255
6. 如果给一个有符号的类型,赋值一个超出它范围的值,结果是未知的(又时也翻译为未定义的),即不清楚会是什么样的结果。程序可能工作,也可能崩溃,或者产生垃圾值。
建议:避免使用未知结果和依赖于机器的实现
未知结果的行为源于编译器不需要去检查的错误(有时候是不能被检查).即使代码编译通过,还是可能在程序执行的时候出错.
不幸的是,包含未知结果的程序在某些编译器下也能够执行成功,但是却不能保证在其他的编译器下也能够执行成功,甚至不能保证,在相同编译器下再次编译也能执行成功.此外也不能保证,程序对一组输入有效的情况下,对另外的输入也有效.
同样的,程序应该避免使用依赖于实现的行为.例如假定int的大小是固定的,那么这种程序就不能认为是可移植的.当程序移植到其他的机器上的时候,依赖于实现的代码可能会运行失败.从先前的代码中跟踪这种错误,是一件令人不爽的事.
当我们使用一种类型,但是此时更期望其他类型的时候,编译器运用这些类型转换。例如,当我们使用一个非bool类型作为条件的时候,算数类型将会转换成bool类型,转换的规则与赋值给bool类型一样。
int i=42;
if(i) //条件将会转成true
i=0;
如果值为0,那么条件将是false,否则,就是true。
同样的道理,当我们在算数表达式中,使用bool类型的时候,他的值总是转换成0或者1.
因此,在算数表达式中使用bool类型是不推荐的。
涉及无符号的表达式
尽管我们不可能有意的将一个负数赋值给一个无符号的对象,但是,我们写出的一些代码,可能隐式的进行了这种赋值。例如,当我们在算数表达式中使用了unsigned int 和int的时候,通常int会被转换成unsigned int.转换的规则就如同将int值赋值给unsigned int.
unsigned u = 10;
int i = -42;
std::cout << i+ i << std::endl; //prints -84;
std::cout << i+u<< std::endl; // 如果int为32位,打印:4294967264
在第一个表达式中,我们将两个int值相加,得到了我们期望的结果。
在第二个表达式中,相加之前,-42的int值会被转换成unsigned int然后再相加。将一个负数值转成一个unsigned int类型,就好像,我们将一个负数值赋值给一个unsigned int类型,结果就是,如同上面讲的:值环绕——即将负值与unsigned int的模相加。
如果我们从一个无符号中减去一个值,不管操作数是否为无符号,我们必须保证结果不能为负数。
unsigned u1 = 42 ,u2 = 10;
std::cout << u1 - u2 << std::endl ;//ok:结果为32
std::cout << u2-u1 << std::endl;//ok:但是结果将会发生值环绕——即负值,加上无符号的模
一个无符号不能小于零,这个事实也影响了我们写循环的方式。例如,在1.4.1的练习中,通过递减10到0来写循环。可能会写成下面这样:
for(int i=10;i>=0;--i)
std::cout << i << std::endl;
写完之后,我们发现,我们不需要打印负值,因此使用了unsigned int来重写这个循环。那么,你将永远不会结束这个循环。
for (unsigned u = 10;u>=0;--u)
std::cout << u << std::endl;
思考:当u等于0的时候的情况。在此种情况下,我们会打印0然后,执行循环里面的自减运算符。结果就是0自减,变为-1,但是-1并不适合一个unsigned int类型,此时会发生值环绕。如果int类型为32位,那么-1将会变成4294967295.
写这个循环的另外一种方法是使用while循环。它可以让我们在打印之前递减(而不是在之后)。
unsigned u =11;//
while( u > 0){
--u;//先递减,目的是最后一个数为0
std::cout << u << std::endl;
}
这个循环,先递减循环控制值。在最后一次循环中,进入循环的值将为1,我们是先递减,因此在最后一次循环中,将会打印0。接着在while的下一次条件表达式中,值为0,while循环则退出。因为最先递减,然后才打印,所以我们不得不比第一次打印的值大1,基于此原因,初始化u为11。这样就可以正常打印10.
注意:不要混合使用有符号和无符号类型
当有符号类型为负数的时候,有符合和无符号类型的表达式,将会产生令人惊讶的结果。因为有符号数将会转换成无符号数。例如,在一个a*b的表达式中。如果a是-1,b是1.且a和b都是int类型,则结果为-1.但是,如果a是int,b是unsigned
int,那么结果就依赖于int的位数。在我们的测试机中,这个表达式的结果为4294967295
2.1.3 字面量
像42这样的值,一见便知,所以叫做字面量(有时也称作字面值)。每一种字面量都有类型.字面量的格式和值决定了它的类型.
整数和浮点字面量
我们可以使用十进制,八进制,十六进制来写一个整数的字面量.八进制的字面量,在前面加上数字0.十六进制的字面量在前面加上0x或者0X.我们可以写20这个值,使用下面的三种形式:
20 /*十进制*/ 024 /*八进制*/ 0x14 /*十六进制*/
一个整数字面量的类型依赖于它的值和表现形式.默认情况下,十进制字面量是带有符号的.但是,八进制和十六进制可以是有符号和无符号的.一个特定十进制字面量的类型是int,long,long long,三种类型中的最小的能够存储这个字面量的类型(此例中,是第一种类型,int型).八进制和十六进制的类型是能够容纳其数值的int,unsigned int ,long ,unsigned long ,long long ,unsigned long long中最小的类型.
当一个字面量的值太大以至于不能放在最大的相应类型中时,会报错.这里没有short类型的字面量.下图展示了我们可以使用后缀来覆盖他们的默认类型.
尽管整型值可以被存放在有符号类型中,但是从技术上来讲,十进制字面量的值从不会为负数。如果我们写了一个看起来是负数的值,如-42,这个负号不是字面量的一部分。负号仅仅是为了取操作数的负值。
浮点字面量可以表示为:一个小数或者一个用指数表示的科学计数。使用科学记数法,指数通过使用E或者e来表明,如:
3.14159 3.14159E0 0. .0e0 .001
默认情况下,浮点字面量的值为double类型。我们可以使用表2.2中所示的后缀来改变这种默认类型。
字符和字符串字面量
被单引号括起来的字符是类型char的字面量值。0个或者多个被双引号括起来的字符序列是字符串字面量,如:
‘a’ //字符字面量
“Hello world !” //字符串字面量
一个字符串字面量,其实是char类型的常量数组。数组类型将在3.5.4中讨论。编译器在每个字符串字面量的末尾加上一个空字符(‘\0’)。因此实际的字符串长度,比表面看到的要大1.例如,‘A’表示的是字符A,“A”表示的是含有两个字符的数组,一个字符为A另外一个字符为空字符(‘\0’)。
如果两个字符串字面量紧紧相邻,且他们由空格,tab键,换行符分割,那么他们将会被连接成一个字符串字面量。因此,当我们需要使用字符串字面量,且这个字面量较长,不适合放在单行的时候,我们可以采取这种形式,如:
//多行字符串字面量
std::cout << “a really ,really long streing literfal ”
“that spans two lines” << std::endl;
转义字符
一些字符,例如退格字符,控制字符等,他们不可见,这种字符称为不可打印字符。在c++中还有一些字符具有特殊的意义(单引号,双引号,问号,反斜杠)。因此c++中不能直接使用这两种字符,如果要使用这些字符,需要使用转义字符。一个转义字符总是以一个反斜杠开头。c++定义了几个转移字符,如:
换行 \n 水平tab \t 报警(响铃) \a
竖直tab \v 退格 \b 双引号 \”
反斜杠 \\ 问号 \? 单引号 \’
回车 \r 进纸 \f
使用转义字符,就跟使用单个字符是一样的:
std::cout << ‘\n’; //换行
std::cout << “\tHi!\n” //先打印一个tab,然后是Hi!,接着换行
我们也可以使用通用的转义字符,格式为:\x后面跟一个或者多个十六进制,或者\后面跟一个,两个,三个八进制。其中数字部分,为字符对应的数值。一些例子(假定字符集为Latin-1):
\7 (响铃) \12 (换行) \40(空格)
\0(空制符) \115(‘M’) \x4d(‘M’)
转义字符的使用,跟其其他任何字符的使用一样:
std::cout << “Hi \x4d0\115!\n”; //打印 Hi MOM!,然后换行
std::cout << “\115” << ‘\n’; //打印M然后换行
注意:如果反斜杠后面跟的八进制超出了三个,那么只有前面三个才是转义字符。例如,”\1234”有两个字符,一个为\123,另外一个为4.相反,\x将会使用后面四个数字,”\x1234”代表一个16位的字符。因为,大部分的机器都是8位的字符,所以这种字符可能用的场景并不多。通常情况下,超过8位的字符是随着表2.2中的前缀一起被用在扩展字符集中。
指定字面量的类型
可以使用表2.2中的前缀或者后缀,覆盖一个整型,浮点型,字符型字面量的默认类型。
L’a’ //类型变为wchar_t
u8”hi!” //utf-8字符串
42ULL //unsigned long long
1E-3F //科学记数法表示的浮点型
3.14159L //long double
经验: 当你要写long类型的时候,最好使用大写的L,小写l常常与1混淆。
对于一个整型字面量来说,我们可以分开指定符号和大小。如果后缀含有U,表示这是一个无符号的字面量,它将是,unsigned int,unsigned long ,unsigned long long 中最适合存放这个字面量的最小类型。如果后缀包含L,则这个类型至少为long。如果后缀包含LL,则类型为long long 或者unsiged long long.可以将U和L,LL放在一起使用,如,一个字面量有UL的后缀,则表示unsigned long 或者unsigned long long,具体类型则是选择适合存放这个字面量的最小类型。
布尔和指针字面量
true和false是bool类型的字面量:
bool test = false;
nullptr 是指针类型的字面量。在2.3.2节中将会有更多关于指针的讨论。
2.2 变量
变量让程序能够通过名字操作存储空间。在c++中的变量都有相应的类型。类型决定了变量空间的大小和布局。类型还决定了这个变量空间存储值的范围。类型还决定了可以被运用在这个变量上的操作。c++程序员一般将“变量”和“对象”互换使用。
2.2.1 变量的定义
一个简单的变量定义包括:一个类型说明符,然后是一个,或者多个,被逗号分隔的变量名,最后是一个分号。每一个变量的类型都由类型说明符指定。定义的时候,可以给变量赋初始值。
int sum = 0, value,//sum, value,units_sold的类型为int
units_sold = 0;//sum,units_sold的初始值为0
Sales_item item;//item类型为Sales_item
//string是一个库类型,代表了一个可变长的字符序列。
std::string book(“0-201-78345-X”);//book变量被一个字符串字面量初始化。
book的定义使用了std::string库类型。像iostream一样,string也被定义在了std命名空间中。在第三章,将会讨论更多关于string类型的特性。现在,只需要知道string是一个可变长的字符序列即可。string库提供几种方式,让我们初始化string对象。其中一种是将字符串字面量值复制给对象(2.1.3)。因此,book被初始化保存字符串0-201-78345-X.
术语:对象是什么?
c++程序员喜欢使用对象这个术语。通常情况下,一个对象表示了一段内存,这段内存存储着值和相应的类型。
一些人仅仅在类类型的情况下,才使用术语“对象”。还有一些人,将命名和未命名的对象区分开来,他们使用术语“变量”来表示命名的对象。还有一些人,将对象和值区分开来,他们使用术语“对象”来表示可以被程序改变的数据,而使用术语”值”来表示那些只读的数据。在本书中,我们遵守大部分人的习惯用法。我们使用术语“对象”,而不管是否为内置类型还是类类型,也不管是否命名,也不管是否可读写。
初始化
在变量被创建的时候,给他值,就表示变量被初始化了。初始化变量的值可以是一个复杂的表达式。当定义了两个或者多个变量的时,变量随着定义马上就可用了,因此,可以用前一个变量的值,来初始化后一个变量。
//在初始化discount之前,price已经被定义和初始化了
double price = 109.99,dicount = price * 0.16;
//调用applyDiscount 并且使用了返回值来初始化salePrice
double salePrice = applyDiscount(price,discount);
初始化是c++中非常复杂的一个主题,我们将反反复复的讨论这个主题。许多程序员都对于使用=符号来初始化一个变量感到困惑,因为,他们常常将赋值和初始化认为是同一种,但是在c++中,赋值和初始化是两个不同的操作。这个概念是比较困惑的,因为在其他语言中,这两者的区别可以被忽略掉。就算在c++里面,这两者的区别有时也无关紧要。但是,这个概念是非常重要的,并且我们会在后面反复提及。
警告: 初始化不是赋值。初始化发生在:变量创建的同时,给定一个值。赋值是:使用一个新的值覆盖掉以前的旧值。
列表初始化
c++定义了几种初始化方式,这也是初始化这个主题复杂的原因之一。例如,我们可以使用下面四种方式来定义int类型的units_sold变量。
int units_sold = 0;
int units_sold = {0};
int units_sold(0);
int units_sold{0};
大括号用来初始化,是c++11新标准的一部分。这种形式在新标准以前,仅仅被用在一些受限的地方中。出于在3.3.1节中我们将学习他的原因,此处不做过多介绍。这种形式的初始化,被称为列表初始化。现在,无论是初始化一个对象,还是赋值一个对象,都可以使用这种用花括号括起来的形式。
当使用内置类型的变量的时候,列表初始化有一个非常重要的特性:如果初始化值可能导致信息丢失,那么编译器将报错。
long double ld = 3.1415926536;
int a{ld}, b ={ld};//错误:变小的类型转换
int c(ld),d =ld;//ok,但是值将会缩短,丢失部分信息
编译器拒绝初始化a和b,因为使用一个long double类型来初始化一个int类型,这可能导致部分信息丢失。至少ld的小数部分将会被丢掉,并且,ld的整数部分对于int来说也可能太大。
此处展示的区别可能看起来无关紧要,因为,我们不太可能直接用long double来初始化一个int。但是,这种转换可能会无意的发生,如16章介绍的一样。我们将在3.2.1,3.3.1中再次讨论更多关于这种格式的初始化。
默认初始化
当我们定义个没有初始化的变量的时候,变量会默认被初始化。这些变量会被初始化为默认的值.默认的值依赖于变量的类型和变量定义的地方。
内置类型如果没有被显示的初始化,那么它的默认值依赖于定义的位置。定义在函数外面的变量被初始化为0.然而如6.1.1节中介绍的一样,被定义在函数内的内置类型,不会被初始化。未被初始化的变量,它的值是未知的。复制或者存取这个未被初始化的变量是一种错误的操作。
每一种类都控制了这种类类型被初始化时的操作。尤其,在定义对象的时候是否可以不用初始化,也被类控制。如果类允许这种行为,那么他将决定对象初始化的值是什么。
大多数的类都可以定义没有显示初始值的对象。这些类提供了一个合适的默认值。例如,上面讲到的一样,string类,如果我们没有提供一个初始化的值,那么这个string对象默认为一个空字符串:
std::string empty;//隐式的初始化字符串为空
Sales_item item;//默认初始化Sales_item对象
一些类需要为每一个对象显式的初始化。如果对这种类,不初始化而创建对象,那么编译器将会报错。
注意: 在函数内部的未初始化的内置类型,他的值是未知的。没有被显式初始化的类类型,他的值由定义他的类决定。
注意:未初始化的变量导致运行时问题
一个未初始化的变量有一个不确定的值。使用这种值是错误的,并且这种错误很难调试。虽然大部分的编译器会对部分使用了未初始化的变量给出警告,但是编译器不强制要求检测这种错误。
使用未初始化的变量将会带来无法预估的后果。有时,我们非常幸运,在存取这个对象的时候,会马上报错,然后我们追踪这个错误的位置,此时是非常容易发现变量没有被相应的初始化。但是还有一些时候,程序会运行完,然后得出错误的结果。更坏的情况是,程序产生的结果,在某一次是正确的,在后续运行中又产生了错误的结果。并且将一些其他代码添加到其他位置,此时报错了,可能导致我们认为:程序原来是对的,是新添加的代码导致了错误的结果。
建议: 我们建议初始化内置类型的每一个对象。虽然不是必须的,除非你能够保证省略初始化没有问题,否则这种方式是更简单、更安全的。
2.2.2 变量的声明和定义
为了把程序写在多个逻辑段里面,c++支持分离式编译。分离式编译可以让我们将程序放在几个文件中,每个文件单独编译。
当我们将程序放在几个文件中的时候,我们需要通过一种方式,来提供跨文件的代码访问。例如,一个文件中代码需要用到另外一个文件中定义的变量。考虑一个实际的例子,std::cout和std::cin,他们被定义在标准库中,我们的程序也可以使用这些对象。
为了支持分离式编译,c++区别声明和定义。声明只是创造了一个程序可以使用的名字。一个文件如果想要使用定义在其他地方的名字,只需要包括有那个声明文件即可。定义是创建一个相应的实体。
变量的声明:指定了变量的类型和名字,他和变量的定义相同。另外,变量的定义还会分配存储空间,并尽可能的提供一个初始值。
为了得到一个声明,而不是定义,我们将增加关键字,并且不提供一个显式的初始化。
extern int i;//只有声明没有定义 i
int j;//声明并且定义 j
任何含有显示初始化的声明都是定义。我们可以给用了extern的变量提供一个初始值,但是这将覆盖掉extern带来的效果,即,如果一个extern 声明有初始化值,那么它就是一个定义,而不是声明。
extern double pi = 3.1416://为定义
在一个函数内部,给一个extern关键字修饰的对象初始化,是错误的。
注意: 变量必须被定义一次,但是可以声明多次。
对于声明和定义两者的区别,可能有点晦涩难懂,但是这是非常重要的区别。为了在多个文件中使用变量需要声明与定义分开,我们必须定义变量在一个文件中,并且只能在一个文件中,其他文件只能声明这个变量,而不能定义变量。
重要概念:静态类型
c++是一种静态类型语言,静态类型意味着,编译的时候会进行类型的检查。
正如所见,一个对象的类型约束了这个对象可以执行的操作。在c++中,编译器检查我们所写的操作是否被这个类型所支持。如果我们写了一个不支持的操作,那么编译器将会报错,并且不会产生一个可执行文件。
当我们的程序变得越来越复杂的时候,我们会发现,这种静态类型可以帮助我们找到bug。因此,一系列的静态检查,必须先让编译器知道,例如,我们使用的每个实体的类型。所以,我们必须在使用变量之前,给每个变量声明一种类型。
2.2.3 标识符
c++中的标志符由字符,数字,下划线组成。并且对于标识符名字的长度没有限制。标识符必须由字符或者下划线开始,大小写敏感。
//定义了四个不同的int变量
int somename ,someName,SomeName, SOMENAME;
c++保留了一些名字,被列在表2.3和表2.4中,这些名字不应该被作为标识符使用。
c++11 标准也保留了一些名字用在标准库中.所以,我们程序里面自定义的标识符不能包含连续两个的下划线,也不能以一个下划线,紧跟一个大写字符开头.同时,定义在函数体之外的标识符也不能以下划线开头.
变量名命名的一些约定俗成规则
此处有一些常用的变量名命名规则,使用下面的这些规则可以提高程序的可读性.
1.标识符应该具有相应的意义
2.变量名通常为小写,如index,而不是Index或者INDEX
3.像Sales_item,这种类定义一样,我们在定义类的时候,常常以大写字母开始
4.多个单词应该有明显的区分,如student_loan 或者studentLoan,而不建议写成studentloan.
经验 变量命名的一些约定俗成,一旦坚持使用,将会发挥它最大的功效.
2.2.4 名字的作用域
在程序的任何位置,被使用的名字都指向一个实体,如变量,函数,类型等等.但是,一个名字在程序的不同地方,可以指向不同的实体.
作用域是程序的一部分,在作用域中,一个名字具有特定的意义.在c++中的大多数作用域被大括号限定.
同一个名字在不同的作用域可以指向不同的实体.名字从声明处开始,到声明所在的作用域结束.
思考下面来自于1.4.2节中的例子
#include <iostrem>
int main(){
int sum =0;
//将1到10相加
for(int val = 1;val<=10;++val)
sum += val;
std::cout << “Sum of 1 to 10 inclusive is ”
<< sum << std::endl;
return 0;
}
这个程序定义了三个名:main,sum,val,并且使用了std命名空间中的cout和endl。main被定义在所有大括号之外。main像其他函数外部定义的名字一样,具有全局的作用于。一旦定义在了全局作用域,那么,它在程序的整个运行期间有效。名字sum定义在main函数的函数体内,因此他的作用域从它的声明处开始,持续到函数的剩下部分,但是不会超出函数体。这种在某个大括号内的作用域称为块作用域。变量sum具有块作用域。名字val被定义在了for语句的作用域内。他只能在for语句中被使用,不能在main函数的其他地方使用
嵌套作用域
作用域可以包含其他的作用域。被包含的作用域称为内部作用域,包含的作用域被称为外部作用域。
一旦一个名字被定义在了一个作用域内,那么这个名字就可以被这个作用域内的内部作用域访问。定义在外部作用域内的名字,也可以被内部作用域再次定义。如:
#include <iostrem>
//程序仅仅为了说明,实际中,这种写法很low
//定义一个全局变量,和同名的本地变量
int reused = 42;//reused具有全局作用域
int main(){
int unique = 0;//unique具有块作用域
//输出(1):使用全局变量reused,输出42 0
std::cout << reused << “ ”<< unique << std::endl;
int reused = 0;//一个新的本地变量,全局变量reused将被隐藏。
//输出(2):使用了本地变量reused ,输出0 0
std::cout << reused << “ ”<< unique << std::endl;
//输出(3):显示的使用全局变量reused,输出42 0
std::cout << ::reused << “ ”<< unique << std::endl;
return 0;
}
输出(1)在本地变量reused之前,因此,使用的是全局变量的reused,输出为:42 0.输出(2)在本地变量reused作用域内,因此,使用的是本地变量reused,输出为:0 0.输出(3)使用作用域运算符,覆盖了默认的作用域。因全局作用域没有名字,因此,作用域运算符的左侧操作数是空,他表示的是:在全局作用域中获取右边操作数对应的那个名字。因此输出(3)使用了全局作用域的reused,输出为42 0。
警告 将本地变量定义成和全局变量同一个名字,通常是不好的习惯,因为程序可能更想使用全局变量,此时容易使用到本地变量。
2.3 复合类型
复合类型就是,依据其他类型来定义的一种类型。c++有几种复合类型,其中引用和指针,将在本章中讲解。
定义一个复合类型的变量比至今我们所学的所有变量的定义都复杂。在2.2节中指出:简单的声明语句由类型和跟在类型后面的变量名组成。更通用的描述是:一条声明语句由:一个基类型和跟在基类型后面的一组声明符组成。每个声明符命名了一个变量,并且指定了这个变量的类型,这个类型与声明语句最开始的那个基类型相关。
迄今为止,我们接触到的声明符就是变量名。这些变量的类型就是声明语句的基类型。更复杂的声明符可以是:从声明语句的基类型中构建一个复合类型,然后再将这个类型指定给一个变量。
2.3.1 引用
注意:
c++11新标准中介绍了一种新的引用,我们称之为“右值引用”,他们将在13.6.1节中介绍它。这种引用主要用于内置类。从技术上面来讲,我们使用术语“引用”,表示的是左值引用。
引用为对象定义了一个名字。引用类型,引用其他类型。定义一个引用,只需要写如下形式的声明符即可,&d,这个d就是我们声明的名字。
int ival = 1024;
int &refVal = ival; //refVal 指向ival
int &refVal2; //错误:引用必须被初始化
通常情况下,初始化一个变量的时候,直接将初始值复制进创建的变量中。但是,初始化一个引用的时候,直接将引用和初始值绑定在一起,而不是复制初始值。一旦初始化之后,引用一直和它绑定的对象绑定在一起。不准对一个已经绑定了的引用,再次绑定其他的对象。因此c++中,引用必须初始化。
引用即别名
注意: 引用不是对象,引用只是一个已经存在对象的别名而已。
当一个引用被定义之后,所有作用在引用上面的操作,实际上是作用在了与引用绑定的对象上。
refVal =2 ; //将值2赋值给引用,实际上是赋值给变量ival
int ii = refVal; //等价于 ii == ival
我们赋值给引用,相当于赋值给这个引用绑定的对象。我们从一个引用获取值,相当于从这个引用绑定的对象中获取值。同样的,当我们使用一个引用作为初始值时,实际上是使用了这个引用绑定的对象,作为初始值。
//正确:refVal3被绑定到了ival上
int &refVal3 = refVal;
//用refVal绑定对象的值,初始化i
int i = refVal; //初始化i的值与ival相同
因为引用不是对象,因此我们无法定义一个引用来指向另外一个引用。
引用的定义
在一个定义中,我们可以定义多个引用。每个引用标识符都必须以&符号开头。
int i = 1024, i2 = 2048; //i和i2都是int类型
int &r = i,r2 = i2;//r是绑定到i的引用,r2是一个int类型
int i3 = 1024, &ri = i3;//i3是一个int类型,ri是绑定i3的引用
int &r3 = i3,&r4 = i2;//r3和r4都是引用
除了2.4.1节和15.2.3节中介绍的例外之外,其他的引用类型必须和绑定的对象类型严格匹配。而且,引用必须和一个对象绑定,而不能和一个字面值或者一个表达式的值进行绑定,原因将在2.4.1节中介绍。
int &refVal4 = 10;//错误:初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval;//错误:初始值必须是一个int类型的对象
2.3.2 指针
指针是一种指向其他类型的复合类型。跟引用一样,指针用来间接的存取对象。跟引用不一样的是,指针是一个对象,它可以被赋值和复制,在其生命周期中,可以指向几个不同的对象。指针也不像引用,它不必再定义的时候初始化。跟其他内置类型一样,如果指针被定义在了块作用域,并且没有被初始化,那么它的值是不确定的。
警告 指针常常难以理解。有经验的程序员常常在调试指针错误的时候,也备受折磨。
通过写*d,这种格式的声明符,我们就能够定义一个指针类型,此处d就是被定义的名字。对于每一个指针变量来说,*必须重复书写,如下。
int *ip1,*ip2;//ip1和ip2都是指针,指向int类型
double dp ,*dp2;//dp2是一个指针,指向double类型,dp是一个double类型
获取一个对象的地址
一个指针保存一个对象的地址。通过使用取地址运算符(&),获取一个对象的地址。
int ival = 42;
int *p = &ival;//p保存有ival的地址,p是一个指向ival的指针。
第二条语句,定义指向int的指针p,并且初始化p指向ival对象。因为引用不是对象,他们没有地址,所以,我们不能定义一个指向引用的指针。
除了2.4.2和15.2.3节中介绍的例外,指针类型和,指向的对象类型必须严格匹配:
double dval;
double *pd = &dval;//正确,初始化一个double类型的地址
double *dp2 = pd;//正确,初始化一个指向double类型的指针
int *pi = pd;//错误,pi和pd的类型不同
pi = *dval;//错误:将double类型的地址赋值给一个int类型的指针
指针之所以要严格匹配,是因为,通过指针的类型,来推断指向的对象的类型。因为类型决定了对象可用的操作和内存空间的布局。如果一个指针指向不符合类型的对象,那么操作这个对象将会发生错误。
指针的值
存储在指针中的值,可以有四种状态
1. 可以指向一个对象
2. 可以指向一个对象末尾紧邻的位置。
3. 可以为一个空指针,表示没有指定到任何对象上
4. 可以为无效状态,超出上面3种的都是无效状态
尝试读取一个无效指针的值,是一种错误的行为。这种错误编译器不强制检查,就像我们使用了一个未被初始化的变量一样,编译器也不强制检查。因此,我们必须知道使用的指针是否是有效的。
尽管在上述2,3情况下指针是有效的,但是在这种情况下的指针是受限的。因为这种情况下的指针没有指向任何对象,我们就不能通过这个指针来存取对象。如果我们尝试存取这种情况下的指针,那么结果将是未知的。
使用一个指针来存取对象
当一个指针指向一个对象的时候,我们可以使用解引用运算符(*)来存取这个对象。
int ival = 42;
int *p = &ival; //p保存有ival的地址,p是一个指针,指向ival
cout << *p ;//*产生p指针指向的对象;打印42
解引用一个指针,将会产生这个指针指向的那个对象。通过给解引用的结果赋值,就相当于给那个对象赋值。
*p =0;//*产生那个对象,通过p给ival赋了一个新值
cout << *p ;//打印 0
当我们赋值给*p的时候,就相当于赋值给了p指向的对象。
注意: 我们只能解引用一个有效的指针
关键概念:一些符号具有多个意思 一些符号同时被用做运算符和声明语句的一部分,例如,&和*。符号使用的上下文,决定了这个符号代表的意思:
int i = 42; int &r = i; //&跟在一个类型的后面,他是声明的一部分,r是一个引用 int *p; //*跟在一个一类型的后面,它是声明的一部分,p是一个指针 p = &i; //& 被用在了一个表达式中,当作一个取地址运算符 *p = i; //*被用在了一个表达式中,当作一个解引用运算符 int &r2 = *p; //&声明的一部分,*是解引用运算符。
在声明中,&和*用来表示一个复合类型。在表达式中,这两个符号用来表示一个运算符。因为同一个符号在不同的上下文具有不同的意思,因此可以当作两个不同的符号来看待。
空指针
空指针就是没有指向任何对象的指针。在尝试使用一个指针的时候,代码可以检查这个指针是否为空。下面有几种方式,创建几个空指针:
int *p1 = nullptrl; //等价于 int *p1 = 0;
int *p2 = 0; //直接初始化指针为0
//必须包含头文件cstdlib
int *p3 = NULL: //等价于 int *p3 = 0
最直接的方式是使用nullptr字面量来初始化一个指针,这个字面量在c++11新标准中被引入。nullptr是一种可以转换成任何指针类型的字面量。我们也可以直接初始化指针为0,如p2的定义一样。
老一点的程序有时也使用一个预处理变量——NULL,它在cstdlib头文件中被定义为0。
在2.6.3节中我们将讨论一部分预处理相关的细节。现在只需要知道:预处理是编译之前的一段处理过程。预处理变量由预处理器管理,并且不是std命名空间的一部分。因此我们直接使用,不用加std前缀。
当我们使用一个预处理变量的时候,预处理器自动用变量的值代替变量。因此初始化一个指针NULL,就等价于初始化指针0.现代c++程序应该避免使用NULL,而用nullptr来代替。
赋值一个int变量给一个指针是非法的,即使这个变量的值为0.
int zero = 0;
pi = zero ;//错误:不能讲int赋值给一个指针
建议:初始化所有的指针 一个未初始化的指针常常是运行时错误的源头。
正如使用未初始化的变量一样,使用一个未初始化的指针,这种行为的结果是无法确定的。使用一个未初始化的指针,几乎总是会产生一个运行时的崩溃。但是,调试产生崩溃的原因是非常困难的。当我们使用一个未初始化的指针时,大多数编译器,将指针当前的bit位内容当做地址。使用一个未初始化的指针,就相当于,在假定的地址,存取假定的对象。如果指针指向的内存位置有bit位,是没有办法分辨这个指针是有效的还是无效的。
我们建议,初始化所有的指针变量。如果有可能,应该在一个对象被定义之后,再定义指向它的指针。如果一个指针没有对象与之绑定,应该初始化这个对象为nullptr或者0.通过这种方式,程序可以检测这个指针是否指向一个对象。
赋值和指针
指针和引用提供了一种间接存取对象的方式。但是他们两者具有非常大的差别。最大的差别是引用不是对象。一旦我们定义一个引用,就没办法再让这个引用绑定到其他的对象。
当我们使用引用的时候,总是使用引用绑定的对象。
指针和它保存的地址,就没有这种限制。像其他变量一样(非引用),当赋值指针的时候,指针就有了一个新值。赋值让一个指针值向了另外一个对象:
int i = 42;
int *pi = 0; //pi被初始化,但是没有指向对象
int *pi2 = &i; //pi2 被初始化保存i的地址
int *pi3; //如果pi3被定义在一个块作用域中,pi3的值是不确定的
pi3 = pi2; //pi3和pi2指向相同的对象
pi2 = 0; //pi2现在不指向任何对象
一个赋值语句到底改变的是指针还是指针指向的对象,他是难以直接观察到的。我们应该记住最重要的原则是:赋值改变的是它的左操作数。当我们写下如下的语句时:
pi = &ival; //pi的值被改变,pi现在指向ival
赋值一个新值给pi,它改变了pi保存的地址。另一方面,当我们写下如下的语句时:
*pi = 0; //在ival中的值被改变,pi没有改变
此时*pi被改变(也就是pi指向的对象)
其他指针操作
只要指针有效,我们就可以将指针运用在条件表达式中。就像在条件表示中使用算数值一样,如果指针为0,则条件表达式为false:
int ival = 1024;
int *pi = 0; //pi 是一个有效的指针,为空指针
int *pi2 = &ival; //pi2是一个有效的指针,他保存着ival的地址
if(pi) //pi的值为0,因此条件表达式的值为false
if(pi2) //pi2指向ival,pi2的值不为0,因此条件表达式为true
任何非零的指针,都会被当作true
给定两个类型相同的有效指针,可以使用等于运算符(==)或者不等于运算符(!=)来进行比较,比较的结果是bool类型。如果两个指针保存有相同的地址,那么两个指针就是相等的,否则不等。两个指针保存有相同的地址,有下面几种情况:1.他们都为空,2.他们都指向同一个地址,3.他们都指向同一个对象的下一个地址。需要注意的是,下面这种情况是有可能发生的:一个指针指向一个对象,另外一个指针指向不同对象的下一个对象,但是他们保存的地址是相同的,这种情况下比较指针也是相等的。
因为这种操作使用了指针的值,因此在条件表达式或者比较运算中,指针必须是有效的。如果使用了无效的指针,那么结果将是无法预料的。
3.5.节中将会介绍另外的指针操作。
*void 指针
void *类型的指针,是一种特殊类型的指针,这种类型的指针可以保存任何对象的地址。跟其他类型的指针一样,void *指针保存有一个地址,但是这个地址上面的对象的类型却是不清楚的:
double obj = 3.14, *pd = &obj;
//正确:void *可以保存任何类型的地址
void *pv = &obj;//obj可以是任何类型的对象
pv = pd; //pv可以保存任何类型对象的指针
使用void *指针能够做的事比较有限:可以和其他指针比较,也可以向函数传递或者从函数返回,还可以赋值给其他void 的指针。不能使用void指针来操作上面的对象,因为我们不知道这个对象具体的类型,而类型又决定了对象所支持的操作。
通常情况下,void*指针就是把内存当作内存在处理,不能使用这个指针来存取这个内存上面的对象。在19.1.1小节中将介绍使用void*指针。4.11.3小节将介绍怎么获取void*指针所存的地址。
2.3.3 理解符合类型的声明
正如所见,变量的定义,由一个基类型和一系列的声明符组成。在同一条语句中,每一个声明符,与基类型有关,但是可以于其声明符不同。因此,单个定义语句可以定义多个不同类型的变量。
int i=1024,*p = &i,&r=i;
警告:许多程序员总是困惑于基类型和类型修饰符的关系,其实类型修饰符是声明符的一部分。
定义多个变量
经常有人误解:在同一个语句中的类型修饰符(*,&)会作用于这个语句中的所有变量。造成这种误解的部分原因是:可以在类型修饰符和名字之间加空格:
int* p ; //这是合法的语句,但是会造成误解
之所以说这种定义容易造成误解是因为这种定义似乎是该语句中每个变量的类型。尽管如此写,但是类型依旧是int,而不是int *。星号*修饰p的类型,他对于同一个语句下的其他对象并不起作用。
int* p1,p2;//p1是一个指向int的指针,p2是一个int对象
通常有两种风格来定义指针或者引用。第一种,将类型修饰符和标识符放在一起。
int *p1, *p2;//pi和p2都是指向int的指针
这种格式强调变量具有符合类型。
第二种,将类型修饰符与类型放在一起,但是每个语句只定义一个变量。
int* p1;//p1指向int的指针
int* p2;//p2只想int的指针
这种格式强调,本次声明了一个复合类型。
提示: 对于指针和变量的声明没有单一而准确的格式。最重要的原则就是坚持使用一种风格。
在本书中我们使用第一种风格,将类型修饰符和变量名放在一起。
指向指针的指针
通常情况下,对于声明符有几个类型修饰符是没有限制的。当多个修饰符一起修饰的时候,我们只需要按照逻辑来理解即可,但这也并不是那么好理解。例如,思考如下的指针:指针是内存中的一个对象,像其他对象一样他也有地址,因此,我们可以在另外一个指针中保存这个指针的地址。
可以通过*来表明每个指针的级别。就是说,当写下**,表示是一个指向指针的指针,***表示一个指向指针的指针的指针,依此类推:
int ival = 1024;
int *pi = &ival; //pi指向一个int对象
int ** ppi = π //ppi是一个指向指针的指针
此处pi是一个指向int类型的指针。ppi是一个指针,这个指针指向一另外一个指针,后一个指针指向一个int对象。可以使用下面的方式来表示
就像解引用一个指向int类型的指针,会产生一个int类型一样,解引用一个指向指针的指针,会产生另外一个指针。为了存取下面的对象,我们必须解引用这个指针两次:
cout << “The value of ival \n”
<< “direct value : ”<<ival << “\n”
<< “indirect value:” << *pi << “\n”
<< “doubly indirect value:”<<**ppi
<<end;
这个程序打印ival的值三次,分别通过:1,直接打印;2,解引用pi;3,解引用两次ppi
指向指针的引用
一个引用不是对象,因此,不可能有一个指针指向引用。但是因为一个指针是对象,我们可以定义一个引用绑定指针。
int i = 42;
int *p ;
int *&r = p; //r是一个绑定指针的引用
r = &i; //r绑定一个指针,赋值&i给r,使p指向了i
*r = 0; //解引用r产生i,它是p指向的对象,这个语句改变i的值为0
理解r类型的方法是,从右往左读定义。离变量名最近的符号直接影响变量的类型,因此,可以看到r是一个引用。剩下的声明符决定了r绑定的类型,剩下的是一个*,在这里r绑定的类型是一个指针类型。最后,声明语句的基类型是int,表明:r是一个引用,这个引用绑定的是一个指针,然后这个指针指向一个int类型。
建议: 从右往左的读声明语句,更容易理解复合的指针和引用。
2.4 const 限定词
有时我们想定义一个不能改变值的变量。例如,我们想定义一个变量,用作缓冲的大小。使用变量作为缓冲大小,使得更改更为容易。另一方面,我们还得防止代码更改这个变量。可以定义一个不能改变值的变量来避免这种问题,只需要使用const进行修饰:
const int bufSize = 512; //输入buffer大小
定义bufSize为一个常量,任何对它的赋值都会产生错误:
bufSize = 512; //错误:尝试写常量对象
因为一个常量对象被创建之后不能改变它的值,因此常量对象必须被初始化.通常,初始值可以是任意复杂的表达式:
const int I = get_size(); //正确:运行时初始化
const int j= 42; //正确:在编译时初始化
const int k; //错误:k是一个未初始化的const对象
初始化和const
正如我们观察到的那样,一个对象的类型,决定了这个对象可以执行的操作.一个const类型的对象,可以执行非const对象的大多数操作,但不是全部操作.唯一的限制就是:我们只能使用那些不能改变对象的操作.例如,我们可以使用const int的对象进行算数运算,就跟普通的int对象是一样;也可以将const int对象转变成bool,也跟普通的int对象一样;等等.
在不改变一个对象的值的操作中还有一种是初始化,当我们使用一个对象初始化另外一个对象的时候,对象是否为const无关紧要.
Int I =42;
const int ci = I; //正确:i的值被复制到ci中
int j = ci; //正确:ci的值被复制到j中
尽管ci是一个const int对象,但是它的值为int类型.ci的常量属性,仅仅与可能改变ci的值的操作有关.当我们用ci初始化j的时候,我们不关心ci是否为一个const对象,因为ci的值不会被改变.复制一个对象,不会改变这个对象的值.
默认情况下,const对象在本文件内有效
当一个const对象在编译的时候被确定初始化,例如bufSize的定义:
const int bufSize = 512; //input的buffer大小
编译器通常会使用变量的值直接代替变量.即,编译器将直接使用512来代替bufSize的使用.
为了用值代替变量,编译器不得不查看变量的初始值.当程序分开在几个文件中的时候,每个使用了这个const对象的文件,都必须要有这个变量初始值的访问权限.
为了查看这个变量的初始值,变量必须被定义在每个文件中.为了支持这种用法,也为了在避免同一个变量的多次定义。const变量默认为文件内有效。当我们在多个文件中定义同名的const对象的时候,就好像是我们在每个文件中定义了一个不同的变量一样。
有时,初始值不是一个常量表达式,而且需要跨文件共享。在此种情况下,我们就不希望编译器在每个文件都生成不同的变量。相反,我们想const对象的行为,就跟非const对象一样:定义const在一个文件,然后在其他文件中定义这个对象的声明。
为了定义const变量的单个实例。在定义和声明处使用extern 关键字
//file_1.cc定义并且初始化了一个const变量,这个变量在其他文件也也会被访问
extern const int bufSize = fcn();
//file_1.h
extern const int bufSize;//这个变量的定义在file_1.cc文件中
file_1.cc文件中定义并初始化了bufSize,因为声明中包含初始化值,所以这是一个定义。又因为bufSize是一个const变量,那么为了在其他文件中使用这个变量,我们需要使用extern关键字。
在file_1.h中的声明,也使用了extern关键字。在这种情况下,extern表明bufSize被定义在此文件之外的其他地方。
注意: 为了在多个文件中共享const变量,必须在定义的时候使用extern关键字
2.4.1 const的引用
像其他对象一样,我们可以绑定一个引用到一个const对象上,将此引用称为const的引用。和其他普通的引用不同,不能通过const的引用去改变它绑定的对象。
const int ci = 1024;
const int &r1 = ci; //正确:引用和其绑定的对象都是const
r1 = 42; //错误:r1是一个const的引用
int &r2 = ci; //错误:const对象的非const引用
因为不能直接赋值给ci,所以也不能通过引用来改变ci的值。如果r2的初始化正确,那么我们就能够通过r2来改变绑定的对象。基于此原因,所以r2的初始化是错误的。
术语:cost引用就是绑定到const的引用
c++程序员常常将绑定到const的引用缩写为const引用。如果你时刻谨记这只是是缩写,那么这样做也是可以。从技术上来讲,是不存在const引用的。因为引用不是对象,因此无法让引用自身不变。事实上,不可能让一个引用,绑定不同的对象,在这种情况下来说,所有的引用都是不变的。引用绑定的对象是否为一个常量,决定了这个引用可以做的操作,而不会影响引用本身的绑定。
初始化和const引用
在2.3.1小节中,提及过:引用类型必须与绑定的类型相同,但是有两个特例.第一个特例就是:可以使用任何表达式初始化,绑定到const的引用.前提是,这个表达式可以转换成引用的类型.尤其是,可以将const引用绑定到,非const的对象,也可以绑定到字面量,以及绑定大多数的常用表达式:
int I =42;
const int &r1 = I; //可以绑定一个const int &到普通的int对象 正确:r1是一个const引用
const int &r2 = 42; // 正确:r2是一个consti应用
const int &r3 = r1*2; //正确:r3是一个const引用
int &r4 = r*2; //错误:r4是一个普通,非const引用
要想理解这种初始化规则的最简单的方法就是:想一想当我们绑定不同的类型时,会发生什么
double dval = 3.14;
const int &ri = dval;
此处,ri绑定到一个int.在ri上面的操作将是整型所支持的操作,但是dval是一个浮点型不是整型.所以为了保证ri所绑定对象为int,编译器将编译成类似下面的代码:
const int temp = dval; //创建一个临时的const int
const int &ri = temp;//绑定ri到这个临时对象
在上例中,ri绑定到了一个临时对象中.当编译器需要一个位置来存放来表达式的结果时,就创建一个未命名的对象,这个对象就是临时对象.
现在来思考一下:当ri不是const的时候,允许这种初始化,将会发生什么情况?如果ri不是const,那么就可以给ri赋值,此时就能改变ri绑定的对象.ri绑定的对象为临时对象,不是dval.程序员将ri绑定在了dval身上,那么就希望,赋值给ri,就会改变dval的值.因为绑定到临时对象,几乎是程序员没有预料到的,所以c++语言就禁止了这种用法.
一个const引用可以绑定到一个非const的对象
明白这一点是非常重要的:一个const引用,限制的仅仅是这个引用.将一个const引用绑定到一个对象,跟这个对象是否为const无关紧要.因为被绑定的对象可以是非const的,它可以通过其他方式改变:
int i = 42;
int &r1 = i; //r1绑定到了i
const int &r2 = i; //r2也绑定到了i,并且不能通过r2来改变i
r1 = 0; //r1不是const,所以现在i是0
r2 = 0; //错误:r2是一个const引用
绑定r2到i是合法的.但是不能通过r2来改变i的值.尽管如此,i的值仍然能够被改变.可以直接给i赋值来改变,也可以通过其他的引用来改变,如r1.
2.4.2 指针和const
和引用一样,我们可以定义指针,指向一个const或者非const类型.跟绑定到const的引用一样,指向const的指针,不能用来改变它所指向的对象.存放const对象的地址,只能使用指向const的指针.
Const double pi = 3.14; //pi是一个const,它的值不能被改变
double *ptr = π //错误:ptr是一个普通的指针
const double *cptr = π //正确:cptr指向了一个const对象
cptr = 42; //错误:不能赋值*cptr
在2.3.2节中,提及:一个指针的类型必须和它所指的对象类型严格匹配,但是有两种情况除外.第一种例外就是:可以使用指向const的指针,指向一个非const对象.
Double dval = 3.14; //dval是一个double类型,值可以被改变
cptr = &dval; //正确:不能通过cptr改变dval的值
跟const引用一样,指向一个const的指针没有规定指向的对象必须是一个const对象.指向const的指针仅仅影响这个指针可以做什么.记住一点非常重要:一个指向const 的指针不能保证指向的对象不会被修改.
建议: 试着这样想,或许有帮助:指向const的指针或者引用,只不过是指针以为自己指向了一个const的对象.
const指针
跟引用不同的是,指针是对象,因此,跟其他对象一样,他可以自己为const.跟其他const对象一样,const指针必须被初始化,一旦初始化,就不能被改变.通过在*后面放一个const来表明这个指针是常量.这种写法表明:这是个指针,并且不能被改变
int errNumb = 0;
int *const curErr = &errNumb; //curErr总是指向errNumb
const double pi = 3.14159;
const double * const pip = π //pip是一个指向const的const指针
正如在2.3.3节所说的那样,理解这种声明最简单的方法是从右往左读.此处,离curErr最近的是符号const,表明curErr自己为一个const对象.这个对象的类型,由声明语句的剩下部分来表示.下一个符号是*,意味着curErr是一个const的指针对象.声明语句的基类型使curErr的声明更加完整,它表明这是一个const的指针,指向一个int类型.同理可得,pip是一个const指针,它指向一个const的double类型.
事实上,一个const指针,并不能规定:不能通过指针来改变它指向的对象.是否有能力去改变这个对象,完全依赖于指针指向的类型而不是指针的类型.例如,pip是一个指向const的const指针.被pip所指对象的值,和pip都不能被改变.换句话说,curErr指向了一个普通的,非const的int类型,可以使用curErr去改变errNumb的值:
*pip = 2.72; //错误:pip是一个指向const的指针
//是否curErr指向的对象是否为0
if(*curErr){
errorHandler();
*curErr = 0; //正确:curErr所指的对象被复位
}
2.4.3 顶层const
正如所见:指针是一个可以指向其他对象的对象.因此,可以分开讨论,指针是否为const,以及指针指向的对象是否为const.使用术语:顶层const来表示,指针本身为const.当一个指针,指向一个const对象的时候,这个对象我们称为:底层const
更通用的说法是:顶层const表示对象本身为const.顶层对象可以用来描述任何类型的对象,例如,任一内置算数类型,类类型,指针类型.底层const用来表示:复合类型的基类型,例如,指针和引用.注意:不像其他类型,指针的顶层const和底层const是分开的:
int i = 0;
int * const p1 = &i; //无法改变p1的值,它是顶层const
const int ci = 42; //无法改变ci的值,它是顶层const
const int *p2 = &ci; //无法改变p2的值,它是底层const
const int * const p3 = p2; //靠近右边的是顶层const,靠近左边的是底层const
const int &r = ci; //在引用中的const始终为底层const
当复制对象的时候,顶层const常常被忽略.
i = ci; //正确:复制ci的值,顶层const ci被忽略
p2 = p3; //正确:类型匹配,并且p3属于顶层const,将被忽略
复制一个对象,不会改变被复制对象的值.因此,不管是复制进来,还是复制出去,跟const没有关系.
另一方面,底层const不会被忽略的情况为:复制一个对象的时候,两个对象必须要有相同的底层const或者在他们之间能够进行类型转换.通常,可以将非const转成const,反之则不行.
Int *p = p3; //错误:p3有一个底层const,但是p没有
p2 = p3; //正确:p2,p3有相同的底层const
p2 = &i; //正确:可以将int *转换成const int *
int &r = ci; //错误:不能将const int绑定在int &上
const int &r2 = i; //正确:可以将int绑定在const int & 上
p3有顶层const和底层const.当复制p3的时候,可以忽略顶层const,但是事实是它指向了一个const对象.因此,不能使用p3来初始化p,因为p指向了一个非const对象.另一方面,可以将p3赋值给p2.因为两个都有相同的底层const类型.这跟p3是const指针没有关系.
2.4.4. constexpr和常量表达式
一个表达式他的值不能被改变,并且能够在编译的时候计算出来,那么这个表达式就是常量表达式。一个字面量是一个常量表达式。一个被常量表达式初始化的const对象,也是常量表达式。后面会提到,在c++语言中,将会有几个情况用到常量表达式。
一个对象或者表达式是否为常量表达式,依赖于对象的类型和初始值。例如:
const int max_files = 20; //max_files 是一个常量表达式
const int limit = maxfiles+1; //limit是一个常量表达式
int staff_size = 27; //staff_size不是一个常量表达式
const int sz = get_size(); //sz不是一个常量表达式
尽管staff_size由字面量初始化,但是他也不是一个常量表达式,因为他是一个普通的int对象,不是const int。另一方面,尽管sz是const,但是他的初始化值,只有在运行的时候才能知道。所以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变量的初始值,但是,在6.5.2小节中将会介绍c++11新标准可以定义一种函数,这种函数可以作为constexpr初始值。这种函数必须足够简单到编译器可以在编译的时候计算他的值。可以使用constexpr函数来初始化constexpr变量。
建议 通常,你想将变量作为常量表达式使用的时候,使用constexpr修饰符常常是一个好的习惯。
字面值类型
因为常量表达式在编译时就能够被计算出来,所以在使用constexpr来声明类型时,这些类型有一定的限制。在constexpr中使用的类型,成为“字面值类型”,因为他们简单到可以通过字面意思得到。
迄今为止,我们使用过的类型,如,算数类型,引用,指针都是字面值类型。而Sales_item类和IO库,以及string类型都不是字面值类型,因此不能将他们定义成constexpr.在7.5.6小节以及19.3小节,我们将介绍其他字面值类型。
尽管我们可以定义constexpr的指针和引用,但是初始化他们的值却是严格受限的。可以使用nullptr字面量和0来初始化constexpr指针。也可以用固定地址的对象来初始化指针。
6.1.1小节将会介绍,定义在函数内部的变量通常没有一个固定的地址,因此不能使用constexpr指针来指向这种变量.换句话说,定义于函数外的变量的地址是一个常量表达式,可以用来初始化constexpr指针.同样在6.1.1小节中,将会介绍,函数体内也可以定义跨越函数体调用的变量,就跟在函数体外面定义的变量一样,它也有固定的地址.因此也可以用constexpr引用来绑定,也可以用constexpr指针来指向这种变量.
指针和constexpr
当在constexpr中定义了一个指针,一定要明白:constexpr修饰符作用于指针,而不是指针指向的类型.
const int *p = nullptr; //p是一个指向const int的指针
constexpr int *q = nullptr; //q是一个const指针,指向int类型
尽管写法看起来类似,但是p和q的类型却是完全不同的。p是一个指向const的指针。而q是一个const指针。之所以如此,是因为:constexpr强制作用于顶层const。
跟其他常量指针一样,一个constexpr指针可以指向一个const或者非const类型:
constexpr int *np = nullptr; //np是一个常量指针,指向一个int类型,int值为空。
int j = 0;
constexpr int i = 42; //i的类型是const int
//i和j必须定义在函数外
constexpr const int *p = &i; //p是一个const指针,指向一个const int 类型i
constexpr int *p1 = &j; //p1是一个const指针,指向一个int类型j
2.5 处理类型
当我们的程序变得复杂的时候,我们将发现使用的类型也会变得复杂。类型变得复杂体现在两方面。一是,一些类型难以拼写;二是,有时难以确定使用哪一种类型。
2.5.1类型别名
类型别名是一个类型的代名词。类型别名让我们简化了复杂类型的定义,使复杂类型更容易使用。类型别名也更容易理解这个类型的用途。
有两种方法可以定义类型别名。传统上,直接使用typedef关键字:
typedef double wages; //wages是double的别名
typedef wages base,*p; //base是double的别名,p是double *的别名
关键字typedef可以作为一个声明语句的基类型。包含typedef的声明语句,是定义别名的,而不是定义变量的。正如其他声明语句一样,声明符也可以包含类型修饰符,这些类型修饰符可以定义复合类型。
c++ 11新标准引入了第二种声明别名的方式,通过使用别名声明符来声明:
using SI = Sales_item; //SI是Sales_item的别名
一个别名的声明语句以关键字using开头,然后紧跟着别名名字和等于符号。别名声明语句,将等号左边的名字声明为等号右边类型的别名。
类型的别名,可以出现在类型出现的任何地方:
wages hourly,weekly; //跟double hourly,weekly;一样
SI item; //跟Sales_item item 一样
指针,const,和类型别名
一个使用了类型别名的声明,并且这个类型别名代表一个复合类型.在这个声明中使用了const关键字,那么这个声明语句将会产生令人意想不到的结果.例如,下面的声明使用了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的指针.
使用类型别名时,简单的替换别名对应的类型来解释声明语句是不正确的:
const char * cstr = 0; //const pstring cstr错误的解释
上述声明是错误的.当声明语句中使用pstring时,基类型是指针.而重新使用char *的时候,基类型是char.而*是声明符的一部分.此例中,const char是基类型.cstr是一个指针,指向一个const char而不是一个const 指针指向char.
2.5.2 auto 类型指示符
常常需要将一个表达式的值存储在某个变量中。为了声明这个变量,必须知道这个表达式的类型。当在编程的时候,判断一个表达式的类型比较困难,有时是不可能判断类型的。因此,在c++11新标准中,通过使用auto类型指示符让编译器推导出类型。不像其他的类型指示符,(例如double)专门指定一种类型。auto让编译器通过初始值来决定变量的类型。因此,一个变量使用了auto,那么他就必须要有初始值,用于推导它的类型:
//item类型从val1和val2相加的结果进行推倒
auto item = val1+val2; //item被val1+val2的结果初始化
此处,item的类型将通过val1加val2的返回值进行推导。如果val1和val2是Sales_item类型。那么item将是Sales_item类型。如果这两个变量类型是double,那么itme的类型也是double。依此类推。
跟其他类型指示符一样,也可以使用auto来定义多个变量。因为一个声明语句只有一个基类型,因此,这些变量的初始值必须具有一致的类型:
auto i = 0, *p = &i; //正确:i是int,p是一个指向int的指针
auto sz = 0,pi = 3.14; //错误:sz和pi类型不一致
复合类型,const,auto
编译器推导出来的类型,并不总是和初始值相同。因为编译器需要调整类型来满足常见的初始化规则。
首先,正如所见,当我们使用引用的时候,实际上使用的是引用绑定的那个对象。编译器使用这个对象来推导auto的类型:
int i = 0,&r = i;
auto a = r; //a是一个int类型(r是i的一个别名,他的类型为int)
第二, auto通常忽略顶层const,底层const被保留:
const int ci = i,&cr = ci;
auto b = ci; //b是一个int类型(在ci中的顶层const被忽略掉)
auto c = cr; //c是一个int类型(cr是ci的别名,ci是一个顶层const)
auto d = &i; //d是一个int * 类型
auto e = &ci; //e是一个const int *(一个const对象取地址,这个地址是一个底层const被保留)
如果我们想要保留顶层的const,必须显式 的声明:
const auto f = ci; //ci的类型为int,f的类型为const int
还可以将引用用在auto推导类型上,常用的推导规则仍然适用:
auto &g =ci; //g是一个const int &,它绑定到ci
auto &h = 42; //错误:不能将一个普通引用,绑定到一个字面量上
当将引用用在auto的类型推导的时候,顶层const不会被忽略。当绑定引用到初始值时,const并不算顶层const。
当在一条语句中,定义多个变量时,切记:引用和指针是声明符的一部分,不是基类型的一部分。跟往常一样,初始值必须具有一致的类型:
auto k = ci,&l =i ; //k是int;l是int&
auto &m =ci,*p = &ci; //m是const int &,p是一个指向cont int的指针
//错误:来自于i的类型为int,从&ci上面推导出的类型为const int
auto &n =i,*p2 = &ci;
2.5.3 decltype 类型指示符
有时,我们想让编译器自动推导一个表达式的类型,但是又不想使用这个表达式来初始化这个变量。基于此原因,c++11新标准引入了第二个类型指示符,decltype,它返回操作数的类型。编译器分析表达式的类型,但是不会计算这个表达式:
decltype(f()) sum =x; //sum的类型跟f返回的类型一样
此处,编译器并不会调用f,但是使用了f返回值的类型作为sum的类型。decltype处理顶层const和引用,跟auto的处理有点不同。当decltype应用的表达式是一个变量时,decltype返回的是这个变量的类型,包括顶层const和引用:
const int ci = 0,&cj =ci;
decltype(ci) x = 0; //x类型为const int
decltype(cj) y = x; //y的类型为const int &并且绑定到x
decltype(cj) z=; //错误;z是引用,必须被初始化
因为cj是引用,所以decltype(cj)也是一个引用。跟其他引用一样,z必须被初始化。
引用是唯一没有被作为别名对待的地方就是decltype的上下文。
decltype和引用
当decltype表达式不是一个变量时,就使用的是表达式结果的类型。在4.1.1小节中,一些表达式会导致decltype产生引用类型。通常来讲,此种情况下decltype表达式返回的引用所绑定的对象,可以作为赋值语句的左值。
//decltype返回的类型可以是引用类型
int i = 42, *p = &i,&r = i;
decltype(r+0) b; //正确:加法产生一个int,b是一个未初始化的int类型
decltype(*p) c ; //错误:c是一个int &,必须被初始化
此处,r是引用,因此decltype®是一个引用类型。如果想要使用r绑定的类型,直接在表达式中使用r即可,r+0,这个表达式产生的值为非引用类型。
另一方面,解引用操作符,使decltype返回引用类型。正如所见,当解引用指针时,我们得到了这个指针指向的对象,并且可以赋值给这个对象。因此,decltype(*p) 返回的对象为int &,而不是int。
decltype和auto另一个不同的地方在于:decltype还依赖于表达式的格式。在小括号内的变量会影响decltype的返回类型。当decltype直接应用变量,没有小括号的时候,得到的是这个变量的类型。当使用一层或者多层小括号的时候,编译器将这个变量当作表达式来对待。变量是可以作为赋值语句的左值的特殊表达式。因此,decltype作用在这样的表达式下会得到一个引用:
//括号包围的变量的decltype总是一个引用
decltype((i)) d; //错误:d是一个int& 必须被初始化
decltype(i) e; //正确:e是一个未初始化的int
警告: decltype((变量)) 总是一个引用类型。但是decltype(变量) 只有在变量是引用的时候,才会是引用类型。
2.6 定义我们自己的数据结构
从最基本上来说,数据结构是数据和数据操作策略的组织形式。例如,我们的Sales_item类,把ISBN,销售额,单价组织在一起,并且提供了一系列的操作,例如isbn函数,<<,>>,+还有+=操作符。
在c++里面,通过定义一个类来定义自己的数据类型。就跟我们自己定义的Sales_item类类似,库string,istream,和ostream,也被定义成了类。c++对类的支持广泛,事实上本书第3部分和4部分,将大量的介绍类相关的特性。尽管Sales_item类比较简单,但是现在还没有能力完整定义这个类,直到第14章介绍完如何编写自定义运算符之后才有这个能力。
2.6.1 定义Sales_data类型
尽管我们不能够编写Sales_item类,但是我们可以编写一个简单类来组织相同的数据类型。对于这个简单类的策略是:用户能够直接存取这个数据元素,并且为这些数据元素实现必要的操作函数。
因为这个数据结构不支持所有的操作符,所以命名她为Sales_data,以示Sales_item的区别。定义这个类如下:
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0
};
这个类以关键字struct 开始,紧跟着类名,和类定义的实体。类的实体被大括号包围,并且也形成了一个新的作用域。定义在类内部的名字必须是唯一的,但是可以在类的外部再次使用这个名字。
类体的末尾必须跟一个分号,这个分号是必须的,因为类体后面可以直接定义变量:
struct Sales_data{/*....*/} accum,trans,*salesptr;
//等价于上一条语句,并且比上一条语句更好
struct Sales_data{/*...*/};
Sales_data accum,trans,*salesptr;
分号标志着声明符列表的结束。通常,在一个类定义后面直接定义一个对象并不算好的编程习惯。如果这样做,容易引起混淆,因为两种不同的实体,(类和对象)被放在一个单一的语句里面。
警告 在类定义后面忘记分号是,新手程序员常犯的错误。
类数据成员
类体定义了类的成员。我们现在定义的类仅有数据成员。数据成员决定这个类对应的对象的内容。每一个对象都有自己的数据成员。修改一个对象的数据成员,不会改变其他对象的数据成员。
定义数据成员与定义普通的变量是相同的:给出一个基类型,然后跟着一个或者多个声明符。Sales_data有三个数据成员:一个string类型的bookNo;一个unsigned类型的units_sold;一个double类型的revenue。每一个Sales_data对象都有这三个数据成员。
在c++11新标准下,可以为数据成员提供类内初始值来初始化他们。当创建一个对象的时候,类内初始值被用来初始化数据成员。没有初始值的数据成员有一个默认的初始值。因此,当创建Sales_data对象的时候,units_sold和revenue被初始化为0,bookNo被初始化为空字符串。
类内初始值的限制与2.1.1节介绍的类似:他们必须被大括号括起来或者在赋值符(=)后面,但不能使用圆括号。
在7.2小节中,我们将介绍c++定义数据类型的第二种关键字,class。也会介绍为什么此处要使用struct。在第七章介绍类相关特性之前,应该使用struct来定义我们数据类型。
2.6.2 使用Sales_data类
跟Sales_item不一样,Sales_data不支持任何运算符。Sales_data的用户不得不自己编写需要的操作运算。我们将编写一版来自于1.5.2小节的程序,这个程序打印两个交易记录之和。这个程序的输入如下:
0-201-78345-X 3 20.00
0-201-78345-X 2 25.00
每一个交易记录都有一个ISBN号,售卖数,以及单价。
相加两个Sales_data对象
因为Sales_data没有提供运算符,所以,不得不手动编写输入,输出和相加操作。假设Sales_data类被定义在Sales_data.h文件中。2.6.3小节将介绍怎么定义这个头文件。
因为这个程序比我们迄今为止写的任何程序都长,所以我们分段解释。总体来讲,我们的程序有下面的一个结构:
#include <iostream>
#include <string>
#include “Sales_data.h”
int main(){
Sales_data data1,data2;
//将数据读入data1和data2的代码
//检查data1和data2是否用相同ISBN号
//并打印data1和data2的和
}
跟以前的程序一样,最开始是需要的头文件,然后定义保存输入的变量。注意,跟Sales_item程序不一样,新的这个程序包含了string的头文件。我们需要这个头文件,因为需要操作bookNo成员,而他的类型就是string。
读数据进Sales_data对象
尽管还没有详细介绍string类型,(这部分在第三章和第十章中介绍)但是也需要知道一点关于string类型,目的是为了定义和使用ISBN成员。string类型存储一组字符,它的操作包括>>,<<,==运算符,分别对应,读,写和比较字符串。有了这个知识,我们可以开始编写读入第一个交易记录的代码了:
double price = 0; //每本书的单价,用来计算总售价
//读入第一个交易记录:ISBN,售卖数,单价
std::cin >> data1.bookNo >> data1.units_sold>>price;
data1.revenue = data1.units_sold * price;
销售记录包含每本书的单价,但是使用的数据结构存储的是总的售价。因此将单价存储在double类型的price上,用于计算总的售价。
std::cin >> data1.bookNo >> data1.units_sold>>price;
使用点运算符,将数据读入data1的bookNo和units_sold成员里面。
代码的最后一个语句,将data1.units_sold和price相乘放入data1的revenue成员中。
程序将重复相同的操作,将数据读入data2中:
//读入第二个交易记录
std::cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold*price;
打印两个Sales_data对象的和
程序其他的任务还包括:检查交易记录是否有相同的ISBN,如果相同,则打印他们的和,否则打印错误信息:
if(data1.bookNo == data2.bookNo){
unsigned totalCnt = data1.units_sold + data2.units_sold;
double totalRevenue = data1.revenue+data2.revenue;
//打印,ISBN,总售价,每本书的单价
std:cout << data1.bookNo << “ ”<< totalCnt << “ ”<< totalRevenue << “ ”;
if(totalCnt != 0)
std::cout << totalRevenue / totalCnt << std::endl;
else
std::cout << “(no sales)” << std::endl;
return 0;//表示成功
}else{//交易记录没有相同的ISBN号
std::cerr << “Data must refer to the same ISBN” << std::endl;
return -1;//表示失败
}
第一条语句,比较data1和data2的bookNo。如果他们相等,那么执行大括号内部的代码。这部分代码,将两个变量的成员相加。因为需要打印平均值,所以一开始就将units_sold和revenue分别存在了totalCnt,和totalRevenue里面。然后再打印这些值。在下一条语句检查是否有书被卖出去。如果有,就计算书的平均价格。如果没有卖出,就打印一条没有卖出的消息。
2.6.3 编写头文件
尽管在19.7小节会介绍,可以在函数内部定义类,但是这种类是受限制的。因此,类通常不定义在函数内部。当在函数体外定义一个类时,任何给定的源文件可能只有一处这个类的定义。另外,如果在几个分开的文件中使用这个类,那么这个类的定义在每个文件中都必须相同。
为了保证类的定义在每个文件中都是相同的,类通常被定义在头文件中。通常,类被存储在头文件,头文件的名字来自于类名。例如,string类型,被定义在string头文件中。同样,我们也将Sales_data类定义在Sales_data.h头文件中
头文件通常包含只能被定义一次的实体(例如类的定义,const,constexpr变量等)。头文件经常需要用到其他头文件的功能。例如,Sales_data类有string成员,Sales_data.h必须#include包含string头文件。因此,使用了Sales_data的程序将包含两次string头文件。一次是直接在程序里面包含,一次是在Sales_data.h中被包含。因为一个头文件可能被包含多次,所以,需要一种安全的方式来写我们的头文件。
注意 一旦头文件更新,使用这个头文件的源文件也必须重新编译来使这些更新生效。
预处理器概述
对于头文件被包含多次,最常用的技术是依赖预处理器。预处理器,继承于c,他是一段运行在编译之前的程序,他可以改变程序的源文本。
我们的程序其实已经在依赖预处理器的功能了,那就是#include。当预处理器看到#include时,就使用给定的头文件的内容代替这个语句。
c++程序也使用预处理器来定义头文件保护符。头文件保护符依赖于预处理器变量(2.3.2小节)。预处理变量只有两种状态:defined和undefined.#define指令带有一个名字,并且将这个名字定义为一个预处理变量,使他的状态为defined。c++有两个其他的指令用于测试一个给定的预处理变量是否为defined状态:#ifdef 指令,如果变量为defined则,返回true。#ifndef指令,如果变量为undefined,则返回true。如果返回true,那么#ifdef和#ifndef后面跟着的代码将会被处理,直到遇到#endif指令。
可以使用这个功能,来保护头文件的多次包含:
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data{
std::string bookNo;
unsigned units_solds = 0;
double revenue = 0.0;
};
#endif
Sales_data.h第一次被包含的时候,#ifndef 返回true。预处理器将处理#ifndef到#endif之间的所有代码。因此,SALES_DATA_H被定义,状态为defined,并且Sales_data.h的内容被复制到程序中。如果后面再次包含Sales_data.h头文件,#ifndef指令将返回false,因此,#ifndef和#endif之间的代码将会被忽略。
警告 预处理变量无视c++的作用域规则。
用于头文件保护符的预处理变量,在整个程序中必须是唯一的。通常为了保证唯一性,直接基于类的名字来给头文件保护符命名。为了避免和其他实体的命名冲突,预处理变量通常为大写。
经验 头文件即使不会被其他头文件包含,也应该有头文件保护符。头文件保护符书写简单,只需要习惯性的定义他们就可以,不用判断它们是否必须。
小结:译(略)
专业术语:译(略)
题外话:最近半年加班严重,脑袋比较浆糊,译文中可能出现一些*符号没有显示出来,可提示一下,谢谢。