《C++ Primer 中文版》第一二章读书笔记及习题解答

第一章 开始

简介

  本章介绍了C++大部分基础内容:类型、变量、表达式、语句及函数。简要介绍了如何编译及运行程序。
  学习并完成本章练习后,将具备编写、编译、运行简单程序的能力。后续会更加详细的解释本章提到的语言特性。

1.1 编写一个简单的C++程序

  类型是程序设计最基本的概念之一,一种类型不仅定义了数据元素的内容,还定义了这类数据上可以进行的运算。
  程序所处理的数据都保存在变量里,而每个变量都有自己的类型。如果一个名为v的变量的类型为T,我们通常说“v具有类型T,或v是一个T类型变量”。

1.1.1 编译、运行程序

  很多PC机上的编译器都具备集成开发环境(Integrated Developed Environment,IDE),将编译器与其他程序创建和分析工具包装在一起。大部分编译器,包括集成IDE的编译器都会提供一个命令行界面。建议先把精力集中于C++语言本身,一旦掌握了语言,IDE通常是很容易学习的。
  无论你使用命令行界面或者IDE,大多数编译器都要求程序源码存储在一个或多个文件中。程序文件通常被称为源文件(source file)。在大多数系统中,源文件以一个后缀为结尾,后缀是由一个句点(.)后接一个或多个字符组成的。后缀告诉系统这个文件是一个C++程序。不同编译器使用不同的后缀名约定,最常见的包括.cc,.cxx,.cpp,.cp及.c。
  如果我们正在使用命令行界面或者IDE,那么通常是在一个控制台窗口内(例如UNIX系统中的外壳程序窗口或者Windows系统中的命令提示符窗口)编译程序。假定我们的main函数保持在文件prog1.cc中,那么可以用以下命令来编译它:

$ CC prog1.cc

  其中,CC是编译器程序的名字,$是系统提示符。编译器生成一个可执行文件。Windows系统会将这个可执行文件命名为prog1.exe。UNIX系统中的编译器通常将可执行文件命名为a.out。
  为了在Windows系统中运行一个可执行文件,我们需要提供可执行文件的文件名,可以忽略其扩展名.exe:

$ prog1

  在一些系统中,即使文件就在当前目录或文件夹中,也必须显式的指出文件的位置。在此情况下我们可以键入:

$ .\prog1

  “.” 后跟一个反斜线指出该文件就在当前目录中。
为了在UNIX系统中运行一个可执行文件,我们需要使用全文件名,包括文件扩展名:

$ a.out

  如果需要指定文件位置,需要用一个“.”后跟一个斜线来指出可执行文件位于当前目录。

$ ./a.out

  访问main的返回值方法依赖于系统。在UNIX和Windows系统中,执行完一个程序后,都可以通过echo命令获得其返回值。
  在UNIX系统中,通过以下命令获得状态:

$ echo $?

  在Windows系统中查看状态可键入:

$ echo %ERRORLEVEL%

  运行GNU编辑器和微软Visual Studio 2010编辑器的命令见本书第四页。

1.2 初识输入输出

  C++并没有定义用于输入输出(IO)的语句,取而代之的是全面的标准库(standard library)来提供IO机制(以及很多其他设施)。本书会经常使用iostream库。它包含两个基础类型istream和ostream,分别别表示输入流和输出流。一个流就是一个字符序列,是从IO设备读入或写入IO设备的。术语“流”(stream)想表达的是,随时间的推移,字符是按顺序生成或消耗的。
  标准库定义了四个IO对象:
cin为istream类型的对象,也被称为标准输入(standard input)。
cout为ostream类型的对象,也被称为标准输出(standard output)。
cerr通常用来输出警告和错误信息,因此也被称为标准错误(standard error)。
clog用来输出程序运行时的一般性错误。
  系统通常将程序所允许的窗口与这些对象关联起来。因此我们读取cin,数据将从程序正在运行的窗口读入,当我们向cout、cerr、和clog写入数据时,将会写到同一个窗口。
  在之后的书店程序中,我们需要将多条记录合并成单一汇总记录。通过使用IO库,我们可以扩展main函数,使之能够提示用户输入两个数,然后输出它们的和。

#include <iostream>
int main(){
	int v1=0,v2=0;
	std::cin>>v1>>v2;
	std::cout<<"the sum of"<<v1<<"and"<<v2
			<<"is"<<v1+v2<<std::endl;
return 0;

  前缀std::指出名字cin、cout和endl是定义在名为std的命名空间(namespace)中的。命名空间可以帮助我们避免不经意间的名字定义冲突,以及使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间std中。
  这种通过使用命名空间来使用标准库有一个小缺点:当使用标准库中的一个名字时,必须显式说明我们想使用来自命名空间std中的名字。例如,需要写出std::cout,通过使用作用域运算符:: 来指出我们想使用定义在命名空间std中的名字cout。
  有一种较为简单且最为安全的方式是在使用cout之前就使用using声明(using declaration)。按照规定每个using声明会引入命名空间中的一个成员,因此每个名字都需要独立的using声明。以下一个程序可以充分解释这个特性。

#include <iostream>
int main(){
using std::cout;
	int v1=0,v2=0;
	std::cin>>v1;//显式地从std中使用cin,正确
	cin>>v2;//未使用using声明,错误
	std::cout<<"the sum of"<<v1<<"and"<<v2//使用了using声明,正确
			<<"is"<<v1+v2<<std::endl;
return 0;

  还有一种特别简单但充满风险地方式是使用using指示(using directive),using指示使某个特定地命名空间中的所有名字都可见,这样我们就不必再为它们添加任何前缀限定符了。简写的名字从using指示开始,一直到using指示所在的作用域结束都能使用。

#include <iostream>
using namespace std;
int main(){
	int v1=0,v2=0;
	cin>>v1>>v2;
	cout<<"the sum of"<<v1<<"and"<<v2
			<<"is"<<v1+v2<<std::endl;
return 0;

  提示:避免using指示。using指示一次性注入某个命名空间的所有名字,这种方式看似简单实则充满了风险:过于复杂,详情见《C++ Primer》第704页。但using指示也并非一无是处,例如在命名空间本身的实现文件中就可以使用using指示。在我们目前阶段写的玩具代码中,这种方法还是可以使用的。

  endl这是一个被称为操纵符(manipulator)的特殊值。写入endl的效果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅仅停留在内存中等待写入流。我们在调试程序时经常会使用打印语句来帮助我们知晓程序进程以及结果,这类语句应该保证一直刷新流,否则程序崩溃,输出可能还留在缓冲区,从而导致关于程序崩溃位置的错误推断。

1.3 注释简介

  C++中的注释分为两类:单行注释和界定符对注释。
  单行注释以双斜线(//)开始,以换行符结束。当前双斜线右侧的所有内容都会被编译器忽略,这种注释可以包含任何文本,包括额外的双斜线。
  另一种注释继承自C语言的两个界定符(/**/)。这种注释以/开始,以/结束,可以包含出*/外的任何内容,包括换行符。编译器将落在/**/之间的所有内容都当作注释。我们采取的风格时在注释的每一行都以一个*号开头,从而指出整个范围都是注释的一部分。
  程序中通常包含两种形式的注释。注释界定符对通常用于多行注释,而双斜线注释常用于半行和单行附注。

#include <iostream>
using namespace std;
/*
 *简单主函数:
 *读取两个数,求他们的和
*/
int main(){
	//提示用户输入两个数
	int v1=0,v2=0;
	cin>>v1>>v2;//保存我们输入的数据
	cout<<"the sum of"<<v1<<"and"<<v2
			<<"is"<<v1+v2<<std::endl;
return 0;

  最后要注意注释界定符不能嵌套。编译器对这种问题给出的错误信息可能是难以理解、令人迷惑的。例如,在你的系统中编译以下程序就会产生错误。

/*
 *注释对/* */不能嵌套
 *不能嵌套这几个字会被认为是源码,
 *像剩余程序一样被处理。
*/
//你看在博客里自带的代码块里都不能正常显示注释了。
int main(){
	return 0;
}

  我们通常需要在调试阶段注释掉一些代码。由于这些代码可能包含界定符对形式的注释,因此可能导致注释嵌套错误,因此最好的方式是用单行注释方式注释掉代码段的每一行。

// /* 
// * 单行注释中的内容都会被忽略
// * 包括嵌套的注释对一样会被忽略
// */

1.4 控制流

1.41 while语句

1.42 for语句

1.43 读取数量不定的输入数据

常见的写法如下:

#include <iostream>
using namespace std;

int main(){
	int sum=0,value=0;
	while(cin>>value)
		sum+=value;
	cout<<"sum is:"<<sum<<endl;
	return 0;
}

  当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,则检测成功。当遇到文件结束符(end-of-file),或遇到一个无效输入时(对于本例,输入的不是一个整数),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变假。
  那我们如何从键盘输入文件结束符呢?对于如何指出文件结束,不同的操作系统有不同的约定。在Windows系统中,输入文件结束符的方法是敲Ctrl+Z,然后再按Enter或Return键。在UNIX系统中,包括MAC OS X系统中,文件结束符是用Ctrl+D。
再谈编译:
  编译器的一部分工作是寻找程序文本中的错误。编译器没有能力检查出一个程序是否有逻辑错误,但可以检查出形式(form)上的错误。下面列出了编译器可以给出的最常见的错误。
语法错误(syntax error):程序员犯了C++语言上的错误。
类型错误(type error):C++中的每一个数据项都有其类型。一个类型错误最典型的例子是,向一个期望参数为int的函数传递一个字符串字面值常量。
声明错误(declaration error):C++程序中的每个名字都要先声明后使用。名字声明失败通常会导致一条错误信息。两种常见的声明错误是:来自标准库的名字忘记使用std::、标识符名字拼写错误。
  错误信息通常包含一个行号和一条简短描述,描述了编译器认为的我们所犯的错误。按照报告的顺序来逐个更正错误是一个好习惯。因为一个单个错误通常会具有传递效应,导致编译器在其报告中反应比实际数量多得多的错误信息。另一个好习惯是更正一个错误之后立即重新编译代码,或最多修正了一小部分明显错误后就重新编译。这就是所谓的“编辑-编译-调试”(edit-compile-debug)周期。

1.4.4 if 语句

  我们可以使用if语句来统计在输入中每个值连续出现了多少次,为之后的书店程序打下基础。

#include <iostream>
using namespace std;

int main(){
	//currval存放我们正在统计的数,val存放我们新读入的数
	int currval=0,val=0;
	if(cin>>currval){
		int cnt=1;//统计正在处理的当前值个数
		while(cin>>val){
			if(val==currval)
				++cnt;
			else{
				cout<<currval<<" occurs "<<cnt<<" times"<<endl;
				currval=val;//记录新值
				cnt=1;//重置计数器
			}
		}
		//记录打印文件中最后一个值的个数
		cout<<currval<<" occurs "<<cnt<<" times"<<endl;	
	}
	return 0;
}

注意,在输入结束后要键入Crtl+Z,再按enter才能结束输入。

1.5 类简介

  类机制是C++机制中最重要的特性之一。在之前我们使用标准库设施,那就必须包含相关的头文件。类似的,我们也需要使用头文件访问自己的应用程序所定义的类。我们通常使用.h作为头文件的后缀,但也有一些程序员习惯.H、.hpp、或.hxx。标准库头文件通常不带后缀。编译器一般不关心头文件名的形式,但有的IDE对此有特定的要求。

1.5.1 Sales_item类

为解决之后的书店程序,我们定义了一个新的类:Sales_item,它的作用式表示一本书的总销售额、售出册数和平均售价。每个类实际上都定义了一个新的类型,其类型名就是类名。因此我们的Sales_item类定义了一个名为Sales_item的类型。与内置类型一样,我们可以定义类类型的变量,当我写如下语句时:

Sales_item item;

  是想表达item是一个Sales_item类型的对象。我们通常将一个Sales_item类型的对象简单说成一个Sales_item对象,或更简单的一个Sales_item。
  为了完成之后的书店程序,Sales_item的作者定义了可以执行的所有动作:

调用一个isbn的函数从一个Sales_item对象中提取ISBN书号。

用输入运算符>>和输出运算符<<读写Sales_item类型的对象。

用赋值运算符=将一个Sales_item对象的值赋予另一个Sales_item对象。

用加法运算符+将两个Sales_item对象相加。两个对象必须表示同一本书(相同的ISBN)。加法的结果是一个新的Sales_item对象,其ISBN和之前两个运算对象相同,而其总销售额和售出册数是两个运算对象相应值之和。

使用复合赋值运算符+=将一个Sales_item对象加到另一个对象上。

  下面这个程序从标准输入读入数据,存入一个Sales_item对象中,然后将Sales_item的内容写回标准输出:

#include <iostream>
#include "Sales_item.h"
using namespace std;

int main(){
	Sales_item book;
	//读入ISBN号,售出册数和销售价格
	cin>>book;
	//写入ISBN号,售出册数,销售价格和平均价格
	cout<<book<<endl;
	return 0;
}

  上述程序当我们输入0-201-70353-X 4 24.99,则输出为0-201-70353-X 4 99.96 24.99。
  包含来自标准库的头文件,应该用<>来包围头文件(例如istream),对于不属于标准库的头文件,则用双引号包围(例如Sales_item.h)。
  将两个Sales_item对象相加:

#include <iostream>
#include "Sales_item.h"
using namespace std;

int main(){
	Sales_item item1,item2;
	cin>>item1>>item2;
	cout<<item1+item2<<endl;
	return 0;
}

  当输入0-201-78345-X 3 20.00 0-201-78345-X 2 25.00,输出为0-201-78345-X 5 110 22。与int型加法不同的是,类对象相加是采用全新的和的概念——两个对象的成员对应相加的结果。

文件重定向//目前这块还不太懂
  当测试程序时,反复从键盘敲入这些销售记录作为程序输入很是麻烦。大多数操作系统支持文件重定向,这种机制允许我们将标准输入和标准输出与命名文件关联起来。

$ addItems <infile >outfile 

  假定$是操作系统提示符,我们的加法程序已经编译为名为addItems.exe的可执行文件(在UNIX系统重视addItems),则上述命令会从一个名为infile的文件读取销售记录,并将输出结果写入到一个名为outfile的文件中,这两个文件都位于当前目录中。

1.5.2 初始成员函数

  什么是成员函数?成员函数是定义为类的一部分的函数,有时也被称为方法(method)。在上一节将两个ISBN相同的Sales_item对象相加的练习题(1.21)中我们使用了compareIsbn函数,这里我们使用item1.isbn()函数来重写这个程序。

#include <iostream>
#include "Sales_item.h"
using namespace std;
int main(){
	Sales_item item1,item2;
	cout<<"请输入两条ISBN相同的销售记录:"<<endl;
	cin>>item1>>item2;
	if(item1.isbn()==item2.isbn()){//ISBN是否相同
		cout<<"汇总信息:ISBN、售出本数、销售额和平均售价为 "<<item1+item2<<endl;
		return 0;
	}
	else{
		cout<<"两条销售记录的ISBN不同"<<endl;
		return -1;
	}
	return 0;
}

  item1.isbn(),使用点运算符(.)来表达我们需要“名为item1的对象的isbn成员”。点运算符只能运用于类类型的对象。其左侧运算对象是类类型的对象,右侧运算对象必须是该类型的一个成员名,运算结果为右侧运算对象指定的成员。
  当我们使用点运算符来访问一个成员函数,通常是想调用调用该函数。我们使用调用运算符(())来调用一个函数。圆括号里放置实参(argument)列表(可能为空)。当然这里使用的isbn函数并不接受参数。因此item1.isbn()的作用是:调用名为item1的对象的成员函数isbn,此函数返回itme1中保存的ISBN书号。

1.6 书店程序

  到现在我们已经准备好完成书店程序的所有步骤。我们需要从一个文件中读取销售记录,生成本书的销售报告,显示售出册数、总销售额和平均售价。我们假定每个ISBN书号的所有销售记录在文件中是聚在一起保存的。
  我们的程序会将每个ISBN的所有数据合并起来,存入名为total的变量中。我们使用另一个trans变量保存读取的每条销售记录。如果trans和total指向相同的ISBN,我们会更新total的值。否则我们会打印total的值,并将其重置为刚刚读取的数据(trans):

#include <iostream>
#include "Sales_item.h"
using namespace std;

int main(){
	Sales_item total;//保存某一ISBN类书籍的书籍
	cout<<"请输入书籍数据"<<endl; 
	if(cin>>total) {
		Sales_item trans;//保存现在输入的某一个ISBN类书籍数据 
		while(cin>>trans){
			if(total.isbn() ==trans.isbn()){
				total+=trans;
			}
			else{
				//打印前一个ISBN的书籍书籍
				cout<<total<<endl;
				total=trans; //更新为当前ISBN书籍 
			}
		}
		cout<<total<<endl;//打印最后一类ISBN书籍 
	}
	else{
		cerr<<"NO DATA?!"<<endl;
		return -1;
	}
	return 0;
}

小结

  本章介绍了一些C++语言的知识,以使我们能够编译运行简单的C++程序。我们看到了如何定义一个main函数,它是操作系统执行你的程序的调用入口。我们还学会了如何定义变量,如何输入输出,以及如何编写if、for和while语句。本章最后简单介绍了一下C++最基本的特性——类。我们学会了对于其他人定义的类应该是如何创建、使用其对象。在后续章节中,我们会介绍如何定义自己的类。

习题解答

1.1 节练习

  练习1.1 查阅你使用的编译器的文档,确定它所使用的文件命名约定。编译并运行第2页的main程序。
解答:
  首先利用编辑器(如Linux系统中的vim或Windows系统中的Visual studio自带的编辑器)输入main函数,保存为.cpp或.cc后缀的源程序文件。然后按书中说明运行GNU或微软编译器,将源文件编译为可执行文件。最后执行程序观察结果。

  练习1.2 改写程序,让它返回-1.返回值通常被当作程序错误的标识。
解答:
  Windows 7操作系统并不处理或报告程序返回的错误标识,直观上返回-1的程序和返回0的程序在执行结果上并无不同。但环境变量ERRORLEVEL记录了上一个程序的返回值。因此,在控制台窗口执行修改后的程序,接着执行echo %ERRORLEVEL%,会输出-1。在Linux系统中执行echo $?有类似效果。

1.2节练习

   练习1.3 编写程序,在标准输出上打印hello,world。
解答:

#include <iostream>
using namespace std;
int main(){
	cout<<"hello,world"<<endl;
	return 0;
}

  练习1.4 我们在1.2节使用加法运算符+来将两个数相加。现在编写程序使用乘法运算符*来打印两个数的积。
解答:

#include <iostream>
using namespace std;
int main(){
	cout<<"请输入两个数"<<endl;
	int v1,v2;
	cin>>v1>>v2;
	cout<<v1<<"和"<<v2<<"的积为"
		<<v1*v2<<endl;
	return 0;
}

  练习1.5 我们之前将所有输出操作放在一条长语句中,现在重新程序,将每个运算对象的打印操作放在一条独立的语句中。
解答:

#include <iostream>
using namespace std;
int main(){
	cout<<"请输入两个数";
	cout<<endl;
	int v1,v2;
	cin>>v1>>v2;
	cout<<v1;
	cout<<"和";
	cout<<v2;
	cout<<"的积为";
	cout<<v1*v2;
	cout<<endl;
	return 0;
}

  练习1.6 解释下面程序是否合法:

std::cout<<"The sum of "<<v1;
		 <<"and"<<v2;
		 <<"is"<<v1+v2<<std::endl;

如果程序合法,它输出是什么?如果不合法,原因何在?怎样修正?
解答:
  这段代码不合法。前两行的末尾有分号,表示语句结束,第二三行为两条新语句,而这两条语句在”<<"前面缺少输出流,应在“<<"之前加上std::cout。

1.3节 练习

  练习1.7 编译一个包含不正确的嵌套注释的程序,观察编译器返回的错误信息。
解答:
对不正确的嵌套注释,不同编译器给出的错误信息可能是不同,而且非常难以理解。
在这里插入图片描述
  原因是编译器将第一个”*/"看作注释结束,之后的中文文字看作下一条语句,从而给出非法字符的错误信息。如果“*/”之后是英文文字,或是使用其他编译器进行编译,给出的可能是完全不同的错误信息。而且这些错误信息都很难直接与注释错误嵌套挂钩,程序员需要有一定的经验才能快速定位错误,确定错误原因。

练习1.8 指出下列哪些输出语句是合法的(如果有的话)

std::cout<<"/*";
std::cout<<"*/";
std::cout<</* "*/" */;
std::cout<</* "*/" /* "/*" */;

预测编译这些语句会产生什么样的结果,实际编译这些语句来验证你的答案(编写一个小程序,每次将上述一条语句作为主体),改正每个编译错误。
解答:
  第一条第二条显然是正确的。
  第三条语句的第一个双引号被注释掉了,因此<<运算符后真正被编译的内容是" */,编译器认为这是一个不完整的字符串,所以会报告:
   error missing terminting " character
  即,缺少结束的双引号。在分号前补一个双引号,就成正确的了。
  第四条看起来复杂,其实是正确的。第一个双引号被注释掉了,第四个也被注释掉了,第二个和第三个双引号之间的/*被认为是字符串的文字内容。但是这样的程序风格显然是不好的。

1.41节练习

  练习1.9 编写程序,使用while循环将50到100的整数相加。
解答:

#include <iostream>
using namespace std;

int main() {
	int sum=0;
	int i=50;
	while(i<=100){
		sum+=i;
		i++;
	}
	cout<<"50到100之间的整数之和为"<<sum<<endl;
return 0;
}

  练习 1.10 除了++运算符将运算对象的值增加1以外还有一个递减(- -)的运算符,它可以实现将值减少1。编写程序,使用递减运算符在循环中按减序打印出10到0之间的整数。
解答:

#include <iostream>
using namespace std;

int main() {
	int sum=0;
	int i=10;
	while(i>=0){
		sum+=i;
		i--;
	}
	cout<<"10到0之间的整数之和为"<<sum<<endl;
return 0;
}

  练习1.11 编写程序,提示用户输入两个整数,打印出这两个整数所指定的范围内所有整数。
解答:

#include <iostream>
using namespace std;

int main() {
	int v1,v2;
	cin>>v1>>v2;
	int sum=0;
	if(v1<v2){
		int i=v1;
		while(i<=v2){
			sum+=i;
			i++;
		}
	}
	else{
		int i=v1;
		while(i>=v2){
			sum+=i;
			i--;
		}
	}
	cout<<v1<<"和"<<v2<<"之间的和为:"<<sum<<endl;
return 0;
}

  练习1.12 下面的for循环完成了什么功能?sum终值是多少?

int sum=0;
for(int i=-100;i<=100;++i)
	sum+=i;

解答:
  此循环将-100到100之间的整数相加,sum的终值是0。

  练习 1.13 使用for循环重写1.41节的所有练习题。
解答:

#include <iostream>
using namespace std;

int main(){
	int sum=0;
	for(int i=50;i<=100;i++)
		sum+=i;
	cout<<"50到100之间所有整数之和为:"<<sum<<<endl;
	return 0;
}
#include <iostream>
using namespace std;

int main(){
	for(int i=10;i>=0;i--)
		cout<<i<<" ";
	cout<<endl;
	return 0;	
}
#include <iostream>
using namespace std;

int main(){
	cout<<"请输入两个数:"<<endl;
	int v1,v2;
	cin>>v1,v2;
	if(v1>v2){
		for(int i=v1;i>=v2;i--){//从大到小打印
			cout<<i<<" ";
	}
	else{
		for(int i=v1;i<=v2;i++){//从小到大打印
			cout<<i<<" ";
	cout<<endl;
	return 0;
}

  练习1.14 对比for循环和while循环,两个形式的优缺点各是什么?
解答:
  在循环次数已知的情况下,for循环的形式更为简洁。而在循环次数未知的情况下,while循环实现更适合。用特定条件控制循环是否执行,循环体中执行的语句可能导致循环判定条件发生变化。

  练习1.15 编写程序,包含第14页“再探编译”中讨论的常见错误。熟悉编译器生成的错误信息。
解答:
  对于复杂程序中的错误,编译器给出的错误信息很可能无法定位真正的错误位置并给出准确的错误原因。而且不同的编译器对同一个程序给出的错误信息有可能是有很大差别的。一方面是,很多时候并不存在唯一错误,编译器包括人类只能给出他认为最有可能的错误原因。另一方面,不同编译器对同样错误也可能有自己不同的解释方式。
  因此,使用不同的编译器,编译一些错误的程序,观察编译器给出的错误信息。对今后在大型软件中查找、修改编译错误是很有帮助的。

1.4.3节练习

  练习1.16 编写程序,从cin读取一组数,输出其和。
解答:

#include <iostream>
using namespace std;

int main(){
	int sum=0,value=0;
	cout<<"请输入一些数,按Ctrl+Z表示结束"<<endl;
	for(:cin>>value;)
		sum+=value;
	cout<<"读入的数之和为:"<<sun<<endl;
	return 0;
}

1.4.4节练习

  练习1.17 如果输入的所有值都是相等的,本节程序会输出什么呢?如果没有重复值又会是怎么样的?
解答:
  如果所有值都相等,则输出语句打印这唯一的一个值和它出现的次数。
  若没有重复值,则输出语句打印每个值和出现次数1。

  练习1.18 编译运行本节程序,给它们输入全都相等的值。再次运行程序,输入没有重复的值。
解答:
  按题目要求输入即可,最后别忘用Ctrl+Z表示输入结束。

  练习1.19 修改你为1.4.1节练习1.10编写的程序(打印一个范围的数),使其能够处理用户输入的第一个数比第二个数小的情况。
解答:
  练习1.11的程序以及实现了此功能。

1.5.1节练习

  练习1.20 在网站http://www.informit.com/title/0321714113上,第一章的代码目录包含了头文件Sales_item.h。将它拷贝到你自己的工作目录中,用它编写一个程序,读取一组书籍销售记录,将每条记录打印到标准输出上。
解答:

#include <iostream>
#include "Sales_item.h"
using namespace std;

int main(){
	Sales_item book;
	cout<<"请输入销售记录"<<endl;
	while(cin>>book)
		cout<<"ISBN、售出本数、销售额和平均售价为 "<<book<<endl;
	return 0;
}

  练习1.21 编写程序,读取两个ISBN相同的Sales_item对象,输出它们的和
解答:

#include <iostream>
#include "Sales_item.h"
using namespace std;
int main(){
	Sales_item trans1,trans2;
	cout<<"请输入两条ISBN相同的销售记录:"<<endl;
	cin>>trans1>>trans2;
	if(compareIsbn(trans1,trans2))//ISBN是否相同
		cout<<"汇总信息:ISBN、售出本数、销售额和平均售价为 "<<trans1+trans2<<endl;
	else
		cout<<"两条销售记录的ISBN不同"<<endl;
	return 0;
}

  练习1.22 编写程序,读取多个具有相同ISBN的销售记录,输出所有记录的和。
解答:

#include <iostream>
#include "Sales_item.h"
using namespace std;

int main(){
	Sales_item total,trans;//total代表总共,trans代表目前
	cout<<"请输入几条ISBN相同的销售记录:"<<endl;
	if(cin>>total){
		while(cin>>trans){
			if(compareIsbn(total,trans))//ISBN相同
				total+=trans;
			else{
				cout<<"ISBN不同"<<endl;
				return -1;//报错
			}
		}
		cout<<"汇总信息:ISBN、售出本数、销售额和平均价格为 :"<<total<<endl;
	}
	else{
		cout<<"没有数据"<<endl;
		return -1;
	}
	return 0;
}

1.5.2节练习

  练习1.23 编写程序,读取多条销售记录,并统计每个ISBN(每本书)有几条销售记录。
解答:

#include <iostream>
#include "Sales_item.h"
using namespace std;

int main(){
	Sales_item trans1,trans2;
	int num=1;
	cout<<"请输入若干销售记录"<<endl;
	if(cin>>trans1){
		while(cin>>trans2){
			if(trans1.isbn()==trans2.isbn())//isbn相同,数量加1
				num++;
			else{//遇到isbn不同的书籍,要输出之前的isbn书籍销售记录
				cout<<trans1.isbn()<<"共有"<<num<<"条销售记录"<<endl;
				trans1=trans2;//将trans2更新到trans1
				num=1;//更新num为1
			}
		cout<<trans1.isbn()<<"共有"<<num<<"条销售记录"<<endl;//输出最后一种书籍的销售记录
	}
	else{
		cout<<"没有数据"<<endl;
		return -1;//没有数据报错
	}
	return 0;
}

  练习1.24 输入多个ISBN的多条销售记录来测试上一个程序,每个ISBN的记录应该聚在一起。
解答:
  从网站http://www.informit.com/title/0321714113上将数据文件重定向至此程序或自己创建销售记录进行测试。
  还不太会使用文件重定向,涉及到的目录问题还有待学习

第二章 变量和基本类型

简介

  数据类型是程序的基础:它告诉我们数据的意义以及我们能过够在数据上执行的操作。
  C++语言支持广泛的数据类型。它定义了几种基本内置类型(如字符、整型、浮点型等),同时也为程序员提供了自定义数据类型的机制。基于此,C++标准库定义了一些更加复杂的数据类型,比如可变字符串和向量等。本章将主要讲述内置类型,并初步了解C++语言是如何支持更复杂数据类型的。

2.1 基本内置类型

  C++定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型。算术类型包括字符、整型数、布尔值和浮点数。 空类型不对应具体的值,仅用于特殊场合,例如函数返回空类型。

2.1.1 算术类型

  算术类型分为两类:整型(integral type,包括字符和布尔类型)和浮点型。
  算术类型的尺寸(也就是该类型数据占据的比特数)在不同机器上是有区别的。在《C++ Primer》第30页表2.1列出了C++标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。某一类型在不同机器上所占比特数不同,它所能表示的数据范围也不同。
  C++提供了几种字符类型,其中多数支持国际化。**基本字符类型是char,一个char的大小和一个机器字节一样。**其他字符类型用于扩展字符集,如wchar_t、char16_t、char32_t。
  除字符和布尔类型之外,其他整型用于表示不同尺寸的整数。C++语言规定一个int至少和一个short一样大一个long至少和一个int一样大。一个long long(在C++ 11 中新定义的)至少和一个long一样大。有关内置类型在机器上的实现涉及计算机组成原理见本书第31页。
  浮点型可以表示单精度、双精度、和扩展精度值。C++标准指定了一个浮点数有效位数的最小值,但大多数编译器都实现了更高的精度。通常,float以一个字(32比特)来表示,double以两个字(64比特)来表示,long double以三个字或四个字来表示。
  除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数负数和0。无符号类型只能表示大于等于0的值。之前介绍的int、short、long和long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型。
  与其他整型不同,字符型被分为了三种:char、signed char和unsigned char。尽管字符型有三种,但是字符的表现形式只有两种,带符号的和不带符号的。类型char会根据编译器表现为上述两种形式之一。
  那么在编写程序时该如何选择类型呢?一些经验准则如下:

当明确数值不可能为负时,选用无符号类型

② 使用int执行整数运算。在实际应用中,short太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long。

③ 在算术表达式中,不要使用char和bool,只有在存放字符或布尔值时才使用它们。因为char在有的机器上时有符号的,在有些机器上又是无符号的,进行算术运算时特别容易出问题。假如你要使用一个不大的整数,那么应该明确指出它的类型时signed char还是unsigned char。

执行浮点数运算时选用double。 这是因为float通常经常精度不够且运算代价和double相差无几。long double提供的精度一般没有必要而且运算代价较高。

2.1.2 类型转换

  顾名思义,就是将某一对象从一种给定的类型转换(convert)为另一种相关类型。当我们在程序的某处使用了一种类型而其实对象应该是其他类型时,程序会自动进行类型转换,在之后的4.11节会更加详细的介绍。当我们把一种算术类型的值赋给另一种类型时,这种转换就会发生,而决定转换过程的是类型所能表示的值的范围:
① 非布尔类型算术值赋给布尔类型时,初始值为0则结果为false,否则为true。

② 布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。

③ 浮点数赋给整数类型时,会进行近似处理,结果值只保留浮点数中小数点之前的部分。

④ 整数值赋给浮点类型时,小数部分记为0,如果该整数所占空间超过了浮点数类型的容量,精度可能会有损失。

赋给无符号类型一个超出它表示范围的值时,结果时初始值对无符号类型表示数值总数取模后的余数。例如一个8比特大小的unsigned char 可以表示0至255区间的值(即该无符号类型表示数值总数为256),因此把-1赋给8比特大小的unsigned char 所得的结果是255。

赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃、可能生成垃圾数据。

  无法预知的行为源于编译器无须(有时是不能)检测的错误。即使代码编译通过了,如果程序执行了一条未定义的表达式,仍有可能产生错误。在某些情况或某些编译器下,含有无法预知行为的程序也能正确执行,但我们却无法保证同样一个程序在别的编译器下能正常工作,甚至已经编译通过的代码再次执行也可能出错。此外,也不能认为这样的程序对一组输入有效,对另一组输入就有效。所以说,程序应该尽量避免依赖于实现环境的行为。当程序移植到别的机器上后,依赖实现环境的程序就可能发生错误,这样的程序就称为不可移植。

  把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值,否则结果时取模后的值。有点不知所云?把这个例子放到for循环里试试。当我们打算通过for循环把10到0的值递减的输出,应该用的是int i=10。因为我们不打算输出负数,所以能否改用unsigned i=10?答案是否定的。当i等于0时,–i会使的结果变为-1,而-1并不符合无符号数的要求,那么它会自动地转换成一个合法的无符号数,即-1对32位数表示数值总数取模,结果是4294967295。
  我们应切勿混用带符号类型和无符号类型,当带符号取值为负时会导致异常结果,这是因为带符号数会自动转换成无符号数。当int a=-1,unsigned b=1,则a*b的结果须视当前机器上int所占位数而定(32位是4294967295)。

2.1.3 字面值常量

  对象的值一望便知,这样的值被称为字面值常量(literal)。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
整型和浮点型字面值
  我们可以将整型的字面值写作十进制数、八进制或十六进制的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。 整型字面值具体的数据类型由它的值和符号决定。默认情况下,十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的。十进制字面值的类型是int、long、long long中尺寸最小的int。八进制和十六进制字面值的类型是能容纳其数值的int、unsigned int、long、unsigned long、long long和unsigned long中尺寸最小的。类型short没有对应的字面值。
  尽管整型字面值可以储存在带符号数据类型中,但严格来说,十进制字面值不会是负数。如果我们使用了一个形如-42的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。
  浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识。默认浮点型字面值是一个double,我们可以使用本书第37页的表2.2中的后缀来表示其他浮点型。
字符和字符串字面型
  由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。字符串型字面值的类型实际上是由常量字符构成的数组(array),该类型在本书3.5.4节介绍。编译器在每个字符串的结尾处添加一个空字符(’\0’),因此字符串字面值的实际长度要比他它的内容多1。
转义序列
  有两种字符程序员不能直接使用:一类是不可打印的(nonprintable)的字符,如退格或其他控制字符,因为它们没有可视的图符;另一类是在C++语言中含有特殊含义的字符(单引号、双引号、问号、反斜线)。在这些情况下就需要用到转义序列(escape sequence) ,转义序列均以反斜线作为开始,C++语言规范的转义序列在本书第36页。注意,如果反斜线\后面跟着的八进制数字超过3个,只有前三个数字与\构成转义序列。例如\1234就表示两个字符,\123对应八进制数123,4代表字符4。相反,\x要用到后面跟着的所有数字。例如\x1234表示一个16位的字符,该字符由4个十六进制数所对应的比特唯一确定。
指定字面值的类型
  通过添加本书表2.2中所列的前缀和后缀,可以改变整型、浮点数和字符型字面值的默认类型。
布尔字面值和指针字面值
  true和false是布尔类型的字面值,nullptr是指针字面值,本书2.3.2节会更加详细的介绍。

2.2 变量

  变量提供一个名字显而易见的,可供程序操作的存储空间。C++中每个变量都有其数据类型,而数据类型决定了变量所占内存空间大小和布局方式、该空间能储存的值的范围,已经变量能参与的运算。对于C++程序员来说,“变量(variable)”和“对象(object)”一般可以互换使用。,本书认为对象是具有某种数据类型的内存空间,我们使用对象这个词时,并不严格区分是类还是内置类型,也不区分是否命名或是否只读。

2.2.1 变量定义

  变量的基本形式是:类型说明符(type specifier,用来指定后面列表中变量名的类型)加一个或多个变量名组成的列表,它们以逗号分隔,以分号结束,定义时还可以为一个或多个变量赋初值。

初始值
  当对象在创建时获得了一个特定的值,则说这个对象被初始化(initialized)了。当一次定义了两个或多个变量时,对象的名字随着定义就可以立即使用了,因此我们可以用先定义的变量值取初始化后面定义的其他变量。
  虽然初始化和赋值都用=,但在C++语言中初始化不是赋值,而在许多其他编程语言中它们的区别几乎可以忽略不记。初始化的含义时创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值代替。

列表初始化
  初始化问题复杂性一部分体现在C++语言定义了初始化的好几种不同的形式。一般有以下四种:

int units_sold=0;
int units_sold={0};
int units_sold{0};
int units_sold(0);

  在C++11新标准中用花括号来初始化变量得到了全面应用,这种初始化形式被称为列表初始化(list initialization)。现在我,无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值了。
  当我们将这种初始化形式用于内置类型的变量时,有一个重要特定:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器会报错:

long double id=3.1415926536;
int a{ld},b={ld};//错误,列表初始化,转换未执行,因为存在丢失信息的风险
int c(ld),d=ld;//正确,不是列表初始化,转换执行,且确实丢失了部分值

  使用long double 的值列表初始化int变量可能会丢失数据,编译器拒绝初始化请求。看起来不关紧要,毕竟我们不会蠢到故意用long double的值去初始化int型变量,但这种初始化有可能在不经意间发生。我们将在本书第76页和88页对列表初始化做更多介绍。

默认初始化
  如果定义变量时并没有指定初始值,则变量被默认初始化(default initialized),此时变量被赋予了默认值,而这个默认值由变量的类型决定,同时定义变量的位置也会对此有所影响。
  如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。**定义于函数体内部的内置类型将不被初始化(uninitiated)。一个未被初始化的内置类型变量的值是未定义的。**如果拷贝或以其他形式访问此类值将引发错误。
  每个类各自决定其初始化对象的方式,是否允许不经初始化就定义对象也由类决定,如果类允许这样的行为,它将决定对象的初始值到底是什么。
  绝大多数类都支持无需显式初始化就定义对象,这样的类提供了一个合适的默认值:

string str;//str未显式的初始化为一个空串
Sales_item item;//被默认初始化的Sales_item对象

  而一些类要求每个对象都必须显式初始化,此时如果创建了一个该类的对象而为对其做明确的初始化操作,将引发错误。
  使用未初始化的变量将引发不可预见的后果,有时很幸运的导致程序崩溃,有时程序会一直执行并产生错误的结果,而更糟糕的情况是,程序时对时错无法把握。所以我们建议:初始化每一个内置类型的变量。

2.2.2 变量声明和定义的关系

  为了允许把程序分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分隔为若干个文件,每个文件可以被独立编译。如果程序被分为多个文件,那么就需要有在文件间共享代码的方法。
  为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果像使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
  如果想声明一个变量而非定义,就在变量名前添加关键字extern,而且不显式地初始化变量:

extern int i;//声明i而非定义i
int j;//声明并定义j

  如果给extern关键字标记地变量赋予一个初始值,那么它就成为了定义,抵消了extern的作用:

extern double pi =3.1415;//定义

  而在一个函数体内部试图初始化一个extern 关键字标记的变量,将引发错误。变量只能被定义一次,但可以被多次声明。 声明和定义的区别看起来微不足道,但实际非常重要。如果在多个文件中使用同一个变量,就必须声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其声明,却绝对不能重复定义。 关于C++对分离式编译的支持我们将在本身第67页和186页进行详细介绍。
静态类型
  C++是一种静态类型(statically typed)语言,即在编译阶段检查类型。这个过程被称为类型检查(type checking)。当编译器检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并不会生成执行文件。程序越复杂,静态类型检查越有助于发现问题。前提是编译器必须知道每个实体对象的类型,这就要求我们在使用某一个变量时必须声明其类型。

2.2.3 标识符

  C++的标识符(identifier)由字母、数字和下划线组成,其中必须以字母或者下划线开头,不出现连续两个下划线也不能以下划线紧连大写字母开头,此外,定义在函数体外的标识符不能以下划线开头。标识符的长度没有限制,但是对大小写敏感。在本身第43页表2.3和表2.4中,C++保留了一些名字供语言本身使用,这些名字不能用作标识符。同时C++也为标准库保留了一些名字。
变量命名规范
  变量命名有许多约定俗成的规范,希望坚持使用:
    标识符要能体现实际含义。
    变量名一般用小写字母,如index。
    用户自定义的类名一般以大写字母开头,如Sales_item.
    标识符由多个单词组成时,单词之间要有明显区分,如student_loan或studentLoan。

2.2.4 名字的作用域

  不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体:变量、函数、类型等。然后,同一个名字如果出现在程序的不同位置,就可能指向不同的实体,这就涉及到作用域的问题。
  作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字声明语句,终于声明语句所在的作用域末端。 本书第11页是一个典型的示例:

#include <iostream>
using namespace std;

int mian(){
	int sum =0;
	for(int val=1;val<=10;++val)
		sum+=val;
	cout<<"sum of 1 to 10 inclusive is ";
		<<sum <<endl;
	return 0;
}

  这段程序共定义了三个名字:main、sum和val。同时使用了命名空间名字std,该空间提供了cout和cin。名字main定义于所有花括号之外,像这样的名字一般拥有全局作用域(global scope)。这种名字一旦声明之后,在整个程序都可以使用。sum定义于main函数所限定的范围之内,从sum声明开始直到main函数结束都可以访问,出了main函数不可访问,因此成变量sum拥有块作用域(block scope)。名字val定义于for语句之内,在for语句之内可以使用val,但在main函数的其他部分就不能访问了,更不能在其他函数中访问。我们建议在对象第一次使用的地方附近定义,这样更容易找到变量的定义,而且我们也会赋予它一个比较合理的初始值。

嵌套的作用域
  作用域包含彼此,被包含(或者说被嵌套)的作用域被称为内层作用域(inner scope),包含着别的作用域的作用域叫做外层作用域(outer scope)。作用域中一旦声明了某一个名字,则它所嵌套的所用作用域都可以访问该名字,同时允许在内层作用域中重新定义外层作用域已有的名字:

#include <iostream>
using namespace std;

int reused=42;//它具有全局作用域
int main(){
	int unique=0;//它具有块作用域
	cout<<reused<<" "<<unique<<endl;//输出结果应为42 0
	int reused=0;//新建局部变量reused,覆盖了全局变量reused
	cout<<reused<<" "<<unique<<endl;//输出结果应为0 0
	cout<<::reused<<" "<<unique<<endl;//显示访问全局变量reused,输出42 0
	return 0;
}

  第一个输出发生在定义的局部变量reused之前,所以这条语句使用全局变量定义的reused。第二个输出发生在定义的局部变量reused之后,此时局部变量reused正在作用域之内(in scope),因此第二天输出使用的是局部变量reused而不是全局变量。第三个输出使用作用域操作符来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。 我们建议当函数有可能用到某全局变量时,就不宜再定义一个同名的局部变量。

2.3 复合类型

  复合类型(compound type)是指基于其他类型定义的类型。C++语言有几种复合类型,本章将介绍其中的两种:引用和指针。
  之前我们介绍了什么是声明变量,但更通用的描述是:一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。 每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。到目前为止,我们所接触的声明语句中,声明符就是变量名,此时变量的类型也就是声明的基本数据类型。其实还有更加复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。

2.3.1 引用

  在C++11中新增了一种引用:右值引用(rvalue reference),这种引用主要用于内置类,我们将在本书的第13.6.1节做更加详细的介绍。严格来说,当我们使用术语“引用”时,指的就是“左值引用(lvalue reference)”。
  引用(reference)为对象起了另一个名字,引用类型引用(refers to)另外一种类型。通过声明符写成&d的形式来定义引用类型,其中d是声明的变量名:

int ival =1024;
int &refval=ival; //refVal指向ival(是ival的另一个名字)
int &refval2;//报错,引用必须被初始化

  一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)到一起,而不是将初始值拷贝给引用。 一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,所以引用必须初始化

引用即别名
  引用并非对象,相反的它只是为一个已经存在的对象所起的另外一个名字。定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的:

refVal=2;//把2赋给refVal所指向的对象,此处即是赋给ival
int ii=refVal;//与ii==ival的执行结果一样

  为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了引用绑定的对象的值。以引用作为初始值,实际上是以与引用绑定的对象作为初始值。

int &refVal3=refVal;//正确,refVal3绑定到了那个与refVal绑定的对象,即ival
int i =refVal;//正确,利用与refVal绑定的对象的值进行初始化变量i,i被初始化为ival的值

  注意一点,引用并不是对象,所有不能定义引用的引用。

引用的定义
  C++允许在一条语句中定义多个引用,其中每个引用标识符都必须以&开头:

int i=1024,i2=2048;
int &r=i,r2=i2;
int i3=1024,&ri=i3;
int &r3=i3,&r4=i2;

   除了本书2.4.1节和15.2.3节将要介绍的两种例外情况,其他有引用的类型都要与之绑定的对象严格匹配。而且,引用只能绑定到对象上,而不能与字面值或某个表达式的结算结果绑定到一起,原因见本书2.4.1节。

int &refVal4=10;//错误,引用类型的初始值必须是变量
double dval=3.14;
int &refVal5=dval;//错误,此处的引用类型的初始值必须是int型变量

2.3.2 指针

  指针(pointer)是指向(point to)另外一种类型的复合类型。指针通常难以理解,即使有经验的程序员也常常因为调试指针引发的错误而备受折磨。与引用类似,指针也实现了对其他对象的间接访问。但是指针与引用相比又有许多不同点:1.指针本身就是一个对象,允许对指针的赋值和拷贝,而且在指针的生命周期内可以先后指向几个不同的对象。2.指针无需在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有初始化,也将拥有一个不确定的值。
  定义指针类型的方法将声明符写成*d的形式,其中d是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*

int *ip1,*ip2;//ip1,ip2都是指向int型对象的指针
double dp,*dp2;//dp是double型变量,而dp2是指向double型对象的指针

获取对象的地址

  指针存放某个对象的地址,要想获取该地址,就需要使用取地址符(操纵符&):

int ival=42;
int *p=&ival;//指针p存放ival的地址,或者说p是指向变量ival的指针

  第二条语句把p定义成一个指向int型变量的指针,随后初始化p令其指向名为ival的int型对象。因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。除了2.4.2节和15.2.3节介绍的两种例外情况,其他所有指针都要和它所指向的对象严格匹配:

double dval;
double *pd=&dval;//正确,pd初始值是double型类型变量dval的地址
double *pd2=pd;//正确,pd2的初始值是指向double型对象的指针

int *pi=pd;//错误,指针pi的类型与pd的类型不匹配
pi=&dval;//错误,试图把double变量dval的地址赋给int型指针pi

  因为在声明语句中指针的类型实际上被用于指定它所指向的对象的类型,所以二者必须匹配。否则对该对象的操作将发生错误。

指针值
  指针的值(即地址)应属于下列4种状态之一:
1.指向一个对象。
2.指向紧邻对象所占空间的下一个位置。
3.空指针,意味着没有指向任何对象。
4.无效指针,也就是上述情况之外的其他值。试图拷贝或以其他方式访问无效指针的值都将引发错误,这一点和试图使用未经初始化的变量一样。因此程序员必须清楚任意给定的指针是否有效。
  尽管第二种和第三种形式的指针是有效的,但其使用同样受到限制。显然这些指针没有指向任何具体的对象,所以试图访问此类指针(假定的)对象的行为是不被允许的。

利用指针访问对象
  如果一个指针指向了一个对象,则允许使用解引用符(操纵符*)来访问该对象,解引用符仅适用于那些确实指向了某个对象的有效指针:

int ival=42;
int *p=&ival;//p存放着变量ival的地址,或者说p是指向变量ival的指针
cout<<*p;//由符号*得到指针p所指的对象,输出42

  对指针的解引用会得出所指对象,因此如果给解引用的结果赋值,实际上就是给指针所指的对象赋值

*p=0;//由符号*得到指针p所指的对象,即可经由p为变量ival赋值,为*p赋值实际上是为p所指对象赋值
cout<<*p;//输出0

空指针

  空指针(null pointer)不指向任何对象,在试图使用一个指针之前,代码可以首先检验它是否为空,以下是几种生成空指针的方法:

int *p1=nullptr;
int *p2=0;
int *p3=NULL;//以上三种形式等价

  上述代码第一行是C++11新标准刚引入的一种方法,也是得到空指针最直接的方法。第二行的办法就是直接将p2初始化为字面常量0来生成空指针。第三行的办法用到一个名为NULL的预处理变量(preprocessor varialbe)来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0。现在就只需知道预处理器是运行于编译过程之前的一段程序;在用到一个预处理变量时,预处理器会自动地将它替换为它的实际值就可以了。本书会在2.6.3节再稍微介绍一点关于预处理器的知识。预处理器不属于命名空间std,它由预处理器负责管理,因此我们可以直接使用预处理变量而无需在前面加上std。
  在C++新标准中我们最好使用nullptr,同时尽量避免使用NULL。
  把int变量直接赋给指针是一种错误的操作,即使int变量的值恰好等于0也不行。

int zero =0;
pi=zero;//错误

  使用未经初始化的指针是引发运行错误的一大原因。在大多数编译器环境下,如果使用了未经初始化的指针,则指针所占内存空间的当前内容将被看作一个地址值。 访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。更糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法还是不合法的了。因此我们建议初始化所有指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。 如果实在不清楚指针之后要指向何处,就把它初始化为nullptr或者0,这样程序就能检测并没有它没有指向任何具体的对象。

赋值和指针
  指针和引用都是提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的一点就是引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
  而指针和它存放的地址之间就没有这种限制了。和任何其他变量一样(除了引用),给指针赋值就是令他存放一个新的地址,从而指向一个新的对象。
  有时候要搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值是不太容易的。最好的办法就是记住赋值永远改变的是等号左侧的对象

pi=&ival;//pi的值被改变,现在pi指向了ival
*pi=0;//ival的值被改变,指针pi并没有改变,是指针所指的那个对象ival发生改变。

其他指针操作

  只要指针拥有合法值,就能将它用在条件表达式中,和采用算术值作为条件遵循的规则类似,如果指针的值是0,条件取false,任何非0指针对应的条件都是true。
  对于类型相同的合法指针,可以用相等操作符(==)或不相等操作符(!=)来比较它们,比较结果为布尔类型。它们存放的地址相同,则它们相等;反之它们不相等。这里两个指针存放的地址相同有三种情况:1.它们都为空。2.它们都指向同一个对象。3.都指向了同一个对象的下一个地址。需要注意的是,一个指针指向某对象,同时另一个指针指向另外一个对象的下一个地址,此时也可能出现两个指针相等的情况。
  因为上述操作需要用到指针的值,所有无论是作为条件出现还是参与比较运算,都必须使用合法指针,使用非法指针作为条件或进行比较都会引发不可以预计的后果。本书3.5.3节将会介绍更多关于指针的操作。

void 指针
  void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着地址和其他指针不同的是,我们对该地址到底是一个什么类型的对象并不了解:

double obj=3.14,*pd=&obj;
void *pv=&obj;//正确,void*可以存放任意类型地址,其中obj可以是任意类型对象
pv=pd;//pv可以存放任意类型指针

  利用void指针能做的事情比较有限:拿它和别的指针比较、作为函数的输入或输出、或者赋给另外一个void指针。我们不能直接操作void指针所指对象,因为我们并不知道这个对象到底是一个什么类型,也就无法确定在这个指针上能做哪些操作。
  概括来说,以void
的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象,关于这点在本书19.1.1节会有更详细的介绍,4.11.3节将讲述获取void*指针所存地址的方法。

2.3.3 理解复合类型的声明

  如前所述,变量的定义包括一个基本数据类型(base type)和一组声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可以定义出不同类型的变量:

int i=1024,*p=&i,&r=i;//i是一个int型变量,p是一个int型指针,r是一个int型引用

  很多程序员容易迷惑于基本数据类型和类型修饰符的关系,其实后者不过是声明符的一部分罢了。

定义多个变量

  经常有一种观点会误认为,在定义语句中,类型修饰符(*或&)作用于本次定义的全部变量。造成这种错误看法的原因很多,其中之一是我们可以把空格写在类型修饰符和变量名中间:

int* p;//合法,但是容易产生误导

  我们说这种写法可能产生误导是因为int放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int。*仅仅修饰了p而已,对该声明语句的其他变量,它并不产生任何作用:

int* p1,p2;//p是指向int型变量的指针,p2是int型变量

  涉及指针和引用的声明一般有两种写法。第一种把修饰符和变量标识符写在一起:

int *p1,*p2;//这种形式强调变量具有的复合类型。

第二种是把修饰符和类型名写在一起,并且每条语句只定义了一个变量:

int* p1;
int* p2;//这种形式着重强调本次声明定义了一种复合类型

  上述两种定义指针或引用的不同方法没有孰对孰错,关键是选择并坚持一种写法。本书采用将(或是&)与变量名连起来。*

指向指针的指针

  一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
  通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:

int ival=1024;
int *pi=&ival;//pi指向一个int型的数
int **ppi=&pi;//pi指向一个int型的指针

  解引用int型指针会得到一个int型的数,同样,解引用指向指针的指针会得到一个指针。此时为了访问最原始的那个对象,需要对指针的指针做两次解引用:

cout<<"The value of ival\n"
	<<"direct value: "<<ival<<"\n"//直接输出ival的值
	<<"indirect value: "<<*pi<<"\n"//通过指针输出ival的值
	<<"doubly indirect value: "<<**ppi<<endl;//通过指向pi指针的指针ppi
	//来输出ival的值

指向指针的引用

  引用本身并不是对象,因此不能定义指向引用的指针。但指针是对象,所以存在指向指针的引用:

int i=42;
int *p;//p是一个int型指针
int *&r=p;//r是一个对指针p的引用

r=&i;//r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0

  要理解r的类型到底是什么,最简单的方法是从右到左阅读r的定义离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用来确定r这个引用的类型是什么,此例中符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。

2.4 const限定符

  有时候我们希望定义一种值不能改变的变量。例如用一个变量来表示缓冲区的大小。为了满足这个需要,可以用关键字const对变量的类型加以限定:

const int bufSize=512;//输入缓冲区大小

这样就把bufSize定义成了一个常量,任何试图为它赋值的行为都将引发错误。因为const对象一旦创建,其值就不能再改变,所以const对象必须初始化。跟之前一样,初始值可以是任何复杂的表达式:

const int i=get.size();
const int j=42;
const int k;//错误,未经初始化

初始化和const
  正如之前反复提到的,对象的类型决定了其上的操作。与非const类型能参与的操作相比,const并非可以适合所有操作。主要的限制就是const类型的对象只能执行不改变其内容的操作。在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都无所谓:

int i=42;
const int ci=i;
int j=cci;

  当用ci去初始化j时,根本无须在意ci是不是一个常量。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的变量没有什么关系了。

默认状态下,const对象仅在文件内有效

  编译器在编译过程中,会把所有用到该变量的地方都替换成对应的值。在上例中就是把代码中所有用到bufSize的地方,然后用512 替换。为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,就必须在每个用到该变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效,当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
  某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在不同文件中共享,我们不希望编译器为每个文件分别生成独立的变量。相反我们希望这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在其他文件中声明并使用它。
  解决办法就是:对于const变量不管是声明还是定义都添加extern关键字(extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定),这样只需一次定义就可以了:

//file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize=fcn();
//file_1.h头文件
extern const int bufSize;//与file_1.cc中定义的bufSize是同一个

  如上述程序所示,file_1.cc定义并初始化了bufSize。因为这条语句包含了初始值,所以它是一次定义。然而,因为bufSize是一个常量,必须用extern加以限定使其被其他文件使用。file_1.h头文件中的声明也由extern做了限定,其作用是指明bufSize并未本文件所有,它的定义将在别处出现。如果想在多个文件中共享const对象,必须在变量的定义前添加extern关键字。

2.4.1 const的引用

  我们可以将引用绑定到const对象上,就像绑定到其他对象上一样。我们称之为对常量的引用。但与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:

const int ci=1024;
const int &r1=ci;//正确,引用及其对应的对象都是常量
r1=42;//错误,r1是对常量的引用,因为不允许直接为ci赋值,
//当然也不能通过引用去改变ci。
int &r2=ci;//错误,试图让一个非常量引用指向一个常量对象

  因为不允许直接为常量ci赋值,当然也就不能通过非常量引用去改变ci。因此对r2的初始化是错误的。这么想,假设该初始化合法,则可以通过非常量引用r2来改变引用对象(常量)的值,这显然是错误的。

  C++程序员经常把词组对const的引用简称为常量引用。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用恒定不变。事实上,**由于C++语言并不支持随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。**引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。

初始化和对const的应用

  2.3.1节提到,引用的类型必须和其所引用的类型一致,但是有两个例外。第一种例外情况就是**在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能够转换成(2.1.2节,第32页)引用的类型即可。**尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式:

int i=42;
const int &r1=i;//正确,const int &允许绑定到一个普通非常量int对象上
const int &r2=42;//正确,r2是一个常量引用,允许将常量引用绑定到字面值上
cosnt int &r3=r1*2;//正确,r3是一个常量引用,允许将常量引用绑定到一般表达式上
int &r4=r1*2;//错误,r4是一个普通的非常量引用,引用只能绑定到对象上,
//而不能与字面值或某个表达式的结算结果绑定到一起

  要想理解这种例外的情况的原因,最简单的方法是弄清楚当一个常量引用被绑定到另外一种类型时到底发生了什么:

double dval=3.14;
const int &ri=dval;//正确

  此处ri引用了一个int型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数。因此为了确保让ri绑定到一个整数,编译器把上述代码变成了如下形式:

const int temp=dval;//由双精度浮点数生成了一个临时的整型变量,temp求值结果为3
const int &ri=temp;//让ri绑定到这个临时量

  在这种情况下,ri绑定到了一个临时量(temporary)对象,所谓临时两对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。C++程序员常常把这个临时量对象成为临时量。

double dval=3.14;
int &ri=dval;//错误

  那么当ri不是const常量时呢?如果执行了上面的初始化过程将会带来什么后果呢?**如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值。**注意,此时绑定的对象是一个临时量而非dval。程序员既然让ri引用dval,就肯定想通过ri改变dval的值。C++语言也就把这种行为定为非法。(这里还没有搞懂)

对const的引用可能引用一个非const的对象

  必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量并未作限定。因此对象也可能是个非常量所以允许通过其他途径改变它的值:

int i=42;
int &r1=i;//引用绑定对象i
const int &r2=i;//r2也可以绑定到对象i,但不允许通过r2修改i的值
r1=0;//r1并非常量,可以通过r1修改i的值
r2=0;//r2是一个常量引用,不能通过r2修改i的值

  r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以像第四行一样,通过r1一样绑定到i的其他非常量引用来修改。

2.4.2 指针和const

  与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:

const double pi=3.14;//pi是个常量,它的值不能改变
double *ptr =&pi;//错误,ptr是一个普通指针,不能存放常量对象的地址
const double *cptr=&pi;//正确,cptr是一个指向常量的指针,可以指向一个双精度常量pi
*cptr=42;//错误,指向常量的指针不能用于改变其所指对象的值

  2.3.2节提到,指针的类型必须与其所指对象的类型一致,但是有两种例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象:

double dval=3.14;//dval是一个双精度浮点数
cptr=&dval;//正确,允许令一个指向常量的指针指向一个非常量对象
//但是不能通过cptr改变dval

  和常量引用一样,指向常量的指针也没有规定其所指对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。 换句话说,所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象地值。

const指针

  指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一经初始化完成,则它的值(也就是它存放的指向对象的地址值)就不能再改变了。把*放在const关键字之前说明指针是一个常量,这样书写形式隐藏了一层含义,即不变的是指针本身而不是指向的那个值:

int errNumb=0;
int *const curErr=&errNumb;//curErr将一直指向errEumb
const double pi=3.14159;
const double *const pip =&pi;//pip是一个指向常量对象的常量指针

  要想弄清楚这些声明的含义,最有效的方法是从右向左阅读。此例中,离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是*,意思是一个常量指针(curErr是一个常量,是一个指针,即常量指针)。最后该声明语句的基本数据类型部分确定了常量指针指向的是一个int型对象。与之相似,我们也能推断出pip是一个常量指针,它指向的对象是一个double型常量。
  指针本身是一个常量并不意味着不能通过修改指针其所指对象的值,能否这样做完全依赖于它所指对象的类型。例如上例中的pip是一个指向常量的常量指针,则不论是pip所指对象值还是pip自己存储的地址都是不能改变的。相反,curErr指针指向的是一个一般的非常量int型,所以就可以通过curErr指针去间接修改errNub的值:

*pip =2.72;//错误,pip指针指向的是一个常量,不能修改其值
if(*curErr){//若curErr所指对象errNub的值不为0
	errorHandler();
	*curErr=0;//正确,通过curErr去间接的修改errNub的值为0
}

总结
  对常量的引用(常量引用):对于它引用的对象本身是不是常量并未限制,因此它引用的对象可以是常量也可以是非常量。不能通过常量引用本身去修改它所绑定的对象(无论是否为常量),但可以通过其他方式改变它所绑对象的值。
  指向常量的指针:和常量引用类似,指向常量的指针也没有规定其所指对象必须是一个常量,仅仅要求不能通过指向常量的指针来改变它所指对象(无论是否是常量)的值,可以通过其他方式改变它所绑对象的值。
  常量指针:把指针本身定为常量,其必须初始化。它既可以指向常量也可以指向一个非常量。

2.4.3 顶层const

  如前所述,指针本身就是一个对象,它又可以指向另一个对象。因此指针本身是不是一个常量和它所指的对象是不是一个常量是两个问题。用名词顶层const(top-level const)表示指针本身是个常量。而用名词底层const(low-level const)表示指针所指的对象是一个常量。
  更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是底层const也可以顶层const这一点和其他类型相比区别明显:

int i=0;
int *const p1=&i;//这是顶层const,表示指针本身是个常量(const指针)不能改变p1的值
const int ci=42;//这是顶层const,不能改变ci的值
const int *p2=&ci;//这是底层const,表示指向常量的指针,允许改变p2的值
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资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:

int *p=p3;//错误,p3包含底层const的定义,而p没有
p2=p3;//正确,p2和p3都是底层const
p2=&i;//正确,int*能转换成const int*
int &r=ci;//错误,普通的int&不能绑定到int常量上
cosnt int &r2=i;//const int&可以绑定到一个普通int上

  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节将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。这里我们建议,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。

字面值类型
  常量表达式得值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称作为“字面值类型”(literal type)。
  到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。 自定义类Sales_item、IO库、string类则不属于,也就不能定义成constexpr。其他一些字面值类型将在7.5.6节和19.3节介绍。
  尽管指针和引用都能定义成constexpr,但他们的初始值却受到严格限制。一个constexpr指针的初始值必须是nulptr或者0,或者是存储于某个固定地址的对象。
  6.1.1节将要提到,函数体内定义的变量一般来说并不存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。 同样在6.1.1节中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样由固定的地址。因此constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。

指针和constexpr

  必须明确一点,在constexpr声明中如果定义了一个指针,则限定符constexpr仅对该指针有效,而与指针所指的对象无关:

const int *p=nullprt;//p是一个指向整型常量的指针
constexpr int *q=nullptr;//q是一个指向常数的常量指针(声明为constexpr的变量一定是一个常量)

  p和q的类型相差甚远!p是一个指向常量的指针,而q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为顶层const。 与常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量:

constexpr int *np=nullptr;//np是一个指向int型的常量指针,其值为空
int j=0;
constexpr int i=42;//i的类型是整型常量
//i 和j都必须定义在函数体之外
constexpr const int *p =&i;//p是常量指针,指向整型常量i,(指向常量)
constexpr int *p1=&j;//p1是常量指针,指向整数j,(指向非常量)

2.5 处理类型

  随着程序越来越复杂,程序中用到的类型也越来越复杂。这种复杂性体现在两方面:1.一些类型难以拼写,既难记也容易写错,还无法明确体现其真实目的。2.有时候根本搞不清到底需要的类型是什么,程序员不得不回头从程序的上下文中寻求帮助。

2.5.1 类型别名

  类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的目的。
  有两种方法可以定义类型别名。传统的方法是使用关键字typedef:

typedef double wages;//wages是double的同义词
typedef wages base,*p;//base是double的同义词,p是double*的同义词?

  其中关键字typedef作为声明语句的基本数据类型的一部分出现。含有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;//pstring是char*的别名,pstring是一个指针!
const pstring cstr=0;//cstr是指向char的常量指针?
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针

  上述两条声明语句的基本数据类型都是const pstring,和过去一样,const是对给定类型的修饰。pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。 我们也可以用之前的方法来理解:从右到左阅读来第二行代码,离变量名最近的符号对变量的类型由最直接的影响。离cstr最近的是pstring,说明cstr是一个指向char的指针,下一个符号是限定符const,说明cstr是一个常量指针(指向char的常量指针)。
  遇到一条使用了类型别名的声明语句是,人们往往会错误地尝试把类型别名替换成它本来地样子,以理解该语句的含义:

const char *cstr=0;//是对const pstring cstr的错误理解

  再强调一遍,这种理解是错误的。声明语句中用到pstring时,其基本数据类型是指向char的指针。可是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char 成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,而后者则声明了一个指向const char 的指针。

2.5.2 auto类型说明符

  编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而做到这一点并不容易,有时甚至根本做不到。为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属类型的类型。 和原来那些只对应一种特定类型到说明符(如double)不同,auto让编译器通过初始值来推算变量的类型。显然auto定义的变量必须有初始值:

//由val1和val2相加的结果可以推算出item的类型
auto item=val1+val2;//item初始化为val1和val2相加的结果

  如果val1和val2是类Sales_item的对象,则item类型就是Sales_item,以此类推。
  使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

auto i=0,*p=&i;//正确,i是整数,p是整型指针
auto sz=0,pi=3.14;//错误,sz和pi的类型不一致

复合类型、常量和auto
  编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
  首先,正如我们熟知的,使用引用其实就是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型

int i=0,&r=i;
auto=r;//a是一个整数(r是i的别名,而i是一个整数)

  其次**,auto一般会忽略顶层const,同时底层const则会保留下来**,比如当初始值是一个指向常量的指针时:

const int ci=i,&cr=ci;
auto b=ci;//b是一个整数(ci的顶层const特性被忽略掉了)
auto c=cr;//c是一个整数(cr是ci的别名,ci本身是一个顶层const,被忽略)
auto d=&i;//d是一个整型指针(整数的地址就是指向整数的指针)
auto e=&ci;//e是一个指向整数常量的指针(对常量对象取地址是一种底层const)

  如果希望推断出的auto类型是一个底层const,需要明确指出

const auto f=ci;//ci的推演类型是int,f是const int,明确指出,顶层const不被忽略

  还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:

auto &g=ci;//ci是一个整型常量,设置为一个类型为auto的引用时,初始值中的顶层const属性仍然保留
//所以g是一个整型常量引用,绑定到ci
auto &h=41;//错误,不能为非常量引用绑定字面值
const auto &j=42;//正确,可以为常量引用绑定字面值

  设置为一个类型为auto的引用时,初始值中的顶层const属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。
  要在一条语句中定义多个变量,切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须时同一种类型

auto k=ci,&l=i;//k是整数(顶层const被忽略),l是整型引用
auto &m=ci,*p=&ci;//m是对整型常量的引用(见上面代码第一行),p是指向整型常量的指针(见上上段代码最后一行)
auto &n=i;*p2=&ci;//错误,i的类型是int,而&ci的类型是const int(不是同一种类型)

2.5.3 decltype类型指示符

  我们有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

decltype(f()) sum=x;//sum的类型就是函数f的返回类型

  在上述代码运行过程中,编译器并不实际调用函数f,而是使用当调用发生时的返回值类型作为sum的类型。换句话说,编译器为sum指定的类型就是假如f被调用的话将要返回的那个类型。
  decltype处理顶层const和引用的方式与auto稍有不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)

const int ci=0,&cj=ci;//ci是一个整型常量,cj是一个指向整型的常量引用
decltype(ci) x=0;//x的类型是const int
decltype(cj) y=x;//y的类型是const int&,y绑定到变量x
decltype(cj) z;//因为cj是一个引用,它的结果就是引用类型,所以z是一个引用,则它必须初始化

  需要特殊指出的是,引用从来都是作为其所指对象的同义词出现,只有用在decltype处是个例外。

decltype和引用

  如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。如4.1.1节将要介绍的,有些表达式将向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作为表达式的一部分,如r+0,显然这个表达式的结果是一个具体值42而非一个引用。另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个引用赋值。*因此,decltype(p)的结果类型就是int&,而非int。
  delctype和auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关。有一种情况需要特别注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是改变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当作一个表达式变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:

//decltype 的表达式如果加上了括号的变量,结果将是引用
decltype((i)) d;//错误,d是int型引用,必须初始化
decltype (i) e;//正确,e是一个未初始化的int

  切记,decltype((variable))的结果永远都是引用,而decltype(variable)的结果只有当(variable)本身就是个引用时,才会是引用。

2.6 自定义数据结构

  从最基本的层面理解,数据结构就是把一组相关的数据元素数组织起来然后使用它们的策略和方法。举一个例子,我们的Sales_item类把书本的ISBN编号、售出量及销售收入等数据组织在一起,并且提供诸如isbn函数、>>、<<、+、+=等运算在内的一系列操作,Sales_item类就是一个数据结构。
  C++语言允许用户以类的形式自定义数据类型,而库类型string、istream、ostream等也都是以类的形式定义的。C++语言对类的支持甚多,事实上本书的第三部分和第四部分都将大篇幅地介绍与类有关的知识。尽管Sales_item类非常简单,但是要想给出它的完整定义可在第14章介绍自定义运算符之后。

2.6.1 定义Sales_data类型

  尽管我们还写不出完整的Sales_item类,但是我们可以尝试着把那些数据类型组织起来形成一个简单点的类。初步的想法是用户能直接访问其中的数据元素,也能实现一些基本的操作。
  既然我们筹划的这个数据类型不带有任何运算功能,不妨把它命名为Sales_data。它的初步定义如下:

struct Sales_data{
	string bookNo;
	unsigned units_sold=0;
	double revenue=0.0;
};

  我们的类以struct开始,紧跟着类名和类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域。类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。
  类体右侧的表示结束的花括号后必须写一个分号,这是因为类体后面可以紧跟变量名以示该类型对象的定义,所以分号必不可少:

struct Sales_data{
	string bookNo;
	unsigned units_sold=0;
	double revenue=0.0;
}accum,trans,*salesptr;

struct Sales_data{//与上段代码等价,但推荐这么写
	string bookNo;
	unsigned units_sold=0;
	double revenue=0.0;
};
Sales_data accum,trans,*salesptr;

  分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句中,一会定义类,一会定义变量,我们不建议这么做。

类数据成员

  类体定义类的成员,我们的类只有数据成员(data member)。类的数据对象定义了类的对象的具体内容,每个对象都有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响到其他Sales_data的对象。
  定义数据成员的方法和定义普通变量一样:首先说明一个基本类型,随后紧跟一个或多个声明符。我们的类有三个数据成员:一个名为bookNo的string成员、一个名为units_sold的unsigned成员和一个名为revenue的double成员。每个Sales_data的对象都包括这三个数据成员。
  C++11新标准规定,可以为数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化(2.2.1)。因此当定义Sales_data的对象时,units_sold和revenue都将被初始化为0,bookNo将被初始化为空串。
  对类内初始值的限制如之前2.2.1节介绍的类似:或者放在花括号里,或者放在等号右边,记住不能使用圆括号。
  7.2节将要介绍,用户可以使用C++语言提供的另外一个关键字class来定义自己的数据结构,到时我们会解释现在我们使用struct的原因。

2.6.2 使用Sales_data类

  我们自己定义的Sales_data类没有提供任何操作,如果我们想执行什么操作,就必须自己动手实现。例如我们将参照1.5.2节的例子写一段程序实现求两次交易相加结果的功能。程序的输入是下面两条交易记录,每笔交易记录着图书的ISBN编号、售出数量和售出单价:

0-201-78345-x 3 20.00
0-201-78345-x 2 25.00

添加两个Sales_data对象

  假设已知Sales_data类定义于Sales_data.h文件中,2.6.3节将详细介绍定义头文件的方法。因为程序较长,所以接下来分成几个部分来介绍。总的来说,程序的结构如下:

#include <iostream>
#include <string>
#include "Sales_data.h"
using namespace std;

int main(){
	Sales_data data1,data2;
	//读入data1和data2的代码
	//检查data1和data2的ISBN是否相同的代码
	//如果相同,求data1和data2的总和
}

  和原来的程序一样,先把所需的头文件包含进来并且定义变量用于接收输入。和Sales_item类不同的是,新程序还包含了string头文件,因为我们的代码中将用到string类型成员变量bookNo。

Sales_data对象读入程序

  第三章和第十章将详细介绍string类型的细节,我们现在就只需知道string类型就是字符的序列,它的操作有>>、<<和==等,功能分别是读入字符串、写出字符串和比较字符串。这样我们就能书写代码读入第一笔交易了:

double price=0;//书的单价,用于计算销售收入
//读入第一笔交易:ISBN、销售数量、单价
cin>>data1.bookNo>>data1.units_sold>>price;
//计算销售收入
data1.revenue=data1.units_sold*price;//收入等于销售额乘单价

  交易信息记录的是书售出的单价,而数据结构存储的是一次交易的销售收入,因此需要将单价读入到double变量price,然后再计算销售收入revenue。输入语句:

cin>>data1.bookNo>>data1.units_sold>>price;

使用点操作符读入对象data1的bookNo成员和units_sold成员。
  最后一条语句把data1.units_sold和price的乘积赋值给data1的revenue成员。接下来读入对象data2的数据:

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 togalRevenue = data1.revenue+data2.revenue;
	//输出ISBN、总销售量、总销售额、平均价格
	cout<<data1.bookNo<<" "<<tatalCnt<<" "<<totalRevenue<<" ";
	if(totalCnt!=0)
		cout<<totalRevenue/totalCnt<<endl;
	else
		cout<<"(no sales)"<<endl;
	return 0;//标示成功
}
else{
	cerr<<"Data must refer to the same ISBN"<<endl;
	return -1;//标示失败
}

  在第一个if语句中比较了data1和data2的bookNo成员是否相同。如果相同执行第一个if语句花括号里的操作,首先计算units_sold的和并赋给变量totalCnt,然后计算revenue的和并赋给totalRevenue,输出这些值。接下来检查是否售出了书籍,如果是,计算并输出每本书的平均价格;如果售量为零,输出一条相应的信息。

小结

  类型是C++编程的基础。
  类型规定了其对象的存储要求和所能执行的操作。C++语言提供了一套基础内置类型,如int,char等,这些类型与实现它们的机器硬件密切相关。类型分为常量和非常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外还能定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。
  C++语言运行用户以类的形式自定义类型。C++库通过类提供了一套高级抽象类型,如输入输出和string等。

习题解答

2.1.1节练习

  练习2.1 类型int、long、long long、和short的区别是什么?无符号类型和带符号类型的区别是什么?float和double的区别是什么?
解答:
  在C++语言中int、long、long long和short都属于整型,区别是C++标准规定的尺寸的最小值(即该类型在内存中所占的比特数)不同。其中short是短整型,占16位;int是整型,占16位;long 和long long均为长整型,分别占32位和64位。当然C++标准允许不同的编译器赋予这些类型更大的尺寸。
  大多数整型都可以分为无符号类型和带符号类型,在无符号类型中所有的比特都用来储存数值,但仅能表示大于等于0的值;带符号类型则可以表示正数负数和0。
  float和double分别是单精度浮点数和双精度浮点数,区别主要是所占比特数不同,以及默认规定的有效位数不同。

  练习2.2 计算按揭贷款时,对于利率、本金和付款分别应选择何种数据类型?
解答:
  在实际应用中,利率、本金和付款既有可能是整数,也有可能是普通的实数。因此应该选用一种浮点数来表示。根据这节的类型选取建议,我们应该选用double。

2.1.2节练习

  练习2.3 读程序写结果。编写程序检查你的估计是否正确。

unsigned u=10,u2=42;
cout<<u2-u<<endl;//1
cout<<u-u2<<ednl;//2

int i=10,i2=42;
cout<<i2-i<<endl;//3
cout<<i-i2<<endl;//4
cout<<i-u<<endl;//5
cout<<u-i<<endl;//6

解答:
  1:32;
  2:4294967264(编译环境中int位数为32位),直接计算结果为-32,所以表示为无符号类型时自动加上了模,结果为上述数值。
  3:32
  4:-32
  5:0,计算时编译器先把带符号数转换为无符号数,因为i为正数,因此转换后不会异常情况,两个式子计算结果都是0。
  6:0

2.1.3节练习

  练习2.5 指出下述字面值的数据类型,并说明每一组内几种字面值的区别。
(a)‘a’, L’a’, “a”, L"a"
(b)10,10u,10L,10uL, 012, 0xC
(c)3.14,3.14f, 3.14L
(d)10,10u,10.,10e-2
解答:

(a) 'a’表示字符a,L’a’表示宽字符型字面值a且类型是wchar_t,"a"表示字符串a,L"a"表示宽字符型字符串a。
(b)10是一个整数类型字面值,10u表示一个无符号数,10L表示一个长整型数,10uL表示一个无符号长整型数,012是八进制数对应十进制10,0xC是一个十六进制数对应十进制12。
©3.14是浮点型字面值,3.14f表示一个float类型的单精度浮点数,3.14L表示一个long double 类型的扩展精度浮点数。
(d)10是一个整数,10u是一个无符号整数,10.是一个浮点数,10e-2是科学计数法表示的浮点数,大小为10*10^(-2)=0.1。

  练习2.6 下面两组定义是否有区别,如果有,请叙述。

int mouth =9,day=7;
int mouth =09,day=07;

解答:
第一行是正确的十进制数9和7。第二行以0开头是八进制数,而09已经超出了八进制数所能表示的范围。

  练习2.7 下述字面值表示何种含义?它们各自的数据类型是什么?
(a)“who goes with F\145rgus?\012”
(b)3.14e1L
(c)1024f
(d)3.14L
解答:
(a)是一个字符串,包含两个转义字符\145表示字符e和\012表示一个换行符。因此该字符输出结果为:who goes with Fergus?

(b)是一个科学计数法表示的扩展精度浮点数,大小为3.14*10^1=31.4。
(c)它想要表达一个单精度浮点数,但形式不对,将会报错。正确的写法为1024.f。
(d)是一个扩展精度浮点数,类型是long double,大小为3.14。

  练习2.8 请利用转义字符编写一段程序,要求先输出2M,然后转到新一行。修改程序使其先输出2,再输出制表符,再输出M,最后转到新一行。
解答:

#include <iostream>
using namespace std;

int main(){
	cout<<"2\x4d\012";
	cout<<"2\tM\n";
	return 0;
}

2.2.1节练习

  练习2.9 解释下列定义的含义,对于非法的定义,请说明错在何处并将其改正。

cin>>int input_value;
int i ={3.14};
double salary=wage=9999.99;
int i=3.14;

解答:
  第一行是错误的,因为输入运算符右侧需要一个明确的变量名称(已经定义的变量)。改正:int input_value; cin>>input_value;
  第二行将引起警告,因为将浮点数列表初始化给整型变量i将会造成小数点后面数据丢失,是一种不被建议的窄化操作。
  第三行是错误的,在声明语句中声明多个变量要用逗号将变量名隔开,而不能直接用赋值运算符连接(python中是可以的)。改正:doouble salary,wage;salary=wage=9999.99;
  第四行将引发警告,会造成小数部分丢失,与第二行一样是不被建议的窄化操作。

  练习2.10 下列变量的初始值分别是什么?

string global_str;
int global_int;
int main(){
	int local_int;
	string local_str;
}

解答:
  string类型本身支持接受无参数的初始化方式,所以不论变量定义在函数内还是函数外,都被默认初始化为空串。
  对于内置类型int来说,global_int (全局变量)被定义在函数体之外,根据C++的规定,它将被默认初始化为0;而变量local_int(局部变量)定义在main函数内部,将不会被初始化,如果程序试图拷贝或输出未初始化的变量,将遇到未定义的奇异值。

2.2.2节练习

  练习2.11 指出下面语句是声明还是定义

extern int ix=1024;
int iy;
extern int iz;

解答:
  第一行定义了变量ix(声明带了初始化)。第二行声明并定义了iy。第三行声明了变量iz。

2.2.3节练习

  练习2.12 请指出下面的名字中哪些是非法的。

int double =3.14;
int _;
int catch-22;
int 1_or_2=1;
double Double =3.14;

解答:
  第一行非法,因为double是C++关键字,代表一种数据类型,不能作为变量的名字。第三行非法,标识符中只能出现字母下划线和数字不能出现符号(-)。第四行非法,标识符必须以字母和下划线开头不能以数字开头。

2.2.4 节练习

  练习2.13 下面程序中j的值是多少?

int i=42;
int main(){
	int i=100;
	int j=i;
}

解答:
  j的值为100,因为C++允许在内层作用域中重新定义外层作用域中已有的名字(但不建议这么做),内层作用域重新定义了i,所以j的值为100.
  练习2.14 下面程序合法吗?如果合法,它将输出什么?

int i=100,sum=0;
for(int i=0;i!=10;++i)
	sum+=i;
cout<<i<<" "<<sum<<endl;

解答:
  合法,输出100和45。因为输出语句位于外层循环中,所以输出的i是外层作用域定义的i,值为100。而sum在外层作用域初始定义为0,在for循环中并没有重新定义,所以sum的终值是不断累积的,值为45。

2.3.1节练习

  练习2.15 下面哪个定义是不合法的,为什么?

int ival=1.01;
int &rval1=1.01;
int &rval2=ival;
int &rval3;

解答:
  第二行不合法,引用必须指向一个实际存在的对象而非字面值。第四行不合法,因为无法令引用重新绑定到另外一个对象上,所以引用必须初始化。

  练习2.16 下列哪些赋值是不合法的?为什么?哪些是合法的,它们执行了怎么样的操作?

int i=0,&r1=i;
double d=0,&r2=d;
//各行相互独立
r2=3.14159;
r2=r1;
i=r2;
r1=d;

解答:
  第一行合法,这是引用赋值,实际上是把值赋给了与引用绑定的对象,即把3.14159赋给了double型变量d。第二行合法,以引用作为初始值实际上是引用绑定的对象作为初始值,即把r1绑定的对象i赋给了变量d。第三行合法,把d的值赋给了int型变量i,但d是浮点数,而i是整数,该语句实际上执行了窄化操作。第四行合法,同样是把d的值赋给了整型变量i,也是窄化操作。

  练习2.17 执行下面的代码会输出什么结果?

int i,&ri=i;
i=5;
ri=10;
cout<<i<<" "<<ri<<endl;

解答:
  程序输出结果为10 10。

2.3.2节练习

  练习2.18 编写代码分别更改指针的值和指针所指对象的值。
解答:一个满足要求的简单示例如下:

#include <iostream>
using namespace std;

int main(){
	int i=5,j=10;
	int *p=&i;
	cout<<p<<" "<<*p<<endl;//输出i的地址和5
	p=&j;//改变了指针的值,使指针指向j
	cout<<p<<" "<<*p<<endl;//输出j的地址和10
	*p=20;//改变了p指针所指对象j的值
	cout<<p<<" "<<*p<<endl;//输出j的地址和20
	j=30;//改变了j对象的值
	cout<<p<<" "<<*p<<endl;//输出j的地址和30
	return 0;
}

  练习2.19 说明指针和引用的主要区别。
解答:
  指针指向内中某个对象,而引用绑定到内存中的某个对象。它们都实现了对其他对象的间接访问。二者的区别主要有两方面:1.指针本身就是一个对象,允许对指针的赋值和拷贝,而且在指针的生命周期内它可以指向几个不同的对象;引用不是一个对象,无法令其重新绑定到另一个对象上。2.指针无需在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值;引用则必须在定义时赋初值。

  练习2.20 请叙述下面这段代码的作用。

int i=42;
int *p1-&i;
*p1=*p1 * *p1;

解答:
  第一行定义了一个整型变量i并初始化为42;第二行定义了一个整型指针p1,令其指向变量i;第三行通过解引用符取出p1所指对象的值进行平方,再赋给p1所指对象的变量i。

  练习2.21 请解释下述定义。在下面这些定义中有非法的么?如果有,为什么?

int i=0;
//以下三行相互独立
double *dp=&i;
int *ip=i;
int *p=&i;

解答:
  第一行非法,dp是一个double型指针,而i是一个整型变量,类型不匹配。第二行非法,不能直接把int型变量赋给int指针,正确的方法是通过取地址运算符&i得到变量i的内存地址,再将它赋给int型指针。第三行合法。

  练习2.22 假设p是一个int型指针,请说明下述代码的含义。

if(p)//
if(*p)//

解答:
  指针作为if条件语句的条件时,实际检验的是指针本身的值,即指针所指的地址值。如果指针指向一个真实存在的变量,则其值必不为0,此时条件为真;如果指针没有指向任何对象或者是无效指针,则对p的使用将引发不可预见的结果。
  解引用符*p作为if语句的条件时,实际检验的是指针所指对象的内容,在上面的例子中,如果指针p所指的int型对象的值为0,则条件为假;否则,条件为真。

  练习2.23 给定指针p,你能知道它是否指向了一个合法的对象吗?如果能,请叙述思路,如果不能也请说明原因。
解答:
  在C++程序中,应该尽量初始化所有指针,并且尽可能等定义了对象之后再定义指向它的指针。如果实在不清楚指针之后应该指向何处,就把它初始化为nullptr或0。
  如果不注意初始化所有指针而贸然判断指针的值,则有可能引发不可预见的结果。一种处理的办法是把if(p)置于try结构中,当程序块顺利执行时,表示p指向了合法的对象;当程序块出错跳转到catch语句时,表示p没有指向合法的对象。

  练习2.24 下面这段代码中为什么p合法而lp非法?

int i=42;
void *p=&i;
long *lp=&i;

解答:
  p是合法的,因为void*是一种特殊的指针类型,可用于存放任意对象的地址。lp是非法的,因为lp是一个长整型指针,而i只是一个普通整型数,二者类型不匹配。

2.3.3节练习

  练习2.25 说明下列变量的类型和值

//以下三行相互独立
int* ip,i,&r=i;
int i,*ip=0;
int* ip,ip2;

解答:
  第一行:ip是一个int型指针,它的值是所指int型变量在内存中的地址;i是int型变量,它的值是一个整型数;r是一个指向int型指针的引用,此处指向的是i指针。第二行:i是个整型变量;ip是一个int型指针,但它不指向任何具体的对象,它的值被初始化为0。第三行:ip是一个int型指针指向一个int型变量,它的值是它所指整型数的值;ip2是一个整型变量。

2.4节练习

  练习2.26 下面哪些语句合法?如果有不合法,请说明。

//以下四行被看作是顺序执行
const int buf;
int cnt=0;
const int sz=cnt;
++cnt;++sz;

解答:
  第一行不合法,const对象一经创建后其值不可改变,所以const对象必须初始化,该语句应该修改为const int buf=10;第四行不合法,sz是一个const值,其值不应该被修改,自然无法进行自增操作。

2.4.2节练习

  练习2.27 下面哪些初始化是合法?请说明原因。

//以下七行相互独立
int i=-1,&r=0;
int *const p2=&i2;
const int i=-1,&r=0;
const int *const p3=&i2;
const int *p1=&i2;
const int &const r2;
const int i2=i,&r=i;

解答:
  第一行非法,因为r是非常量引用,不能引用字面值0。第二行合法,p2是一个常量指针,p2中存的是i2的地址永久不变。第三行合法,i是int型常量,r是一个常量引用,此时可以绑定到字面值常量0。第四行合法,p3是一个指向int型常量的常量指针,p3永远指向变量i2,且p3指向的是常量,即我们不能通过p3来改变i2的值。第五行合法,p1是指向int型常量的指针,即不能通过p1来改变i2的值。第六行非法,引用本身不是对象,因此不能让引用恒定不变,不能引用常量。 第七行合法,i2是一个常量,r是一个常量引用,指向一个int型。

  练习2.28 说明下面的这些定义是什么意思,挑出其中不合法的。

//以下五行相互独立
int i,*const cp;
int *p1,*const p2;
const int ic,&r=ic;
const int *const p3;
const int *p;

解答:
  第一行不合法,i是一个int型变量,cp是一个常量指针,其值不可改变,所以必须初始化。第二行不合法,p2是一个常量指针,其值不可改变,所以必须初始化。第三行非法,因为ic是一个常量,其值必须初始化。r是一个常量引用,可以绑定常量对象和非常量对象。第四行非法,p3是一个指向int型常量的常量指针,其必须初始化。另外p3指向的对象是常量,即我们不能通过p3来改变它指向对象的值。第五行合法,p是一个指向常量的指针,但p没有指向任何值。

  练习2.29 假设已有上一个练习中定义的那些变量,则下面语句那些是合法的?请说明原因。

i=ic;
p1=p3;
p1=&ic;
p3=&ic;
p2=p1;
ic=*p3;

解答:
  第一行合法。第二行非法,普通指针p1指向了一个常量,从语法上说,p1的值可以随意更改,这显然是不合理的。第三行非法,普通指针p1指向一个常量,错误情况和第二行类似。第四行非法,p3是一个常量指针,一经初始化它的值不可再改变。第五行非法,p2是一个常量指针,一经初始化它的值不可再更改。第六行非法,ic是一个常量,一经初始化它的值不可再更改。

2.4.3节练习

  练习2.30 对于以下这些语句,请说明对象被声明成了顶层const还是底层cosnt?

const int v2=0;
int v1=v2;
int *p1=&v1,&r1=v1;
const int *p2=&v2,*const p3 =&i,&r2=v2;

解答:
  v2和p3是顶层const,分别表示一个整型常量和一个指向整型常量的常量指针;p2和r2是底层const,分别表示一个指向常量的指针和指向常量的引用(常量引用)。

  练习2.31 假设已有上面练习所做的声明,则下面的哪些语句是合法的?请说明顶层const和底层const在每个例子中有何体现。

r1=v2;
p1=p2;p2=p1;
p1=p3;p2=p3;

解答:
  在执行拷贝操作时,顶层const和底层const区别明显。其中顶层const不受影响,这是因为拷贝操作并不会改变被拷贝对象的值。底层const的限制则不容忽视,拷入和拷出的对象必须时具有相同的底层const资格,或者两个对象的数据类型必须能够转换。 一般来说非常量可以转换成常量,反之则不行。
  r1=v2是合法的,r1是一个非常量引用,v2是一个常量(顶层const)把v2拷贝给r1不会对v2有任何影响;p1=p2是非法的,p1是普通指针,指向的对象可以是任意值,p2是指向常量的指针(底层const),令p1指向p2所指的内容,则有可能通过p1错误的改变常量的值;p2=p1是合法的,与上一条语句相反,p2可以指向一个非常量,只不过我们不会通过p2来更改它所指的值;p1=p3是非法的,p3包含底层const的定义(p3所指的对象是常量),不能把p3的值赋给普通指针;
p2=p3是合法的,p2和p3包含相同的底层const,p3的顶层const则可以忽略不记。

2.4.4节练习

  练习2.32 下面的代码是否合法?如果非法,请设法将其修改。

int null=0,*p=null;

解答:
  非法,null是一个int变量,p是一个int型指针,二者不能直接绑定。单从语法角度来说可以将代码修改为:int null=0,*p=&null; 显然这种改法和代码的原意不一定相符,另一种改法为:int null=0,*p=nullptr;

2.5.2节练习

   练习2.33 利用本节定义的变量,判断下列语句的运行结果。

a=42;b=42;c=42;
d=42;e=42;g=42;

解答:
  第一条合法,根据本节的定义,r是一个引用,是i的别名,而i是一个整数,所以a的类型推断结果是一个整数;第二条合法,根据本节的定义,ci是一个整型常量,b是一个整型(ci的顶层const被忽略)。第三条合法,根据本节的定义,cr是ci的别名,而ci是一个整型常量,所以c是一个整数(顶层const被忽略)。第四条非法,i是一个整数,&i是i的地址,所以d的类型推断是一个整型指针。第五条非法,ci是一个整型常量,&ci是整型常量的地址,所以e的类型推断结果是一个指向整型常量的指针。第六条非法,ci是一个整型常量,所以g的类型推断为一个整型常量引用。因为d和e都是指针,所以不能直接用字面值常量为其赋值。g绑定到了整型常量,所以不能修改它的值。

  练习2.34 基于上一个练习中的变量和语句编写一段代码,输出赋值前后变量的内容,你刚才的推断对吗?如果不对,反复研读本节的示例直到你明白错在何处为止。
解答:

#include <iostream>
using namespace std;
 
int main(){
	int i=0,&r=i;
	auto a=r;//r是i的别名,所以a是一个整数 
	const int ci=i,&cr=ci;
	auto b=ci;//b是一个整数(ci的顶层const特性被忽略)
	auto c=cr;//c是一个整数,cr是ci的别名,原因同上。
	auto d=&i;//d是一个整型指针
	auto e=&ci;//ci是整数,所以e是一个指向整型常量的指针
	auto &g=ci;//g是一个整型常量引用,绑定到ci
	cout<<a<<" "<<b<<" "<<c<<" "<<d<<" "<<e<<" "<<g<<endl;
	a=42;
	b=42;
	c=42;
	//d=42;//错误 
	//e=42;//错误 
	//g=42;//错误 
	cout<<a<<" "<<b<<" "<<c<<endl;
	//" "<<d<<" "<<e<<" "<<g<<endl; 
	return 0; 
}

  练习2.35 判断下列定义推断出的类型是什么,然后编写程序验证。

const int i=42;
auto j=i;const auto &k=i;auto *p=&i;
const auto j2=i,&k2=i;

解答:
  i是一个整型常量,j是一个整型(顶层const特性被忽略)。k的类型推断结果为整型常量。p的类型被推断为指向整型常量的指针。j2的类型推断结果为整型,k2的类型推断为整型。
根据赋值的类型来推断被赋值的类型;

#include <iostream>
#include <typeinfo>
using namespace std;

int main(){
	const int i=42;
	auto j=i;
	const auto &k=i;
	auto *p=&i;
	const auto j2=i,&k2=i;
	cout<<typeid(i).name()<<endl;
	cout<<typeid(j).name()<<endl;
	cout<<typeid(k).name()<<endl;
	cout<<typeid(p).name()<<endl;
	cout<<typeid(j2).name()<<endl;
	cout<<typeid(k2).name()<<endl;
	
	return 0;
}

2.5.3节练习

  练习2.36 关于下面的代码,请指出每个变量的类型以及程序结束时它们各自的值。

int a=3,b=4;
decltype(a) c=a;
decltype((b)) d=a;
++c;
++d;

解答:
  a和b都是int型,它们的值分别为3和4。因为decltype(a)c=a使用的是未加括号的变量,所以c的类型就是a的类型,它目前的值为3,程序结束时它的值为4。因为decltype((b))使用的是带括号的变量,所以d的类型是引用,该语句等同于int &d=a;所以d是a的一个别名。
  当执行++c和++d操作后,c的值变为4,d的值变为4,因为d是a的一个别名,所以a也变为4,所以程序结束后,abcd的值都为4。

  练习2.37 赋值时会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说如果i是int,则表达式i=x的类型就是int&。根据这一特点,请指出下面的代码中每一个变量的类型和值。

int a=3,b=4;
decltype(a) c=a;
decltype(a=b) d=a;

解答:
  decltype的参数既可以是普通变量,也可以是一个表达式。当参数是普通变量时,推断出的类型就是该变量的类型;当参数是表达式时,推断出的类型是引用。
  c是int,值为3。表达式a=b作为decltype的参数,编译器分析表达式并得到它的类型作为d的推断类型,但不是实际计算该表达式,所以a的值不发生变化仍然是3。d的类型是int&,d是a的别名,值为3。b的值一直没有改变,为4。

  练习2.38 说明由decltype指定类型和由auto指定类型有何区别。请举出一个例子,decltype指定的类型与auto指定的类型一样;再举一个例子,decltype指定的类型与auto指定的类型不一样。
解答:
  auto和decltype的区别主要有三个方面:
1.auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
2.编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto一般会忽略顶层const,而把底层const保留下列。与之相反,decltype会保留变量的顶层const。
3.与auto不同,decltype的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上一层或多层括号,则编译器将推断得到引用类型。
  下面用以说明的示例如下:

#include <iostream>
#include <typeinfo>
using namespace std;

int main(){
	int a=3;
	auto c1=a;
	decltype(a) c2=a;
	decltype((a)) c3=a;
	
	const int d=5;
	auto f1=d;
	decltype(d) f2=d;
	cout<<typeid(c1).name()<<endl;
	cout<<typeid(c2).name()<<endl;
	cout<<typeid(c3).name()<<endl;
	cout<<typeid(d).name()<<endl;
	cout<<typeid(f1).name()<<endl;
	cout<<typeid(f2).name()<<endl;
	
	c1++;
	c2++;
	c3++;
	f1++;
	//f2++;错误,f2时整型常量不能自增 
	cout<<a<<" "<<c1<<" "<<c2<<" "<<c3<<" "<<f1<<" "<<f2<<endl; 
	
	return 0;
}

  对于第一组类型推断来说,a是一个非常量int,所以c1的推断结果是整数,c2的推断结果也是整数,c3的推断结果由于加了一对括号所以是int&。c1,c2,c3执行自增操作,因为c3是a的别名,所以a也自增,因此最后c1,c2,c3和a的值都为4。
  对于第二组类型推断来说,d是一个常量整数,含有顶层const,使用auto类型推断时会自动忽略掉顶层const,所以f1的类型是整型;decltype则会保留顶层const,所以f2的类型是整型常量。f1可以正常执行自增操作,f2因为是常量,所以不能执行自增操作。

2.6.1节练习

  练习2.39 编译下面程序观察其运行结果,注意,如果忘了写类定义体后面的分号会是什么情况?记录下相关信息,以后可能会有用。

struct Foo{/*此处为空*/}//注意,没有分号哦
int main(){
	return 0;
}

解答:
  该程序无法编译通过,原因时缺少了分号。因为类体后面可以紧跟变量名以式对该类型对象的定义,所以在类体右侧表示结束的花括号之后必须写一个分号。

  练习2.40 根据自己的理解写出Sales_data类型,最好与书中的例子 有所区别。
解答:原书中的程序包括三个数据成员,我们新设计的Sales_data类细化了销售收入的计算方式,在保留bookNo和units_sold的基础上个,新增了sellingprice(零售价、原价)、saleprice(实售价、折扣价)、discount(折扣),其中discount=saleprice/sellingprice。

struct Sales_data{
	string bookNo;//书籍编号
	unsigned units_sold=0;//销售量
	double   sellingprice=0.0;//零售价
	double saleprice=0.0;//实售价
	double discount=0.0;//折扣
};

2.6.2节练习

  练习2.41 使用你自己的Sales_data类重写1.5.1节,1.5.2节和1.6节的练习。眼下先把Sales_data类的定义和main函数放在同一个文件里。

#include <iostream>
#include <string>
using namespace std;

class Sales_data{
//友元函数
friend std::istream& operator >> (std::istream&, Sales_data&);
//友元函数
friend std::ostream& operator << (std::ostream&&, const Sales_data&);
//友元函数
friend bool operator < (const Sales_data&, const Sales_data&);
//友元函数
friend bool operator ==(const Sales_data&, const Sales_data&); 

public://构造函数的3种形式
	Sales_data() = default;
	Sales_data(const std::string &book): bookNo(book) { }
	Sales_data(std::istream &is) { is >> *this; }
	
public:
	Sales_data& operator +=(const Sales_data&);
	std::string isbn() const { return bookNo; }
	
private:
	std::string bookNo;//书籍编号,隐式初始化为空串
	unsigned units_sold = 0;//销售量,显式初始化为0
	double sellingprice = 0.0;//原始价格,显式初始化为0.0
	double saleprice = 0.0;//实售价格,显式初始化为0.0
	double discount =0.0;//折扣,显式初始化为0.0
	
}; 

inline bool compareIsbn(const Sales_data &lhs,const Sales_data &rhs)
{ return lhs.isbn()== rhs.isbn();}

Sales_data operator +(const Sales_data&, const Sales_data&);


inline bool operator == (const Sales_data &lhs,const Sales_data &rhs)
{
	return lhs.units_sold == rhs.units_sold && 
		lhs.sellingprice== rhs.sellingprice && 
		lhs.saleprice ==rhs.saleprice &&
		lhs.isbn() == rhs.isbn();
}

inline bool operator !=(const Sales_data &lhs, const Sales_data &rhs)
{
	return !(lhs == rhs);//基于运算符== 给出!=的定义 
}

Sales_data& Sales_data::operator +=(const Sales_data& rhs)
{
	units_sold += rhs.units_sold;
	saleprice=(rhs.saleprice * rhs.units_sold+saleprice*units_sold)
	/(rhs.units_sold+units_sold);
	if(sellingprice != 0)
		discount = saleprice/ sellingprice;
	return *this;
} 

Sales_data operator +(const Sales_data& lhs,const Sales_data& rhs)
{
	Sales_data ret(lhs);//把lns的内容拷贝到临时变量ret中,这种做法便于运算
	ret+=rhs;//把rhs的内容加入其中
	return ret;//返回ret 
}

std::istream& operator>>(std::istream& in,Sales_data& s)
{
	in>> s.bookNo >> s.units_sold>>s.sellingprice>>s.saleprice>>s.saleprice;
	if(in&& s.sellingprice!=0)
		s.discount =s.saleprice/s.sellingprice;
	else
		s=Sales_data();//输入错误,重置输入的数据
	return in; 
} 
std::ostream& operator <<(std::ostream& out, const Sales_data& s)
{
	out <<s.isbn()<<" "<<s.units_sold<< " "
		<<s.sellingprice<<" "<<s.saleprice<< " " <<s.discount;
	return out;  
}

int main()
{
	Sales_data book;
	std::cout<<"请输入销售记录:"<<std::endl;
	while (std::cin>>book)
	{
		std::cout<<"ISBN、售出本数、原始价格、实售价格、折扣为"<<book<<std::endl; 
	} 
	Sales_data trans1,trans2;
	std::cout<<"请输入两条ISBN相同的销售记录:"<<std::endl;
	std::cin>>trans1>>trans2;
	
	if(compareIsbn(trans1,trans2))
		std::cout<<"汇总信息:ISBN、售出本数、原始价格、实售价格、折扣为 "
		<<trans1+trans2<<std::endl;
	else
		std::cout<<"两条销售记录的ISBN不同"<<std::endl;
		
	Sales_data total,trans;
	std::cout<<"请输入几条ISBN相同的销售记录:"<<std::endl;
	if(std::cin>>total)
	{
		while(std::cin>>trans)
			if(compareIsbn(total,trans))//isbn不同
				total=total+trans;
			else
			{
				std::cout<<"当前书籍ISBN不同"<<std::endl;
				break; 
			}
		std::cout<<"有效汇总信息:isbn、售出本数、原始价格、实售价格、折扣为"
		<<total<<std::endl; 
	} 
	else
	{
		std::cout<<"没有数据"<<std::endl;
		return -1; 
	}
	
	int num=1;
	std::cout<<"请输入若干销售记录:"<<std::endl;
	if(std::cin>>trans1)
	{
		while(std::cin>>trans2)
		{
			if(compareIsbn(trans1,trans2))//ISBN相同
				num++; 
			else{
				std::cout<<trans1.isbn()<<"共有"<<num<<"条销售记录"<<std::endl;
				trans1=trans2;
				num=1; 
			}
		}
		std::cout<<trans1.isbn()<<"共有"<<num<<"条销售记录"<<std::endl; 
	} 
	else
	{
		std::cout<<"没有数据"<<std::endl;
		return -1;	
	}
	return 0;
}

  练习2.42 根据你自己的理解重写一个Sales_data.h头文件,并以此为基础重做2.6.2节(第67页)的练习。
解答:

//Sales_data.h头文件的内容是:
#ifndef SALES_DATA_H_INCLUDED
#define SALES_DATA_H_INCLUDED

#include <iostream>
#include <string>

class Sales_data{
//友元函数
friend std::istream& operator >>(std::istream&,Sales_data&);
//友元函数
friend std::ostream& operator >>(std::ostream&,const Sales_data&);
//友元函数
friend bool operator < (const Sales_data&,const Sales_data&);
//友元函数
friend bool operator == (const Sales_data&,const Sales_data&); 
public: //构造函数的3种形式
	Sales_data() = default;
	Sales_data(const std::string &book): bookNo(book) { }
	Sales_data(std::istream &is) { is>>*this;}
public:
	Sales_data& operator += (const Sales_data&);
	std::string isbn() const {return bookNo;}
private:
	std::string bookNo;//书籍编号,隐式初始化为空串
	unsigned units_sold = 0;//销售量,显示初始化为0
	double sellingprice= 0.0;//原始价格,显示初始化为0.0
	double saleprice =0.0;//实售价格,显示初始化为0.0
	double discount =0.0;//折扣,显示初始化为0.0 
	
}; 

inline bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{ return lhs.isbn() == rhs.isbn();}

Sales_data operator + (const Sales_data&,const Sales_data&);

inline bool operator == (const Sales_data &lhs,const Sales_data &rhs)
{
	return lhs.units_sold == rhs.units_sold && 
		lhs.sellingprice== rhs.sellingprice && 
		lhs.saleprice ==rhs.saleprice &&
		lhs.isbn() == rhs.isbn();
}

inline bool operator !=(const Sales_data &lhs, const Sales_data &rhs)
{
	return !(lhs == rhs);//基于运算符== 给出!=的定义 
}

Sales_data& Sales_data::operator +=(const Sales_data& rhs)
{
	units_sold += rhs.units_sold;
	saleprice=(rhs.saleprice * rhs.units_sold+saleprice*units_sold)
	/(rhs.units_sold+units_sold);
	if(sellingprice != 0)
		discount = saleprice/ sellingprice;
	return *this;
} 

Sales_data operator +(const Sales_data& lhs,const Sales_data& rhs)
{
	Sales_data ret(lhs);//把lns的内容拷贝到临时变量ret中,这种做法便于运算
	ret+=rhs;//把rhs的内容加入其中
	return ret;//返回ret 
}


std::istream& operator>>(std::istream& in,Sales_data& s)
{
	in>> s.bookNo >> s.units_sold>>s.sellingprice>>s.saleprice>>s.saleprice;
	if(in&& s.sellingprice!=0)
		s.discount =s.saleprice/s.sellingprice;
	else
		s=Sales_data();//输入错误,重置输入的数据
	return in; 
}  

std::ostream& operator <<(std::ostream& out, const Sales_data& s)
{
	out <<s.isbn()<<" "<<s.units_sold<< " "
		<<s.sellingprice<<" "<<s.saleprice<< " " <<s.discount;
	return out;  
}

#endif //SALES_DATA_H_INCLUDED

//main.cpp源文件内容是:
#include <iostream>
#include "Sales_data.h"

int main()
{
	Sales_data book;
	std::cout<<"请输入销售记录:"<<std::endl;
	while (std::cin>>book)
	{
		std::cout<<"ISBN、售出本数、原始价格、实售价格、折扣为"<<book<<std::endl; 
	} 
	Sales_data trans1,trans2;
	std::cout<<"请输入两条ISBN相同的销售记录:"<<std::endl;
	std::cin>>trans1>>trans2;
	
	if(compareIsbn(trans1,trans2))
		std::cout<<"汇总信息:ISBN、售出本数、原始价格、实售价格、折扣为 "
		<<trans1+trans2<<std::endl;
	else
		std::cout<<"两条销售记录的ISBN不同"<<std::endl;
		
	Sales_data total,trans;
	std::cout<<"请输入几条ISBN相同的销售记录:"<<std::endl;
	if(std::cin>>total)
	{
		while(std::cin>>trans)
			if(compareIsbn(total,trans))//isbn不同
				total=total+trans;
			else
			{
				std::cout<<"当前书籍ISBN不同"<<std::endl;
				break; 
			}
		std::cout<<"有效汇总信息:isbn、售出本数、原始价格、实售价格、折扣为"
		<<total<<std::endl; 
	} 
	else
	{
		std::cout<<"没有数据"<<std::endl;
		return -1; 
	}
	
	int num=1;
	std::cout<<"请输入若干销售记录:"<<std::endl;
	if(std::cin>>trans1)
	{
		while(std::cin>>trans2)
		{
			if(compareIsbn(trans1,trans2))//ISBN相同
				num++; 
			else{
				std::cout<<trans1.isbn()<<"共有"<<num<<"条销售记录"<<std::endl;
				trans1=trans2;
				num=1; 
			}
		}
		std::cout<<trans1.isbn()<<"共有"<<num<<"条销售记录"<<std::endl; 
	} 
	else
	{
		std::cout<<"没有数据"<<std::endl;
		return -1;	
	}
	return 0;
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值