C++ Primer 笔记

 第1章 开始
1、C++程序
程序源文件的名字,一般包括两部分:文件名以及文件后缀。文件后缀一般用来标识文件的内容
头文件:一般以.h后缀结尾
程序文本文件:C++一般以.cpp后缀结尾
C++标准库中的名字都是在一个称作std 的名字空间中声明的,这些名字在我们的程序文本文件中是不可见的。除非我们显式地使它们可见using 指示符告诉编译器要使用在名字空间std 中声明的名字
2、预处理器指示符
预处理器指示符用“#”号标识,这个符号将放在程序中该行的最起始一列上。处理这些指示符的程序被称做预处理器,通常捆绑在编译器中
#include
#include 指示符读入指定文件的内容,它有两种格式
#include <some_file.h>
#include "my_file.h"
如果文件名用尖括号< 和> 括起来表明这个文件是一个工程或标准头文件,查找过程会检查预定义的目录。我们可以通过设置搜索路径环境变量或命令行选项来修改这些目录
如果文件名用一对引号括起来,则表明该文件是用户提供的头文件,查找该文件时将从当前文件目录开始
#ifndef
由于嵌套包含文件的原因,一个头文件可能会被多次包含在一个源文件中.条件指示符可防止这种头文件的重复处理
#ifndef BOOKSTORE_H
#define BOOKSTORE_H
/* Bookstore.h 的内容 */
#endif
条件指示符#ifndef 检查BOOKSTORE_H 在前面是否已经被定义,这里BOOKSTORE_H是一个预处理器常量,习惯上预处理器常量往往被写成大写字母。如果BOOKSTORE_H在前面没有被定义,则条件指示符的值为真。于是从#ifndef 到#endif 之间的所有语句都被包含进来进行处理,相反如果#ifndef 指示符的值为假则它与#endif 指示符之间的行将被忽略
为了保证头文件只被处理一次,把如下#define 指示符
#define BOOKSTORE_H
放在#ifndef 后面,这样在头文件的内容第一次被处理时,BOOKSTORE_H 将被定义,从而防止了在程序文本文件中以后#ifndef 指示符的值为真
#ifdef
#ifdef 指示符常被用来判断一个预处理器常量是否已被定义,以便有条件地包含程序代码
我们在编译程序时可以使用-D 选项,并且在后面写上预处理器常量的名字,这样就能在命令行中定义预处理器常量
$ CC -DDEBUG main.C
也可以在程序中用#define 指示符定义预处理器常量
3、注释
    注释是用来帮助程序员读程序的语言结构,它是一种程序礼仪,可以用来概括程序的算法、标识变量的意义、或者阐明一段比较难懂的程序代码。注释不会增加程序的可执行代码的长度。在代码生成以前,编译器会将注释从程序中剔除掉
C++中有两种注释符号:
一种是注释对/* */ ,与C 语言中的一样。注释的开始用/*标记,编译器会把/*与*/之间的代码当作注释。
第二种注释符是双斜线// ,它可用来注释一个单行,程序行中注释符右边的内容都将被当作注释而被编译器忽略
第2章 C++浏览
1、内置数组数据类型
C++为基本算术数据类型提供了内置的支持
另外,标准库还支持基本类抽象的组合
在内置数据类型与标准类库之间是复合类型,特别是指针和数组。
数组array 是一种顺序容器它包含单一类型的元素
2、动态内存分配和指针
    在C++中对象可以静态分配——即编译器在处理程序源代码时分配,也可以动态分配——即程序执行时,调用运行时刻库函数来分配。这两种内存分配方法的主要区别是效率与灵活性之间的平衡准则不同。出于静态内存分配是在程序执行之前进行的,因而效率比较高。但是它缺少灵活性,它要求在程序执行之前就知道所需内存的类型和数量。一般来说,存储未知数目的元素需要动态内存分配的灵活性
C++支持用指针类型来存放对象的内存地址值。C++预定义了一个专门的取地址操作符& ,当我们把它应用在一个对象上时,返回的是对象的地址值。为了访问指针所指向的实际对象,我们必须先用解引用操作符* 来解除指针的引用
在C++中指针的主要用处是管理和操纵动态分配的内存
静态与动态内存分配的两个主要区别是:
1.静态对象是有名字的变量,我们直接对其进行操作。而动态对象是没有名字的变量,我们通过指针间接地对它进行操作
2.静态对象的分配与释放由编译器自动处理,程序员需要理解这一点,但不需要做任何事情。相反动态对象的分配与释放,必须由程序员显式地管理,相对来说比较容易出错,它通过new 和delete 两个表达式来完成
delete表达式的两个版本:
删除单个对象 delete 指针;
删除对象数组 delete[] 指针;
如果忘了删除动态分配的内存,程序就会在结束时出现内存泄露问题。内存泄露是指一块动态分配的内存,我们不再拥有指向这块内存的指针,因此我们没有办法将它返还给程序供以后重新使用
3、基于对象的设计
    支持基于对象设计的类的一般形式如下
class classname {
public:
// 公有操作集合
private:
// 私有实现代码
};
类定义包括两个部分:类头,由关键字class 与相关联的类名构成。类体由花括号括起来,以分号结束。类头本身也用作类的声明。
通过使用两个成员访问操作符中的一个,我们可以调用一个有名字的成员函数。这两个操作符为用于类对象的点操作符. 以及用于类对象指针的箭头操作符->
在类定义中被定义的成员函数会被自动当作是内联函数,此外我们也可以用inline 关键字显式地要求一个函数被视为内联函数。
构造函数
程序设计中的一个常见错误是使用事先并没向被正确初始化的对象。实际上,这是一个极为常见的错误。所以C++为用户定义的类提供了一种自动初始化机制:类构造函数,构造函数是一种特殊的类成员函数,专门用于初始化对象。如果构造函数被定义了,那么在类的每个对象第一次被使用之前,这构造函数就被自动应用在对象上
函数重载
C++支持被称为函数重载的机制,函数重载允许两个或更多个函数使用同一个名字。限制条件是它们的参数表必须不同:参数类型不同或参数的数目不同。根据不同的参数表编译器就能够判断出对某个特定的调用应该选择哪一个版本的重载函数
重载函数在运行时刻的行为与非重载函数完全一样,主要的负担是在编译时刻用来决定中该调用哪个实例所需要的时间
 
在类体的外面定义类的成员函数,惟一的语法区别是要指出成员函数属于哪个类,这可以通过类域操作符来实现,类名::
 
析构函数
每个类对象在被程序最后一次使用之后,它的析构函数就会被自动调用。我们通过在类的名字前面加一个波浪线~, 来标识析构函数。一般地,析构函数会释放在类对象使用和构造过程中所获得的资源
 
类定义以及相关的常数值或 typedef名通常存储在头文件中,并且头文件以类名来命名。
不在类定义内部定义的类成员函数都存储在与类名同名的程序文本文件中。这些函数不用随每个使用相关类的程序而重新编译,这些成员函数通过预编译之后被保存在类库中
4、面向对象程序设计
子类型与基类共享公共的接口——公有操作的公共集。由于共享公共接口,允许了子类和基类在程序内部可互换使用,而无需考虑对象的实际类型。从某种意义上来说公共接口封装了单个子类型中与类型相关的细节,类之间的类型/子类型关系形成了继承或派生层次关系
虚拟函数
为了把一个类设计成基类,设计考虑是找出类型相关的成员函数,并把这些成员函数标记为virtual。对于类型相关的成员函数,它的算法由特定的基类或派生类的行为或实现来决定
对于一个非虚拟函数的调用,编译器在编译时刻选择被调用的函数,而非虚拟函数的决定则要等到运行时刻。在执行程序内部的每一个调用点上,系统根据被调用对象的实际基类或派生类的类型来决定选择哪一个虚拟函数实例
派生类对象的初始化过程
派生类对象实际上由几部分构成:每个基类是一个类的子对象,它在新定义的派生类中有独立的一部分。派生类对象的初始化过程是这样的,首先自动调用每个基类的构造函数来初始化相关的基类子对象,然后再执行派生类的构造函数。从设计的角度来看,派生类的构造函数应该只初始化那些在派生类中被定义的数据成员,而不是基类中的数据成员
因为基类的构造函数并没有被派生类继承(析构函数和拷贝赋值操作符同样也没有),我们需要派生类的构造函数作为向基类构造函数传递参数的接口
 
C++支持另外两种形式的继承:多继承,也译多重继承,也就是一个类可以从两个或多个基类派生而来。以及虚拟继承,在这种继承方式下,基类的单个实例在多个派生类之间共享
5、泛型设计(函数模板和类模板)
C++的模板设施提供了一种机制,它能够将类或函数定义内部的类型和值参数化,这些参数在其他方面不变的代码中用作占位符,以后,这些参数会被绑定到实际类型上,可能是内置的类型,也可能是用户定义的类型
关键字template 引入模板,模板参数由一对尖括号< > 括起来,类型标识符由关键字class标明,类型标识符作为实际类型的占位符
当我们为类模板定义对象时,编译器必须为相关的对象分配内存。为了做到这一点,形式模板参数被绑定到指定的实际参数类型上。不是所有类模板的成员函数都能自动地随类模板的实例化而被实例化,只有真正被程序使用到的成员函数才会被实例化。
模板机制也支持面向对象的程序设计,类模板可以作为基类或派生类
6、基于异常的设计
异常是指在运行时刻程序出现的反常情形,例如数组下标越界、打开文件失败以及动态可用内存耗尽
异常处理为“响应运行时刻的程序异常”提供了一个标准的语言级设施,它支持统一的语法和风格,也允许每个程序员进行微调
异常处理机制的主要构成:
1.程序中异常出现的点。一旦识别出程序异常,就会导致抛出(raise或throw)异常。异常被抛出时,正常的程序就被刮起,直到异常被处理完毕
2.程序中异常被处理的点。程序异常的抛出与处理位于独立的函数或成员函数调用中。找到处理代码通常要涉及到展开程序调用栈。一旦异常被处理完毕,就恢复正常的程序执行。但不是在发生异常的地方恢复执行过程,而是在处理异常的地方恢复执行过程
如果异常机制按照函数被调用的顺序回查每个函数直道main()函数,仍然没有找到处理代码,那么它将调用标准库函数terminate()。缺省情况下,terminate()函数结束程序
一种特殊的、能够处理全部异常的catch子句如下:
catch(...)
{
 //处理所有异常,虽然它无法访问异常对象
}
7、名字空间
名字空间机制允许我们封装名字,否则这些名字就有可能会污染全局名字空间。一般来说,只有当我们希望自己的代码被外部软件开发部门使用时,才使用名字空间
关键字namespace后面的名字标识了一个名字空间,它独立于全局名字空间,我们可以在里面放一些希望声明在函数或类体之外的实体
为了提供有意义的名字空间,同时程序员又能很方便地访问在名字空间内定义的实体,C++提供了别名设施。名字空间别名允许用一个可替代的、短的或更一般的名字与一个现有的名字空间关联起来
//提供一个更一般化的别名
namaspace LIB = IBM_Canada_Laboratory;
using指示符使名字空间内的所有声明都可见,这样这些声明能够不加限定地使用
using namespace IBM_Canada_Liboratory;
using声明提供了选择更为精细的名字可视性机制,它允许使名字空间中的单个声明可见
using IBM_Canada_Liboratory::Matrix;
为了防止标准C++库的组件污染用户程序的全局名字空间,所有标准C++库的组件都被声明在一个被称为std的名字空间内
8、标准数组——向量
在标准C++中,数组类是C++标准库的一部分,现在它不叫数组,而叫向量了
向量是一个类模板
#include <vector>
vector<int> vec0;//空的vector
vector<long> vec1(10);
标准vector类模板支持使用下标操作符,另一种遍历方法是使用迭代器对来标记向量的开始处和结束处。迭代器是一个支持指针类型对象的类对象。Vector类模板提供了一对操作begin()和end(),它们分别返回指向“向量开始处”和“结束处后一个”的迭代器,这一对迭代器合起来可以标记出待遍历元素的范围
vector<int>::iterator iter=vec.begin();
iter++;
*iter=3;
iterator是vector类模板中用typedef定义的类型
能够应用到向量上的操作惊人地多,但是它们并不是作为vevtor类模板的成员函数提供的。它们是以一个独立的泛型算法集的形式,由标准库提供。泛型算法接受一对迭代器,它们标记了要遍历的元素的范围,泛型算法还能接受指向内置数组的指针对
 
标准库还提供了对map关联数组的支持,即数组元素可以被整数之外的其他东西索引
第3章 C++数据类型
我们编写的程序以及所保存的程序数据在计算机的内存中是以二进制位序列的方式存放的。位是含有0或1值的一个单元,在物理上它的值是个负或正电荷
通过对内存进行组织,我们可以引用特定的位集合,用地址引用内存
类型抽象使我们能够对一个定长的位序列进行有意义的解释
1、文字常量
C++预定义了一组数值数据类型(内置数据类型),可以用来表示整数、浮点数和单个字符。此外,还定义了用来表示字符串的字符数组
当一个数值,出现在程序中时,它被称为文字常量:称之为“文字”是因为我们只能以它的值的形式指代它,称之为“常量”是因为它的值不能被改变。每个文字都有相应的类型。文字常量是不可寻址的,尽管它的值也存储在机器内存的某个地方,但是我们没有办法访问它的地址
整数文字常量可以被写成十进制、八进制或者十六进制的形式(这不会改变该整数值得位序列)
在缺省情况下,整形文字常量被当作是一个int型的有符号值。我们可以在文字常量后面加一个“L”将其指定为long类型。类似地,我们可以在整数文字常量的后面加上“U”,将其指定为一个无符号数。此外,我们还可以指定无符号long型的文字常量
浮点型文字常量可以被写成科学计数法或普通的十进制形式。浮点型文字常量在缺省情况下被认为是double型,单精度文字常量由值后面的“F”来标示。类似地,扩展精度中值后面跟的“L”表示
单词ture和false是bool型的文字常量
可打印的文字字符常量可以写成用单引号括起来的形式,一部分不可打印的字符、单引号、双引号以及反斜杠用转义序列来表示
字符串文字常量由零个或多个用双引号括起来的字符组成。不可打印字符可以由相应的转义序列来表示。字符串文字常量的类型是常量字符数组。它由字符串文字本身以及编译器加上的表示结束的空字符构成。空字符是C和C++用来标记字符串结束的符号
2、变量
    变量为我们提供了一个有名字的内存存储区,可以通过程序对其进行读、写和处理。C++中的每个符号变量都与一个特定的数据类型相关联,这个类型决定了相关内存的大小、布局、能够存储在该内存区的值的范围以及可以应用其上的操作集
变量和文字常量都有存储区,并且有相关的类型。区别在于变量是可寻址的,对于每一个变量,都有两个值与其相关联:
1.它的数据值,存储在某个内存地址中
2.它的地址值——即,存储数据值的那块内存的地址
变量的定义会引起相关内存的分配,因为一个对象只能有一个位置,所以程序中的每个对象只能被定义一次
在C++中,程序在使用对象之前必须先知道该对象,这对“编译器保证对象在使用时的类型正确性”是必须的。引用一个未知对象将会引起编译错误
对象声明的作用是使程序知道该对象的类型和名字。它由关键字extern以及跟在后面的对象类型以及对象的名字构成。声明不是定义,不会引起内存的分配。实际上,它只是说明了在程序之外的某处有这个变量的定义
变量的定义
一个简单的对象定义由一个类型指示符后面跟一个名字构成,以分号结束
一个简单的定义指定了变量的类型和标识符,它并不提供初始值。如果一个变量是在全局域内定义的,那么系统会保证给它提供初始值0。如果变量是在局部域内定义的,或是通过new表达式动态分配的,则系统不会向它提供初始值0。这些对象被称为是未初始化的。未初始化的对象不是没有值,而是它的值是未定义的
初始的第一个值可以在对象的定义中指定,一个被声明了初始值的对象也被称为已经初始化的。C++支持两种形式的初始化,第一种形式是使用赋值操作符的显示语法形式,在隐式形式中,初始值被放在括号中
3、指针类型
指针持有另一个对象的地址,是我们能够间接地操作这个对象
每个指针都有一个相关的类型,不同数据类型的指针之间的区别不是在指针的表示上,也不是在指针所持有的值上——对所有类型的指针这两方面都是相同的。不同之处在于指针所指向的对象的类型上。指针的类型可以指示编译器怎样解释特定地址上内存的内容,以及该内存区域应该跨越多少内存单元
我们通过在标识符前加一个解引用操作符(*)来定义指针。在逗号分割的标识符列表中,每个将被用作指针的标识符前都必须加上解引用操作符
不允许不同指针类型之间相互赋值,因为不同类型的指针对内存的存储布局和内容的解释完全不同。C++提供了一种特殊的指针类型:(void *)类型指针,它可以被任何数据指针类型的地址值赋值。void *v表示相关的值是个地址,但该地址的对象类型不知道。我们不能够操作空类型指针所指的对象,只能传送该地址值或将它与其他地址值进行比较
C++提供了解引用操作符(*)来间接地读和写指针所指向的对象
指针可以让它的地址值增加或减少一个整数值,这类指针操作,被称为指针的算术运算
4、字符串类型
C++提供了两种字符串的表示:C风格的字符串和C++引入的String类类型
C风格字符串
C风格字符串起源于C语言,并在C++中继续得到支持
字符串被存储在一个字符数组中,一般通过一个char*类型的指针操纵它。标准C库为操纵C风格字符串提供了一组函数。(标准C库作为标准C++的一部分被包含在其中)
指向C风格字符串的字符指针总是指向一个相关联的字符数组
C风格字符串的长度可以为0,有两种方式:字符指针被置为0,因而它不指向任何对象。或者,指针已经被设置,但是它指向的数组只包含一个空字符
字符串类型
标准C++提供了支持字符串操作的string类
String类型能够自动将C风格的字符串转换成string对象,(重载赋值操作符=)但是,反向的转换不能自动执行,为实现这种转换,必须显式地调用c_str()操作
5、const限定修识符
const类型限定修识符把一个对象转换成一个常量。常量定义后就不能被修改,所以它必须被初始化,未初始化的常量定义将导致编译错误
指向const对象的指针
试图将一个非const对象的指针指向一个常量对象的动作都将引起编译错误。我们必须声明一个指向常量的指针来指向一个常量对象,如:const int *cptr;
const对象的地址只能赋值给指向const对象的指针,但是,指向const对象的指针可以被赋以一个非const对象的地址
在实际的程序中,指向const的指针常被用作函数的形式参数,它作为一个约定来保证:被传递给函数的实际对象在函数中不会被修改
const指针
指向非const对象的const指针:意味着不能赋给这种指针其他的地址值,但可以修改其指向的对象
int *const curErr = &errNumb;
指向const对象的const指针:这种指针指向的对象的值以及它的地址本身都不能被改变
6、引用类型
引用有时候又被称为别名,它可以用作对象的另一个名字。通过引用我们可以间接地操纵对象,使用方式类似于指针。但是不需要指针的语法。在实际的程序中,引用主要被用作函数的形式参数,通常将类对象传递给一个函数
引用类型由类型标识符和一个取地址操作符来定义,引用必须被初始化。一旦引用已经定义,它就不能再指向其他的对象。引用的所有操作实际上都被应用在它所指的对象身上,包括取地址操作符。
const引用
    const int &引用变量名=初始化值;
const引用可以用不同类型的对象初始化(只要能从一种类型转换到另一种类型即可),也可以是不可寻址的值,如文字常量,const引用是只读的。
引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,引用实际上指向该对象,但用户不能访问它。
指针和引用有两个主要区别:引用必须总是指向一个对象。如果用一个引用给另一个引用赋值,那么改变的是被引用的对象而不是引用本身
7、布尔类型
布尔型对象可以被赋以文字值true和false
当表达式需要一个算术值时,布尔对象和布尔文字都被隐式地提升成int:false变成0,而true变成1。如果有必要,算术值和指针值也能隐式地被转换成布尔类型的值。0或空指针被转换成false,所有其他的值都被转换成true
8、枚举类型
枚举定义了整数常量,而且还把它们组成一个集合
enum open_modes {input=1,output,append};
open_modes是一个枚举类型。每个被命名的枚举定义了一个唯一的类型,它可以被用作类型标识符。Input是枚举成员,它们代表了能用来初始化和赋值open_modes类型变量的值的全集。如果我们给枚举变量传递枚举成员之外的值,编译器就会报错。而且,给枚举变量传递一个与枚举成员相等的整数值,编译器仍然会将其标记为错误
我们不能做到的是打印枚举成员的实际枚举名。第二件不能做的事情是,我们不能使用枚举成员进行迭代,C++不支持在枚举成员之间的前后移动
枚举类型用关键字enum,加上一个自选的枚举类型名来定义,类型名后面跟一个用花括号括起来的枚举成员列表,枚举成员之间用逗号分开。缺省情况下,第一个枚举成员被赋以值0,后面的每个枚举成员依次比前面的大1。我们也可以显式地把一个值赋给一个枚举成员
我们可以定义枚举类型的对象,它可以参与表达式运算,也可以作为参数传递给函数。枚举类型的对象能够被初始化,但是只能被一个相同枚举类型的对象或枚举成员集中的某个值初始化或赋值。必要时,枚举类型会自动提升成算数类型
9、数组类型
数组是一个单一数据类型对象的集合。其中单个对象并没有被命名,但是我们可以通过它在数组中的位置对它进行访问。这种访问形式被称为索引访问
数组定义由类型名标识符和维数组成,维数指定数组中包含的元素的数目,它被写在一对方括号里边。我们必须为数组指定一个大于等于1 的维数。维数值必须是常量表达式——即必须能在编译时刻计算出它的值,这意味着非const 的变量不能被用来指定数组的维数。
C++没有提供编译时刻或运行时刻对数组下标的范围检查。除了程序员自己注意细节,并彻底地测试自己的程序之外,没有别的办法可防止数组越界
数组与指针类型的关系
数组标识符代表数组中第一个元素的地址它的类型是数组元素类型的指针
10、vector容器类型
vector类为内置数组提供了一种替代表示,与string类一样,vector类是随标准C++引入的标准库的一部分
使用vector 有两种不同的形式,即所谓的数组习惯和STL习惯。
在数组习惯用法中,我们模仿内置数组的用法:定义一个已知长度的vector:
vector<int>ivec(10);
vector 的元素被初始化为与其类型相关的缺省值,算术和指针类型的缺省值是0,对于class 类型缺省值可通过调用这类的缺省构造函数获得。
与内置数组不同,vector可以被另一个vector初始化,或被赋给另一个vector
在STL中,对vector的习惯用法完全不同。我们不是定义一个已知大小的vector,而是定义一个空vector:
vector< string > text;
我们向vector 中插入元素,而不再是索引元素,以及向元素赋值。例如,push_back()操作,就是在vector的后面插入一个元素
11、typedef名字
typedef 机制为我们提供了一种通用的类型定义设施,可以用来为内置的或用户定义的数据类型引入助记符号
typedef定义以关键字typedef开始,后面是数据类型和标识符。这里的标识符即typedef名字,它并没有引入一种新的类型,而只是为现有类型引入了一个助记符号。
12、类类型
类的成员函数可以被定义在类的定义中,也可以定义在外面。我们应该把成员函数的定义放在一个程序文本文件中,并且把含有该类定义的头文件包含进来
内联函数在每个调用点上被展开,因此,这样做可以消除函数调用相关的额外消耗。只要该函数被调用足够多次,内联函数就能够显著地提高性能。在类定义内部定义的成员函数在缺省情况下被设置为inline,在类外而定义的成员函数必须显式地声明为inline。在类体外定义的内联成员函数,应该被包含在含有该类定义的头文件中
第4章 表达式
1、表达式
表达式由一个或多个操作数(operand)构成。最简单的表达式由一个文字常量或一个对象构成,一般地表达式的结果是操作数的右值
在更一般的情况下表达式由一个或多个操作数以及应用在这些操作数上的操作构成
应用在操作数上的操作由操作符(operator)表示
作用在一个操作数上的操作符被称为一元操作符,比如取地址操作符(&)和解引用操作符(*)。作用在两个操作数上的操作符,比如加法操作符减法操作符被称为二元操作符。有些操作符既能表示一元操作也能表示二元操作确切地说,是相同的符号用来表示两个不同的操作
表达式的计算是指执行一个或多个操作,最后产生一个结果除非特别声明一般来说,表达式的结果是个右值。算术表达式结果的类型由操作数的类型来决定。当存在多种数据类型时,编译器将根据一套预定义的类型转换规则集进行类型转换
当两个或两个以上的操作符被组合起来的时候这样的表达式被称为复合表达式,是否能够成功地计算表达式要取决于子表达式的计算顺序。一般来说子表达式的计算顺序由操作符的优先级和结合性来决定
2、算术操作符
在某些实例中,算术表达式的计算会导致不正确或未定义的值,这些情况被称为算术异常,但是不会导致抛出实际的异常。算术异常要归咎于算术的自然本质(比如除以0)或归咎于计算机的自然本质——比如溢出(指结果值超出了被赋值对象的类型长度)。
static_cast<int> ( byte_value )
强制转换使编译器把一个对象或表达式从它当前的类型转换成程序员指定的类型
3、等于、关系和逻辑操作
等于、关系和逻辑操作符的计算结果是布尔常量true 或false。如果这些操作符用在要求整数值的上下文环境中,它们的结果将被提升成1或0
当逻辑与(&&)操作符的两个操作数都为true 时,结果值才会是true。对于逻辑或(||) 操作符只要两个操作数之一为true,它的值就为true。这些操作数被保证按从左至右的顺序计算。只要能够得到表达式的值(true 或false)运算就会结束。
二元关系操作符左右操作数的计算顺序,在标准C 和C++中都是未定义的,因此计算过程必须是与顺序无关的
4、赋值操作符
赋值和初始化有时候会被混淆,因为它们都使用同一个操作符(=)一个对象只能被初始化一次,也就是在它被定义的时候,但是在程序中可以被赋值多次。
当我们把不同类型的表达式赋值给一个对象时,编译器会试着隐式地将右操作数的类型转换成被赋值对象的类型,如果这种类型转换是有可能的。如果不可能进行隐式的类型转换,那么赋值操作被标记为编译时刻错误。
赋值操作符的左操作数必须是左值——即它必须有一个相关联的可写的地址值,赋值的结果是实际上被放在左操作数相关内存中的值
赋值操作符也可以被连接在一起,只要每个被赋值的操作数都是相同的数据类型
C++提供了一套复合赋值操作符,复合赋值操作符的一般语法格式是
a op= b;
这里的op=可以是下列十个操作符之一
+=,-=, *=, /=, %=
<<= ,>>=, &= ,^= ,|=
5、复数操作
标准库提供的复数complex 类是基于对象的类抽象的完美模型。通过使用操作符重载,我们几乎可以像使用简单内置类型一样容易地使用复数类型的对象
6、条件操作符
条件操作符为简单的if-else 语句提供了一种便利的替代表示法,条件操作符的语法格式如下:
expr1 ? expr2 : expr3;
expr1的计算结果不是true 就是false。如果它是true,则expr2 被计算,否则expr3 被计算
7、sizeof操作符
    siseof 操作符的作用是返回一个对象或类型名的字节长度。它有以下三种形式:
sizeof (type name );
sizeof ( object );
sizeof object;
返回值的类型是size_t 这是一种与机器相关的typedef 定义我们可以在cstddef 头文件中找到它的定义。
当sizeof 操作符应用在数组上时例如上面例子中的ia,它返回整个数组的字节长度,而不是第一个元素的长度,也不是ia包含的元素的个数
应用在指针类型上的sizeof 操作符返回的是包含该类型地址所需的内存长度,但是应用在引用类型上的sizeof 操作符返回的是包含被引用对象所需的内存长度
sizeof 操作符在编译时刻计算,因此被看作是常量表达式。它可以用在任何需要常量表达式的地方。如数组的维数或模板的非类型参数
8、new和delete表达式
系统为每个程序都提供了一个在程序执行时可用的内存池。这个可用内存池被称为程序的空闲存储区或堆。运行时刻的内存分配被称为动态内存分配。动态内存分配由new 表达式应用在一个类型指示符上来完成,类型指示符可以是内置类型或用户定义类型。new 表达式返回指向新分配的对象的指针
例如:
int *pi = new int;
从空闲存储区中分配了一个int型的对象,并用它的地址初始化pi。在空闲存储区内实际分配的对象并没有被初始化。我们可以如下指定一个初始值:
int *pi = new int( 1024 );
它不但分配了这个对象而且用1024 将其初始化。
要动态分配一个对象数组我们可以写成:
int *pia = new int[ 10 ];
它从空闲存储区中分配了一个数组其中含有10 个int 型对象并用它的地址初始化pin,而数组的元素没有被初始化。没有语法能为动态分配的数组的元素指定一个显式的初始值集合(在类对象数组的情况下如果我们定义了缺省构造函数,那么它将被顺次应用在数组的每一个元素上)
所有从空闲存储区分配的对象都是未命名的,这是它的另一个特点。new表达式并不返回实际被分配的对象,而是返回这个对象的地址。对象的所有操作都通过这个地址间接来完成
当对象完成了使命时,我们必须显式地把对象的内存返还给空闲存储区。我们通过把delete 表达式应用在指向我们用new表达式分配的对象指针上来做到这一点,delete 表达式不应该被应用在不是通过new 表达式分配的指针上。例如:
delete pi;
释放了pi 指向的int 对象,将其返还给空闲存储区类似地
delete ps;
在ps 指向的string 类对象上应用string 的析构函数后,释放其存储区并将其返还给空闲存储区。最后:
delete [] pia;
释放了pia 指向的10 个int 对象的数组,并把相关的内存区返还给空闲存储区。在关键字delete 与指针之间的空方括号表示delete的一种特殊语法,它释放由new 表达式分配的数组的存储区
9、逗号操作符
    逗号表达式是一系列由逗号分开的表达式。这些表达式从左向右计算。逗号表达式的结
果是最右边表达式的值
10、位操作符
位操作符:~,<<,>>,&,^,|,&=,^=,|=   
位操作符把操作数解释成有序的位集合,这些位可能是独立的,也可能组成域。每个位可以含有0或1。位操作符允许程序员设置或测试独立的位或位域。如果一个对象被用作一组位或位域的离散集合。那么这样的对象称为位向量。位向量是一种用来记录一组项目或条件的是/否信息的紧缩方法
例如,在编译器中,类型声明的限定修饰符,如const 和volatile,有时就被存储在位向量中。iostream 库用位向量表示格式状态,例如输出的整数是以十进制、十六进制,还是八进制显示
在C++中,有两种方式支持位向量。在C语言和标准C++之前,它用内置整值类型来表示位向量,典型的情况是用unsigned int。标准库提供了一个bitset类,它支持位向量的类抽象。
按位非操作符(~) 翻转操作数的每一位,每个1被设置为0,而每个0被设置为1
移位操作符(<<,>>) 将其左边操作数的位向左或右移动某些位。操作数中移到外面的位被丢弃。左移操作符(<<)从右边开始用0补空位。如果操作数是无符号数,则右移操作符(>>)从左边开始插入0,否则的话,它或者插入符号位的拷贝,或者插入0,这由具体实现定义
按位与操作符(&) 需要两个整值操作数。在每个位所在处如果两个操作数都含有1,则结果该位为1。否则为0
按位异或操作符(^)需要两个整值操作数。在每个位所在处如果两个操作数只有一个含有1,则结果该位为1,否则为0
按位或操作符(|)需要两个整值操作数。在每个位所在处,如果两个操作数有一个或者两个含有1,则结果该位为1,否则为0。
11、bitset 操作
要使用bitset类,我们必须包含相关的头文件:
#include <bitset>
bitset有三种声明方式。在缺省定义中我们只需简单地指明位向量的长度。例如:
bitset<32> bitvec;
还有两种方法可以构造bitset对象,它们都提供了将某位初始化为1的方式。一种方法是,为构造函数显式地提供一个无符号参数。bitset对象的前N位被初始化为参数的相应位值。例如:
bitset< 32 > bitvec2( 0xffff );
将bitvec2 的低16位设为1
我们还可以传递一个代表0 和1 的集合的字符串参数来构造bitset对象,如下所示:
// 与bitvec3 的初始化等价
string bitval( "1010" );
bitset< 32 > bitvec4( bitval );
bitvec4和bitvec3的第1和3位都被设置为1,而其他位保持为0
我们还可以标记用来初始化bitset的字符串的范围。例如,在下面的语句中:
// 从位置6 开始, 长度为4: 1010
string bitval( "1111110101100011010101" );
bitset< 32 > bitvec5( bitval, 6, 4 );
bitvec5 的第1和第3位被初始化为1,其他位为0
bitset 类支持两个成员函数,它们能将bitset对象转换成其他类型。一种情况是用to_string()操作,将任意bitset 对象转换成string 表示:
string bitval( bitvec3.to_string() );
另一种情况是用to_ulong()操作,将任意bitset 对象转换成unsigned long 型的整数表示,只要该bitset 对象的底层表示能用一个unsigned long 来表示。在需要把bitset 对象传递给C或标准C++之前的程序时这尤其有用
12、优先级
操作符优先级是指复合表达式中操作符计算的顺序
用括号把一些子表达式括起来,可以改变优先级。在复合表达式计算中,第一个动作是计算所有括号中的子表达式,再用计算的结果代替每个子表达式,然后继续计算。里边的括号比外面的括号先计算
操作符具有优先级和结合性
13、类型转换
C++并不是把两个不同类型的值加在一起,而是提供了一组算术转换,以便在执行算术运算前,将两个操作数转换成共同的类型。转换规则是,小类型总是被提升成大类型,以防止精度损失。这些转换由编译器自动完成,无需程序员介入。因此它们也被称为隐式类型转换
如果赋位操作符的左右两边的类型不同,那么,有可能的话,右边操作数会被转换成左边的类型。
如果原意的话,我们可以通过指定显式类型转换,来禁止标准算术转换
ival = static_cast< int >( 3.541 ) + 3;
在本例中,我们显式地指示编译器将double 型的值转换成int 型,而不是遵循标准C++算术转换
14.1、隐式类型转换
    C++定义了一组内置类型对象之间的标准转换,在必要时它们被编译器隐式地应用到对象上。隐式类型转换发生在下列这些典型的情况下:
(1)在混合类型的算术表达式中,在这种情况下,最宽的数据类型成为目标转换类型。这也被称为算术转换
(2)用一种类型的表达式赋值给另一种类型的对象。在这种情况下,目标转换类型是被赋值对象的类型
(3)把一个表达式传递给一个函数调用,表达式的类型与形式参数的类型不相同。在这种情况下,目标转换类型是形式参数的类型
(4)从一个函数返回一个表达式,表达式的类型与返回类型不相同。在这种情况下目标转换类型是函数的返回类型
14.2、算术转换
    算术转换保证了二元操作符的两个操作数被提升为共同的类型,然后再用它表示结果的类型。两个通用的指导原则如下:
(1)为防止精度损失,如果必要的话,类型总是被提升为较宽的类型
(2)所有含有小于整型的有序类型的算术表达式,在计算之前其类型都会被转换成整型
在确定共同的目标提升类型之前,编译器将在所有小于int的整值类型上施加一个被称为整值提升的过程。在进行整值提升时,类型char,signed char,unsigned char和short int都被提升为类型int
14.3、显式转换
    显式转换也被称为强制类型转换,包括下列命名的强制类型转换操作符:
static_cast、dynamic_cast、const_cast和reinterpret_cast。虽然有时候确实需要强制类型转换,但是它们也是程序错误的源泉。通过使用它们,程序员关闭了C++语言的类型检查设施
任何非const数据类型的指针都可以被赋值给void*型的指针。void*型指针被用于“对象的确切类型未知”或者在“特定环境下对象的类型会发生变化”的情况下。有时void*型的指针被称为泛型指针,因为它可以指向任意数据类型的指针
但是,void*型指针不能直接被解除引用,因为没有类型信息可用来指导编译器怎样解释底层的位模式。相反,void*的指针必须先被转换成某种特定类型的指针。但是在C++中,不存在从void*型指针到特殊类型的指针之间的自动转换。
何时需要显式类型转换:
(1)把void*型的指针赋值给任意显式类型时C++要求显式强制转换的原因
(2)执行显式强制转换的第二个原因是希望改变通常的标准转换
(3)进行显式强制转换的第三个原因是要避免出现多种转换可能的歧义情况
显式转换符号的一股形式如下:
cast-name< type >( expression );
这里的cast-name 是static_cast、const_cast、dynamic_cast和reinterpret_cast之一
const_cast 正如其名字所暗示的将转换掉表达式的常量性,例如:
extern char *string_copy( char* );
const char *pc_str;
char *pc = string_copy( const_cast< char* >( pc_str ));
编译器隐式执行的任何类型转换都可以由static_cast 显式完成:
double d = 97.0;
char ch = static_cast< char >( d );
从一个较大类型到一个较小类型的赋值,会导致编译器产生一个警告以提醒我们潜在的精度损失。当我们提供显式强制转换时,警告消息被关闭。强制转换告诉编译器我们不关心潜在的精度损失
reinterpre_cast 通常对于操作数的位模式执行一个比较低层次的重新解释
dynamic_cast支持在运行时刻识别由指针或引用指向的类对象
14.4、旧式强制类型转换
    前面给出的强制转换符号语法,有时被称为新式强制转换符号,它是由标准C++引入的。在它之前,显式强制转换由非常通用的强制转换语法(现在被称为旧式强制转换符号)来实现。虽然标准C++仍然支持旧式强制转换符号。但是我们建议。只有当我们为C 语言或标准C++之前的编译器编写代码时才使用这种语法。
旧式强制转换符号有下列两种形式:
// C++强制转换符号
type (expr);
// C 语言强制转换符号
(type) expr;
旧式强制转换可以用来代替标准C++中的static_cast cons_cast 或reinterpret_cast
第5章 语句
1、简单语句和复合语句
    程序语句最简单的形式是空语句,形式如下:
; // 空语句
空语句被用在“程序的语法上要求一条语句而逻辑上却不需要”的时候
简单语句由单个语句构成
条件和循环语句在语法上只允许执行一条指定的相关语句。然而在实践中,这是远远不够的。在逻辑上,程序经常需要执行两条或多条语句构成的序列。在这样的情况下,我们用一个复合语句来代替单个语句
复合语句是由一对花括号括起来的语句序列。复合语句被视为一个独立的单元。它可以出现在程序中任何单个语句可以出现的地方。
包含一条或多条声明语句的复合语句,也称为语句块。块引入了程序中的局部域,在块中声明的标识符,只在该块中可见
2、声明语句
    在C++中对象的定义,如:
int ival;
被视为C++语言的一条语句(称作声明语句,尽管在这种情况下称为定义语句更准确),一般它可以被放在程序中任何允许语句出现的地方
声明语句展示了声明的局部性,即声明语句出现在被定义对象首次被使用的局部域内
在C中,对象的定义并不被视为C语言的语句,块中的所有对象定义必须出现在任何程序语句之前。出于这种需要,C程序员使自己习惯于在每个当前块的顶部定义全部对象
由于对象的定义是C++语言的一条语句,所以可以将对象定义放在任何其他语句能够出现的地方。C++允许在if else-if switch while 和for 循环的条件部分出现声明,以此来鼓励使用局部声明
3、if语句
    C++语言提供if 语句的动机是:根据指定的表达式是否为true,有条件地执行一条语句或语句块。if 语句的语法形式如下:
if ( condition )
statement
condition(条件)必须被放在括号内。它可以是表达式,如:
if ( a + b > c ) { ... }
或是一条具有初始化功能的声明语句,如:
if ( int ival = compute_value() ) { ... }
在condition 中定义的对象,只在与if 相关的语句或语句块中可见
4、switch语句
    switch 语句由以下部分构成:
(1)关键字switch,后面是一个要被计算的表达式,表达式被放在括号中。
(2)一组case 标签,它由关键字case 后接一个常量表达式及其冒号构成。此常量表达式将被用来与switch 表达式的结果做比较。
(3)与一个或一组case 标签相关联的语句序列
(4)可选的default标签。default 标签也被看作是一种else子句。如果switch表达式与任意一个case 标签都不匹配,则default 标签后面的语句被计算。
关键字case后面的值必须是一种整数类型的常量表达式。另外任意两个case 标签不能有同样的值,如果有则导致编译错误
普遍的误解是:只有与被匹配的case标签相关联的语句才被执行。实际上,程序从该点开始执行并继续越过case边界直到switch语句结束。程序员必须显式地告诉编译器停止执行switch 中的语句,这可以通过在switch 语句内的每个执行单元后指定一个break 语句来完成。在大多数条件下一个case标签的最后一条语句是break。当遇到break语句时,switch语句被终止。控制权被转移到紧跟在switch结束花括号后面的语句上
声明语句也可以被放在switch语句的条件中,如下所示:
switch( int ival = get_response() )
ival 被初始化,并且读初始化值成为与每个case 标签作比较的值。ival 在整个switch 语句中是可见的,但在其外面并不可见
把一条声明语句放在与case 或default 相关联的语句中是非法的,除非它被放在一个语句块中
5、for循环
    for 循环的语法形式如下
for ( init-statement; condition; expression )
statement
init-statement 初始化语句可以是声明语句或表达式
condition 条件语句用作循环控制
expression 表达式在循环每次迭代后被计算,一般用它来修改在init-statement中被初始化的、在condition中被测试的变量
在init-statement 中可以定义多个对象,但只能出现一个声明语句,因此,所有对象都必须是相同的类型
在for 循环的condition部分中定义的对象很难管理:它的最终计算结果必须为false,否则循环将水远不会终止
6、while语句
    while 循环的语法形式如下:
while ( condition )
statement
condition计算结果为true 多少次,则循环就迭代多少次,语句或语句块也被执行多少次。执行序列如下:
(1)计算condition
(2)如果condition 为true 则执行statement 语句
7、do while语句
    do while 循环的语法形式如下:
do
statement
while ( condition );
statement 在condition 被计算之前执行。如果condition 的计算结果为false,则循环终止
不像其他循环语句,do while循环的条件(即condition 部分)不支持对象定义
8、break语句
break 语句终止最近的while、do while、for或switch 语句。程序的执行权被传递给紧接着被终止语句之后的语句。
一般来说,break语句只能出现在循环或switch 语句中
当break出现在嵌套的switch或循环语句中时,里层的switch或循环语句被终止并不影响外层的switch或循环
9、continue语句
continue 语句导致最近的循环语句的当前迭代结束,执行权被传递给条件计算部分。不像break 语句终止的是整个循环,continue 语句只终止当前的迭代
continue 语句只有出现在循环语句中才是合法的
第7章 函数
1、概述
一般来说,函数由一个名字来表示。函数的操作数称为参数,由一个位于括号中、并且用逗号分隔的参数表指定。函数的结果被称为返回值,返问值的类型被称为函数返回类型。不产生值的函数返回类型是void,意思是什么都不返回。函数执行的动作在函数体中指定。函数体包含在花括号中,有时也称为函数块。函数返回类型、以及其后的函数名、参数表和函数体构成了函数定义
当函数名后面紧跟着调用操作符时,这个函数就被执行了。如果函数被定义为应该接收参数,则在调用这个函数时,就需要为这些参数提供实参。且这些实参被放在调用操作符中,而两个相邻的实参用逗号分隔。这种安排称为“向函数传递参数”
函数调用会导致两件事情发生。如果函数已经被声明为inline,则函数体可能已经在编译期间它的调用点上就被展开。如果没有被声明为inline,则函数在运行时才被调用。函数调用会使程序控制权被传送给正在被调用的函数,而当前活动函数的执行被挂起。当被调用的函数完成时,主调函数在调用语句之后的语句上恢复执行。函数在执行完函数体的最后一条语句或遇到返回语句后完成
我们必须在调用函数之前就声明该函数,否则会引起编译错误。当然,函数定义也可以被用作声明。但是,函数在程序中只能被定义一次。典型情况下函数定义被放在单独的程序文本文件中,或者与其他相关的函数定义放在同一个文本文件中。要想在其他文件而不是包含函数定义的文件中使用该函数,我们必须要用到另外一种函数声明机制
函数声明由函数返回类型、函数名和参数表构成。这三个元素被称为函数声明或函数原型,一个函数可在一个文件中被声明多次。
函数声明描述了函数的接口,它描述了函数必须接收的信息类型,以及它返回的信息类型返回类型,如果存在返回值的话。
2、函数原型
函数原型由函数返回类型、函数名以及参数表构成。函数原型描述的是函数的接口,它详细描述了调用函数时需要提供的参数的类型和个数,以及函数返回值的类型
2.1、函数返回类型
函数返回类型可以是预定义类型(如int 或double)、复合类型(如int&或double*)、用户定义类型(如枚举类或void 后者意指函数不返回值)
函数类型和内置数组类型不能作为返回类型
但是,类类型和容器类型可以被直接返回(这种方式效率比较低)
在C++标准化之前,如果缺少显式返回类型的话,返回值会被假定为int类型。在标准C++中,返回类型不能被省略
2.2、函数参数表
函数的参数表不能省略,没有任何参数的函数可以用空参数表或含有单个关键字void 的参数表来表示。
参数表中不能出现同名的参数,函数定义的参数表中的参数名允许在函数体中访问这个参数。函数声明中的参数名不是必需的,如果名字存在的话,它应该被用作辅助文档
2.3、参数类型检查
函数的参数表为编译器提供了必需的信息,使它能够在函数调用时对给出的实参进行类型检查
C++是一种强类型语言,每个函数调用的实参在编译期间都要经过类型检查。若实参类型与相应的参数类型不匹配,如果有可能,就会应用一个隐式的类型转换。如果不可能进行隐式转换或者实参的个数不正确,就会产生一个编译错误。这就是函数必须先被声明才能被使用的原因。编译器必须根据函数参数表,对函数凋用的实参执行类型检查,就此而言,声明是必不可少的
3、参数传递
所有的函数都使用在程序运行栈中分配的存储区。该存储区一直保持与该函数相关联,直到函数结束为止。那时,存储区将自动释放以便重新使用。该函数的整个存储区被称为活动记录
系统在函数的活动记录中为函数的每个参数都提供了存储区,参数的存储长度由它的类型来决定。参数传递是指用函数调用的实参值来初始化函数参数存储区的过程
C++中参数传递的缺省初始化方法是把实参的值拷贝到参数的存储区中,这被称为按值
传递。
按值传递时,函数不会访问当前调用的实参。函数处理的值是它本地的拷贝这些拷贝,被存储在运行栈中,因此改变这些值不会影响实参的值。一旦函数结束了,函数的活动记录将从栈中弹出,这些局部值也就消失了
在按值传递的情况下,实参的内容没有被改变。这意味着程序员在函数调用时无需保存和恢复实参的值。如果没有按值传递机制,那么每个没有被声明为const 的参数就可能会随每次函数调用而被改变。按值传递的危害最小,需要用户做的工作也最少。毫无疑问,按值传递是参数传递合理的缺省机制
按值传递并不是在所有的情况下都适合。不适合的情况包括:
(1)当大型的类对象必须作为参数传递时,对实际的应用程序而言,分配对象并拷贝到栈中的时间和空间开销往往过大
(2)当实参的值必须被修改时
3.1、引用参数
把参数声明成引用,实际上改变了缺省的按值传递参数的传递机制。在按值传递时,函数操纵的是实参的本地拷贝。当参数是引用时,函数接收的是实参的左值而不是值的拷贝。这意味着函数知道实参在内存中的位置,因而能够改变它的值或取它的地址
何时应该将一个参数指定为引用参数:
(1)被调用函数改变实参的值时
(2)向主调函数返回额外的结果
(3)向函数传递大型类对象
在按值传递情况下,整个对象将随每次调用而被拷贝。尽管按值传递对内置数据类型的对象和小型类对象比较满意,但是对于大型类对象,它的效率就太低了。使用引用参数,函数可以访问被指定为实参的类对象,而不必在函数的活动记录中拷贝它
如果引用参数不希望在被调用的函数内部被修改,那么把参数声明为const 型的引用是个不错的办法。这种方式能够使编译器防止无意的改变
3.2、引用和指针参数的关系
    引用必须被初始化为指向一个对象,一旦初始化了,它就不能再指向其他对象。指针可以指向一系列不同的对象,也可以什么都不指向。
因为指针可能指向一个对象或没有任何对象,所以函数在确定指针实际指向一个有效的对象之前不能安全地解引用一个指针
另一方面,对于引用参数函数,不需要保证它指向一个对象。引用必须指向一个对象,甚至在我们不希望这样时也是如此
如果一个参数可能在函数中指向不同的对象,或者这个参数可能不指向任何对象,则必须使用指针参数
引用参数的一个重要用法是,它允许我们在有效地实现重载操作符的同时,还能保证用法的直观性
3.3、数组参数
在C++中,数组永远不会按值传递。它是传递第一个元素的指针
例如,如下声明:
void putValues( int[ 10 ] );
被编译器视为:
void putValues( int* );
数组的长度与参数声明无关
因为数组被传递为指针,所以这对程序员有两个含义:
(1)在被调函数内对参数数组的改变将被应用到数组实参上而不是本地拷贝上
(2)数组长度不是参数类型的一部分
另外一种机制是将参数声明为数组的引用。当参数是一个数组类型的引用时,数组长度成为参数和实参类型的一部分,编译器检查数组实参的长度与在函数参数类型中指定的长度是否匹配。例如:
void putValues( int (&arr)[10] );
int main() {
int i, j[ 2 ];
putValues( i ); // 错误: 实参不是 10 个 int 的数组
putValues( j ); // 错误: 实参不是 10 个 int 的数组
return 0;
}
参数也可以是多维数组,这样的参数必须指明第一维以外的所有维的长度。例如:
void putValues( int matrix[][10], int rowSize );
把matrix 声明成一个二维数组,每行由10 个列元素构成。matrix可以被等价地声明为
int (*matrix)[10]
3.4、抽象容器类型参数
容器类型实际上是类类型,它比内置数组数据类型提供了更多的功能
当容器类型的参数按值传递时,容器以及全部元素都被拷贝到被调函数的本地拷贝中。因为拷贝的效率非常低,所以把容器类型的参数声明为引用参数比较好。
当一个函数不会修改参数的值时,我们把参数声明为const 类型的引用更为合适
3.5、缺省实参
函数可以用参数表中的初始化语法为一个或多个参数指定缺省实参
调用包含缺省实参的,函数时我们可以(也可以不)为该参数提供实参。如果提供了实参,则它将覆盖缺省的实参值。否则函数将使用缺省实参值
设计带有缺省实参函数的部分工作就是排列参数表中的参数,使最可能取用户指定值的参数先出现,而最可能使用缺省实参的参数出现在后面
一个参数只能在一个文件中被指定一次缺省实参
习惯上,缺省实参在公共头文件包含的函数声明中指定,而不是在函数定义中。如果缺省实参在函数定义的参数表中提供,则缺省实参只能用在包含该函数定义的文本文件的函数调用中
3.6、省略号
    有时候我们无法列出传递给函数的所有实参的类型和数目。在这种情况下,我们可以用省略号... 指定函数参数表
省略号挂起类型检查机制。它们的出现告知编译器,当函数被调用时,可以有0 个或多个实参,而实参的类型未知。省略号有下列两种形式:
void foo( parm_list, ... );
void foo( ... );
第一种形式为特定数目的函数参数提供了声明。在这种情况下,当函数被调用时,对于与显式声明的参数相对应的实参进行类型检查,而对于与省略号对应的实参则挂起类型检查。在第一种形式中参数声明后面的逗号是可选的
4、返回一个值
return 语句被放在函数体内,这条语句结束当前正在执行的函数。在程序执行期间遇到return 语句时,程序控制权被返回给调用此函数的函数。
一个具有返回值的函数(即函数返回类型没有被声明为void)必须返回一个值,缺少返回值将引起编译错误
如果被返回的值的类型与函数返回类型不匹配,那么如果可能的话将应用隐式类型转换。如果无法隐式转换,则产生一个编译错误
缺省情况下函数的返回值是按值传递的,这意味着得到控制权的函数将接收返回语句中指定的表达式的拷贝。该缺省行为可以被改变,一个函数可以被声明为返回一个指针或一个引用
当声明一个返回引用的函数时,程序员应当知道下面两个易犯的错误:
(1)返回一个指向局部对象的引用。局部对象的生命期随函数的结束而结束。在函数结束后,该引用变成未定义内存的别名
(2)函数返回一个左值,对返回值的任何修改都将改变被返回的实际对象
4.1、参数和返回值与全局对象
一个程序中的各种函数可以通过两种机制进行通信。一种方法是使用全局对象,另一种方法是使用函数参数表和返回值
全局对象被定义在函数定义之外
5、递归
直接或间接调用自己的函数被称为递归函数
由于与函数调用相关的额外开销,递归函数可能比非递归数执行得慢一些。但是递归函数可能更小且更易于理解
6、内联函数
调用函数必须拷贝实参,保存机器的寄存器,程序还必须转向一个新位置
若一个函数被指定为inline函数,则它将在程序中每个调用点上被内联地展开
7、main():处理命令行
    通常,在执行程序时,我们会传递命令行选项。例如,我们可能写如下命令行:
prog -d -o ofile data0
实际上,命令行选项是main()的实参。在main()函数中,我们可以通过一个名为argv 的C 风格字符串数组访问它
int main( int argc, char *argv[] ) { ... }
argc包含命令行选项的个数,argv包含aygc个C风格字符串,代表了由空格分隔的命令选项
8、指向函数的指针
8.1、指向函数的指针的类型
函数返回类型和参数表的不同组合,代表了各不相同的函数类型
int printf( const char*, ... );
int strlen( const char* );
int (*pfce)( const char*, ... ); // 可以指向 printf()
int (*pfc)( const char* ); // 可以指向 strlen()
8.2、初始化和赋值
    不带下标操作符的数组名会被解释成指向首元素的指针。当一个函数名没有被调用操作符修饰时,会被解释成指向该类型函数的指针。例如,表达式:
lexicoCompare;
被解释成类型
int (*)( const string &, const string & );
的指针
将取地址操作符作用在函数名上也能产生指向该函数类型的指针。因此lexicoCompare和&lexioCompare 类型相同
指向函数的指针可以如下被赋值:
pfi = lexicoCompare;
pfi2 = pfi;
只有当赋值操作符左边指针的参数表和返回类型与右边函数或指针的参数表和返回类型完全匹配时,初始化和赋值才是正确的。如果不匹配,则将产生编译错误消息。在指向函数类型的指针之间不存在隐式类型转换
函数指针可以用0 来初始化或赋值,以表示该指针不指向任何函数
8.3、调用
指向函数的指针可以被用来调用它所指向的函数。调用函数时,不需要解引用操作符。无论是用函数名直接调用函数,还是用指针间接调用函数,两者的写法是一样的
也可以用显式的指针符号写出
(*pf)( ia, iaSize );
这两种形式产生相同的结果,但是第二种形式让读者更清楚该调用是通过函数指针执行的
8.4、函数指针的数组
    // typedefs 使声明更易读
typedef int (*PFV)(); // 定义函数类型指针的typedef
PFV testCases[10];
8.5、参数和返回类型
    函数参数的类型不能是函数类型,函数类型的参数将被自动转换成该函数类型的指针。例如:
// typedef 表示一个函数类型
typedef int functype( const string &, const string & );
void sort( string *, string *, functype );
编译器把sort()当作已经声明为
void sort( string *, string *,
int (*)( const string &, const string & ) );
    除了用作参数类型之外,函数指针也可以被用作函数返回值的类型。例如:
int (*ff( int ))( int*, int );
该声明将ff()声明为一个函数,它有一个int型的参数,返回一个指向函数的指针,类型为:
int (*)(int *,int)
函数不能声明返回一个函数类型,如果是则产生编译错误
第8章、域和生命期
1、域
    C++支持三种形式的域:局部域,名字空间域以及类域
局部域是包含在函数定义的程序文本部分。每一个函数都有一个独立的局部域,在函数中的每个复合语句也有一个独立的局部域
名字空间域是不包含在函数声明、函数定义或者类定义内的程序文本部分。程序的最外层的名字空间域被称作全局域,或全局名字空间域。对象、函数、类型以及模板都可以在全局域中定义。程序员也可以利用名字空间定义来定义用户声明的名字空间,它们被嵌套在全局域内
每个类定义都引入了一个独立的类域
同一个名字在不同的域中可以引用不同的实体
由声明引入的名字从声明点直到声明它的域结束为止都是可见的(包含其中的嵌套域)
在C++中,如果一个名字被用在表达式中,则在使用之前必须先声明它。名字解析是把表达式中的一个名字与某一个声明相关联的过程,也是给出这个名字的意义的过程。
域和名字解析是编译时刻的概念,它们应用在程序文本的某一部分上。这些概念给出了源文件中的程序文本的意义。编译器根据域规则和名字解析规则解释它所读入的程序文本
1.1、局部域
局部域是包含在函数定义的程序文本区。每一个函数都有一个独立的局部域。在函数中,每个复合语句也有它自己的局部域。局部域可以被嵌套
局部域内的名字解析是这样进行的:首先查找使用该名字的域,如果找到一个声明,则该名字被解析。如果没有找到,则查找包含该域的域。这个过程会一直继续下去,直到找到一个声明或已经查找完整个全局域。如果后一种情况发生(即没有找到该名字的声明),则这个名字的用法将被标记为错误
因为在名字解析期间查找域的顺序由内向外,所以在外围域中的声明被嵌套域中的同名声明所隐藏
2、全局对象和函数
全局域内的函数声明将引入全局函数,而在全局域内的变量声明将引入全局对象。全局对象是一个运行时刻实体,它在程序的整个执行过程中都存在。全局对象占据的存储区的生命期从程序启动开始,在程序终止时结束
2.1、声明和定义
函数声明指定了该函数的名字以及函数的返回类型和参数表。除了这些信息,函数定义还为函数提供了函数体它是包含在花括号中的一个语句序列。在函数被调用之前,函数体必须先被声明
在一个程序中,一个全局对象只能有一个定义。
关键字extern为声明但不定义一个对象提供了一种方法。实际上,它类似于函数声明,承诺了该对象会在其他地方被定义:或者在此文本文件中的其他地方,或者在程序的其他文本文件中
extern 声明不会引起内存被分配,它可以在同一文件中或同一程序的不同文件中出现多次。典型情况下,全局对象的声明只在公共的头文件中出现一次,当一个程序文件需要引用这个全局对象时,它可以包含这个头文件
既指定了关键字extern,又指定了一个显式初始值的全局对象声明将被视为该对象的定义。编译器将会为其分配存储区,而且该对象后续的定义都被标记为错误
关键字extern也可以在函数声明中指定,惟一的影响是将该声明的隐式属性“在其他地方定义”变为显式的
2.2、头文件
    头文件为所有extern 对象声明,函数声明,以及inline 函数定义提供了一个集中的位置:这被称作声明的局部化。如果一个文件要使用或定义一个对象或函数时,它必须包含include 相应的头文件
头文件提供了两个安全保证:第一,保证所有文件都包含同一个全局对象或函数的同一
份声明;第二,如果需要修改声明则只需改变一个头文件
设计头文件应该注意的问题:
(1)头文件提供的声明逻辑上应该属于一个组
(2)头文件不应该含有非inline函数或变量的定义。但是符号常量定义以及inline函数定义是特殊的定义,符号常量和inline函数可以被定义多次
3、局部对象
在局部域中的变量声明引入了局部对象,有三种局部对象:自动对象、寄存器对象以及局部静态对象
3.1、自动对象
自动对象的存储分配发生在定义它的函数被调用时。分配给自动变量的存储区来自于程序的运行栈,它是函数的活动记录的一部分
当一个自动变量的地址被存储在一个生命期长于它的指针时,该指针被称为空悬指针。这是一个严重的程序员错误,因为它所指的内容是不可预测的
3.2、寄存器自动对象
在函数中频繁被使用的自动变量可以用register 声明。如果可能的话编译器会把该对象装载到机器的寄存器中,如果不能够的话,则对象仍位于内存中
3.3、静态局部对象
    我们也能够在函数定义或者函数定义的复合语句中,声明可在整个程序运行期间一直存在的局部对象。当一个局部变量的值必须在多个函数调用之间保持有效时,我们不能使用普通的自动对象,自动对象的值在函数结束时被丢弃
这种情形的一种解决方案是把局部对象声明为static,静态局部对象具有静态存储持续期间。虽然它的值在函数调用之间保持有效,但是其名字的可视性仍限制在其局部域内。静态局部对象在程序执行到该对象的声明处时被首次初始化。
未经初始化的静态局部对象会被程序自动初始化为0,相反自动对象的值会是任意的,除非它被显式初始化
4、动态分配的对象
全局对象和局部对象的生命期是严格定义,的程序员不能以任何方式改变它们的生命期。但是,有时候需要创建一些生命期能被程序员控制的对象,它们的分配和释放可以根据程序运行中的操作来决定
第三种对象允许程序员完全控制它的分配与释放。这样的对象被称为动态分配的对象。动态分配的对象被分配在程序的空闲存储区的可用内存池中。程序员用new 表达式创建动态分配的对象,用delete 表达式结束此类对象的生命期。动态分配的对象可以是单个对象,也可以是对象的数组。在空闲存储区中分配的数组的长度可以在运行时刻计算
4.1、单个对象的动态分配与释放
new 表达式是由关键字new及其后面的类型指示符构成的,该类型指示符可以是内置类型或class 类型
空闲存储区的一个特点是,其中分配的对象没有名字。new表达式没有返回实际分配的对象,而是返回指向该对象的指针,对该对象的全部操作都要通过这个指针间接完成
空闲存储区的第二个特点是分配的内存是未初始化的。空闲存储区的内存包含随机的位模式,它是程序运行前该内存上次被使用留下的结果
我们建议对用new 表达式创建的对象进行初始化,程序员可以按如下方式初始化一个int 型对象
int *pi = new int( 0 );
括号内的常量给出了一个初始值,它被用来初始化new 表达式创建的对象
new 表达式的操作序列如下:从空闲存储区分配对象。然后用括号内的值初始化该对象
在类对象的情况下,括号中的值被传递给该类相关的构造函数,它在该对象被成功分配之后才被调用
当指针pi 所指对象的内存被释放时,它的生命期也随之结束。当pi 成为delete 表达式的操作数时,该内存被释放。例如:
delete pi;
释放了pi指向的内存,结束了int 型对象的生命期。通过把delete 表达式放在程序中的适当位置上,程序员就可以控制在何时结束对象的生命期。delete 表达式调用库操作符delete(),把内存还给空闲存储区。因为空闲存储区是有限的资源,所以当我们不再需要已分配的内存时就应该马上将其返还给空闲存储区这是很重要的
在delete表达式之后,pi被称作空悬指针,即指向无效内存的指针。空悬指针是程序错误的一个根源,它很难被检测到。一个比较好的办法是在指针指向的对象被释放后将该指针设置为0。这样可以清楚地表明该指针不再指向任何对象
delete表达式只能应用在指向的内存是用new 表达式从空闲存储区分配的指针上。将delete 表达式应用在指向空闲存储区以外内存的指针上,会使程序运行期间出现未定义的行为
4.3、数组的动态分配与释放
new 表达式也可以在空闲存储区中分配数组。在这种情况下,new 表达式中的类型指示符后面必须有一对方括号,里面的维数是数组的长度,且该组数可以是一个复杂的表达式。new 表达式返回指向数组第一个元素的指针
一般地,在空闲存储区上分配的数组不能给出初始化值集。在空闲存储区中创建的内置类型的数组必须在for 循环中被初始化,即数组的元素被一个接一个地初始化
对于用new 表达式分配的数组,只有第一维可以用运行时刻计算的表达式来指定。其他维必须是在编译时刻已知的常量值
用来释放数组的delete 表达式形式如下:
delete [] str1;
空的方括号是必需的。它告诉编译器,该指针指向空闲存储区中的数组而不是单个对象
4.4、常量对象的动态分配与释放
程序员可能希望在空闲存储区创建一个对象,但是一旦它被初始化了就要防止程序改变该对象的值
const int *pci = new const int(1024);
在空闲存储区创建的const 对象有一些特殊的属性。首先,const 对象必须被初始化,如果省略了括号中的初始值,就会产生编译错误。第二,用 new 表达式返回的值作为初始值的指针必须是一个指向const 类型的指针
4.5、定位new表达式
    new 表达式的第三种形式可以允许程序员要求将对象创建在已经被分配好的内存中。这种形式的new 表达式被称为定位new 表达式。程序员在new 表达式中指定待创建对象所在的内存地址。new 表达式的形式如下:
new (place_address) type -specifier
place_address 必须是个指针
不存在与定位new 表达式相匹配的delete 表达式。其实我们并不需要这样的delete 表达式,因为定位new 表达式并不分配内存。
delete [] buf;
当字符缓冲被删除时,它所包含的任何对象的生命期也就都结束了
5、名字空间
    名字空间允许我们更好地处理全局名字空间污染问题。库的作者可以定义一个名字空间,从而把库中的名字隐藏在全局名字空间之外。例如:
namespace cplusplus_primer {
class matrix { /* ... */ };
void inverse ( matrix & );
}
名字空间cplusplus_primer是用户声明的名字空间
    每个用户声明的名字空间代表一个不同的名字空间域。用户声明的名字空间域可以包含其他嵌套的名字空间定义,以及函数、对象、模板和类型的声明或定义,在一个名字空间内声明的实体被称为名字空间成员
名字空间成员的名字会自动地与该名字空间名复合或被其限定修饰。例如:在名字空间cplusplus_primer中声明的matrix类的名字是cplusplus_primer::matrix。在程序中我们可以用限定修饰名来使用名字空间cplusplus_primer 的成员
5.1、名字空间定义
    用户声明的名字空间定义以关键字namespace 开头,后面是名字空间的名字。该名字在它被定义的域中必须是惟一的。如果在同样的名字空间域中有其他实体与被定义的名字空间同名,就会发生错误。
在名字空间名之后是由花括号{} 括起来的声明块。所有可以出现在全局名字空间域中的声明都可以被放在用户声明的名字空间中,类、变量(带有初始化)、函数(带有定义)以及模板
名字空间的定义不一定是连续的,名字空间的定义是可积累的
名字空间的定义可以非连续,这对生成一个库很有帮助。它使我们更容易将库的源代码组织成接口和实现部分
5.2、域操作符(::)
用户声明的名字空间成员名自动被加上前缀,名字空间名后面加上域操作符::。名字空间成员名由该名字空间名进行限定修饰
名字空间成员的声明被隐藏在其名字空间中,除非我们为编译器指定查找声明的名字空间,否则编译器将在当前的域及嵌套包含当前域的域中查找该名字的声明
5.3、嵌套名字空间
用户声明的名字空间可以包含嵌套的名字空间,我们可以用嵌套的名字空间来进一步改善库中代码的组织结构
嵌套名字空间的成员声明被隐藏在该嵌套域中。这样的成员会被自动地加上最外层名字空间名以及嵌套名字空间名形成的前缀
在外围名字空间中声明的实体被嵌套的名字空间中声明的同名实体所隐藏,类似的情况,在名字空间中声明的实体被局部域中声明的实体所隐藏
5.4、名字空间成员定义
名字空间成员的定义可以出现在名字空间定义内看,也可以在名字空间定义之外定义名字空间成员,在这种情况下名字空间成员的名字必须被外围名字空间名限定修饰
只有当一个名字空间成员在名字空间定义中已经被声明过,它才能在该名字空间定义之外被定义
5.5、ODR和名字空间成员
    虽然名字空间成员名是被限定修饰的,但是名字空间成员也是一个全局实体。ODR 要求非inline 函数和对象在一个程序中只能被定义一次,这也同样适用于名字空间成员。为了符合这样的要求,使用名字空间的程序一般组织如下:
(1)作为名字空间成员的函数和对象的声明被放在头文件中,该文件将被包含在要使用该名字空间的文件中
(2)这些成员的定义可以出现在某一个实现文件中
与全局域中的对象声明一样,我们必须用关键字extern 来指明只是声明名字空间成员,而不是定义它们
5.6、未命名的名字空间
在C++中,我们可以用未命名的名字空间声明一个局部于某一文件的实体。未命名的名字空间以关键字namespace 开头,因为该名字空间是没有名字的,所以在关键字namespace 后面没有名字,而在关键字namespace 后面使用花括号包含声明块
不像其他名字空间,未命名的名字空间的定义局部于一个特定的文件,不能跨越多个文本文件
未命名名字空间成员名只在特定的文件中可见,在构成程序的其他文件中是不可见的
在引入标准C++名字空间之前,解决此类声明局部化问题的常见方案是使用从C 语言中继承来的关键字static,未命名名字空间的成员与被声明为static 的全局实体具有类似的特性。在C 中被声明为static的全局实体在声明它的文件之外是不可见的
6、使用名字空间成员
6.1、名字空间别名
名字空间别名可以用来把一个较短的同义词与一个名字空间名关联起来
namespace IBM = International_Business_Machines;
名字空间别名的声明以关键字namespace 开头,后面是一个较短的别名,然后是赋值操作符,最后是原来的名字空间名
6.2、using声明
    通过使名字空间成员的名字可见,来在程序中用该名字的非限定修饰方式引用这个成员,而不用前缀namespace_name::name 也是可行的。如果该成员被用using 指示符声明,那么这就能够做到这一点
using 声明以关键字using开头,后面是名字空间成员名。using 声明中的成员名必须是限定修饰名。例如:
using cplusplus_primer::MatrixLib::matrix;
using 声明同其他声明的行为一样;它有一个域,它引入的名字从该声明开始直到其所在的域结束都是可见的。using 声明可以出现在全局域和任意名字空间中,同时它也可以出现在局部域中。与其他声明一样,using 声明引入的名字有以下特性:
(1)它在该域中必须惟一
(2)由外围域中的声明引入的相同名字被其隐藏
(3)它被嵌套域中的相同名字的声明隐藏
6.3、using指示符
using 指示符以关键字using 开头,后面是关键字namespace,然后是名字空间名。using 指示符允许我们让来自特定名字空间的所有名字的简短形式都可见。这些成员可以被直接使用,而不要求其名字被限定修饰
using 指示符并没有为名字空间成员的名字声明局部的别名,而是把名字空间的成员转移到包含该名字空间定义的那个域中。比如如下代码:
namespace A {
int i, j;
}
对域中有如下using 声明的代码来说:
using namespace A;
看起来就像
int i, j;
使用using指示符应该注意的问题:
(1)using指示符是域内的
(2)由using指示符引起的二义性错误是在该名字被使用时才被检测到,而不是遇到using指示符
(3)使用限定修饰名不受using指示符的影响
6.4、标准名字空间std
标准C++库中的所有组件都是在一个被称为std 的名字空间中声明和定义的。在标准头文件中声明的函数、对象和类模板,都被声明在名字空间std 中
第13章、类
1、类定义
类定义包含两部分:
类头,由关键字class 及其后面的类名构成
类体,由一对花括号包围起来
类定义后面必须接一个分号或一列声明。例如:
class Screen { /* ... */ };
class Screen { /* ... */ } myScreen, yourScreen;
在类体中,对类的数据成员和成员函数进行声明,并指定这些类成员的访问级别。类体定义了类成员表
类体定义了一个域,在类体中的类成员声明把这些成员名字引入到它们的类的域中。如果两个类有同名的成员,那么程序不会出错,并且这两个成员将指向不同的对象
在引入类类型之后我们可以以两种方式引用这种类类型
1 指定关键字class,后面紧跟类名
2 只指定类名
1.1、数据成员
    数据成员的声明看起来很像在块域或名字空间域中的变量声明。但是,除了静态(static)数据成员外,数据成员不能在类体中被显式地初始化。例如:
class First {
int memi = 0; // 错误
double memd = 0.0; // 错误
};
类的数据成员通过类的构造函数进行初始化
1.2、成员函数
类的成员函数被声明在类体中,成员函数的声明看起来像是名字空间域中所出现的函数声明,成员函数的定义也可以被放在类体内
成员函数与普通函数的不同:
(1)成员函数被声明在它的类中,这意味着该成员函数名在类域之外是不可见的。我们可以通过“点. 或箭头-> 成员访问操作符”引用成员函数
(2)成员函数拥有访问该类的公有和私有成员的特权,而一般来说,普通函数只能访问类的公有成员
成员函数可以是重载的函数重载函数,但是,一个成员函数只能重载自己类的其他成员函数,一个类的成员函数与在其他类或名字空间中声明的函数无关,因此也不能重载它们
1.3、成员访问
信息隐藏是为了防止程序的函数直接访问类类型的内部表示而提供的一种形式化机制。类成员的访问限制是通过类体内被标记为public、private以及protected的部分来指定的
公有成员在程序的任何地方都可以被访问。实行信息隐藏的类将其public 成员限制在成员函数上,这种函数定义了可以被一般程序用来操纵该类类型对象的操作
私有成员只能被成员函数和类的友元访问,实行信息隐藏的类把其数据成员声明为private
被保护成员对派生类就像public 成员一样,对其他程序则表现得像private
一个类可以包含多个public、private、protected区。每个区一直有效,直到另一个区标签或类体的结束右括号出现为止。如果没有指定访问限定符,则缺省情况下,在类体的开始左括号后面的区是private 区
1.4、友元
    在某些情况下,允许某个函数而不是整个程序可以访问类的私有成员,这样做会比较方便。友元机制允许一个类授权其他的函数访问它的非公有成员
友元声明以关键字friend开头,它只能出现在类的声明中。由于友元不是授权友谊的类的成员。所以它们不受其在类体中被声明的public private 和protected 区的影响
一个友元或许是一个名字空间函数、另一个前面定义的类的一个成员函数,也可能是一个完整的类。在使一个类成为友元时,友元类的所有成员函数都被给予访问“授权友谊的类的非公有成员”的权力
1.5、类声明和类定义
    一旦到了类体的结尾,即结束右括号,我们就说一个类被定义了一次。一旦定义了一个类,则该类的所有成员就都是已知的,类的大小也是已知的了
我们也可以声明一个类但是并不定义它。例如:
class Screen; // Screen 类的声明
这个声明向程序引入了一个名字Screen,指示Screen 为一个类类型
但是我们只能以有限的方式使用已经被声明但还没有被定义的类类型。如果没有定义类,那么我们就不能定义这类类型的对象,因为类类型的大小不知道,编译器不知道为这种类类型的对象预留多少存储空间
但是,我们可以声明指向该类类型的指针或引用。允许指针和引用是因为它们都有固定的大小,这与它们指向的对象的大小无关。但是,因为该类的大小和类成员都是未知的,所以要等到完全定义了该类,我们才能将解引用操作符* 应用在这样的指针上,或者使用指针或引用来指向某一个类成员
因为只有当一个类的类体已经完整时,它才被视为已经被定义,所以一个类不能有自身类型的数据成员。但是当一个类的类头被看到时,它就被视为已经被声明了,所以一个类可以用指向自身类型的指针或引用作为数据成员
2、类对象
类的定义不会引起存储区分配,只有当定义一个类的对象时系统才会分配存储区
类类型的对象有一个域,它是由对象定义在程序文本文件中的位置决定的。一个类的对象可能被定义在一个与“类类型被定义的域”不同的域中
一个对象可以被同一类类型的另一个对象初始化或赋值。缺省情况下拷贝一个类对象与拷贝它的全部数据成员等价。例如:
Screen bufScreen = myScreen;
// bufScreen._height = myScreen._height
// bufScreen._width = myScreen._width
// bufScreen._cursor = myScreen._cursor
缺省情况下,当一个类对象被指定为函数实参或函数返回值时,它就被按值传递。我们也可以把一个函数参数或返回类型声明为一个类类型的指针或引用
我们必须用成员访问操作符来访问类对象的数据成员或成员函数,点成员访问操作符.与类对象或引用联用,箭头访问操作符-> 与类对象的指针联用
3、类成员函数
类的成员函数是一组操作的集合,用户可以在该类的对象上执行这些操作
3.1、inline 和非inline 成员函数
在类定义中定义的函数被自动作为inline 函数处理
一两行以上的成员函数最好被定义在类体之外。这要求一个特殊的声明语法,来标识一个函数是一个类的成员:成员函数名必须被它的类名限定修饰qualified
成员函数必须先在其类体内被声明,而且类体必须在成员函数被定义之前先出现
通常,在类体外定义的成员函数不是inline 的。但是,这样的函数也可以被声明为inline函数,可以通过显式地在类体中出现的函数声明上使用关键字inline,或者通过在类体外出现的函数定义上显式使用关键字inline,或者两者都用
由于内联函数必须在调用它的每个文本文件中,被定义所以没有在类体中定义的内联成员函数必须被放在类定义出现的头文件中
3.2、访问类成员
    无论成员函数是在类体内还是外面,我们都说它在类域内。这有两个含义:
(1)成员函数的定义可以引用任何一个类成员,无论该成员是私有的还是公有的,都不会破坏类访问限制
(2)成员函数可以直接访问它所属的类的成员,而无需使用点或箭头成员访问操作符
3.3、特殊的成员函数
    有一组特殊的成员函数可以管理类对象并处理诸如初始化、赋值、内存管理、类型转换以及析构等活动,这些函数通常由编译器隐式调用
初始化成员函数被称为构造函数,每次定义一个类对象或由new 表达式分配一个类对象时都会调用它。构造函数的名字必须与类名相同
3.4、const成员函数
通常,程序中任何试图修改const 对象的动作都会被标记为编译错误
为尊重类对象的常量性,编译器必须区分不安全与安全的成员函数(即区分试图修改类对象与不试图修改类对象的函数)
类的设计者通过把成员函数声明为const,以表明它们不修改类对象。例如:
class Screen {
public:
char get() const { return _screen[_cursor]; }
// ...
}
只有被声明为const 的成员函数才能被一个const 类对象调用。关键字const 被放在成员函数的参数表和函数体之间。对于在类体之外定义的const 成员函数,我们必须在它的定义和声明中同时指定关键字const
const成员函数可以被相同参数表的非const 成员函数重载
构造函数和析构函数是两个例外,即使构造函数和析构函数不是const 成员函数,const类对象也可以调用它们。当构造函数执行结束、类对象已经被初始化时,类对象的常量性就被建立起来了。析构函数一被调用,常量性就消失。所以一个const 类对象从构造完成时刻到析构开始时刻这段时间内被认为是const
4、隐含的this指针
每个类的成员函数都含有一个指向被调用对象的指针
要想理解这一点,一种方法是看一看编译器是怎样实现this指针的。为支持this 指针,必须要应用两个转变:
(1)改变类成员函数的定义,用额外的参数;this指针,来定义每个成员函数
(2)改变每个类成员函数的调用,加上一个额外的实参——被调用对象的地址
5、静态类成员
静态数据成员被当作该类类型的全局对象。对于非静态数据成员,每个类对象都有自己的拷贝,而静态数据成员对每个类类型只有一个拷贝。静态数据成员只有一份,由该类类型的所有对象共享访问
同全局对象相比,使用静态数据成员有两个优势:
(1)静态数据成员没有进入程序的全局名字空,间因此不存在与程序中其他全局名字冲突的可能性
(2)可以实现信息隐藏,静态成员可以是private 成员,而全局对象不能
一般地,静态数据成员在该类定义之外被初始化。如同一个成员函数被定义在类定义之外一样,在这种定义中的静态成员的名字必须被其类名限定修饰。例如,下面是interestRate的初始化
// 静态类成员的显式初始化
#include "account.h"
double Account::_interestRate = 0.0589;
与全局对象一样,对于静态数据成员,在程序中也只能提供一个定义。这意味着,静态数据成员的初始化不应该被放在头文件中,而应该放在含有类的非inline 函数定义的文件中
静态数据成员的惟一性本质(独立于类的任何对象而存在的惟一实例),使它能够以独特的方式被使用,这些方式对于非static 数据成员来说是非法的
(1)静态数据成员的类型可以是其所属类,而非static 数据成员只能被声明为该类的对象的指针或引用例
(2)静态数据成员可以被作为类成员函数的缺省实参,而非static 成员不能
5.1、静态成员函数
    静态成员函数的声明除了在类体中的函数声明前加上关键字static,以及不能声明为const 或volatile 之外,与非静态成员函数相同,出现在类体外的函数定义不能指定关键字static
静态成员函数没有this 指针,因此在静态成员函数中隐式或显式地引用这个指针都将导致编译时刻错误。试图访问隐式引用this指针的非静态数据成员也会导致编译时刻错误
我们可以用成员访问操作符点. 和箭头-> 为一个类对象或指向类对象的指针调用静态成员函数,也可以用限定修饰名直接访问或调用静态成员函数,而无需声明类对象
6、指向类成员的指针
6.1、类成员的类型
函数指针不能被赋值为成员函数的地址,即使返回类型和参数表完全匹配
成员函数有一个非成员函数不具有的属性——它的类。指向成员函数的指针必须与向其赋值的函数类型匹配,不是两个而是三个方面都要匹配:
(1)参数的类型和个数
(2)返回类型
(3)它所属的类类型
函数指针存储函数的地址,可以被用来直接调用那个函数。
成员函数指针首先必须被绑定在一个对象或者一个指针上,才能得到被调用对象的this 指针,然后才调用指针所指的成员函数。虽然普通函数指针和成员函数指针都被称作指针,但是它们是不同的事物
成员函数指针的声明要求扩展的语法,它要考虑类的类型。对指向类数据成员的指针也是这样
在数据成员指针和普通指针之间的不匹配也是由于这两种指针的表示上的区别。普通指针含有引用一个对象所需的全部信息。数据成员指针在被用来访问数据成员之前,必须先被绑定到一个对象或指针
定义一个成员函数指针需要指定函数返回类型、参数表和类。例如指向Screen 成员函数并且能够引用成员函数height()和width()的指针类型如下:
int (Screen::*) ()
这种类型指定了一个指向类Screen 的成员函数的指针,它没有参数,返回值类型为int
6.2、使用指向类成员的指针
类成员的指针必须总是通过特定的对象或指向该类类型的对象的指针来访问。我们通过使用两个指向成员操作符的指针(针对类对象和引用的.* ,以及针对指向类对象的指针的->*来做到这一点)
Screen myScreen, *bufScreen;
// 直接调用成员函数
if ( myScreen.height() == bufScreen->height() )
bufScreen->copy( myScreen );
// 通过成员指针的等价调用
if ( (myScreen.*pmfi)() == (bufScreen->*pmfi)() )
(bufScreen->*pmfS)( myScreen );
类似地,指向数据成员的指针可以按下列方式被访问:
typedef short Screen::*ps_Screen;
Screen myScreen, *tmpScreen = new Screen( 10, 10 );
ps_Screen pH = &Screen::_height;
ps_Screen pW = &Screen::_width;
tmpScreen->*pH = myScreen.*pH;
tmpScreen->*pW = myScreen.*pW;
6.3、静态类成员的指针
在非静态类成员的指针和静态类成员的指针之间有一个区别。指向类成员的指针语法不能被用来引用类的静态成员。静态类成员是属于该类的全局对象和函数。它们的指针是普通指针(请记住静态成员函数没有this 指针。)
指向静态类成员的指针的声明看起来与非类成员的指针相同。解引用该指针不需要类对象
7、嵌套类
    一个类可以在另一个类中定义,这样的类被称为嵌套类。嵌套类是其外围类的一个成员。嵌套类的定义可以出现在其外围类的公有、私有或保护区中
嵌套类的名字在其外围类域中是可见的,但是在其他类域或名字空间中是不可见的。这意味着,嵌套类的名字不会与外围域中声明的相同名字冲突
与非嵌套类一样,嵌套类可以有与自身同样类型的成员
私有成员是指这样的成员,它只能在该类的成员或友元定义中被访问。除非外围类被声明为嵌套类的友元,否则它没有权利访问嵌套类的私有成员
第14章 类的初始化、赋值和析构
1、类的构造函数
构造函数与类同名,我们以此来标识构造函数
构造函数上惟一的语法限制是,它不能指定返回类型,甚至void 也不行
C++语言对于一个类可以声明多少个构造函数没有限制,只要每个构造函数的参数表是惟一的即可
C++要求,在类对象首次被使用之前,构造函数将被应用在该对象上
用缺省构造函数初始化类对象的正确声明是去掉尾部的小括号:
// ok: 定义了一个类对象
Account newAccount;
只有当没有构造函数或声明了缺省构造函数时,我们才能不指定实参集来定义类对象。一旦一个类声明了一个或者多个构造函数,类对象就不能被定义为不调用任何构造函数的实例。尤其是如果一个类声明了一个包含多个参数的构造函数,但没有声明缺省构造函数,则每个类对象的定义都必须提供所需的实参
因为容器类要求它们的类元素或者提供缺省的构造函数,或者不提供构造函数。类似地,对于类对象的动态数组,在分配内存的时候也要求或者有缺省构造函数,或者没有构造函数。在实践中,如果定义了其他构造函数,则也有必要提供一个缺省构造函数
类的初始化有一种可替换的语法成员:初始化,是由逗号分开的成员名及其初值的列表。例如,缺省的Account 构造函数可以这样写:
// 使用成员初始化表的缺省 Account 构造函数
inline Account::
Account()
: _name( 0 ),
_balance( 0.0 ), _acct_nmbr( 0 )
{}
成员初始化表只能在构造函数定义中被指定,而不是在其声明中。该初始化表被放在参数表和构造函数体之间,由冒号开始
缺省情况下,单参数构造函数被用作转换操作符
2.1、缺省构造函数
缺省构造函数是指不需要用户指定实参就能够被调用的构造函数,这并不意味着它不能接受实参。只意味着构造函数的每个参数都有一个缺省值与之关联
如果声明一个静态对象如下:
// 静态范围
// 每个对象相义的内存被初始化为0
Account global_scope_acct;
static Account file_scope_acct;
Account foo()
{
static Account local_static_acct;
// ...
}
则保证成员被初始化为0(对非类对象也一样)
局部定义或动态分配的对象会被一个随机值初始化,该值是程序运行栈中该内存上一次被使用的结果
如果我们想初始化内置或复合型的数据成员,则我们必须在一个或一组构造函数中显式地完成。如果不这样做,就不可能知道局部或动态分配的类对象中的内置和复合型数据成员是一个有效值,还是一个未初始化的值
2.2、限制对象创建
构造函数的可访问性由其声明所在的访问区来决定。我们可以通过把相关的构造函数放到非公有访问区内,从而限制或显式禁止某些形式的对象创建动作
2.3、拷贝构造函数
用一个类对象初始化该类的另一个对象被称为缺省按成员初始化。在概念上,一个类对象向该类的另一个对象作拷贝是通过依次拷贝每个非静态数据成员来实现的。
类的设计者也可以通过提供特殊的拷贝构造函数来改变缺省的行为。如果定义了拷贝构造函数,则在用一个类对象初始化该类另一个对象时它就会被调用
拷贝构造函数有一个指向类对象的引用作为形式参数(传统上被声明为const)
inline Account::
Account( const Accout &rhs )
: _balance( rhs._balance )
{
_name = new char[ strlen(rhs._name)+1 ];
strcpy( _name, rhs._name );
// 不能拷贝 rhs._acct_nmbr
_acct_nmbr = get_unique_acct_nmbr();
}
当我们写
Account acct2( acct1 );
编译器判断是否为Account 类声明了一个显式的拷贝构造函数。如果声明了拷贝构造函数,并且是可以访问的,则调用它。如果声明了拷贝构造函数但是不可访问,则acct2 的定义就是一个编译时刻错误。如果没有声明拷贝构造函数的实例,则执行缺省的按成员初始化
3、类的析构函数
析构函数是一个特殊的由用户定义的成员函数,当该类的对象离开了它的域,或者delete表达式应用到一个该类的对象的指针上时,析构函数会自动被调用
析构函数的名字是在类名前加上波浪线~,它不返回任何值也没有任何参数。因为它不能指定任何参数,所以它也不能被重载
析构函数主要被用来放弃在类对象的构造函数或生命期中获得的资源,如释放互斥锁或删除由操作符new 分配的内存。
一般地,析构函数可以执行类设计者希望在最后一次使用对象之后执行的任何操作
4、类对象数组和vector
    类对象数组与内置类型数组的定义方式相同。例如:
Account table[ 16 ];
定义了一个含有16 个Account 对象的数组,且每个元素依次用Account 缺省构造函数初始化
也可以在数组的初始化表中提供完整的构造函数语法,例如:
Account pooh_pals[] = {
Account( "Piglet", 1000.0 ),
Account( "Eeyore", 1000.0 ),
Account( "Tigger", 1000.0 )
};
对于在堆中分配的类对象数组的元素,我们没有办法提供一组显式的值来做初始化。如果希望支持通过new 表达式分配数组,则类必须提供一个缺省构造函数,或不提供构造函数
4.1、类对象的vector
    当我们定义一个含有五个类对象的vector时,如:
vector< Point > vec( 5 );
元素的初始化过程如下:
(1)创建一个底层类类型的临时对象,在其上应用该类的缺省构造函数
(2)在vector 的每个元素上依次应用拷贝构造函数,用临时类对象的拷贝初始化每一个类对象
(3)删除临时类对象
尽管最终结果等同于定义五个类对象的数组,比如:
Point pa[ 5 ];
但是初始化vector 代价比较大:临时对象的构造和析构,以及拷贝构造函数往往比缺省构造函数计算上更复杂
作为一般的设计原则,类对象的vector 仅仅最适合于元素的插入操作,也就是,我们定义一个空的vector。如果我们先预算出要插入的元素的数目,或者能够比较准确地猜出来,那么我们可以预留相应的存储区。然后,而进行元素插入
把一个类对象插入到每一种容器类型中,都是通过拷贝构造函数来实现的
5、成员初始化表
通过成员初始化表类,数据成员可以被显式初始化成员。初始化表是由逗号分隔的成员、名字实参对
如果成员是类对象,则初始值变成被传递给适当的构造函数的实参,该构造函数然后被应用在成员类对象上
使用成员初始化表和构造函数内使用数据成员的赋值间的区别:
构造函数的执行过程:
1、隐式或显式初始化
隐式初始化指:按照声明的顺序依次调用所有基类的缺省构造函数,然后调用成员类对象的缺省构造函数
显式初始化指:成员初始化表
2、计算阶段
由构造函数体内的所有语句构成,在计算阶段,数据成员的设置被认为是赋值,而不是初始化
对于类对象,在初始化和赋值之间的区别是巨大的。成员类对象应该总是在成员初始化表中被初始化,而不是在构造函数体内被赋值
对于非类数据成员的初始化或赋值,除了两个例外,两者在结果和性能上都是等价的。即更受欢迎的实现是用成员切始化表。两个例外是指任何类型的const和引用数据成员。const 和引用数据成员必须是在成员初始化表中被初始化,否则就会产生编译时刻错误
每个成员在成员初始化表中只能出现一次,初始化的顺序不是由名字在初始化表中的顺序决定,而是由成员在类中被声明的顺序决定的。但是在初始化表中出现(或者在被隐式初始化的成员类对象中)的成员,总是在构造函数体内成员的赋值之前被初始化
6、按成员初始化
    用一个类对象初始化另一个类对象,比如
Account oldAcct( "Anna Livia Plurabelle" );
Account newAcct( oldAcct );
被称为缺省的按成员初始化。缺省是因为它自动发生,无论我们是否提供显式构造函数。按成员是因为初始化的单元是单个非静态数据成员,而不是对整个类对象的按位拷贝
最简单的按成员初始化的概念模型是,想像编译器内部如何产生一个特殊的拷贝构造的数。在拷贝构造函数中,每个非静态数据成员按照声明的顺序被依次初始化
拷贝构造函数的调用时机:
(1)用一个类对象显式地初始化另一个类对象,例如:
Account newAcct( oldAcct );
(2)把一个类对象作为实参传递给一个函数,例如:
extern bool cash_on_hand( Account acct );
if ( cash_on_hand( oldAcct ))
// ...
把一个类对象作为一个函数的返回值传递回来,例如:
extern Account
consolidate_accts( const vector< Account >& )
{
Account final_acct;
// do the finances ...
return final_acct;
}
(3) 非空顺序容器类型的定义,例如:
// 五个 string 拷贝构造函数被调用
vector < string > svec( 5 );
在本例中用string 缺省构造函数创建一个临时对象,然后通过string 拷贝构造函数,该临时对象被依次拷贝到vector 的五个元素中
(4)把一个类对象插入到一个容器类型中,例如:
svec.push_back( string( "pooh" ));
如何改变缺省的按成员初始化:
(1)提供拷贝构造函数
(2)完全不允许不允许按成员初始化,可以通过:
把拷贝构造函数声明为私有的,这可以防止按成员初始化发生在程序的任何一个地方(除了类的成员函数和友元之外)
通过有意不提供一个定义(但是我们仍然需要第1 步中的声明),可以防止在类的成员函数和友元中出现按成员初始化。C++语言不会允许我们阻止类的成员函数和友元访问任何私有类成员。但是通过不提供定义,任何试图调用拷贝构造函数的动作虽然在编译系统中是合法的,但是会产生链接错误,因为无法为它找到可解析的定义
6.1、成员类对象的初始化
当一个成员类对象被识别出来时,则递归应用相同的过程:
该类提供了显式的拷贝构造函数了吗?如果是,则调用该构造函数初始化成员类对象。否则就在成员类对象上应用缺省的按成员初始化。如果类的所有成员都是内置或复合数据类型,则依次对它们进行初始化,这样就完成了成员类对象的初始化。否则,如果有一个或多个成员本身是成员类对象,则递归应用这个过程,直到每个内置和复合数据类型都被处理完
7、按成员赋值
    缺省的按成员赋值所处理的是,用一个类对象向该类的另一个对象的赋值操作。其机制基本上与缺省的按成员初始化相同,但是它利用了一个隐式的拷贝赋值操作符来取代拷贝构造函数。例如:
newAcct = oldAcct;
在缺省情况下用oldAcct 的相应成员的值依次向newAcct 的每个非静态成员赋值。在概念上就好像编译器已经生成下列拷贝赋值操作符:
inline Account&
Account::
operator=( const Account &rhs )
{
_name = rhs._name;
_balance = rhs._balance;
_acct_nmbr = rhs._acct_nmbr;
}
一般来说,如果缺省的按成员初始化对于一个类不合适,则缺省的按成员赋值也不合适
拷贝赋值操作符的一般形式如下:
// 拷贝赋值操作符的一般形式
className&
className::
operator=( const className &rhs )
{
// 保证不会自我拷贝
if ( this != &rhs )
{
// 类拷贝语义在这里
}
// 返回被赋值的对象
return *this;
}
当一个类对象被赋值给该类的另一个对象时,如:
newAcct = oldAcct;
下面几个步骤就会发生:
(1)检查该类,判断它是否提供了一个显式的拷贝赋值操作符
(2)如果是,则检查访问权限,判断是否在这个程序部分它可以被调用
(3) 如果它不能被调用,则会产生一个编译时刻错误。否则,调用它执行赋值操作
(4)如果该类没有提供显式的拷贝赋值操作符,则执行缺省按成员赋值
(5)缺省按成员赋值下,每个内置类型或复合类型的数据成员被赋值给相应的成员
(6)对于每个类成员对象,递归执行l 到6 步,直到所有内置或复合类型的数据成员都被赋值
如果希望完全禁止按成员拷贝的行为,那么就需要像禁止按成员初始化一样:将操作符声明为private,并且不提供实际的定义
第15章 重载操作符和用户定义的转换
1、操作符重载
操作符重载使得程序员能够为类类型的操作数定义预定义的操作符版本,重载的操作符使得类类型对象可以与预定义的操作符一起被使用,使得对于类对象的操纵与内置类型的对象一样直观
重载的操作符在类体中被声明,声明方式同普通成员函数一样,只不过它的名字包含关键字operator,以及紧随其后的一个预定义操作符
1.1、类成员与非类成员
编译器只考虑在左操作数的类中定义的成员重载操作符以及在其基类中定义的重载操作符
声明非类成员的重载操作符也是可以的,例如:
bool operator==( const String &, const String & );
bool operator==( const String &, const char * );
全局重载操作符比成员重载操作符多了一个参数,对于成员操作符隐式的this 指针被用作隐式的第一个参数。例如对于成员操作符如下表达式
flower == "lily"
被编译器重写为
flower.operator==( "lily" )
在成员重载操作符的定义中,我们通过this 指针可以引用左操作数flower。对于全局重载操作符,代表左操作数的参数必须被显式指定
当一个重载操作符是一个名字空间的函数时,对于操作符的第一个和第二个参数,即等于操作符的左和右两个操作数都会考虑转换
一般应该怎样决定是把一个操作符声明为类成员还是名字空间成员呢?在某些情况下,程序员没有选择的余地
(1)如果一个重载操作符是类成员,那么只有当跟它一起被使用的左操作数是该类的对象时,它才会被调用。如果该操作符的左操作数必须是其他的类型,那么重载操作符必须是名字空间成员
(2)C++要求赋值= 下标[] 调用() 和成员访问箭头-> 操作符必须被定义为类成员操作符。任何把这些操作符定义为名字空间成员的定义都会被标记为编译时刻错误
1.2、重载操作符的名字
对于内置类型的操作符,它的预定义意义不能被改变,也不能为内置数据类型定义其他的操作符
程序员只能为类类型或枚举类型的操作数定义重载操作符。我们可以这样来实现:把重载操作符声明为类的成员,或者声明为名字空间成员,同时至少有一个类或枚举类型的参数(按值传递或按引用传递)
预定义的操作符优先级不能改变,在使用重载操作符时,可以用小括号来改变优先级
操作符预定义的操作数个数必须被保留
2、友元
    通过把函数或操作符声明为友元,一个类可以授予这个函数或操作符访问其非公有成员的权利
友元声明以关键字friend开始,它只能出现在类定义中。因为友元不是授权类的成员,所以它不受其所在类的声明区域public private 和protected 的影响。
friend bool operator==( const char *, const String & );
友元声明的最常见用法是,允许非成员的重载操作符访问一个视其为朋友的类的私有成员。原因是,除了提供左和右操作数的对称性外,非成员的重载操作符就像成员函数一样,能够完全访问一个类的私有成员
3、操作符=
一个类对象向该类的另一个对象的赋值可通过拷贝赋值操作符来执行
我们也可以为一个类类型定义其他的赋值操作符。如果一个类类型的对象被赋以一个不是它自己类类型的值,那么它可以定义接受这种其他类型参数的赋值操作符。例如:
为了支持用C 风格字符串向String 类对象的赋值,如:
String car ("Volks");
car = "Studebaker";
4、操作符[]
我们可以为表示容器抽象并能够获取其单独元素的类定义下标操作符operator[]()
5、操作符operator()
    如果一个类类型被定义来表示一个操作时,则可以为这个类类型重载函数调用操作符,以便调用这个操作。例如,absInt类被定义为“将取一个int 型操作数的绝对值的操作”封装起来
class absInt {
public:
int operator()( int val ) {
int result = val < 0 ? - val : val;
return result;
}
};
我们通过向一个类类型的对象应用一个实参表,调用该类重载的operator()操作符
6、操作符->
我们也可以为类类型的对象重载成员访问操作符箭头,它必须被定义为一个类的成员函数。它的作用是赋予一个类类型与指针类似的行为。它通常被用于一个代表智能指针的类类型
// 支持指针行为的重载操作符
class ScreenPtr {
public:
Screen& operator*() { return *ptr; }
Screen* operator->() { return ptr; }
//....
};
成员访问操作符箭头被重载为一元操作符,即它没有参数。当它被用在表达式中时,只能根据左边操作数的类型来选择它。例如,下面给出的语句:
point->action();
将检查point 以决定其类型。如果point 是某一个类类型的指针。则这个语句使用内置成员访问操作符箭头的语义。如果point 是某一个类类型的对象或引用,则查找这个类的重载的成员操作符箭头。如果没有定义成员操作符,则该语句就是错的,因为类对象或引用通常必须使用点成员访问操作符来引用类成员。如果定义了重载的成员访问操作符箭头,则它被绑定到point上并被调用
重载的成员访问操作符箭头的返回类型必须是一个类类型的指针,或者是定义该成员访问操作符箭头的类的一个对象。如果返回类型是一个类类型的指针,则内置成员访问操作符箭头的语义被应用在返回值上。如果返回值是另外一个类的对象或引用,则递归应用该过程直到返回的是指针类型或语句错误
7、操作符++和--
重载的递增和递减操作符的前置和后置实例都可以被定义
前置操作符的声明看起来就像你所期望的那样:
class ScreenPtr {
public:
Screen& operator++();
Screen& operator- - ();
// ...
};
前置的递增和递减操作符被定义为一元操作符函数。
为区分后置操作符与前置操作符的声明,重载的递增和递减后置操作符的声明有一个额外的int 类型的参数。在下面的例子中,它声明了ScreenPtr 类的前置和后置操作符对:
class ScreenPtr {
public:
Screen& operator++(); // 前置操作符
Screen& operator- - ();
Screen& operator++(int); // 后置操作符
Screen& operator- - (int);
// ...
};
额外的整型参数对于后置操作符的用户是透明的,编译器为它提供了缺省值
8、操作符new和delete
在缺省情况下,空闲存贮区中的类对象的分配和释放,由在C++标准库中定义的全局操作符new()和delete()来执行。如果一个类提供了两个分别被称为操作符new()和操作符delete()的成员函数,那么它就可以承接自己的内存管理权。如果在类中定义了这些成员操作符,则它们会被调用,以取代全局操作符来分配和释放该类类型的对象
类成员操作符new()的返回类型必须是void*型,并且有一个size_t 类型的参数,这里的size_t 是一个在系统头文件<cstddef>中被定义的typedef。下面是Screen 类操作符new()的声明:
class Screen {
public:
void *operator new( size_t );
// ...
};
当new 表达式创建一个类类型的对象时,编译器查看该类是否有一个成员操作符new()。如果有则选择这个操作符为该类对象分配内存;否则,调用全局操作符new()
类成员操作符delete()的返回类型必须是void,并且第一个参数的类型是void*。下面是Screen 类的操作符delete()的声明:
class Screen {
public:
void operator delete( void* );
};
当delete 表达式的操作数是指向一个类类型对象的指针时,编译器检查该类是否向一个成员操作符delete()。如果有,则选择该操作符为类对象释放内存;否则调用全局操作符delete()
9、用户定义的转换
类的设计者可以为类类型的对象提供一组用户定义的转换,这些用户定义的转换也是由编译器在需要时隐式地调用的
C++提供了一种机制,通过它,每个类都可以定义一组可被应用在该类型对象上的转换
对于SmallInt,我们定义了一个从SmallInt 对象到int 型的转换。下面是实现:
class SmallInt {
public:
SmallInt( int ival ) : value( ival ) { }
// 转换操作符
// SmallInt ==> int
operator int() { return value; }
// 没有提供重载操作符
private:
int value;
};
用户定义的转换是在类类型和转换函数中指定的类型之间的转换。转换函数定义了转换的意义,以及应用转换时编译器必须执行的动作
9.1、转换函数
转换函数是一种特殊类型的类成员函数,它定义了一个由用户定义的转换,以便把一个类对象转换成某种其他的类型。在类体中通过指定关键字operator,并在其后加上转换的目标类型后我们就可以声明转换函数
转换函数采用如下的一般形式:
operator type();
这里的type 可用内置类型、类类型或typedef 名取代。但是不允许type 表示数组或函数类型。转换函数必须是成员函数,它的声明不能指定返回类型和参数表
9.2、用构造函数作为转换函数
在一个类的构造函数中,凡是只带一个参数的构造函数,例如,SmallInt 的构造函数SmallInt(int) 都定义了一组隐式转换,把构造函数的参数类型转换为该类的类型
第17章 类继承和子类型
1、定义一个类层次结构
1.1、面向对象设计
继承层次结构的主要好处是,我们可以针对抽象基类的公有接口进行编程,而不是针对组成继承层次的个别类型,通过这种方式我们的代码可以不受层次结构变化的影响
当我们在C++中说到多态性时,我们主要指基类的指针或引用可以指向其任意派生类的能力
继承关系通过类派生表来指定。在单继承下,它的一般形式为:
: access-level base-class
这里access-level 是public protected 或private 之一
在派生表中指定的类必须首先定义好,方可被指定为基类
派生类的前向声明不能包括它的派生表,而只是类名——与非派生类一样。例如,下面的NameQuery 的前向声明导致编译时刻错误:
// 错误: 前向声明不能包含派生类的派生表
class NameQuery : public Query;
正确的前向声明如下
// 派生类与非派生类的前向声明只列出类名
class Query;
class NameQuery;
面向对象设计的主要形式是一个抽象基类的定义,以及它的公有派生
2、确定层次的结构
把一个成员指定为public 的标准在基于对象和面向对象的设计之间没有区别,真正的变化在于,是把一个非公有成员声明为protected 还是private
3、基类成员访问
派生类对象实际上是由多个部分组成的。每个基类代表了一个由该基类的非静态数据成员组成的子对象。派生类对象由其基类子对象以及由派生类的非静态数据成员构成的派生部分组成
在派生类中,继承得到的基类子对象的成员可以被直接访问,就好像它们是派生类的成员一样。对于继承而来的基类成员函数的访问也一样,我们调用它们,就好像它们是派生类的成员,或者通过该类的一个对象,或者直接在成员函数中调用
在派生类中直接访问基类成员有一个例外——当基类成员名在派生类中被重用时
为了用已被派生类重用的名字来访问基类的成员,我们必须用它的类域操作符限定修饰基类的成员
C++语言初学者的一个常见的误解是,希望基类和派生类的成员函数构成一个重载函数集
class Diffident {
public:
void mumble( int softness );
// ...
};
class Shy : public Diffident {
public:
// 隐藏了 Diffident::mumble 的可视性
// 它们没有形成一对重载实例
void mumble( string whatYaSay );
void print( int soft, string words );
// ...
};
但是试图在派生类中调用基类实例却导致一个编译时刻错误例如
Shy simon;
// ok: Shy::mumble( string )
simon.mumble( "pardon me" );
// 错误: 期望第一个实参是 string 类型
// Diffident::mumble( int ) 不可见
simon.mumble( 2 );
虽然基类的成员可以被直接访问,但是它们仍然属于基类的域,一个名字的重载候选函数必须都出现在同一个域中
4、基类和派生类的构造
派生类由一个或多个基类子对象以及派生类部分构成
为了把一个或多个实参传递给成员类对象的构造函数,我们通过成员初始化表来实现
为了向基类构造函数传递一个或多个参数,我们也使用成员初始化表
构造函数的调用顺序总是如下:
(1)基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序
(2)成员类对象构造函数。如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序
(3)派生类构造函数
派生类的析构函数调用顺序与它的构造函数调用顺序相反
5、基类和派生类虚拟函数
缺省情况下,类的成员函数是非虚拟的。当一个成员函数为非虚拟的时候,通过一个类对象指针或引用而被调用的该成员函数,就是该类对象的静态类型中定义的成员函数
当成员函数是虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,是在该类对象的动态类型中被定义的成员函数。但是,正如所发生的一个类对象的静态和动态类型是相同的。所以,虚拟函数机制只在使用指针和引用时才会如预期般地起作用
只有在通过基类指针或引用间接指向派生类子类型时,多态性才会起作用。使用基类对象并不会保留派生类的类型身份
通过pointer 和reference 的调用被解析为它们的动态类型,在这个例子中它们都调用NameQuery::print(),而通过object 的调用则总是调用Query::print()
class Query {
public:
virtual ostream& print( ostream &os=cout ) const {}
// ...
};
第一次引入虚拟函数的基类时,必须在类声明中指定virtual 关键字。如果定义被放在类的外面,则不能再次指定关键字virtual。例如,下面的print()定义将导致编译时刻错误
// 错误: 关键字 virtual 只能出现在类定义中
virtual ostream& Query::print( ostream& ) const { ... }
正确的定义必须不包括关键字virtual
引入虚拟函数的类必须定义它,或者把它声明为纯虚拟函数。如果派生类可以提供自己的实例,那么此实例将成为该派生类的活动实例,或各派生类也可以继承基类的活动实例。如果派生类定义了实例,则称之为改写,也译为覆盖了基类的实例
为了使虚拟函数的派生类实例能够改写其基类的活动实例,它的原型必须与基类完全匹配
5.2、纯虚拟函数
    C++语言为我们提供了一种语法结构,通过它可以指明,一个虚拟函数只是提供了一个可被子类型改写的接口。但是,它本身并不能通过虚拟机制被调用。这就是纯虚拟函数。纯虚拟函数的声明如下所示:
class Query {
public:
// 声明纯虚拟函数
virtual ostream& print( ostream&=cout ) const = 0;
// ...
};
这里函数声明后面紧跟赋值0
包含(或继承)一个或多个纯虚拟函数的类被编译器识别为抽象基类。试图创建一个抽象基类的独立类对象会导致编译时刻错误。抽象基类只能作为子对象出现在后续的派生类中
5.3、虚拟函数的静态调用
当用类域操作符调用虚拟函数时,我们改变了虚拟机制,使得虚拟函数在编译时刻被静态解析
Query *pquery = new NameQuery( "dumbo" );
// 通过虚拟机制动态调用 isA()
// 调用 NameQuery::isA() 实例
pquery->isA();
// 在编译时刻静态调用 isA
// 调用 Query::isA 实例
pquery->Query::isA();
5.4、虚拟函数和缺省实参
如果通过基类指针或引用调用派生类实例,则传递给它的缺省实参是由基类指定的。
#include <iostream>
class base {
public:
virtual int foo( int ival = 1024 ) {
cout < "base::foo() -- ival: " < ival < endl;
return ival;
}
// ...
};
class derived : public base {
public:
virtual int foo( int ival = 2048 ) {
cout << "derived::foo() -- ival: " << ival << endl;
return ival;
}
// ...
};
int main()
{
derived *pd = new derived;
base *pb = pd;
int val = pb->foo();
cout << "main() : val through base: "
<< val << endl;
val = pd->foo();
cout << "main() : val through derived: "
<< val << endl;
}
编译并运行它程序产生下列输出
derived::foo() -- ival: 1024
main() : val through base: 1024
derived::foo() -- ival: 2048
main() : val through derived: 2048
在这两个调用中,foo()的派生类实例被正确调用。这是因为,foo()调用的真正实例是在运行时刻根据pd 和pb 指向的实际类型决定的。然而,传递给foo()的缺省实参不是在运行时刻决定的,而是在编译时刻根据被调用函数的对象的类型决定的。当通过pb 调用foo()时,缺省实参中base::foo()的声明决定为1024。当通过pd 调用foo()时,缺省实参由derived::foo()的声明决定为2048
通过基类指针或引用调用派生类实例,则传递给它的缺省实参是由基类指定的
5.5、虚拟析构函数
在继承机制下的析构函数的行为如下:派生类的析构函数先被调用,在用基类指针指向派生类对象的情况下,它是一个虚拟函数调用。完成之后,直接基类的析构函数被静态调用——如果被声明为inline,则被内联展开
作为一般规则,我们建议将类层次结构的根基类(声明了一个或多个虚拟函数)的析构函数声明为虚拟的。但是,不像基类的构造函数,一般地基类的析构函数不应该是protected
如果将基类的虚拟析构函数声明为protected,那么当通过指向基类的指针或引用来调用析构函数时,它是protected,即虚拟函数承接了调用者所属类类型的访问级别,会产生编译错误
5.6、虚拟函数、构造函数和析构函数
派生类对象中构造函数的调用顺序是,先调用基类的构造函数,然后是派生类的构造函数
第18章 多继承和虚拟继承
1、多继承
    为支持多继承,一个类的派生表:
class Bear : public ZooAnimal { ... };
被扩展成支持逗号分割的基类表,例如:
class Panda : public Bear, public Endangered { ... };
每个被列出的基类还必须指定其访问级别:public、protected或private 之一与。单继承一样,只有当一个类的定义已经出现后,它才能被列在多继承的基类表中
在多继承下,派生类含有每个基类的一个基类子对象
基类构造函数被调用的顺序以类派生表中声明的顺序为准,构造函数调用顺序不受基类在成员初始化表中是否存在以及被列出的顺序的影响。
类似地,析构函数调用顺序总是与构造函数顺序相反
在多继承下,派生类可以从两个或者更多个基类中继承同名的成员。然而在这种情况下,直接访问是二义的,将导致编译时刻错误。但是,这个编译时刻错误不是由于对两个成员的非限定修饰访问存在潜在的二义性所触发的,而是由于企图真正访问这两个成员而触发的
2、public、private和protected继承
基类成员在派生类中的访问属性:
基类中的成员               私有成员        公用成员        保护成员
在公用派生类中的访问属性   不可访问        公用            保护
在私有派生类中的访问属性   不可访问        私有            私有
在保护派生类中的访问属性   不可访问        保护            保护
3、继承下的类域
在继承下,派生类的域被嵌套在直接基类的域中。如果一个名字在派生类域中,没有被解析出来,则编译器在外围基类域中查找该名字的定义
在基类和派生类之间名字相同并且原型也相同的成员函数,其行为与同名的数据成员一样,派生类的成员在派生类域中隐藏了基类的成员
4、虚拟继承
在缺省情况下,C++中的继承是按值组合的一种特殊情况
在单继承下,这种由继承支持的、特殊形式的按值组合提供了最有效的、最紧凑的对象表示。在多继承下,当一个基类在派生层次中出现多次时就会有问题
C++语言的解决方案是,提供另一种可替代“按引用组合”的继承机制:虚拟继承。在虚拟继承下,只有一个共享的基类子对象被继承,而无论该基类在派生层次中出现多少次
4.1、虚拟基类声明
    通过用关键字virtual修政一个基类的声明,可以将它指定为被虚拟派生。例如下列声明使得ZooAnimal 成为Bear 和Raccoon 的虚拟基类:
// 关键字 public 和 virtual
// 的顺序不重要
class Bear : public virtual ZooAnimal { ... };
class Raccoon : virtual public ZooAnimal { ... };
虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系。
虚拟继承提供了按引用组合,也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性
如果一个类可以被指定为基类,那么我们就可以将它指定为虚拟基类,而且它可以包含非虚拟基类支持的所有元素
直接派生类实例的声明和实现与非虚拟派生的情形相同,只是要用到关键字virtual
4.2、特殊的初始化语义
如果在一个派生类中有一个或多个虚拟基类间接出现,那么它就需要有特殊的初始化语义
在非虚拟派生中,派生类只能显式初始化其直接基类。然而,在虚拟派生中,只有最终基类可以直接调用其虚拟基类的构造函数
虚拟基类的初始化变成了最终派生类的责任,这个最终派生类是由每个特定类对象的声明来决定的
当一个最终派生类对象被初始化时,在中间派生类的构造函数执行过程中,它们对于虚拟基类构造函数的调用不再被执行;虚拟基类构造函数被调用时,其实参是在最终派生类的初始化表中被指定的。下面是具体实现:
Panda::Panda( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Panda" ),
Bear( name, onExhibit ),
Raccoon( name, onExhibit ),
Endangered( Endangered::environment,
Endangered::critical )
_sleeping( false )
{}
作为中间派生类,所有对虚拟基类构造函数的调用都被自动抑制了
4.3、构造函数与析构函数顺序
无论虚拟基类出现在继承层次中的哪个位置上,它们都是在非虚拟基类之前被构造
编译器按照直接基类在声明中的顺序,来检查虚拟基类的出现情况
一旦调用了虚拟基类的构造函数,则非虚拟基类构造函数就按照声明的顺序被调用
第20章 iostream库
为了在程序中使用iostream库,必须包含相关的头文件如下:
#include <iostream>
输入输出操作是由istream和ostream类提供的(第三个类iostream类同时从istream 和ostream派生,允许双向输入输出)。为了方便,这个库定义了下列三个标准流对象
(1)cin代表标准输入的istream类对象。一般地,cin使我们能够从用户终端读入数据
(2)cout代表标准输出的ostream类对象。一般地,cout使我们能够向用户终端写数据
(3)cerr代表标准错误的ostream类对象。cerr 是导出程序错误消息的地方
输出主要由重载的左移操作符<<来完成。类似地,输入主要由重载的右移操作符>>来完成
除了对用户终端的读写操作之外,iostream库还支持对文件的读写。下列三种类类型提供了文件支持:
(1)ifstream从istream派生,把一个文件绑到程序上用来输入
(2)ofstream 从ostream派生,把一个文件绑到程序上用来输出
(3)fstream 从iostream派生,把一个文件绑到程序上用来输入和输出
为了使用iostream库的文件流组件,我们必须包含相关的头文件
#include <fstream>
C++对于文件的输入输出也支持同样的输入和输出操作符
iostream库还支持内存输入输出,当流被附着在程序内存中的一个字符串上时,我们可以用iostream输入和输出操作符来对它进行读写。可以通过定义下列三种类类型中的一个实例来定义一个iostream字符串对象:
(1)istringstream从istream派生,从一个字符串中读取数据
(2)ostringstream从ostream派生,写入到一个字符串中
(3)stringstream从iostream派生,从字符串中读取或者写入到字符串中
要使用这些类我们必须包含相关的头文件
#include <sstream>
1、输出操作符<<
最常用的输出方法是在cout上应用左移操作符(<<),输出操作符可以接受任何内置数据类型的实参,包括const char*,以及标准库string和complex类类型。iostream库还提供了指针类型的预定义输出操作符,允许显示对象的地址。缺省情况下,这些值以十六进制的形式显示
2、输入
    输入主要由右移操作符(>>)来支持。
3、其他输入/输出操作符
成员函数get()的三种形式:
get(char& ch)从输入流中提取一个字符,包括空白字符,并将它存储在ch中。它返回被应用的istream 对象
ostream成员函数put()提供了另外一种方法,用来将字符输出到输出流中。put()接受char型的实参并返回被调用的ostream类对象
 
get()的第二个版本也从输入流读入一个字符。区别是,它返回该字符值而不是被应用的istream 对象。它的返回类型是int,而不是char,因为它也返回文件尾的标志,该标志通常用-1 来表示,以便与字符集区分开。为测试返回值是否为文件尾,我们将它与iostream 头文件中定义的常量EOF 做比较。被指定用来存放get()返回值的变量,应该被声明为int 类型以便包含字符值和EOF
 
get()的第三个版本具有下列原型:
get(char *sink, streamsize size, char delimiter='/n')
sink 代表一个字符数组,用来存放被读取到的字符。size代表可以从istream 中读入的字符的最大数目。delimiter 表示,如果遇到它就结束读取字符的动作。delimiter 字符本身不会被读入,而是留在istream 中,作为istream 的下一个字符。一种常见的错误是在执行第二个get()之前忘了去掉delimiter。用istream 成员函数ignore()来去掉delimiter,缺省情况下,换行符被用作delimiter
字符读取过程一直进行,直到以下任何一个条件发生。在发生了任何一个条件之后,一个空字符被放在数组中的下一个位置上
size-1 个字符被读入
遇到文件结束符end-of-file
遇到delimiter 字符
get()的返回值是被调用的istream 对象
 
getline()的语法与get()的三参数形式相同,它也返回被调用的istream 对象
getline(char *sink, streamsize size, char delimiter='/n')
因为getline()和get()的三参数形式都可以读入size 个或少于size 个字符,所以我们有必要查询istream,以确定实际读入了多少个字符。istream 成员函数gcount()正好提供了这样的信息,它返回由最后的get()或getline()调用实际提取的字符数。
ostream成员函数write()提供了另外一种方法,可以输出字符数组。它不是输出直到终止空字符为止的所有字符,而是输出某个长度的字符序列,包括内含的空字符。它的函数原型如下:
write( const char *sink, streamsize length )
length 指定要显示的字符个数,write()返回当前被调用的ostream 类对象
与ostream 的write()函数相对应的函数是istream 的read()函数,它的原型被定义如下:
read( char* addr, streamsize size )
read()从输入流中提取size 个连续的字节,并将其放在地址从addr 开始的内存中。gcount()返回由最后一个read()调用提取的字节数,而read()返回当前被调用的istream 类对象
4、重载输出操作符<<
当实现一个类类型时,如果我们希望这个类支持输入和输出操作,那么必须提供重载的输入和输出操作符的实例
重载定义的通用框架如下
// 重载 output 操作符的通用框架
ostream&
operator <<( ostream& os, const ClassType &object )
{
// 准备对象的特定逻辑
// 成员的实际输出
os << // ...
// 返回 ostream 对象
return os;
}
它的第一个实参是一个ostream 对象的引用。第二个一般是一个特定类类型的const 引用,返同类型是一个ostream 引用,且它的值总是该输出操作符所应用的ostream 对象
当输出操作符要求访问非公有成员时,必须将它声明为该类的友元
5、重载输入操作符>>
重载输入操作符>>与重载输出操作符类似
istream& operator>>( istream&, WordCount& );
6、文件输入和输出
    为了打开一个仅被用于输出的文件,我们可以定义一个ofstream输出文件流类对象:
ofstream outfile( "copy.out", ios_base::out );
传递给ofstream 构造函数的实参分别指定了要打开的文件名和打开模式。ofstream 文件可以被打开为输出模式ios_base::out,或附加模式ios_base::app。在缺省情况下,ostream文件以输出模式打开
如果在输出模式下打开已经存在的文件,则所有存储在该文件中的数据都将被丢弃。如果我们希望增加而不是替换现有文件中的数据,则应该以附加模式打开文件,于是,新写到文件中的数据将添加到文件尾部。在这两种模式下,如果文件不存在,程序都会创建一个新文件
在试图读写文件之前,应该先判断它是否已成功打开
if ( ! outFile ) { // 打开失败
cerr << "cannot open "copy.out" for output/n";
exit( -1 );
}
ofstream从ostrearn 类派生且所有ostream 操作都可以应用到一个ofstream 类对象上
 
在定义ifstream 和ofstream 类对象时,我们也可以不指定文件。以后可以通过成员函数open()显式地把一个文件连接到一个类对象上。例如:
ifstream curFile;
// ...
curFile.open( filename.c_str() );
if ( ! curFile ) // 打开失败了吗?
// ...
我们可以通过成员函数close()断开一个文件与程序的连接。例如:
curFile.close();
 
fstream类对象可以打开一个被用于输出或者输入的文件,fstream 类从iostream 类派生而来。fstream类对象还可以打开一个同时被用于输入和输出的文件,例如下面的定义以输入和输出模式打开word.out:
fstream io( "word.out", ios_base::in|ios_base::app );
按位或操作符被用来指定一种以上的模式。通过seekg()或seekp()成员函数,我们可以对fstream 类对象重新定位。这些函数移动到文件中的一个绝对地址,或者从特定位置移动一个偏移。seekg()和seekp()有以下两种形式:
// 设置到文件中固定的位置上
seekg( pos_type current_position );
// 从当前位置向某个方向进行偏移
seekg( off_type offset_position, ios_base::seekdir dir );
在第一个版本中,当前位置被设置为由current_position 指定的某个固定的位置,这里0是文件的开始。
第二种形式使用一个偏移来重新定位文件,该偏移值或者是从当前位置开始计算,或者是到文件开始处的偏移,或者是从文件尾部倒退向后计算,这由第二个实参来指定。dir 可以被设置为以下选项之一:
(1)ios_base::beg 文件的开始
(2) ios_base::cur 文件的当前位置
(3)ios_base::end 文件的结尾
fstream文件中的当前位置由下面两个成员函数之一返回:tellg()或tellp()
// 标记出当前位置
ios_base::pos_type mark = writeFile.tellp();
// ...
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值