序章 快速入门
初窥输入/输出
C++ 并没有直接定义进行输入或输出(I/O)的任何语句,这种功能是由标准库提供的。
本书的大多数例子都使用了处理格式化输入和输出的 iostream 库。
iostream 库的基础是两种命名为 istream 和 ostream 的类型,分别表示输入流和输出流。
流是指要从某种 IO 设备上读入或写出的字符序列。术语“流”试图说明字符是随着时间顺序生成或消耗的。
标准输入与输出对象
标准库默认定义了 4 个 IO 对象。
标准输入 cin
标准输出 cout
标准错误 cerr
执行日志 clog
当我们从 cin 读入时,数据从执行程序的窗口读入,当写到 cin、cerr 或 clog 时,输出写至同一窗口。大部分操作系统都提供了重定向输入或输出流的方法。利用重定向可以将这些流与所选择的文件联系起来。
注意,标准错误cerr 和 标准输入、标准输出、标准日志不一样。
cerr不经过缓存,直接刷新到输出区域。
一个使用IO库的程序
我们可以使用 IO 库来扩充 main 程序,要求用户给出两个数,然后输出它们的和:
#include <iostream>
int main()
{
std::cout << "Enter two numbers:" << std::endl;
int v1, v2;
std::cin >> v1 >> v2;
std::cout << "The sum of " << v1 << " and " << v2
<< " is " << v1 + v2 << std::endl;
return 0;
}
//运行结果:The sum of 3 and 7 is 10
写入到流
该语句的表达式使用输出操作符(<< ),在标准输出上输出提示语:
std::cout << "Enter two numbers:" << std::endl;
操作符将其右操作数写到作为其左操作数的 ostream 对象。
输出操作返回的值是输出流本身。
可以将输出请求链接在一起。输出提示语的那条语句等价于
(std::cout << "Enter two numbers:") << std::endl;
这条语句等价于
std::cout << "Enter two numbers:";
std::cout << std::endl;
endl 是一个特殊值,称为操纵符,将它写入输出流时,具有输出换行的效果,并刷新与设备相关联的用户缓冲区。内核缓冲区再刷新到文件。用户就能看到写入流中的输出。
忘记刷新输出流可能会造成输出停留在缓冲区中,如果程序崩溃,将会导致程序错误推断崩溃位置。
使用标准库中的名字
前缀 std:: 表明 cout 和 endl 是定义在命名空间 std 中的。使用命名空间程序员可以避免与库中定义的名字相同引起冲突。
std::cout 的写法使用了域操作符(scope operator,:: 操作符),表示使用的是定义在命名空间 std 中的cout。
读入流
std::cin >> v1 >> v2;
输入操作符(>> )行为与输出操作符相似。接受一个 istream 对象作为其左操作数,接受一个对象作为其右操作数,它从 istream 操作数读取数据并保存到右操作数中。像输出操作符一样,输入操作符返回其左操作数作为结果。
可以将输入请求序列合并成单个语句。这个输入操作等价于:
std::cin >> v1;
std::cin >> v2;
输入操作的效果是从标准输入读取两个值,将第一个存放在 v1 中,第二个存放在 v2 中。
读入未知数目的输入
假设不知道要对多少个数求和,而是要一直读数直到程序输入结束:
while (std::cin >> value)
sum += value;
从键盘输入文件结束符操作系统使用不同的值作为文件结束符。Windows 系统下“ctrl”+“z”,来输入文件结束符。Unix 系统中,通常用 “ctrl”+“d”。文件结束符EOF
除了输入操作符,还有一个有用的 string IO 操作:getline()
string line;
//读输入直到碰到结束符
while (getline(cin, line))
cout << line << endl;
若要添加换行符来逐行输出需要自行添加。用 endl 来输出一个换行符并刷新输出缓冲区。
第一章 变量和基本类型
1.1 基本内置类型
C++ 算术类型
类型 含义 最小存储空间
bool 布尔 NA(大多数编译器bool占1字节)
char 字符 8 bits
wchar_t 宽字符 16 bits
short 短整型 16 bits
int 整型 32 bits
long 长整型 32/64 bits
float 单精度浮点数 6 有效数字 32位
double 双精度浮点数 10 有效数字 64位
long double 长双精度浮点数 10 有效数字 80位
赋值时数值越界的情况:
对于无符号类型,编译器会将该值求模。mod,取余数并向负无穷舍入。C++允许将负数赋给无符号数,会对负数求模后赋给变量。
比如 -1 赋给无符号16位类型,就是 -1 MOD 255 = -1...256。取值256。
对于有符号类型,由编译器决定实际赋的值,一般也是取模。
当正负号不同时,求模MOD运算和取余%运算不相同,求模向负无穷舍入,取余向零舍入。
整型运算时,用 32 位表示 int 类型和用 64 位表示 long 类型的机器会出现应该选择 int 类型还是 long 类型的难题。在这些机器上,用 long 类型进行计算所付出的运行时代价远远高于用 int 类型进行同样计算的代价,所以选择类型前要先了解程序的细节并且比较 long 类型与 int 类型的实际运行时性能代价。
决定使用哪种浮点型就容易多了:使用 double 类型基本上不会有错。
在 float 类型中隐式的精度损失是不能忽视的,而 double 类型精度代价相对于 float 类型精度代价可以忽略。事实上,有些机器上,double类型比 float 类型的计算要快得多。long double 类型提供的精度通常没有必要,而且还需要承担额外的运行代价。
字面值
像 42 这样的值,在程序中被当作字面值常量。常量代表它的值不能修改。
每个字面值都有相应的类型, 例如:0 是 int 型,3.14159 是 double 型。
只有内置类型存在字面值。
整形字面值规则
定义字面值整数常量可以使用以下三种进制中的任一种:
十进制、八进制和十六进制。
20 // decimal
024 // octal
0x14 // hexadecimal
字面值整数常量的类型默认为 int 或 long 类型。其精度类型决定于字面值——其值适合 int 就是 int 类型,比 int 大的值就是 long 类型。
通过增加后缀,能够强制将字面值整数常量转换为 long、unsigned 或 unsigned long 类型。通过在数值后面加 L的大小写指定常量为 long 类型。
可通过在数值后面加 U的大小写 定义 unsigned 类型。
同时加 L 和 U就能够得到 unsigned long 类型的字面值常量。
128u /* unsigned */
1024UL /* unsigned long */
1L /* long */
8Lu /* unsigned long */
没有 short 类型的字面值常量。
浮点字面值规则
通常可以用十进制或者科学计数法来表示浮点字面值常量。
使用科学计数法时,指数用 E 或者 e 表示。默认的浮点字面值常量为 double 类型。在数值的后面加上 F 或 f 表示强转为单精度float。同样加上 L 或者 l 表示强转为双精度。
下面每一组字面值表示相同的值:
3.14159F .001f 12.345L 0.
3.14159E0f 1E-3F 1.2345E1L 0e0
布尔字面值和字符字面值
单词 true 和 false 是布尔型的字面值。
字符型字面值通常用一对单引号来定义:
'a' '2' ',' ' ' // blank
这些字面值都是 char 类型。在字符字面值前加 L 就能够得到 wchar_t 类型的宽字符字面值。
L'a'//强转为 wchar_t类型
字符串字面值
整型字面值、浮点字面值、布尔字面值和字符字面值都有基本内置类型。
而字符串字面值是一串常量字符。
为了兼容 C 语言,C++ 中所有的字符串字面值都由编译器自动在末尾添加一个空字符'\0'。
在字符串字面值前加 L 就能够得到 wchar_t 类型的宽字符串字面值。以宽字符'\0'结束。
L"a wide string literal"
字符串字面值的连接
两个相邻的仅由空格、制表符或换行符分开的字符串字面值(或宽字符串字面值),可连接成一个新字符串字面值。
如果连接字符串字面值和宽字符串字面值,其结果是未定义的。可能执行也可能崩溃。
非打印字符的转义序列
有些字符是不可打印的。比如退格或者控制符。还有一些语言中有特殊意义的字符,例如单引号、双引号和反斜线符号。
不可打印字符和特殊字符都用转义字符书写。转义字符都以反斜线符号开始,C++ 语言中定义了如下转义字符:
换行符 \n 回车符 \r 反斜线 \\ 疑问号 \? 单引号 \' 双引号 \" 等
也可以用 八进制、十六进制 转 成 ASCII码 来代替符号进行转义。
1.2 什么是变量
左值和右值
左值可以出现在赋值语句的左边或右边。
右值只能出现在赋值的右边,不能出现在赋值语句的左边。
变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。
初始化
C++ 支持两种初始化变量的形式:复制初始化和直接初始化。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:
int ival(1024); // 直接初始化
int ival = 1024; // 复制初始化
使用多个初始化式
初始化内置类型的对象只有一种方法:提供一个值,并且把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有差别。
1.3 const 限定符
const 变量不允许修改,会导致编译错误。
与其他类型不同,const 变量默认是定义为文件的局部私有变量,不能被其他文件访问。
通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象。
非 const 变量默认为 extern。
// 文件一
// 定义一个其他文件可以访问的 const busSize
extern const int bufSize = fcn();
// 文件二
extern const int bufSize; // uses bufSize from file_1
// 使用文件一定义的Bufsize
for (int index = 0; index != bufSize; ++index)
// ...
const 变量默认是定义该变量的文件的局部变量。
允许 const 变量定义在头文件中。由于const是私有变量,因此引用该头文件的每个文件都有了自己的 const 变量,其名称和值都一样。
如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。应在头文件中为它添加 extern 声明,以使其能被多个文件共享。
1.4 引用
引用就是对象的别名。
引用是一种复合类型,通过在变量名前添加“&”符号来定义。
复合类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都“关联到”某一其他类型。不能定义引用类型的引用,但可以定义任何其他类型的引用。
引用必须在定义时用同类型的对象初始化。
int ival = 1024;
int &refVal = ival; // ok: refVal refers to ival
int &refVal2; // error: a reference must be initialized
int &refVal3 = 10; // error: initializer must be an object
对引用的所有操作都作用在该引用绑定的对象上。
在一个类型定义行中定义多个引用,必须在每个引用标识符前添加“&”符号。
int &r = i, r2 = i2; //r是引用 r2是
int &r3 = i3, &r4 = i2; // 定义两个引用
const 引用要用同类型 const变量初始化。
1.5 typedef 名字
typedef 可以用来定义类型的同义词:
typedef double wages; // 把wages定义成double的同义词
typedef wages salary; // 把salary定义成wages的同义词
1.6 enum 枚举
可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式。
1.7 class和struct
如果使用 class 关键字来定义类,任何成员都隐式指定为 private;
如果使用 struct 关键字,那么这些成员都是public。
使用 class 还是 struct 关键字来定义类,仅仅影响默认的初始访问级别。
1.8 预处理器和头文件
#include 只接受头文件作为参数。预处理器用指定的头文件内容替代每个#include。
可以用 #ifndef xxx # define xxx #endif 来避免多重包含。
自定义头文件使用 " " 双引号。会先去用户目录找,然后找系统工作目录。
byte(字节) 最小的可寻址存储单元,大多数机器上一个字节 8 位。
第二章 标准库类型
除基本数据类型外,C++ 还定义了一个内容丰富的抽象数据类型标准库。
最重要的标准库类型是 string 和 vector,它们分别定义了大小可变的字符串和集合。
string 和 vector 往往将迭代器用作配套类型,用于访问 string 中的字符,或 vector 的元素。
另一种标准库类型 bitset,提供了一种更方便的处理位的方式。
3.1. 命名空间的 using 声明
直接说明名字来自 std 命名空间,来引用标准库中的名字。例如,需要从标准输入读取数据时,就用 std::cin。这些名字都用了:: 操作符,作用域操作符。
使用 using 声明可以在不需要加前缀 namespace_name:: 的情况下访问命名空间中的名字。
using namespace::name;
一旦使用了 using 声明,我们就可以直接引用名字,而不需要再引用该名字的命名空间。
一个 using 声明一次只能作用于一个命名空间成员。
// 使用标准库命名空间声明类型名
using std::cin;
using std::cout;
using std::endl;
3.2 标准库string类型
3.2.1 string 类型常见操作
标准库 string 类型
#include <string>
using std::string;
string 对象的定义和初始化
string 标准库支持几个构造函数。
没有明确指定对象初始化式时,系统将使用默认构造函数。
#include <string.h>
string s1;// 默认构造函数 s1 为空串
string s2(s1);// 将 s2 初始化为 s1 的一个副本
string s3("value");// 将 s3 初始化为一个字符串字面值副本
string s4(n, 'c'); //将 s4 初始化为字符 'c' 的 n 个副本
为了与 C 语言兼容,字符串字面值与标准库 string 类型不是同一种类型。
字符串字面值是 const string。
注意,string 在C++中是一个类。有自己的构造函数,和多种操作方法。
string 对象的操作
s.empty() //如果 s 为空串,则返回 true,否则返回 false。
s.size() //返回 s 中字符的个数
s[n] //返回 s 中位置为 n 的字符,位置从 0 开始计数
s1 + s2 //把 s1 和 s2 连接成一个新字符串,返回新生成的字符串
s1 = s2 //把 s1 内容替换为 s2 的副本
v1 == v2 //比较 v1 与 v2 的内容,相等则返回 true,否则返 v1 == v2 回 false
string 的 size 和 empty 操作
string.size() 获取字符串的字节长度,不包括‘\0’。
任何存储 string 的 size 操作结果的变量必须为 string::size_type 类型。特别重要的是,size_type类型是 unsigned int,与 int 混用时记得强转成同类型。
无符号数和有符号数不要混用。无符号数和有符号数运算会转成无符号数类型。
string 类类型和许多其他库类型都定义了一些配套类型。
通过这些配套类型,库类型的使用就能与机器无关。
string::size_type 就是这些配套类型中的一种。它定义为与 unsigned 型具有相同的含义,而且可以保证足够大能够存储任意 string 对象的长度。
任何存储 string 的 size 操作结果的变量必须为 string::size_type 类型。
特别重要的是,还要把 size 的返回值赋给一个 int 变量。
size_type 存储的 string 长度是 int 所能存储的两倍,也就是 unsigned int。
有 16 位 int 型的机器上,int 类型变量最大只能表示 32767 个字符的 string 个字符的 string 对象。为了避免溢出,安全的方法就是使用标准库类型 string::size_type。
string 关系操作符
关系操作符 <,<=,>,>= 分别用于测试一个 string 对象是否小于、小于或等于、大于、大于或等于另一个 string 对象。按照ASCII码。
对象可以通过使用加操作符 + 或者复合赋值操作符 += 连接起来。
从 string 对象获取字符
string 类型通过下标操作符([ ])来访问 string 对象中的单个字符。
下标需要取一个 size_type 类型的值,来标明要访问字符的位置。这个下标中的值通常被称为“下标”或“索引”(index)。
s[s.size() - 1] 则表示 s 的最后一个字符。
任何可产生整型值的表达式可用作下标操作符的索引。比如 s[a*b] = val。但请始终牢记,C++的字符串下标是 string::size_type类型。
3.2.2 string 对象中字符的处理
我们经常要对 string 对象中的单个字符进行处理,例如,通常需要知道某个特殊字符是否为空白字符、字母或数字。
这些函数都在 cctype 头文件中定义。
表 3.3. cctype 中的函数
C++中,C标准库头文件以cname形式(如cctype)存在,它们位于std命名空间内,与C的name.h(如ctype.h)内容相同但命名方式不同。推荐在C++程序中使用cname形式,以避免命名冲突,保持命名空间一致性。
3.3. 标准库 vector 类型
我们把 vector称为容器。一个容器中的所有对象都必须是同一种类型的。
#include <vector>
using std::vector;
vector 是一个类模板。使用模板可以编写类定义或函数定义,用于多个不同的数据类型。因此,我们可以定义保存 string 对象的 vector,保存 int 值的 vector,保存自定义类对象的vector。
以 vector 为例,必须说明 vector 保存何种对象的类型,通过将类型放在类模板名称后面的尖括号中来指定类型:
#include <vector>
using std::vector;
vector<int> ivec; // vector容器,保存int对象
vector<Sales_item> Sales_vec; // vector容器,保存类对象
vector 不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。vector 类型的每一种都指定了其保存元素的类型。因此,vector<int> 和 vector<string> 都是数据类型。
3.3.1. vector 对象的定义和初始化
vector 类定义了好几种构造函数,用来定义和初始化 vector对象。
vector<T> v1; // vector 保存类型为 T 对象。
//默认构造函数 v1 为空。
vector<T> v2(v1); //拷贝构造函数。v2 是 v1 的一个副本。
vector<T> v3(n, i); //填充构造函数。v3 包含 n 个值为 i 的元素。
vector<T> v4(n); //值初始化构造函数。创建一个包含 n 个 T类型元素的Vector,每个元素被初始化成0
3.3.2. vector 对象动态增长
虽然可以对给定元素个数的 vector 对象预先分配内存,但更有效的方法是先初始化一个空 vector 对象,然后再动态地增加元素。
3.3.3. vector 对象的 size
vector的 empty 和 size 操作类似于 string 的相关操作。成员函数 size() 返回相应 vector 类定义的 size_type 的值。
使用 size_type 类型时,必须指出该类型是在哪里定义的。
vector 类型的 size_type 的命名空间总是包括 vector 的元素类型:
vector<int>::size_type // ok
在C++中,std::vector<T>::size_type 是一个类型别名,它表示能够存储 std::vector<T> 容器大小的无符号整数类型。这个类型通常是一个无符号整数类型,比如 std::size_t,但具体类型取决于编译器和库的实现。
使用 std::vector<int>::size_type 而不是直接使用如 int类型去存储容器的大小。
3.3.4. 向 vector 添加元素
push_back() 操作接受一个元素值,并将它作为一个新的元素添加到 vector对象末尾。
text.push_back(word);
3.3.5. vector 的下标操作
使用下标操作符来获取元素。
注意 Vector的下标操作不能添加元素。
关键概念:安全的泛型编程
C++ 程序员习惯于优先选用 != 而不是 < 来编写循环判断条件。
调用 size 成员函数而不保存它返回的值,反映了一种良好的编程习惯。
vector<int> ivec;
for (vector<int>::size_type ix = 0; ix != 10; ++ix)
ivec.push_back(ix); // 正确。使用Push函数给容器添加值
//ivec[ix] = ix; //错误的用法,容器不能用下标添加元素
循环可以容易地增加新元素,如果确实增加了新元素的话,那么测试已保存的 size 值作为循环的结束条件就会有问题。
我们倾向于在每次循环中判断 size的当前值,而不是在进入循环前存储 size 值的副本。
必须是已存在的元素才能用下标操作符进行索引。
通过下标操作给容器进行赋值时,不会添加任何元素。
3.4. 迭代器简介
除了使用下标访问 vector 对象的元素外,标准库还提供了迭代器(iterator)来访问元素。
迭代器是一种检查遍历容器内元素的数据类型。
迭代器类型提供了比下标操作更通用化的方法:所有的标准库容器都定义了相应的迭代器类型,而只有少数的容器支持下标操作。
3.4.1. 容器的 iterator 类型
每种容器类型都定义了自己的迭代器类型,如 vector:
vector<int>::iterator iter;
定义了一个名为 iter 的变量,它的数据类型是 vector<int> 定义的 iterator 类型。
每个标准库容器类型都定义了一个名为 iterator 的类型。
3.4.2. begin 和 end 操作
每种容器都定义了一对命名为 begin 和 end 的函数,用于返回迭代器。begin 返回的迭代器指向第一个元素。end 返回迭代器指向的(末端元素+1)。
vector<int>::iterator iter = ivec.begin();
3.4.3. 迭代器的自增和解引用
迭代器类型可使用解引用操作符(*)来访问迭代器所指向的元素。
解引用后迭代器自增++或者自减--改的是值。没有解引用的话改的就是指向的下标。
*iter = 0;
*iter++; //元素++
iter++; //下标++
假设 iter 指向 vector 对象 ivec 的第一元素,那么 *iter 和 ivec[0] 就是指向同一个元素。
由于 end 操作返回的迭代器不指向任何元素,因此不能对它进行解引用或自增操作。
3.4.4. 迭代器的其他操作
用 == 或 != 操作符来比较两个迭代器,如果两个迭代器对象指向同一个元素,则它们相等,否则就不相等。
/*迭代器做循环*/
for (vector<int>::iterator iter = ivec.begin(); //让迭代器指向第一个元素
iter != ivec.end(); //判断迭代器是不是走完了元素
++iter)
*iter = 0; //给迭代器指向的元素赋值
3.4.5. 常量迭代器 const_iterator
每种容器类型还定义了一种名为 const_iterator 的类型,常量迭代器,只能用于读取容器内元素,但不能改变其值。
我们对普通 iterator 类型解引用时,得到对某个元素的非 const 对象的引用。
我们对 const_iterator 类型解引用时,则可以得到一个指向 const 对象的引用。
使用 const_iterator 类型时时我们得到一个迭代器,它自身的值可以改变,但不能用来改变其所指向的元素的值。可以对迭代器进行自增以及使用解引用操作符来读值,但不能对该元素赋值。
不要把 const_iterator 对象与 const 类型的 iterator 对象混淆起来。
const_iterator 是常量迭代器,指向的内容是常量。
const vector<xxx>::iterator 是常量类型的迭代器。迭代器自身是常量。
声明一个 const 类型的迭代器时必须初始化。且初始化后就不能改变它的值。
vector<int> nums(10); //定义一个10个元素的vector<int>类型容器
const vector<int>::iterator cit = nums.begin(); //定义一个常量迭代器
*cit = 1; // 常量迭代器的值不是常量,可以改变
++cit; // 常量迭代器初始化后不能改变
3.4.6. 迭代器的算术操作
迭代器 iterator 除了支持增量操作符来控制检索位置外,还支持加减一个整型值来调整检索位置。
支持两个迭代器相减来计算间隔距离,该距离是名为 difference_type 的 signed 类型的值。
任何改变 vector 长度的操作都会使已存在的迭代器失效。例如,在调用 push_back 之后,就不能再信赖指向 vector 的迭代器的值了。
3.5. 标准库 bitset 位集
有些程序要处理二进制位的有序集,每个位可能包含 0(关)1(开)值。
标准库提供的 bitset 类简化了位集的处理。要使用 bitset 类就必须包含相关的头文件。
#include <bitset>
using std::bitset;//声明 bitset的命名空间。
3.5.1. bitset 对象的定义和初始化
类似于 vector,bitset 类是一种类模板;而与 vector 不一样的是 bitset 类型对象的区别仅在其长度而不在其类型。在定义 bitset 时,要明确 bitset 含有多少位,在尖括号内给出它的长度值:
bitset<32> bitvec; // 32 位,初始化全0
bitset 对象的构造函数:
bitset<n> b; //b 有 n 位,每位都 0
bitset<n> b(u); //b 是 unsigned long 型 u 的一个副本
bitset<n> b(s); //b 是 string 对象 s 中含有的位串的副本
bitset<n> b(s, pos, n); //b 是 字符串对象 s 中从位置 pos 开始的; n 个位的副本
//省略n就代表后续所有字符
bitset 类模板定义时给出的长度值必须是常量表达式。
用 unsigned 值初始化 bitset 对象
当用 unsigned long 值作为 bitset 对象的初始值时,该值将转化为二进制的位模式。
如果 bitset 类型长度大于 unsigned long 值的二进制位数,则其余的高阶位将置为 0;
如果 bitset 类型长度小于 unsigned long 值的二进制位数,超过长度的高阶位将被丢弃。
用 string 对象初始化 bitset 对象
当用 string 对象初始化 bitset 对象时,string 对象直接表示为位模式。
从 string 对象读入位集的顺序是从右向左。
string strval("1100"); //定义一个数值字符串
bitset<32> bitvec4(strval);//数值字符串直接转2进制,从右向左存储。bitset的0位置放1,1位置放1,其他放0
string 对象和 bitsets 对象之间是反向转化的:string 对象的最右边字符用来初始化 bitset 对象的低阶位。
可以只用某个子串作为初始值:
string str("1111111000000011001101");
bitset<32> bitvec5(str, 5, 4); //用字符串第5个字符开始的4个长度
bitset<32> bitvec6(str, str.size() - 4); //用字符串倒数第4个字符开始的字串
3.5.2. bitset 对象上的操作
bitset 操作用来测试或设置 bitset 对象中的单个或多个二进制位。
第四章 数组和指针
4.1 数组和指针常见问题
C++ 语言提供了两种类似于 vector 和迭代器 iterator 类型的低级复合类型——数组和指针。
数组的长度是固定的。数组一经创建不允许添加新的元素。
指针则可以像迭代器一样用于遍历和检查数组中的元素。
C++ 程序应尽量使用 vector 和迭代器类型,而避免使用低级的数组和指针。设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。
数组没有获取其容量大小的 size 操作,也不提供 push_back 操作在其中自动添加元素。
如果需要更改数组的长度,程序员只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组空间中去。
不允许数组直接复制和赋值
与 vector 不同,一个数组不能用另外一个数组初始化,也不能将一个数组赋值给另一个数组,这些操作都是非法的:
int ia[] = {0, 1, 2}; // ok: array of ints
int ia2[](ia); //不能用一个数组初始化另一个数组
int main()
{
const unsigned array_size = 3;
int ia3[array_size]; // 可以这么定义,但元素没有初始化
ia3 = ia; // 不能将一个数组赋值给另一个数组
return 0;
}
指针的引入
vector 的遍历可使用下标或迭代器实现。
数组的遍历可使用下标或指针实现。指针是数组的迭代器,指向数组的一个元素,同样支持解引用操作符(*) 和自增操作符(++),支持 两个指针指向同一块连续内存单元的指针相减,指针相减=(地址1-地址2)/sizeof(类型),也就是相差的元素个数。
指针和迭代器都提供指向对象的间接访问方式。指针指向单个对象,而迭代器只用于访问容器内的元素。
C++中应尽量避免使用指针和数组。
应采用 vector类型和迭代器取代一般的数组、采用 string 类型取代 C 风格字符串。
指针和 typedef
在 typedef 中使用指针往往会带来意外的结果。
下面是一个几乎所有人刚开始时都会答错的问题。假设给出以下语句:
typedef string *pstring;
const pstring cstr;
请问 cstr 变量是什么类型?
声明 const pstring 时,const 修饰的是 pstring 的类型,这是一个指针。
因此,该声明语句应该是把 cstr 定义为指向 string 类型对象的 const 指针,等价于:
string *const cstr; // 等价于指针常量
可以理解为:首先 cstr是一个常量,其次是一个pstring定义的类型,也就是 string *类型,
因此得到的是 指针常量 string *const cstr。
typedef并不是简单的文本替换。而是意义上的别名。
4.2 C风格字符串
C 风格字符串既不能确切地归结为 C 语言的类型,也不能归结为 C++ 语言的类型,而是以空字符 null 结束的字符数组:
char ca1[] = {'C', '+', '+'}; //结尾不是'\0',不是C风格字符串
char ca2[] = {'C', '+', '+', '\0'}; //声明了'\0'
char ca3[] = "C++"; //自动添加'\0'
const char *cp = "C++"; //自动添加了'\0'
char *cp1 = ca1; //指针
char *cp2 = ca2; //指针
C++ 语言通过(const)char* 类型的指针来操纵 C 风格字符串。直到结束符 ‘\0’ 为止。
const char *cp = "some value";
while (*cp) {
++cp;
}
如果使用输入操作符 >> 或者 输出操作符 <<的对象字符串没有结束符,则结果不可预料。
C 风格字符串的标准库函数
cstring 是 string.h 头文件的 C++ 版本,而 string.h 则是 C 语言提供的标准库。
传递给这些标准库函数例程的指针必须具有非零值,并且指向以 '\0' 结束的字符数组中的第一个元素。
使用 C++ 标准库类型 string,要比使用 C风格标准库 string.h 更加安全。因为C++提供的数据类型会自动扩容,一般是自动分配更大的内存空间。
4.3 动态数组
C 语言程序使用一对标准库函数 malloc 和 free 在自由存储区中分配存储空间,而 C++ 语言则使用 new 和 delete 表达式实现相同的功能。
动态分配数组时,只需指定类型和数组长度,不必为数组对象命名,new 表达式返回指向新分配数组的第一个元素的指针:
int *pia = new int[10]; // array of 10 uninitialized ints
此 new 表达式分配了一个含有 10 个 int 型元素的数组,并返回指向该数组第一个元素的指针,此返回值初始化了指针 pia。
在堆中创建的数组对象是没有名字的,程序员只能通过其地址间接地访问堆中的对象。
初始化动态分配的数组
动态分配数组时,如果数组元素是类,将使用该类的默认构造函数实现初始化;如果数组元素是内置类型,则无初始化。
string *psa = new string[10]; //10个空字符的字符串
int *pia = new int[10]; //10个未初始化的int
也可使用跟在数组长度后面的一对空圆括号,对数组元素做值初始化。
int *pia2 = new int[10] (); //在动态分配的内置类型数组后跟圆括号,进行初始化,全0
动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。
const 对象的动态数组
定义const类型的动态数组时必须初始化。
const int *pci_ok = new const int[100]();//定义常量动态数组,定义时使用圆括号初始化为默认值
C++ 允许定义类类型的 const 数组,但该类类型必须提供默认构造函数:
const string *pcs = new const string[100];//string类类型的数组
允许动态分配空数组
C++ 虽然不允许定义长度为 0 的数组。
但允许使用 new 动态创建长度为 0 的数组。动态分配的空数组不允许解引用。动态分配的空数组允许在指针上加减0,或者减去本身,得0值。
int* p = new int[0];//动态分配一个空数组,元素个数为0
动态空间的释放
C++ 语言为指针提供 delete [] 表达式释放指针所指向的数组空间:
delete [] pia;//释放pia指向的数组空间
在关键字 delete 和指针之间的空方括号对是必不可少的:它告诉编译器该指针指向的是自由存储区中的数组,而并非单个对象。
如果遗漏了空方括号对,这是一个编译器无法发现的错误,至少会导致运行时少释放了内存空间,从而产生内存泄漏。
使用 delete 删除指针后,指针变成悬挂指针。该指针指向的内存已经被释放。一旦删除了指针所指向的对象,应立即将指针置为 0,C++中指针置0或nullptr代表空指针。
混合使用C++标准库类string 和C风格字符串
可以使用 C 风格字符串对 string 对象进行初始化或赋值。
string 类型的加法操作需要两个操作数,可以使用 C 风格字符串作为其中的一个操作数,也允许将 C 风格字符串用作复合赋值操作的右操作数。
反之则不成立:在要求 C 风格字符串的地方不可直接使用标准库 string 类型对象。例如,无法使用 string 对象初始化字符指针。
string 类提供了一个名为 c_str() 的成员函数,来将C++字符串转C风格字符串:
char *str = st2.c_str();//返回C风格字符串,并以'\0'结束
该数组存放了与 string 对象相同的内容,并且以结束符 '\0' 结束。
使用数组初始化 vector 对象
使用数组初始化 vector 对象,必须指出用于初始化的第一个元素以及数组最后一个元素的下一个元素的位置的地址。被标出的元素范围可以是数组的子集。
const size_t arr_size = 6; //定义一个size_t的下标类型
int int_arr[arr_size] = {0, 1, 2, 3, 4, 5}; //数组
//使用数组初始化vector对象
vector<int> ivec(int_arr, //数组
int_arr + arr_size);//数组最后一个元素的下一个元素的位置
多维数组的初始化
和处理一维数组一样,可以使用花括号括起来的初始化式列表来初始化多维数组。
与一维数组一样,可以只对数组的部分元素初始化,其他元素将自动初始化为默认值。
int ia[3][4] = {{ 0 } , { 4 } , { 8 } };//初始化了每行的第一个元素,其他默认0
int ia[3][4] = {0, 3, 6, 9};//初始化了第一行的元素,其他默认0
如果表达式只提供了一个下标,则结果获取的元素是该行下标索引的内层数组。
如 ia[2] 将获得 ia 数组的最后一行,即这一行的内层数组本身,而并非该数组中的任何元素。
用 typedef 简化指向多维数组的指针
以下程序用 typedef 为数组元素类型定义新的类型名:
/*注意,typedef 用在数组上起别名的时候,
可以理解声明了一个新类型,这个类型就是int[4],而int_arr就是这个新类型的名字*/
typedef int int_array[4];
/*声明一个 int[4]类型的指针,*/
int_array *ip = ia;
注意,typedef 用在数组上起别名的时候,可以理解声明了一个新类型,这个类型就是int[4],而 int_arr 就是这个新类型的名字。
第五章 表达式
C++ 还支持操作符重载,允许程序员自定义用于类类型时操作符的含义。标准库正是基于操作符重载定义用于库类型的操作符。
C++提供了一元操作符和二元操作符,和一个三元操作符 : ?。
作用在一个操作数上的操作符称为一元操作符,如取地址操作符(&)和解引用操作符(*);而二元操作符则作用于两个操作数上,如加法操作符(+)和减法操作符(-)。除此之外,C++ 还提供了一个使用三个操作数的三元操作符。
算术操作符溢出导致的结果一般都是按二进制位数"截断"。
对于存储类类型的容器的迭代器或指针,注意解引用操作符的优先级低于点操作符。
Sales_item *sp = &item1; //定义一个指针
(*sp).same_isbn(item2); //通过指针使用类的成员
因为编程时很容易忘记圆括号,而且这类代码又经常使用,所以 C++ 为在点操作符后使用的解引用操作定义了一个同义词:箭头操作符(->)。
假设有一个指向类类型对象的指针(或迭代器),下面的表达式相互等价:
(*p).foo; //通过指针的点操作符使用类变量的属性
p->foo; //通过指针的箭头操作符使用类变量的属性
sizeof 是操作符,用来获得变量所占的字节数。
5.1 隐式转换
隐式转换
整型和浮点型一起运算,整型会隐式转换成浮点型。如果赋值操作的左右操作数类型不相同,则右操作数会被转换为左边的类型。
int ival = 0;
ival = 3.541 + 3; // 通常编译会产生一个警告
算术转换
算术转换规则定义了一个类型转换层次,该层次规定了操作数应按什么次序转换为表达式中最宽的类型。
最简单的转换为整型提升:对于所有比 int 小的整型,它们就会被提升为 int 型,否则将被提升为 unsigned int。如果将 bool 值提升为 int ,则 false 转换为 0,而 true 则转换为 1。
有符号与无符号类型之间的转换
若表达式中使用了无符号( unsigned )数值,所定义的转换规则需保护操作数的精度。unsigned 操作数的转换依赖于机器中整型的相对大小,因此,这类转换本质上依赖于机器。
包含 short 和 int 类型的表达式, short 类型的值转换为 int 。
如果 int 型足够表示所有 unsigned short 型的值,则将 unsigned short 转换为 int。否则两个操作数均转换为 unsigned int 。
bool flag; char cval;
short sval; unsigned short usval;
int ival; unsigned int uival;
long lval; unsigned long ulval;
float fval; double dval;
3.14159L + 'a'; // a先转成int,再转成长整型
dval + ival; // int转换成double
dval + fval; // float 转换成Double
ival = dval; // double截断成int
flag = dval; // 如果double是0就转成 fasle,double是1就转成 true
cval + fval; // char转int,然后int转float
sval + cval; // short和char都转int
cval + lval; // char转long
ival + ulval; // int转unsigned long
usval + ival; // 无符号+有符号,int容得下就都转int,int容不下就都转unsigned int
uival + lval; // 无符号+有符号,long容得下就都转long,long容不下就都转unsigned long
指针隐式转换
在使用数组时,大多数情况下数组都会自动转换为指向第一个元素的指针。
int ia[10]; // 数组
int* ip = ia; // 数组隐式转换成指针,因此可以赋给指针
C++ 还提供了另外两种指针转换:指向任意数据类型的指针都可转换为 void* 类型;整型数值常量 0 可转换为任意指针类型。
int* ptr1 = 0; // 使用整型常量0初始化指针
int* ptr2 = nullptr; // 使用nullptr初始化指针,C++11及以后推荐的方式
// 两者都表示ptr1和ptr2不指向任何有效的int对象
算术类型与bool类型的转换
算术值和指针值都可以转换为 bool 类型。如果指针或算术值为 0,则其 bool 值为 false ,而其他值则为 true。空字符 '\0' 被转换成 0 值。其他字符则转换为 true。
if (cp);
while (*cp);
5.2 显式转换
5.2.1 强制类型转换符
//将 double强制转换为 int 。会出现截断
static_cast<int>(dval);
命名的强制类型转换符号的一般形式如下:
xxx-cast<转换的目标类型>(被转换的值);
其中 cast-name 为 static_cast、dynamic_cast、const_cast 和 reinterpret_cast 之一,type 为转换的目标类型,而 expression 则是被强制转换的值。强制转换的类型指定了在 expression 上执行某种特定类型的转换。
dynamic_cast
将基类指针或引用转换为派生类指针或引用。父子类强转用。
const_cast
转换掉表达式的 const 性质。常量强转用。
static_cast
编译器隐式执行的任何类型转换都可以由 static_cast 显式完成。强转用。
reinterpret_cast
不同基本类型的指针的强制转换。指针强转用。
强制类型转换告诉程序的读者和编译器:我们不关心精度损失。
从一个较大的算术类型到一个较小类型的赋值,编译器通常会产生警告。显式地提供强制类型转换时,警告信息就会被关闭。
int *ip;
char *pc = reinterpret_cast<char*>(ip);//将int*转为char*表达,但用法还是int用法
在引入命名的强制类型转换操作符之前,显式强制转换用圆括号将类型括起来实现:
char *pc = (char*) ip;
效果与使用 reinterpret_cast 符号相同,但这种强制转换的可视性比较差,难以跟踪错误。
第六章 语句
6.1 try 块和异常处理
6.1.1 throw 表达式
系统通过 throw 表达式抛出异常。
if (!item1.same_isbn(item2))
throw runtime_error("Data must refer to same ISBN");
6.1.2 try 块
try 块以关键字 try 开始,后面是用花括号起来的语句序列块。
try 块后面是一个或多个 catch 子句。关键字 catch 的圆括号内单个类型或者单个对象的声明——称为异常说明符,以及通常用花括号括起来的语句块。如果选择了一个 catch 子句来处理异常,则执行相关的块语句。一旦 catch 子句执行结束,程序流程立即继续执行紧随着最后一个 catch 子句的语句。
try {
//程序表达式
} catch (exception-specifier) { //捕获的异常类型
//处理函数
} catch (exception-specifier) {
//处理函数
} //...
在复杂的系统中,try 块也可能嵌套。try 块的嵌套处理流程和函数的链式调用不同,如果程序有异常抛出,会先去找匹配的 catch,没找到则终止这个函数的执行。
如果不存在处理该异常的 catch 子句,程序的运行就要跳转到名为 terminate 的标准库函数,该函数在 exception 头文件中定义。这个标准库函数的行为依赖于系统,通常情况下,它的执行将导致程序非正常退出。
6.1.3 标准异常
C++ 标准库定义了一组类,用于报告在标准库中的函数遇到的问题。程序员可在自己编写的程序中使用这些标准异常类。标准库异常类定义在四个头文件中:
1. exception 头文件定义了最常见的异常类,它的类名是 exception。这个类只通知异常的产生,但不会提供更多的信息。
2. stdexcept 头文件定义了几种常见的异常类,这些类型在下表中列出。
3.new 头文件定义了 bad_alloc 异常类型,提供因无法分配内在而由 new 抛出的异常。
4. type_info 头文件定义了 bad_cast 异常类型,这是类型转换时出现的异常。
6.1.4 使用预处理器进行调试
调试用的代码可以 #ifndef NDEBUG 来通过宏定义控制是否处理。
int main()
{
#ifndef NDEBUG
cerr << "starting main" << endl;
#endif
// ...
}
在开发程序的过程中,只要保持 NDEBUG 未定义就会执行其中的调试语句。开发完成后,要将程序交付给客户时,可通过定义 NDEBUG 预处理变量,删除这些调试语句。大多数的编译器都提供定义 NDEBUG 命令行选项:
$ CC -DNDEBUG main.C
预处理器还定义了其余四种在调试时非常有用的常量:
__FILE__ 文件名
__LINE__ 当前行号
__TIME__ 文件被编译的时间
__DATE__ 文件被编译的日期
可使用这些常量在错误消息中提供更多的信息:
if (word.size() < threshold)
cerr << "Error: " << _ _FILE_ _
<< " : line " << _ _LINE_ _ << endl
<< " Compiled on " << _ _DATE_ _
<< " at " << _ _TIME_ _ << endl
<< " Word read was " << word
<< ": Length too short" << endl;
另一个常见的调试技术是使用 NDEBUG 预处理变量以及 assert 预处理宏。
断言 assert 宏是在 cassert 头文件中定义的,所有使用 assert 的文件都必须包含该头文件。
assert(expr)
只要 NDEBUG 未定义,assert 宏就求解条件表达式 expr,如果结果为false,assert 输出信息并且终止程序的执行。如果该表达式有一个非零(例如,true)值,则 assert 不做任何操作。
第七章 函数
const形参
在调用函数时,如果该函数使用非引用的非 const 形参,则既可给该函数传递 const 实参也可传递非 const 的实参。
void fcn(const int i);//将形参定义为非引用的const类型
则在函数中,不可以改变实参的局部副本。
由于实参仍然是以副本的形式传递,因此传递给函数的的既可以是 const 对象也可以是非 const 对象。
这种用法是为了支持对 C 语言的兼容,因为在 C 语言中,具有 const 形参或非 const 形参的函数并无区别。
容器迭代器作为形参
通常,函数不应该有 vector 或其他标准库容器类型的形参。调用含有普通的非引用 vector 形参的函数将会复制 vector 的每一个元素。C++ 程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器。
// 通过传递迭代器来传递容器
void print(vector<int>::const_iterator beg, //迭代器指向开始
vector<int>::const_iterator end) //迭代器指向结束
{
while (beg != end) {
cout << *beg++;
if (beg != end) cout << " "; // no space after last element
}
cout << endl;
}
数组形参
void printArray(int arr[], int size) {
for(int i = 0; i < size; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
数组形参定义为 int arr[x] 不报错,但是运行可能出现问题,建议使用 int arr[ ]。
引用形参
引用形参其实就是传的地址,但是不像指针一样有自己的地址还能解引用。
通过引用形参和函数的引用返回值类型修改实参的返回值。
char &get_val(string &str, string::size_type ix) //这里&str就是传的实参
{
return str[ix];
}
int main()
{
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // 把s[0]改成A
cout << s << endl; // prints A value
return 0;
}
不要觉得惊讶,返回引用的函数返回一个左值 ,该引用是被返回元素的同义词。
get_val(s, 0) = 'A'; // 把s[0]改成A
枚举值作为函数参数
enum Color {
RED,
GREEN,
BLUE
};
void printColor(Color color) {
switch (color) {
case RED:
std::cout << "The color is red." << std::endl;
break;
case GREEN:
std::cout << "The color is green." << std::endl;
break;
case BLUE:
std::cout << "The color is blue." << std::endl;
break;
default:
std::cout << "Unknown color." << std::endl;
}
}
int main() {
printColor(RED);
printColor(GREEN);
printColor(BLUE);
return 0;
}
7.1 内联函数
将函数指定为 inline 函数,意味将它在程序中每个调用点上“内联地” 展开。
内联函数将在编译时展开,而不是运行时通过函数名动态链接到代码区。
内联函数应该在头文件中定义,这一点不同于其他函数。
如果不在头文件中定义,则 inline 函数可能要在程序中定义不止一次,只要 inline 函数的定义在某个源文件中只出现一次,而且在所有源文件中,其定义必须是完全相同的。
7.2 类的成员函数
每个成员函数(除了 static 成员函数外)都有一个隐含的形参 this。在调用成员函数时,形参 this 初始化为调用函数的对象的地址。
const 成员函数
使用 const 的函数称为常量成员函数。由于隐含形参 this 是指向 const 对象的指针,const 成员函数不能修改调用该函数的对象。只能读取而不能修改调用它们的对象的数据成员。
在类外定义成员函数
在类的定义外面定义成员函数必须指明它们是类的成员:
double Sales_item::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
完整的函数名:
Sales_item::avg_price
7.3 重载函数
相同作用域中的两个函数,名字相同而形参表不同,则称为重载函数。
向上类型转换不容易出现歧义。
void ff(int);
void ff(short);
ff('a'); //优先转int类型,遵循类型转换规则
向下类型转换不容易出现歧义。
extern void manip(long);
extern void manip(float);
manip(3.14); //字面值常量 3.14 的类型为 double。这种类型既可转为 long 型也可转为 float 型。
//由于两者都是可行的标准转换,因此该调用具有二义性。
第八章 标准IO库
8.1. 面向对象的标准库
C++ 的输入/输出(input/output)由标准库提供。标准库定义了一族类型,支持对文件和控制窗口等设备的读写(IO)。还定义了其他一些类型,使 string对象能够像文件一样操作,从而使我们无须 IO 就能实现数据与字符之间的转换。
标准库类型不允许做复制或赋值操作。
ifstream 和 istringstream 提供了从文件或字符串中读取数据的能力,继承自 istream。
ofstream 和 ostringstream 提供了向文件或字符串中写入数据的能力,继承自 ostream。
iostream 同时从 istream 和 ostream 继承而来,支持更广泛的数据源,同时支持读写操作。
多态性允许我们编写接受基类引用参数的函数,这些函数能够处理来自不同源(如控制台、文件、字符串)的输入或输出,从而提高了代码的复用性和灵活性。
国际字符的支持
C++标准库不仅提供了处理char类型数据的流类(如istream, ostream, ifstream, ofstream, istringstream, ostringstream等),还定义了一组相对应的类来支持wchar_t类型数据的处理。这些支持宽字符(wchar_t)的类通过在类名前面加上w前缀来与char类型的版本进行区分。
ASCII码是Unicode编码的基础和子集,UTF-8是实现Unicode编码的一种高效且广泛使用的字符编码方式。
UTF-8是一种可变长度的编码方案,用于在计算机中存储Unicode字符。UTF-8之所以是可变长度,因为英文用ASCII码1字节,其他字符2~3字节。
UTF-16是一种定长编码方案,用于在字处理器、文本编辑器和Windows操作系统中表示Unicode字符。
UTF-32是一种定长编码方案,用于在程序中存储和处理Unicode字符。
例如 A在Unicode中的码点是U+0041,U代表Unicode码点,0041是0x0041;UTF-8则规定了Unicode码的二进制表示01000001B,如果是ASCII码的话同样是0041H,但其他字符就不一定了。
UTF-8编码规则如下:
对于码点在U+0000到U+007F之间的字符(即ASCII字符),UTF-8编码使用一个字节表示,且该字节与ASCII编码相同。
对于码点在U+0080到U+07FF之间的字符,UTF-8编码使用两个字节表示。第一个字节以110开头,后面跟5位表示码点的高位;第二个字节以10开头,后面跟6位表示码点低位。
对于码点在U+0800到U+FFFF之间的字符(即BMP内的非ASCII字符),UTF-8编码使用三个字节表示。第一个字节以1110开头,后面跟4位表示码点的高位部分;接下来的两个字节都以10开头,分别跟6位表示码点的中位和低位部分。
宽字符流类:
wostream、wistream、wiostream:
分别对应于ostream、istream、iostream,用于宽字符的输出、输入和输入输出。
wifstream、wofstream、wfstream:
分别对应于文件输入、输出和文件双向流,但处理的是宽字符数据。
wistringstream、wostringstream、wstringstream:
分别对应于字符串输入、输出和字符串双向流,用于处理宽字符字符串。
宽字符标准I/O对象:
wcin:对应于cin,用于宽字符的标准输入。其实是wistream,表示从流读出。
wcout:对应于cout,用于宽字符的标准输出。其实是wostream,表示输入到流。
wcerr:对应于cerr,用于宽字符的错误输出(通常也是标准输出,但不受缓冲机制的影响,立即显示)。其实是wostream的一个实例,但是不经过缓冲区,直接输出显示屏。
这种设计允许开发者在需要处理Unicode字符集或其他宽字符集时,能够方便地利用C++标准库提供的流类,而无需从头开始编写支持宽字符的代码。通过简单地使用带有w前缀的类和对象,就可以无缝地切换到宽字符的输入输出处理。
IO 对象不可复制或赋值
只有支持复制的元素类型可以存储在 vector 或其他容器类型里,由于流对象不能复制,因此不能存储在 容器类型中。
形参或返回类型也不能为流类型。如果需要传递或返回 IO对象,则必须传递或返回指向该对象的指针或引用。
8.2 条件状态
IO 标准库管理一系列条件状态成员,用来标记给定的 IO 对象是否处于可用状态,或者碰到了哪种特定的错误。
/*流状态类型*/
strm::iostate //机器相关的整型名,由各个 iostream 类定义,用于定义条件状态
/*流状态类型的值*/
strm::badbit //strm::iostate 类型的值,用于指出被破坏的流
strm::failbit //strm::iostate 类型的值,用于指出失败的 IO 操作
strm::eofbit //strm::iostate 类型的值,用于指出流已经到达文件结束符
/*流状态类型的操作*/
s.eof() //如果设置了流 s 的 eofbit 值,则该函数返回 true
s.fail() //如果设置了流 s 的 failbit 值,则该函数返回 trues.bad()
//如果设置了流 s 的 badbit 值,则该函数返回 true
s.good() //如果流 s 处于有效状态,则该函数返回 true
s.clear() //将流 s 中的所有状态值都重设为有效状态
s.clear(flag) //将流 s 中的某个指定条件状态设置为有效。flag 的类型是strm::iostate
s.setstate(flag) //给流 s 添加指定条件。flag 的类型是 strm::iostate
s.rdstate() //返回流 s 的当前条件,返回值类型为 strm::iostate
流必须处于无错误状态,才能用于输入或输出。检测流状态最简单的方法是检查其真值:
if (cin)
// ok to use cin, it is in a valid state
while (cin >> word)
// ok: read operation successful ...
所有流对象都包含一个条件状态成员。
这个状态成员为 iostate 类型,这是由各个 iostream 类分别定义的机器相关的整型。该状态成员以二进制位的形式使用,由 setstate 和 clear 操作管理。
每个 IO 类还定义了三个 iostate 类型的常量值:
badbit //表示系统级故障。流不能再使用
failbit //可恢复错误。比如流要的数值类型却输入了字符。
eofbit //文件结束符错误。表示读到了文件的结束符。
流的状态由
bad()
fail()
eof()
good()
操作提示。如果 bad、fail 或者 eof中的任意一个为 true,则检查流本身将显示该流处于错误状态。类似地,如果这三个条件没有一个为 true,则 good 操作将返回 true。
clear 和 setstate 操作用于改变条件成员的状态。
clear() //将全部条件重设为有效状态
set() //将指定条件设置为错误状态
clear 操作将条件重设为有效状态。
如果我们希望把流重设为有效状态,则可以调用 clear 操作。
使用 setstate 操作可打开某个指定的条件,用于表示某个问题的发生。
/*同时设置或清除多个状态二进制*/
is.setstate(ifstream::badbit | ifstream::failbit);
8.3. 输出缓冲区的管理
每个 IO 对象管理一个缓冲区,用于存储程序读写的数据。
os << "please enter a value: ";
下面几种情况将导致缓冲区的内容被刷新,即写入到真实的输出设备或者文件:
程序结束自动刷新输出缓冲区。
缓冲区满时自动刷新。
使用endl、ends或flush显式刷新输出。
通过unitbuf操作符设置流的状态,使得每次输出后刷新缓冲区。
通过tie函数关联输入输出流,读输入时刷新输出缓冲区。
endl 是在输出流中插入一个换行符\n,并刷新缓冲区。
ends 是在输出流中插入一个空字符NULL,并刷新缓冲区。
flush 是通过系统调用fsync、fdatasync将文件描述符指向的文件的内容直接写到磁盘。
unitbuf 操作符和 nounitbuf 操作符配合使用,开启、关闭每次执行完写操作后都刷新流。
关于输入流与输出流的绑定
当输入流与输出流绑在一起时,任何读输入流的尝试都将首先刷新其输出流关联的缓冲区。标准库将 cout 与 cin 绑在一起,因此语句:
cin >> ival;//这其实是两步,一个标准输入Cin,一个标准输出cout用>>符号表示
导致 cout 关联的缓冲区被刷新。
tie 函数可由 istream 或 ostream 对象调用,将实参流绑在调用该函数的流对象上。调用方的任何IO操作都将刷新实参所关联的缓冲区。
8.4. 文件的输入和输出
头文件<fstream>定义了三种支持文件 IO 的类型:
ifstream,由 istream 派生而来,提供读文件的功能。
ofstream,由 ostream 派生而来,提供写文件的功能。
fstream,由 iostream 派生而来,提供读写同一个文件的功能。
fstream 类型除了继承下来的行为外,
还定义了两个自己的新操作—— open和 close,以及形参为要打开的文件名的构造函数。
ifstream infile; // 文件流输入对象
ofstream outfile; // 文件流输出对象
infile.open("in"); // 打开当前目录下名为in的文件
outfile.open("out"); // 打开当前目录下名为out的文件
fstream 对象一旦打开,就保持与指定的文件相关联。如果要把 fstream 对象与另一个不同的文件关联,则必须先关闭(close)现在的文件,然后打开(open)另一个文件。
//遍历存放了很多文件名的容器
while (it != files.end()) {
ifstream input(it->c_str()); // 打开文件,通过构造函数
//打开失败就退出
if (!input)
break; // 打开失败
while(input >> s) //读取文件到变量
process(s);
++it; // i迭代器自增
next file //下个文件
}
关闭流并不能改变流对象的内部状态。如果最后的读写操作失败了,对象的状态将保持为错误模式,直到执行 clear 操作重新恢复流的状态为止。
如果打算重用已存在的流对象,那么 while 循环必须在每次循环进记得关闭(close)和清空(clear)文件流。如果不进行 clear操作,流的状态将保持。
如果忽略 clear 的调用,则循环只能读入第一个文件。
一旦第一个文件读到了文件结束符,就会将流的内部状态iostate 改成 EOFBIT错误状态。
哪怕关闭了再打开其他文件仍然会失败,因为流的状态没有 clear清除。
8.5 文件流操作文件模式
在打开文件时,无论是调用 open 还是以文件名作为流初始化的一部分来使用构造函数,都需指定文件模式。
using fstream;
in //打开文件做读操作
out //打开文件做写操作
app //在每次写之前找到文件尾
ate //打开文件后立即将文件定位在文件尾
trunc //打开文件时清空已存在的文件流
binary //以二进制模式进行 IO 操作
其实就是用文件流打开文件时的附加操作:
读、
写、
写找文件尾、
找文件尾、
清空文件流、
二进制
ofstream/fstream 支持 out、trunc、app模式,用于写文件;
ifstream/fstream 支持 in 模式,用于读文件。
所有文件均可用ate(打开即定位到文件尾)和binary(以二进制处理文件)模式。
默认时,与 ifstream 流对象关联的文件将以 in 模式打开,该模式允许文件做读的操作;与 ofstream 关联的文件则以 out 模式打开,使文件可写。
以 out 模式打开的文件会被清空,从效果上来看等同于 同时 out | trun模式。
对于 ofstream 打开的文件,要保存文件中存在的数据,唯一方法是显式指定 app 模式打开。
对同一个文件作输入和输出运算
fstream 对象既可以读也可以写它所关联的文件,取决于打开文件时指定的模式。
默认情况下,fstream 对象以 in 和 out 模式同时打开。当文件同时以 in和 out 打开时不清空。如果打开 fstream 所关联的文件时,只使用 out 模式则文件会清空已存在的数据。
// 同时用in和out模式打开文件
fstream inOut("copyOut", fstream::in | fstream::out);
8.6 字符串流
iostream 标准库支持内存中的输入/输出,只要将流与存储在程序内存中的 string 对象捆绑起来即可。
标准库定义了三种类型的字符串流:
istringstream,由 istream 派生而来,提供读 string 的功能。
ostringstream,由 ostream 派生而来,提供写 string 的功能。
stringstream,由 iostream 派生而来,提供读写 string 的功能。
sstream 类型除了继承自iostream的操作外,还自定义了一个有 string 形参的构造函数,这个构造函数将 string 类型的实参复制给 stringstream 对象。对 stringstream 的读写操作实际上读写的就是该对象中的 string 对象。这些类还定义了名为 str 的成员,用来读取或设置 stringstream 对象所操纵的 string 值。
stringstream strm; //创建自由的 stringstream 对象
stringstream strm(s); //创建存储 s 的副本的 stringstream 对象,其中 s 是 string 类型对象
strm.str() //返回 strm 中存储的 string 类型对象
strm.str(s) //将 string 类型的 s 复制给 strm,返回 void
stringstream 对象的使用
#include <iostream>
#include <sstream> // 引入istringstream所需的头文件
#include <string> // 引入string所需的头文件
using namespace std;
int main() {
string line, word; // 分别用于存储从输入中读取的一行和一个单词
// 使用while循环逐行读取输入,直到输入结束(例如遇到文件结束符ctrl+d或换行符\n)
while (getline(cin, line)) {
// 使用字符串流构造函数将字符串绑定到一个流对象上,以便逐词读取
istringstream stream(line);
// 当流中有内容可以输出
while (stream >> word) {
//打印每个字符串,后面跟空格
cout << word << " ";
}
cout << endl; // 每处理完一行后,输出一个换行符
}
return 0;
}
stringstream 提供的转换和/或格式化
stringstream 类(包括 ostringstream 和 istringstream)在 C++ 中被广泛用于在多种数据类型之间实现自动转换的场景。这种机制特别适用于需要将数值型数据转换为字符串表示形式,或者从字符串中恢复数值型数据的场景。
ostringstream 示例
假设我们有两个整数 val1 和 val2,我们希望将它们格式化为一个字符串。
可以通过 ostringstream 来实现:
int val1 = 512, val2 = 1024;
ostringstream format_message;
// 将整数值和描述信息格式化为字符串
format_message << "val1: " << val1 << "\n"
<< "val2: " << val2 << "\n";
// 此时,format_message 包含了一个格式化的字符串:"val1: 512\nval2: 1024\n"
istringstream 示例
接下来,如果我们想要从 format_message 生成的字符串中恢复出原始的数值型数据,我们可以使用 istringstream:
// 从 ostringstream 对象中获取字符串
istringstream input_istring(format_message.str());
// 用于存放被忽略的标签字符串
string dump;
//因为流输入的时候手动添加了\n,因此流输出的时候碰到\n就结束一次,不包括\n
input_istring >> dump >> val1 >> dump >> val2; //dump是val1: 512 然后传给数值型,自动忽略非数值
//dump是val2: 1024 然后传给数值型,自动忽略非数值
// 输出恢复后的数值
cout << val1 << " " << val2 << endl; // 输出: 512 1024