《C++ Primer》Learning Note
程序实例下载地址:http://www.informit.com/title/0321714113
第一章 开始
本章介绍C++的大部分基础内容:类型、变量、表达式、语句及函数。在这个过程中,我们会简要介绍如何编译及运行程序,以期指导读者具备编写、编译及运行简单程序的能力。
1.关于main():
每个C++程序包含一个或多个函数,其中一个必须命名为main,操作系统通过调用main来运行C++程序。main函数的返回类型必须为int,即整数类型,int类型是一种内置类型(built-in type),即语言自身定义的类型。
return 0;
给操作系统返回一个值,int类型。在大多数系统中,main的返回值用来指示状态。返回值0表明成功,非0的返回值的含义由系统定义,通常用来指出错误类型。
2.关于ostream:
std::cout << ”Enter two numbers : ” << std::endl;
<<运算符接受两个运算对象,左侧的运算对象必须是一个ostream对象,右侧的运算对象是要打印的值。此运算符将给定的值写到给定的ostream对象中。输出运算符的计算结果就是其左侧运算对象,也就是我们写入给定值的ostream对象。(ostream为输出流,一个流就是一个字符序列,是从IO设备读出IO设备的。“stream”想要表达的是,随着时间的推移,字符是顺序生成或消耗的)
这条输出语句使用了两次<<运算符。因此运算符将返回其左侧的运算对象,因此第一个运算符的结果成了第二个运算符的左侧运算对象。
第一个输出运算符给用户打印了一条消息。这串消息是一个字符串字面值常量(string literal),是用一对双引号包围的字符序列,在双引号之间的文本被打印到标准输出。
第二个运算符打印endal,这是一个被称为操作符(manipulator)的特殊值。写入endl的结果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅仅停留在内存中等待写入流。
我们在调试时经常添加打印语句,这类语句应该保证“一直”刷新流。否则,如果程序崩溃,输出可能还留在缓冲区中,从而导致关于程序崩溃位置的错误推断。
std::指出cout和endl是定义在名为std的命名空间(namespace)中的。命名空间可以帮助我们避免不经意的名称定义冲突,以及使用库中相同名字导致的冲突,标准库定义的所有名字都在命名空间std中。通过命名空间使用标准库有一个副作用:那就是每一次使用标准库名字时,都需要显式说明我们想使用来自命名空间std中的名字。比如使用作用域运算符(::)指出我们想使用定义在命名空间std中的名字cout。
3.关于istream:
std::cin >> v1>> v2;
输入运算符(<<)和输出运算符类似,它接受一个istream作为其左侧运算对象,接受一个对象作为其右侧运算对象。它从给定的istream中读入数据,并存入给定的对象中。与输出运算符类似,输入运算符返回其左侧运算对象作为计算结果。
4.关于文件结束符问题:
参见:https://blog.csdn.net/huanbia/article/details/50543163
参见:https://bbs.csdn.net/topics/390265495
c++文件结束标志是EOF (the end of file),EOF是宏常量,定义在stdio.h中。
若干标准函数的返回值等于EOF , 表示文件结束,表示文件输入流数据读完了。EOF可被理解为是一个整型常数表达式,
一般编译器定义为-1,或等于十六进制0xffffffff。
视窗系统可用Ctrl+Z组合键产生EOF。unix / Linux用Ctrl+D组合键产生。
while(cin>>word)
一般不要这么写。要那样写的话,在windows上用CTRL+X,在Linux用CTRL+D可结束输入。
通常while(cin>>word)可改写为:
while(true)
{
cin >> word;
if(word == 退出的条件) break;
}
第二章 变量和基本类型
本章主要讲述内置类型(如字符、整型、浮点数等)。
1.字符和字符串字面值:
由单引号括起来的一个字符称为一个char型字面值,双引号括起来的零个或多个字符称为字符串型字面值。字符串型字面值实际上是由常量字符构成的数组,编译器在每个字符串结尾处添加一个空字符(' ’),因此字符串字面值的实际长度比它的内容多1。
2.关于对象:
通常情况下,对象是指一块能存储数据并具有某种类型的内存空间。一些人仅在与类有关的场景下才使用“对象”这个词。另一些人则把已命名的对象和未命名的对象区分开来,他们把命名了的对象叫做变量。还有一些人把对象和值区分开来,其中对象指能被程序修改的数据,值指只读的数据。本书认为对象是具有某种数据类型的内存空间,我们在使用对象这个词时,并不严格区分是类还是内置类型,也不区分是否命名或只读。
3.关于对象初始化:
初始化不是赋值,初始化的含义是创建变量时赋予它一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。建议初始化每一个内置类型的变量。
4.变量声明和定义的关系:
C++支持分离式编译(separate compilation)机制,该机制允许将程序分割成若干个文件,每个文件可以被独立编译。变量在文件之间得以共享的关键在于C++语言将声明和定义区别开来。声明(declaration)使得名字为程序所知,定义(definition)则负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,这一点上与定义相同。但除此之外,定义还申请了存储空间,也可能会为变量赋一个初始值。如果想声明一个变量而非定义它,就要在变量名前面添加关键字extern,且不要显式地初始化变量。
变量能且只能被定义一次,但是可以被多次声明。
5.关于静态类型:
C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。检查类型的过程被称为类型检查(type checking)。对象的类型决定了对象所能参与的运算,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错且不会生成可执行文件。
6.复合类型:
一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。每一个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。之前我们接触的声明语句中声明符就是变量名,变量各类型就是声明的基本数据类型,其实还可能有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。
引用(reference)给对象起了另外一个名字,引用类型引用另外一种类型,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名。引用必须被初始化。一般在初始化变量中,初始值会被拷贝到新建的对象中,然而在定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。定义一个引用之后,对其进行的操作都是在与之绑定的对象上进行的。引用类型的初始值必须是一个对象,且除特例外,引用类型要与对象类型严格匹配。
指针(pointer)是“指向”另外一种数据类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问,然而指针与引用有诸多不同:一、指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向不用的对象;二、指针无须再定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
定义指针类型的方法是将声明符写成*d的形式,其中d是变量名。指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&)。由于引用不是对象,没有实际地址,所以不能定义指向引用的指针。在这里取地址符明显和引用操作有所区别,如取地址符往往在等号右边,而引用初始化符号在等号左边。
如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象。建议初始化所有指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。
7.const对象在文件之间共享:
必须在变量定义之前添加extern关键字。
8.引用类型必须同引用对象类型一致的第一个特例(对const的引用):
P81。
9.指针类型必须与所指对象类型保持一致的第一个特例(pointer to const)
P82。
10.顶层const与底层const:
顶层const可以表示任意的对象是常量(指出对象的值是不变的),底层const则与指针和引用等复合类型的基本类型部分有关(指出引用或者指针本身是不变的,对常量的引用或者常量指针不能想修改该引用或该指针所绑定的对象)。
11.关于decltype和引用(P89):
decltypr(*p) c ;
由于通过p可以对i进行赋值,所以*p实际上是i的一种引用,*p是i的一个别名,而p是i的地址。
12:decltype中括号的影响:
decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。
第三章 字符串、向量和数组
本章将分别介绍数组以及标准库类型string和vector。
1.命名空间的using声明:
目前为止,我们用到的库函数基本上都属于命名空间std,using声明(using declaration)可以让我们无需前缀(形如命名空间::)也能使用所需的名字,using声明具有以下的格式:using namespace::name
另一种写法是:using namespace std;
这样命名空间std内定义的所有标识符都有效(曝光),就好像它们被声明为全局变量一样
2.标准库类型string:
标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件,作为标准库的一部分,string定义在命名空间std中。本节还介绍了string对象的初始化(直接初始化和拷贝初始化),string对象操作(读写与未知数量string对象读写,empty和size操作,大小比较),处理string对象中的字符(检索,大小写转换等)。
1.关于size()函数需要注意的一点:如果一条表达式中有size()函数就不要在使用int了,这样可以避免混用int和unsigned导致的问题,int整型的负值在与size()函数返回值判断大小的时候会自动转换成一个比较大的无符号值。
2.关于字面值与字符串相加:当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string(如果出现冲突可以使用()来调整优先级)。
注:字符串字面值与string对象不是一个类型。
3.建议使用C++版本的C标准库头文件:即在使用C语言标准库时,C++将形如name.h的头文件明明为cname,名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然。
4.使用范围for语句改变字符串中的字符:如果要改变string对象中字符的值,必须把循环变量定义成引用类型。当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上,从而改变元素对应的字符。
5.访问string对象单个字符的方式:一种是适用下标,另外一种是使用迭代器。下标运算符([])接收的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置,返回值是该位置上字符的引用。下标的值称作“下标”或“索引”,任何表达式只要它的值是一个整型值就能作为索引,不过,如果某个索引是带符号类型的值将自动转换成string::size_type表达的无符号类型。
使用下标时必须确保其在合理范围内,也就是说,下标必须大于等于0而小于字符串的size()值,一种简单易行的方法是,总是设下标的类型为string::size_type,因为此类型是无符号数,可以确保下标不会小于0,这时只要保证下标小于size()的值就可以了。
3.标准库类型vector:
标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为vector“容纳着”其他对象,所以它也常被称作容器(container)。
C++中既有类模板,也有函数模板,其中vector是一个类模板。模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,我们通过提供一些额外的信息来制定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。以vector为例,提供的额外信息是vector内所存放对象的类型:
vector<int> ivec; //ivec保存int类型的对象
vector<Sales_item> Sales_vec; //保存Sales_item类型的对象
vector<vector<string>> file; //该向量的元素是vector对象
vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型。vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。除此之外,其他大多数(非引用)内置类型和类类型都可以构成vector对象,甚至组成vector的元素也可以是vector。
本节还介绍了vector对象的定义和初始化、元素增减(vector对象能高效增长,即我们可以高效便捷地向vector对象中添加元素)、vector操作(push_back等)、
如果vector定义中用的是圆括号,说明提供的值是用来构造vector对象的,先后说明了vector元素的数量和元素值;如果用的是花括号,说明我们想列表初始化(list initialize)该vector对象,也就是说,初始化过程会尽可能把花括号内的值当成是元素初始值的列表来处理,只有无法执行列表初始化时才会考虑其他初始化方式。
一旦初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象了。比如vector要求各元素的类型相同,如果使用语句vector<string> v8{10, “hi”};那么很明显此时不能使用列表初始化,确定无法执行列表初始化后,编译器会考虑用给定的值来构造vector对象,即用“hi”初始化这十个元素。
4.迭代器介绍:
类似于指针类型,迭代器(iterator)提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素。迭代器分为有效和无效,有效迭代器或者指向某个元素,或者指向容器中尾元素的下一位置,其他所有情况都属于无效。
本节主要介绍了迭代器的使用(运算符、迭代器在元素间的移动、迭代器类型、begin和end运算符、结合解引用和成员访问操作:->)、迭代器运算。
注:谨记!但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素,这样会使迭代器失效。
5.数组:
数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活性的权衡上又与vector有所不同,数组的大小确定不变,不能随意向数组中添加元素。在不确定元素的确切个数时,推荐使用vector。
本节主要介绍了定义和初始化内置数组、数组元素的访问、指针和数组、指针也可以作为迭代器(尾后指针是数组尾元素之后那个并不存在的元素的地址)、数组对应的标准库函数begin和end、数组指针运算、C风格字符串、与旧代码的接口、多维数组。
在C++语言中,指针和数组有非常紧密的联系,使用数组的时候编译器一般会把它转换为指针。对数组的元素使用取地址符就嫩能够得到指向该元素的指针。在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。所以我们使用数组作为一个auto变量的初始值时,推导得到的类型是指针而非数组。不过,当使用decltype关键字时上述转换不会发生,decltype(ia)返回的类型是数组。
尽管C++支持C风格字符串,在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。对于大多数应用来说,使用标准库string要比使用C风格字符串更安全,更高效。
注:严格来说,C++语言中没有多维数组,通常所说的多维数组实际上是数组的数组。因此对于多维数组来讲,由多维数组名转换得来的指针实际上是指向第一个内层数组的指针。
第四章 表达式
表达式由一个或多个运算对象(operand)组成,对表达式求值将得到一个结果。字符串和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式。
注:除非必须,否则不用递增递减运算符的后置版本,前置版本的递增运算符避免了不必要的工作,它把值增加1后直接返回改变了的运算对象,而后置版本需要将原始值存储下来以便返回这个为修改的内容,这种操作实际上是一种浪费。
第五章 语句
1.范围for语句:
C++11新标准中引入了一种更简单的for语句,可以遍历容器或者其他序列的所有元素,范围for语句(range for statement)的语法形式是:
for (declaration : expression) : statement
expression表示的是一个序列,比如用花括号括起来的初始值列表、数组或者vector或string等类型的对象,这些类型的共同特点是拥有能返回迭代器的begin或end成员。declaration定义一个变量,序列中的每个元素都得能转换成该变量的类型。确保类型相容最简单的办法是使用auto类型说明符,这个关键字可以令编译器帮助我们制定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
2.跳转语句:
跳转语句中断当前的执行过程。C++语言提供了4中跳转语句:break、continue、goto和return。本节介绍前三种跳转语句。
break语句(break statement)负责终止离它最近的while、d0 whie、for或switch语句,并从这些语句之后的第一条语句开始执行。break语句只能出现在迭代语句或者swich语句内部(包括嵌套在此类循环里的语句或者块的内部)。
continue语句(continue statement)终止最近的循环中的当前迭代并立即开始下一次迭代。continue语句只能出现在for、while和do while循环的内部,或者嵌套在此类循环里的语句或块的内部。和break语句类似的是,出现在嵌套循环中的continue语句也仅作用于离它最近的循环。和break语句不同的是,只有当switch语句嵌套在迭代语句的内部时,才能在switch里使用continue。
goto语句(goto statement)的作用是从goto语句无条件跳转到同一个函数内的另一条语句。goto语句的语法形式是
goto label ;
其中,label是用于标识一条语句的标示符。带标签语句(labeled statement)是一类特殊的语句,在它之前有一个标示符和一个冒号:
end : return ; //带标签语句,可以作为goto的目标。
标签标示符独立于变量或其他标示符的名字,因此,标签标示符可以和程序中其他实体标示符使用同一个名字而不会相互干扰。goto语句和控制权转向的那条带标签的语句必须位于同一函数之内。和switch语句类似,goto语句也不能将程序的控制权从变量的作用域之外转移到作用域之内。
3.try语句块和异常处理:
异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:
1.throw表达式(throw expression),异常检测部分使用throw表达式来表示它遇到了无法处理的问题。我们说throw引发(raise)了异常。
throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw表达式后面通常紧跟一个分号,从而构成一条表达式语句。
2.try语句块(try block),异常处理部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句(catch clause)结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句“处理”异常,所以它们也被称作异常处理代码(exception handler)。
try语句块的通用语法形式是:
try{
program-statements
}catch (exception-declaration){
handler-statements
}catch (exception-declaration){
handler-statements
}
try语句块一开始是关键字try,随后紧跟一个块,这个块就像大多数使用那样是花括号括起来的语句序列。跟在try块之后的是一个或多个catch子句。catch子句包括三部分:关键字catch、括号内一个(可能未命名的)对象的声明(称作异常声明,exception declaration)以及一个块。当选中某个catch子句处理异常之后,执行与之对应的块。catch一旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。
3.一套异常类(exception class),用于在throw表达式和相关的catch子句之间传递异常的具体信息。
第六章 函数
本章首先介绍函数的定义和声明,包括参数如何传入函数以及函数如何返回结果、重载函数的放大,以及编译器如何从函数的若干重载形式中选取一个与调用匹配的版本、以及函数指针的相关知识。
1.局部静态对象:
为使函数内部局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义成static类型获得局部静态对象(local static object)。局部静态对象在程序的执行路径第一次经过对象定义语句时被初始化,并且直到程序终止时才被销毁。
2.函数声明:
函数声明与函数的定义非常类似,唯一的区别就是函数声明无须函数体,用一个分号替代即可。函数声明了函数的三要素(返回类型、函数名、形参类型),描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型。函数应该在头文件中声明,在源文件中定义。
3.参数传递:
当形参是引用类型时,我们说它对应的实参被引用传递(passed by reference)或者函数被传引用调用(called by reference),引用形参是它对应的实参的别名。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用(called by value)。
4.返回类型和return语句:
return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。return语句有两种形式:
return;
return expression;
没有返回值的return语句只能用在返回类型是void的函数里。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。通常情况下,void函数要想在它的中间位置提前退出,可以使用return语句。有返回值的return语句提供了函数的结果。
我们允许main函数没有return语句直接结束,如果控制到达了main函数的结尾处而没有return语句,编译器将隐式地插入一条返回0的return语句。main函数非0返回值的具体含义由机器决定,为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量EXIT_FAILURE和EXIT_SUCCESS。
4.内联函数:
内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数。
5.constexpr函数:
constexpr函数是指能用于常量表达式的函数。定义constexpr函数时要注意:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体有且只有一条return语句。
6.函数指针:
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
第七章 类
在C++语言中,我们使用类定义自己的数据类型。通过定义新的类型来反映待解决问题中的各种概念,可以使我们更容易编写、调试和修改程序。本章是第2章关于类的话题的延续,主要关注数据抽象的重要性。数据抽象能帮助我们将对象的具体实现与对象所执行的操作分离开来。
类的基本思想是数据抽象(data abstraction)和封装(encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
类要想实现数据的抽象和封装,需要首先定义一个抽象数据类型(abstract data type)。在抽象类型中,由类的设计者负责考虑类的实现过程,使用该类的程序员则只需要抽象地思考类型做了什么,而无需了解类型的工作细节。
1. 内联函数:
(1)什么是内联函数?
内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。
(2)为什么要引入内联函数?
引入内联函数的主要目的是:解决程序中函数调用的效率问题。
函数调用原理
"编译过程的最终产品是可执行程序--由一组机器语言指令组成。运行程序时,操作系统将这些指令载入计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(如有循环和分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序跳到另一个地址(函数的地址),并在函数结束时返回。下面更详细地介绍这一过程的典型实现。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。"
内联函数
内联函数提供了另一种选择。编译器将使用相应的函数代码替换函数调用。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
2. size_t:
size_t在C语言中就有了。它是一种“整型”类型,里面保存的是一个整数,就像int, long那样。这种整数用来记录一个大小(size)。size_t的全称应该是size type,就是说“一种用来记录大小的数据类型”。