C++ Primer 第一章 快速入门
1.1 编写简单的C++程序
任何一个都要包含一个main函数,因为这个函数是系统执行入口c++也一样,函数格式是 int main(); 和别的主要函数不同C++的主函数有且只有一个且返回一个INT类型的值。一般来说返回0表示执行成功。程序员可以不必定义return 0返回这时候系统会在编译时自动加上retuan 0;
1.2 偷窥输入输出
这里指的输入输出是说从标准设备输入内容或者将内容输出到表顺设备。比如说输出字符到显示器,输出一个图片到显示器。将字符或者二进制内容输出到文件。 或者从键盘输入内容到计算机,从一个文件读取内容到系统等等。
C++提供了标准输入输出库,最常用得是iostream 它的父类 中有istream / ostream 因此可以看出它是输入输出的的功能组合类。一般来说我们常用到的有这么几个
cint; cout; endl; 下面是他们的一个应用实例:
{
std::cout << " 请输入 " << std::endl;
int v1,v2;
std::cint >> v1 >> v2 >> std::endl;
std::cout << " 用户输入了: " << v1 << " and " << v2 << std::endl;
}
这里std::表示是在std这个命名空间下
std::count << “string” 表示要输出字符串 “string”
std::cint >> v1 表示系统等待用户输入一个内容且输入内容会保存到变量v1中
std::endl 是一个操纵符 表示立刻刷新缓冲区,这样输入输出会立刻生效输出的内容会立刻被呈现在屏幕或文件上。
由于这些功能属于标准库儿不是内置类型,所以在使用前应该导入对应的库 #include<iostream>
1.3 注释
和大部分语言一样,类或者代码块的说明以/*开头,以*/结束,可跨行,单行注释用//
1.4 控制结构
有 if , for , while 三种用法,使用和其他语言类似。
可以通过while和std::cint的结合读取多个内容,代码如下:
{
int v1;
while(std::cint >> v1)
{
std::cout << " 用户输入了: " << v1 << std::endl;
}
}
while(std::cint >> v1) 语句会持续让用户输入。std::cint >> v1 将用户输入内容并保存到变量然后返回std::cint对象。
用户输入以回车符为输入结束,语句判断用户输入的内容是否有效若有效返回一个有效的std::cint对象,while判断为true则执行循环体内语句,若输入非法则退出循环继续向后执行。
1.5 类的简介
C++中类型分为内置类型和类类型,类属于类类型。类是一种复杂的数据结构支持继承,C++中的类结构上和其他高级语言类似,但由于指针,引用,常量的大量组合使用使得它类的复杂度比一般语言要高的多。类有非常多得灵活应用,意味着有很多需要掌握的内容,只有深刻理解这些基本概念才能用好学好类用好类因为类本质上是对这些基础概念的一个有序组合。
C++ Primer 第二章 变量和基本类型
2.1 基本内置类型
基本内置类型是C++“自带”的类型,区别于标准库定义的类型。使用时不需要应用标准库就可以使用,我们可以理解为数字型有下面这些
整形:就是整数或者是没有小数位的数。它包括bool(0,!0) , char, wchar_t(非标准英文字符用char无法全部表现所以需要这个类型来表示),short, int ,long。 整形中除了bool外其他类型可以是带符号,也可以是无符号的,无符号的取值范围不能为负,有符号取值可以有正有负,但由于有符号数所占位其中一位是符号位所以它的正数取值范围要比无符号范围小一位。
浮点型:就是带小数的数,包括float , double , long double他们之间的区别是取值范围和精度,可以根据你的需要选择合适的类型。
char , wchar_t 两个类型虽然都是字符型但区别比较大:
在宽度上来说,一个是1byte,一个是2byte(在linux上实际是4byte)
在编码上来说 wchar_t表示unicode编码方式
以上不同说明 wchar_t 可以包含更多内容比如中文,日文等等。
2.2 字面值常量
就是常量本身,比如 21, “name” 表示类两种类型的字面值。有些类型有多重表示法,比如数字可以用不同进制表示,浮点型可以用科学计数法书写等等。
2.3 变量
变量是对数据的有名存储,既然有名就可以对改数据做一些操作如赋值修改等等。这里有两个表达式概念先了解一下
左值:左值可以出现在赋值语句的左面或者右面
右值:右值只能出现在赋值语句的右面儿不能出现在左面
变量是左值,常量是右值,字面值常量也是右值
变量定义和初始化值更具类型不用也不同,内置类型不能隐身初始化,类类型一定要可以隐式初始化。
先看内置类型:
int a ; // 没有初始化内容,它的值由系统分配规则是在栈区和堆区(函数内定义或者类里面定义)都取随机值,在全局区(全局常量,静态变量)都是全零值。
int b=1 ; // 这种叫赋值初始化
int c(1),d(b + 2) ; // 这种叫直接初始化()中不一定是常量可以是一个表达式或变量
这两种初始化是不一样,后面讲到类类型时就会明白一般来说直接初始化更灵活且效率更高。
需要特别强调的是未初始化的内置类型虽然也会有值但是其分配规则导致其值不确定性,所以我们定义内置类型时一定要初始化值而不依赖系统的内部非配规则。
再看类类型定义:
string a; // 隐式初始化初始化成了 “”
string b = "name" ;
string c(b) ;
string d(10,'a') ; // string类特有的初始化方法,等同于10个字符a组成字符串并赋值初始化给变量b
类类型的初始化其实就是构造函数, 隐式初始化实际上就调用类型的默认构造函数来初始化类型对象;直接初始化是调用类的拷贝构造函数;而直赋值初始化是调用赋值操作符重载和拷贝构造函数。相关内容再后续章节会有详细说明。
声明和定义
c++是一门奇怪的语言,我们先看如下代码
{
std::cout << v1 << std::endl;
}
int v1 = 1;
绝大部分语言中这样的写法是没有问题的,但在C++中却会编译不通过,因为C++是严格的顺序编译非类对象代码的,当main函数编译时要用到的变量v1还没有执行定义,c++就会抛出异常说使用了未定义的变量v1,如果吧这个 int v1;语句放到函数前面就没问题。那么我们是不是只能这样做呢? 答案当然是否定的,我们可以不改变现在代码顺序而使用声明解决这个编译错误,可以这样修改代码:
{
extern int v1;
std::cout << v1 << std::endl;
}
int v1;
注意红色新增代码就是一个声明。他的意思是说:HI,系统中的某个地方定义了一个叫v1的int变量,所以你可以放心的使用。
声明是告诉编译器一些信息所以不会分配内存空间也不会产生具体的数据。 声明的对象一旦被使用就一定要在某个地方定义它,否则编译器即使暂时不抛出异常在编译完毕后发现根本没有地方找到相关定义也一样会编译失败。
以上例子只适合变量,同一个文件中常量在使用前一定要先定义,否则即使做了申明也会出错。
声明最大的好处体现在多文件的编程中,比如说我编写了一个头文件并引入到了主文件中,头文件需要使用主文件定义的变量。由于头文件一般是先于主文件编译执行的,所以就会出现未定义的编译错误,如果在头文件中先声明一下这变量再使用就没有问题了。 除了变量C++还有常量(后面会讲到),它和变量有一些不同
extern int a;
// usend a
extern const int b; // 常量声明语法也可写成 const extern int b
头文件中用到了两个对象,但他们是在其他地方定义的所以我们需要申明一下在使用。
#include head.h
int a(1); // 这样定义的变量可以被其他相关文件(head.h)声明并使用
// const int b(2); 对于常量这样写不行是因为这样定义的变量作用范围只在本文件中,头文件虽然做了声明但无法使用这个常量,应该用下面的语法来定义
extern const int b(2); // 这样定义的常量才可以被其他相关文件(head.h)声明并使用
int main()
{
// do...
}
主文件中定义了头文件需要的对象,和变量不同,常量如果在其他地方被申明和使用一定要在定义的时候加上关键字extern因为如果不加的话常量默认作用范围是定义它的那个文件,变量默认是所有相关文件。
虽然C++支持面向对象,但他并不是完全的面向对象他也支持面向过程。因此有一些代码是不属于任何类的,比如说main 函数就不属于任何类。
针对这种情况变量根据其位置可分为两种 全局变量局部变量。广义上说定义在类内部(包括类所属函数内部)和函数内部的变量叫局部变量,除此之外的变量自然就是全局变量了。
2.4 const限定符
限定符表示定义一个常量,常量一但定义则不可更改。不可更改的意思是既不能对常量句柄句柄重新赋值也不能更改常量对象的数据成员(如果是一个自定义类常量C#中是可以更改类成员的但C++不允许这么做)。
常量分为两类 编译时常量 和 运行时常量(在.NET中标示为readonly) 编译时常量是定义后直接初始化的常量,运行时常量值要初始化的值必须要通过代码运行才可以确定的
const int b = getval(); // 运行时常量值来自一个函数的运行结果
const myclass my(1,"tom"); // 自定义类的常量定义都是运行时常量,因为需要运行类的构造函数
常量初始化之后就不可以做任何的更改操作。
2.5 引用
引用就是对某个对象的另外一个别名。引用最重要的作用是函数传参。
int val = 1;
int &refval = val; // 引用了一个变量
int &refval1 = 12; // 错误,12是个字面常量,常量必须要用常量引用
refval = 2; // 等价于 val = 2
常量引用
const int val = 1;
const int &ref1 = val; // 引用了一个一般常量
const int &ref2 = 12; // 引用了一个字面常量
int &ref3 = val; // 错误,常量必须要使用常量引用,ref3是个变量引用
int var = 2;
const int &ref2 = var // 常量引用指向了一个变量,这时候
refval2 = 3; // 不允许通过常量引用来做任何更改操作
varval = 3; // 但是可以用原始变量来更改内容
总之常量引用可以引用常量或变量,但是无论如何都不能通过引用来更改数据内容。
变量引用只能引用变量,用引用可以更改内容效果与用原始变量更改内容是一样的。
对于引用还有一点很重要:非常量引用类型必须严格匹配,常量引用可以在内置类型之间相互引用
int &b = a; // 错误,类型不匹配
const int &c = a; // ok
// 这个操作实际等同于
int temp = a;
const int &c = temp;
如果对非const引用b不做类型匹配限制,b实际就会引用临时变量temp,对b的修改无法反应到变量a,引用失去了其意义。 const引用c没有这样的问题,因为它本身不允许修改。
2.6 typedef
这个关键字非常有用,用来给某个类型指定一个别名,比如
typedef int zx ;
int a(1) ; // 等同于 zx a(1)
在后面我们可以看到使用一些复杂类型或是函数指针时改关键字可以大大减少你的代码整洁度。
2.7 枚举
枚举是一组可选常量值,既然是一组可选值说明包含多个常量。枚举定义语法如下
enum val{val1 = 2, val2 = 4, val3} // 最后一个内容没有显示给值等价于 val3 = 5
如果不指定值默认第一个值从0开始下一个依次+1递增。
枚举的每一项都是一个唯一的const类型值,上面的定义有点类似于:
const val1 = 2; const val2 = 4; const val3 = 5;
由于是const的,所以 val2 = 1 或者 val a = 2; 都不允许。
枚举项和int类型值有对应关系,但是二者只能单向转换,枚举可以自动转成int,而int却不能转成枚举
val a = val2 ; // 枚举之间赋值初始化
int b = val2 ; // 枚举转成int并初始化
val a = 2 ; // int 不能转成枚举,无法初始化
2.8 类类型
一般来说C++吧除了内置类型之外的类型都叫类类型,我们习惯上把自定义的类class称为类类型。 类一般采用先定义类并声明类的成员函数,然后在外部定义成员函数的语法形式。
这部分内容不在这里详述会在后续章节中专门说明。
2.9 编写自己的头文件
从上面的课程我们应该大概知道什么是头文件,一般来说头文件中包含声明而不是定义,但是下面两种情况比较特殊
类定义要放在头文件里。这样如果某个文件需要这个类只需要把头文件include进来即可。
运行时常量也可以在头文件定义,表达式可以在包含文件中定义。这样就能实现在在不同的包含文件得到不同的常量值。
int getval();
const int p = getval();
// mast1.cc
#include " head.h "
int getval()
{
return 100;
}
int main()
{
cout << p << endl; // 输出100
}
// mast2.cc
#include " head.h "
int getval()
{
return 200;
}
int main()
{
cout << p << endl; // 输出200
}
有时候某个头文件可能会被包含多次(直接包含+间接包含)。比如文件包含了头文件A,也包含了头文件B,头文件B同时也包含了A,则文件重复包含了A。这个时候如果A中有定义语句就会产生“重复定义”的编译错误,这个时候可以用#ifndef 把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:
#ifndef <标识>
#define <标识>
...... 头文件代码
#endif
<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef _STDIO_H_
#define _STDIO_H_
...... 头文件代码
#endif
该预定义标示不单能防止头文件被重复包含编译,而且还可以用在不同头文件定义同名对象时出现异常的处理上。
C++ Primer 第三章 标准库类型
3.1 命名空间的using声明
using声明是对某个命名空间做引入。主要作用是简化代码编写。
3.2 标准string类型
首先要明确类型是类类型,意味着它有构造函数,也类似我们自定义的类一样的其他类对象。
它有几种初始化方式如下
string s1 ; // 调用默认构造函数初始化对象
strng s2(s1) ; // 将S2初始化为S1的一个副本
string s3("value") ; // 用一个字符串值初始化对象
string s4(n,'c') ; // 用N个字符‘C’组成字符串作为初始化s4的值
特别要注意的是第一种初始化方式,虽然默认构造函数是没有参数的但是不能因此就写成 string s1()
我们可以复习一下内置类型的默认初始化方式和类类型做个比较:
string s1 ; // 调用默认构造函数初始化
int i ; // 要根据定义位置来确定初始化值,全局变量一律初始化为0, 局部变量是一个随机数,称为未初始化
string类型可以用于标准输入输出。
while(getline(cin, line))
{
cout << line << endl;
}
cin >> line 输入内容并保存到变量line,输入时会忽略输入左面的的空格直到非空格字符才开始读取,直到再次读到空格输入结束。
例如如果你输入" zhang san " 实际line保存的是"zhang" 。
getline(cin,line) 是一个系统函数,可以输入标准行内容,这个函数不会忽略任何内容一直读取用户输入并保存到line,直到用户输入换行函数才结束,结束时函数会返回cin的引用。 如果用户刚开始就输入换行符那么line的内容就是"" 。
上文说过string是类类型所有有很多类成员(属性和成员函数),下面就是一些常用的操作
s.empty() ; // 判断s是否为空,相当于s.size()==0
s.size() ; // s的长度
s[n] ; // n位置的字符(左值返回)
s1+s2 ; // 返回s1和s2连接的串
s1=s2 ; // 把s1替换为s2的副本
s1==s2 ; // 判断s1,s2是否相等
!=,<,<=,>,>= // 按字典顺序比较
s.insert(...) ; // 插入字符操作,有多个重载可用
s.size()函数返回一个表示字符串长度大小的值,其类型并不是我们认为的int类型,而是一个叫string::size_type的类型,为什么不用INT而新创造一个类型呢,原因有如下两点:
1. 取值范围不同,int有固定的取值范围,并且可以取负数,但字符串长度是不可能为负的,并且长度的大小很可能会超过int的范围而导致溢出
2. int的范围大小与机器相关,有的机器上范围大些,有点机器小一些。但是字符串长度应该是个不能随机器发生大小改变的值,所以即使用无符号int来表示串大小也是不合适的
s[n]可以作为左值操作,也就是说改操作既可以返回N位置的字符也可以替换N位置的字符。 n值一定要在有效范围内,负值或者超过串大小会引发严重异常
a[1] = 'b' ; // 字符一定要用单引号,双引号表示字符串
cout << a << endl; // 输出 "abaa"
cctype 头文件所包含的函数主要用来测试字符值,以下是一个列表,但是对于初学者来说自己上机操作一下,后两个返回的是int型,确实很意外,强制转换一下,很简单。
isalnum(c) ; // 假如c是字母或数字,则为true
isalpah(c) ; // 假如c是字母,则为true
iscntrl(c) ; // 假如c是控制字符,则为true
isdigit(c) ; // 假如c是数字,则为true
isgraph(c) ; // 假如c不是空格,则为true
islower(c) ; // 假如c是小写字母,则为true
isprint(c) ; // 假如c是可打印的字符,则为true
ispunct(c) ; // 假如c是标点符号,则为true
isspace(c) ; // 假如c是空白字符,则为true
isupper(c) ; // 假如c是大写字母,则为true
isxdigit(c) ; // 假如c是十六进制数,则为true
tolower(c) ; // 假如c是大写字母,则返回小写字母形式(对应的int值),否则返回c。
toupper(c) ;// 假如c是小写字母,则返回大些字母形式(对应的int值),,否则返回c。
可以举个简单的例子
isalpah(line[0]); // true
isdigit(line[1]); // true
ispunct(line[4]); // true
tolower(line[0]); // 返回A对应的ascii 65
3.3 标准vector类型
C++标准库容器有好几类,后面会详细介绍。为什么在这里单单要先介绍vector容器呢?这个容器最常用。对于大部分应用来说用它足以满足你的要求。
vector是个类模板,如果你了解JAVA或C#范型编程的话可以理解为范型类。泛型最大的好处是只需定义一个类或函数就可以提供不同类型版本的操作。
它的初始化有如下几种方式:
Vector<T> v1 ; // 默认构造函数v1为空
Vector<T> v2(v1) ; // v2是v1的一个副本
Vector<T> v3(n, i) ; // v3包含n个值为i的元素 参数 T 如果是类类型则一定要有拷贝构造函数(未定义的情况下系统会自动分配一个)
Vector<T> v4(n) ; // v4含值初始化的元素个副本 参数 T 如果是类类型则一定要有默认构造函数(未定义的情况下系统会自动分配一个) 如果是内置类型则分配n个0
对于类类型如果不能满足红色标示的要求编译会失败。 关于类类型的拷贝构造函数和默认构造函数后续章节有介绍
Vector对象有几种最重要的操作
v.push_back(t) ; // 在数组的最后添加一个值为t的数据
v.size() ; // 当前使用数据的大小 返回vector<T>::size_type类型的长度值,其意义类似上面讲过的string::size_type
v.empty() ; // 判断vector是否为空
v[n] ; // 返回v中位置为n的元素 和string类型下标操作类似 是个左值操作
v1=v2 ; // 把v1的元素替换为v2元素的副本
v1==v2 ; // 判断v1与v2是否相等
!=、<、<=、>、>= ; // 保持这些操作符惯有含义
关于vector需要注意的是下标不能用来添加元素操作。
list[ 0] = 1; // 错误 list没有任何数据,下标操作只针对存在的元素
list.push_back( 2) ; // 增加了一个值为2的元素
list[ 0] = 1 ; // ok 将第一个元素由2更改成1
3.4 迭代器简介
迭代器是用于对容器做遍历操作的类型。 迭代器和后面要说到的指针非常类似。但它侧重列表的迭代遍历,所有有一些快捷属性可用。
list.begin() // 表示容器的第一项迭代器,如果容器有值指向list[0]
list.end() // 表示容器的哨兵位,也就是最后一项后面的一项,只是用来表示迭代器已遍历到容器末端
iter++ // 迭代器自增表示向后移一位,指向下一个项
vector<string> list(10,"value");
for(vector<string>::iterator iter = list.begin(); iter != list.end(); iter++)
{
cout << *iter << endl; // 要访问迭代器当前值必须解引 *
*iter = "new value"; // 也可以解引后更改其值,可以看出解引是左值操作
}
// 如果想遍历容器又不想使用迭代器可以用下标操作
for(vector<string>::size_type ix = 0; ix != list.size(); ix)
{
cout << list[ix] << endl;
list[ix] = "new value";
}
当容器为空时 list.begin() == list.end()
上面定义的是常规迭代器,还有一种叫常量迭代器的迭代器类型。 我们也能定义迭代器常量,要弄清楚这些拗口的概念可以看下面代码示例
*iter = "new value"; // 这是不允许的,因为const_iterator告诉编译器我代表的是一个常量,所以不能通过任何手段改变其值
const vector<string>::iterator iter // 定义一个常迭代器,迭代器代表的变量,但迭代器本身是常量,所以可以更改代表的内容但无法更改迭代器
*iter = "new value"; // 没问题
iter ++ // 不允许,迭代器是常量所以无法让他指向其他项
const vector<string>::const_iterator iter // 这样定义的迭代器只能读取初始化指向的列表项内容,既无法向后移动迭代器也无法更改项值内容
最后一个定义很有意思:迭代器指向了常量,所以不能通过解引更改常量值,同时迭代器本省也是常量所以无法更改迭代器的指向
迭代器不是每次只能向后移动一位,可以通过迭代器与一个整形字面值相加向后移动多位
iter + n // 迭代器向后移动n位并产生一个指向移位后新位置的迭代器
两个迭代器可以做相减运算结果是类型为difference_type的两个迭代器之间的距离(两个操作数一定要指向同一容器否则报错)
3.5 标准bitset类型
标准库中bitset类型用来处理二进制位的有序集,bitset类型简化了位集的处理,使用bitset时需要包含头文件#include<bitset>
bitset对象的定义和初始化
bitset也是类模板,不过bitset类型对象之间的区别在于长度而不是类型,因此bitset模板的参数是长度类型
初始化方法 | 说明 |
bitset<n> b; | b有n位,每位都为0 |
bitset<n> b(u); | b是unsigned long型u的一个副本 |
bitset<n> b(s); | b是string对象s中含有的位串的副本,s是01串 |
bitset<n> b(s, pos, n); | b是s中从位置pos开始的n个位的副本 |
用unsigned long值初始化bitset对象
用unsigned long值初始化bitset对象的时候,将long值转化为二进制模式,然后拷贝到bitset的右方(bitset右边为低阶位,左边为高阶位),string位数多了将被截断,少了bitset将在前面补零。
用string对象初始化bitset对象
从string对象初始化bitset对象,需要注意的是,复制拷贝相当于从string位模式平移到了bitset。
例如:
string str(”11001010”);
bitset<32> bitvec(str);
这个时候,bitvec是这样的:0000 0000 0000 0000 0000 0000 1100 1010最右边是bitset的低阶位,即bitvec[0],bitset[1] …
bitset对象上的操作
操作调用方式 | 操作说明 |
b.any() | 测试b中是否有存在1的位 |
b.none() | 测试b中是否全0 |
b.count() | 测试b中置1的位个数 |
b.size() | b中所有二进制位个数 |
b[pos] | 访问下标为pos位置的位值 |
b.test(pos) | 测试pos位置的二进制位是否为1 |
b.set() | 将b所有位置1 |
b.set(pos) | 将b中pos位置的位置1 |
b.reset() | 将b所有位置置0 |
b.reset(pos) | 将b中pos位置的位置0 |
b.flip() | 将b中所有位翻转 |
b.flip(pos) | 将b中pos位置上的位翻转 |
b.to_ulong() | 将b转化为unsigned long值 |
os << b | 将b的位集合直接输出到os流 |
注:
Ø b.cout和b.size()返回的是size_t类型,该类型定义在cstddef头文件中(C标准头文件stddef.h的C++版本)
Ø b[pos]可以作为左值,即可以用来改变pos位置的值
C++ Primer 第四章 数组与指针
4.1 数组
数组是同一类数据的集合。数组的特点是 以顺序结构结构存储,一点定义就无法更改数组大小。
数组定义很简单:
int a[2] ; // 定义了一个能容纳两个int类型数据的数组
const int sz = 2 ;
myclass ls[sz] ;
定义数组的时候系统可能会自动初始化数组的每个项目,但也可以显示提供值。
int a[3] {1,2,3} ;
可以既指定数组大小又提供初始化列表,此时列表内元素数不能大于维数; 如果列表提供元素小于维数则从数组第一个元素开始显示初始化,没有提供的数组元素会自动初始化步。
自动初始化遵从下面规则:
Ø对内置类型来说:
int a[2] ; // 表示要自动初始化。如果a是全局定义的则每一项都初始化为0,否则会分配随机数,表示未初始化
也可以显示给值例如:
int a[] {1,2,3} ; // 显示初始化时候可以不必提供数组大小,系统会自动推断大小
Ø对于类类型来说:
myclass ls[2] ; // 无论在是全局定义还非全部定义都会调用类型的默认构造函数,如果类没有默认构造函数则编译出错
Øchar类型数组比较特殊:
char a[]{'a', 'b', 'c'} ; // 标准定义
char b[]{'a', 'b', 'c', '\0'} ; // 一个C风格的字符串,等价于 char b[] = "abc" 二者的字符数组大小都是4
上面两种定义方式唯一不同的就是最后一个字符是否是 '\0' (结束符)如果是则表明定义了一个C风格的字符串。
要注意:虽然char b[] = "abc" 内容只有三个字符,由于这种定义是C风格字符串的定义法所以系统会自动在对应的字符数组后面加上 '\0'
在指定大小定义C风格字符串时要注意前后长度匹配 char b[3] = "abc" 会产生错误,需要存放四个字符大小。
和其他类型定义不同,数组定义是没有类似 int a[](b) 这种定义方式的,因为数组不支持复制操作。
数组最常用的操作办法是小标操作 a[0] 表示第一个项,它是左值操作。
表示数组下标和数组大小的数据类型和bitset一样是size_t 但是数组只有下标操作,没有取大小等其他操作。
int ls[sz];
for(size_t i = 0; i < sz; if++)
{
cout << ls[i] << endl;
ls[i] = i + 1;
}
4.2 指针的引进
指针一般是对对象的存放地址的直接指向,一般定义形式为:
int *cur ; // 定义了一个未初始针未初始化指针值是个随机数
int *cur = 0 ; // 可以给指针赋予初始0值表示该指针没有指向任何对象,不应对其有任何操作 。这个赋值表达式中0值是有约束的,必须是字面常量或者编译时常量值0
int *cur = 121 ; // 也可以给指针赋予一个整数常量,但最好不要这么做。因为该常量表示内存地址而我们并不十分清楚这个地址中存放了什么数据。一单指针做了某些操作就可能破坏这些数据,对一个未知的内存做指针指向是危险的(未初始化指针情况也类似)
应该在定义是就给予其初始化工作:
int a = 123 ;
int *cur = &a ;
有一类比较灵活的指针 *void 可以指向任何类型的变量;
int a = 123 ;
string b = "abc" ;
void p1 = &a ;
void p2 = &b ;
这类指针表示指向某个内存地址的数据,但不清楚改数据类型。void指针操作有限。由于不知道指向的数据类型所以不能对指向数据做任何操作。
有一类指针可以指向另外的指针,也就是指向指针的指针定义如下:
int a = 123 ;
int *cur = &a ;
int **curr = &cur ; 这类指针需要做两次解引(**curr)才能获取真正的对象值。
指针操作一般有两个意思:
Ø对指针指向的数据做操作(需要解引):
int a = 123 ;
int *cur = &a ;
*cur = 456 // 等价于 a = 456; (对所指向的数据做操作需要做解引在做操作(就是在变量引用时保持*号))
Ø对指针本身操作是指改变指针的指向对象:
int a = 123 ;
int *cur = &a ;
int b = 123 ;
int *curb = &b ;
a = b ; // 等价于 a = &b (对指针本身操作不能带解引符号*) a, b两个指针现在都指向了变量 b ,变量 a 现在没有任何指针指向。
上面讲了数组的基本概念,数组和指针有着非常紧密的联系。 由于数组本身只有下标操作太过简单,所以大部分情况下都使用指针来操作数组。
指针不能直接指向数组变量,它是通过指向数组中得某个项并前后移动来操作数组;
int a[count] = {1,2,3,...} ;
int *cur = a ; // 该操作是简化操作实则是指针指向了数组的第一项,等同于 int *cur = &a[0]
也可以明确指定 int *cur = &a[2] ;
指定了数组的指针可以前后移动,导航到当前项的前后项指针:
int *next = cur +1 ;
int *prv = cur -1 ;
int *next3 = cur +3 ; // 当前项向后移动三位的项指针
数组在数据范围前后一位溢出位的指针表示叫哨兵位,一旦指针到了任意一个哨兵位就不能再做一般化处理。
int a[10] = {1,2,3,...} ;
int *cur = &a[0] ;
int *cur1 = cur -1 ; // cur1是哨兵位,表示指针已经移出数组
int *cur2 = cur +10 ; // cur1是哨兵位,表示指针已经移出数组
哨兵位的作用是标识指针已经超出了数组有效范围
两个数组指针可以相减,值是类型为 ptrdiff_t 的项间距(两个操作数中不能有哨兵位)如下:
int a[] = {1,2,3,...} ;
int *cur1 = &a[1] ;
int *cur2 = &a[5] ;
ptrdiff_t pd = cur2 - cur1 ; (也可以交换两个操作数的位置,结果允许为负数)
指针可以在数组有效范围内任意移动,找到相应项后如何操作数组的真实值呢? 只需要解引既可。
int a[] = {1,2,3,...} ;
int *cur = &a[1] ;
*cur = 1 ; // 等同于 a[1] = 1
数组最重要的是下标操作。数组指针也有下标操作,但是意义和数组下标不一样
int a[] = {1,2,3,...} ;
int *cur = &a[1] ;
cur[1] = 2 ; // 等价于 *(cur + 2) = 2 也等价于 a[3] = 2
int i = cur[-2] ; // 等价于 int i = *(cur - 2) 也等价于 int i = a[1]
指针是数组的迭代器,最后让让我们看看用指针如何实现迭代
int arr[count] = { 1, 2, 3...};
for( int *cot = arr; *end = arr + count; cot != end; ++ cot)
{
cout << *cot << endl;
}
可以用const修饰定义指针。
Ø指向常量的指针:
const int a = 123 ;
const int *cur =&a ; // const 必须
需要注意:常量的指针必须是指向常量指针,但指向常量指针可以指向常量也可以指向变量。无论如何都不能对它所指内容做更改,即使它实际指向了变量,但可以更改这个指针的指向
int c =456 ;
cur = &c ; // 更改了指针的指向,现在指向的实际是个变量
*cur = 789 ; // 不允许,虽然指向的是变量但系统认为是常量所以不允许修改
Ø常量指针:
表示不能重新再做指向更改,但也许可以修改它指向的对象的值,这取决于它指向的值是变量还是常量 如下:
const int a = 123 ;
int b = 456 ;
int *const cur1 = &a ; // 此时不允许允许 *cur = 789,因为指向了一个常量
int *const cur2 = &b ; // 此时允许允许 *cur = 789,因为指向了一个变量
cur1 = &b ; // 错误,不允许更改指针的指向
需要注意的是这类指针在定义时必须初始化。
如果我定义了这样一个指针
const int *const cur = &a;
那么既不能更改指向的数据值,也不能重新指向其他数据
4.3 c风格字符串
前面内容了解了char 数组可以表示c风格字符串,既然数组可以用指针表示那么char*也可以表示c风格串
char a[]{'a','b','c','\0'} ; // 第一种数组定义语法
char a[] = “abc” ; // 第2种数组定义语法
char *a = “abc” ; // 指针表示法
c风格的字符串有多个操作函数: strlen(), strcpy(),strcat()以及strcmp()
函数中所说的结束标示NULL实际上就是最后一个字符 '\0' 。
char s2[20] = {'A','N','S','I','\0','C','+','+'};
char s3[6] = {'I','S','O','C','+','+'};
cout<<strlen(s1)<<endl; // 5
cout<<strlen(s2)<<endl; // 4
cout<<strlen(s3)<<endl; // 不确定
strlen(s1)==5,原因是{'m','o','b','i','l'};指定一部分内容的时候,剩余部分会自动赋值为空字符,而'\0'是转义字符表示的就是空字符.
strlen(s2)==4,因为第五个字符是字符串结束符'\0'(==0)。
strlen(s3)==?,因为他没有结束符。
可以创建动态数组,动态数组创建时更具类型不同其初始化值也不同。
int *a = new int[10] ; // 包含10个未初始化元素的数组
int *b = new int[10]() ; // 包含10个初始化为0的元素数组
// 类类型
string *c = new string[10] ; // 包含10个调用了类默认构造函数完成初始化的元素数组
可以看到内置类型需要在后面加上()才可以让元素初始化。
动态数组定义之后一定要手动删除掉,否则会造成内存泄露
delete [] a ; // 方括号必不可少,表示要释放一个动态数组
C++ Primer 第五章 表达式
本章内容比较简单,只做部分说明
1.箭头操作:
c++ 中箭头操作是个复合操作,将解引和调用组合调用了
myclass clsobj ;
myclass *cls = &clsobj ;
cls->show() ; // 等价于(*cls).show()
2. sizeof操作
返回一个对象或类型名的长度,这个操作比较重要,可以查看这篇文章
3. new和delete
new和delete运算符用于动态分配和撤销内存的运算符
Ønew用法:
开辟单变量地址空间
int *a = new int ; // 开辟一个存放数组的存储空间,返回一个指向该存储空间的地址. (和对象初始化一样:内置类型是否全局决定是否初始化,类类型无论如何都会调用默认构造函数来初始化)
int *a = new int(5) ; // 作用同上,但是同时将整数赋值为5
const int *a = new const int(5) ; // 作用同上,但是定义的是一个常量整数
开辟数组空间
一维: int *a = new int[100] ; // 开辟一个大小为100的整型数组空间
二维: int **a = new int[5][6] ;
三维及其以上:依此类推.
一般用法: new 类型 [初值]
Ø delete用法:
int *a = new int;
delete a; //释放单个int的空间
int *a = new int[5] ;
delete [] a ; //释放int数组空间
动态创建的对象不会被系统自动回收(即使动态变量已经超出了有效范围也不会)因此一定要手动 delete 如果忘记了该释放步骤系统内存可能会很快耗尽。
虽然回收了动态内存,但是这时候指针依然指向原来的地址叫做悬垂指针。如果在对该指针做操作可能会破坏内存数据。因此释放了数据空间后应该让指针指向 0 (a = 0)
4. 类型转换
分为隐式转换和显示转换
Ø隐式转换无需程序员介入系统自动实现转换例如:
char a ='a' ;
int b(a) ;
double c = b ;、
隐式转换常发生在小转大的同类类型之间,子类转父类,数组和对应指针,枚举到整形,任意结果到布尔值的转换等等。
Ø显示转换需要借助系统提供的转换函数来实现
static_cast、dynamic_cast、reinterpret_cast和const_cast
用法: static_cast < type-id > ( expression )
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
①用于类层次结构中基类和子类之间指针或引用的转换。
进行上行转换(把子类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
③把空指针转换成目标类型的空指针。
④把任何类型的表达式转换成void类型。
注意: static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
2 dynamic_cast
用法: dynamic_cast < type-id > ( expression)
该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;
如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换时, dynamic_cast和 static_cast的效果是一样的;
在进行下行转换时, dynamic_cast具有类型检查的功能,比 static_cast更安全。
public:
int m_iNum;
virtual void foo();
};
class D: public B{
public:
char *m_szName[ 100];
};
void func(B *pb)
{
D *pd1 = static_cast(pb);
D *pd2 = dynamic_cast(pb);
}
在上面的代码段中,如果pb指向一个D类型的对象,pd1和pd2是一样的,并且对这两个指针执行D类型的任何操作都是安全的;
但是,如果pb指向的是一个B类型的对象,那么pd1将是一个指向该对象的指针,对它进行D类型的操作将是不安全的(如访问m_szName),而pd2将是一个空指针。
另外要注意: B要有虚函数,否则会编译出错;static_cast则没有这个限制。
这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。
另外,dynamic_cast还支持 交叉转换(cross cast)如下代码所示。
public:
int m_iNum;
virtual void
f(){}
};
class B: public A
{
};
class D: public A
{
};
void foo()
{
B *pb = new B;
pb->m_iNum = 100;
D *pd1 = static_cast(pb); // compile error
D *pd2 = dynamic_cast(pb); // pd2 is NULL
delete pb;
}
在函数foo中,使用 static_cast进行转换是不被允许的,将在编译时出错;而使用 dynamic_cast的转换则是允许的,结果是空指针。
3 reinpreter_cast
用法: reinpreter_cast (expression)
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
该运算符的用法比较多。
4 const_cast
用法: const_cast (expression)
该运算符用来修改类型的const或volatile属性。除了const或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
Voiatile和const类试。举如下一例:
{
public:
int m_iNum;
}
void foo()
{
const B b1;
b1.m_iNum = 100; // comile error
B b2 = const_cast(b1);
b2. m_iNum = 200; // fine
}
C++ Primer 第七章 函数
7.1 函数的定义
函数是完整的一个逻辑代码块。 函数接收一些参数并在函数体内做处理。函数可以返回运算结果也可能不返回任何内容。
C++有两类函数 无返回值的void函数 和 有返回值函数。
7.2 参数传递
Ø非引用形参
参数是以复制形式传递的,函数内部对参数修改不一会对调用函数的实参产生影响。看下面代码
{
i = 3;
s = "new";
}
int it = 2;
string str = "old";
cout << it << str << endl; // 输出: 2 old
funct(it, str);
cout << it << str << endl; // 输出还是: 2 old
调用函数funct时传递的是实参的拷贝,虽然在函数内更改了参数值但不影响外部实参。
需要注意的是指针。在函数内对指针参数本身(指针的指向地址)的更改同样不会影响调用实参。不过对指针指向的内容更改却是有影响的。
{
*i = 3; // 对指向内容做更改
}
int it = 2;
string itpr = ⁢
cout << itpr << *itpr << endl; // 输出: 000ffxx0 2
funct(itpr);
cout << itpr << *itpr << endl; // 输出: 000ffxx0 3
函数对指针本身做的更改(指向地址)不会反应到外部实参。 指向内容做更改对外部实参是有影响的,实际上两个指针指向同一内容,任意一个指针所做的更改对其他指针都有反应。
函数体内两个表达式顺序执行对结果有很大影响。若先改变指针指向再改变指向内容值,实际上就表示两个指针指向了不同对象,这时修改指向内容对外部实参没有丝毫影响。
有些时候我们希望传递的参数不允许修改,在参数前增加 const 关键字就可以实现。
void function(const int i)
{
i = 2 ; // 错误,不允许修改常量参数
}
指针比较特殊。指针操作有两个含义:指针指向更改和指向内容更改。这个常量修饰符实际修饰的是指向内容,指向地址还是可以更改的 如下代码:
void function(const int *i)
{
*i = 2 ; // 错误,不允许更改指向内容
i = 0 ; // 允许更爱指针指向地址
}
Ø引用形参
非引用传参方式有个很大的弊端,当需要传递一个很大的参数时拷贝参数副本开销比较大。如果我们要求函数对参数修改能反映到外部的实参时非引用方式也无能为力。
这个时候可以使用引用类型传参。
{
i = 0 ;
j = 0 ; // 不允许更改
}
int it = 2;
int jt = 1;
cout << it << jt << endl; // 输出: 2 1
funct(it, jt);
cout << it << jt << endl; // 输出: 0 1
it以引用方式传递,函数内部修改 i 实际上就是在修改 it 本身。 如果限制函数更改可以增加 const 修饰。j 参数经过修饰后内部不允许做修改。
对于指针来说指针引用(*&parname)形参在函数体内指向更改或者指向内容更改都会反映到外部实参:
{
p = 1100ffxx ; // 修改指向
*p = 6 ; // 修改指向内容 注意此时修改的是新地址的内容值
}
int it = 2;
int pr = ⁢
cout << pr << *pr << endl; // 输出: fff110xx 2
funct(pr);
cout << pr << *pr << endl; // 输出: 1100ffxx 6
函数形参可传递实参一般分为 字面值常量, 普通常量,变量。不同情况下可传递的情况不同
}
int i(1);
const int j(1);
funct(15); // 传递字面值
funct(i); // 传递变量
funct(j); // 传递常量
也可以const限定参数 void funct(const int p) 三类参数传递已然正确,唯一的区别是限定后函数体内不允许更改参数 p
非const引用形参:
void funct(int &p)
{
}
int i(1);
const int j(1);
funct(3); // 不允许传递字面值
funct(i); // 传递变量
funct(j); // 不允许,函数内部可以修改参数值,而参数是常量时修改操作是是非法的
const引用形参:
void funct(const int &p)
{
}
int i(1);
const int j(1);
funct(1); // 传递字面值
funct(i); // 传递变量
funct(j); // 传递常量
应该掌握每种情况下可传递的实参类型。
对于容器参数如果按拷贝传递性能会大大降低。可以传递引用。 但时间操作中传递容器的迭代器会更加方便。迭代器类似于数组指针。
数组也可以作为新参,不过数组比较特殊因为数组不允许赋值,所以数组新参会被转换为对应指针。
void funct(int *i) ; // 推荐写法,表明参数是一个指向数组的指针
void funct(int[]) ;
void funct(int[10]) ;
这三种方式是一样的,实际等价于 void funct(int *i)
第三种形参定义设置了数组大小,但是实际调用中可以传递大小不等于10的数组,因为形参定义的数组大小会被忽略。 所以在函数中的操作不能依赖形参定义的大小。
如果想严格匹配实参和形参数组大小可使用 引用数组,定义方式如下:
void funct(int (&arr)[10]) ; // 表示是一个数组引用,且数组大小是10 。 圆括号必须因为下标操作具有更高优先级
void funct(int &arr[10]) ; // 错误的语法
函数操作数组不但可以如上传递还能可以传递指针,数组大小等参数类型
void funct(int *bej, int *end)
{
for(int *i = bej; i != end; i++)
{
cout << *i << endl;
}
}
int it[]{1,9,7,0,1};
funct(it, it + 5); // 注意:末端哨兵位是最后一位的后一位
// 传递第一位和数组大小
void funct(int *bej, size_t len)
{
for(size_t i = 0; i < len; i++)
{
cout << bej[i] << endl; // 数组指针下标操作等价于 *(beg + i)
}
}
size_t len = 5;
int it[len]{1,9,7,0,1};
funct(it, len);
7.3 函数返回值
函数可以没有返回值(void函数)也可以有返回值。同参数一样函数可返回多种类型。
Ø返回非引用类型
这种情况和形参类似,返回的是对象副本。
{
return 1 ; // 返回字面值副本
return a ; // 返回参数副本
int b(1) ;
return b ; // 返回局部变量副本
}
Ø返回引用类型
返回引用表示可以对返回值做赋值操作。
int &funct(int &a)
{
return a;
}
int i(1);
int &j = funct(i); // 也可以这样操作 funct(i) = 3
cout << i << j << endl // 输出 3 3 j 是 i 的引用
// 可以返回const引用
const int &funct(int &a)
{
return a;
}
int i(1);
int &j = funct(i); // 错误,不能用const引用初始化非const引用
const int &j = funct(i);
j = 3; // 错误,不能为常量赋值
千万记住:不管任何时候都不允许用const引用初始化非const引用(或者说不允许让非 const 引用指向一个const对象)
返回值遵循一个安全约束,即 不允许返回局部对象的引用或指针
int &funct(int a, int &b)
{
int c = 1;
return c; // 标准局部对象 不允许返回
return a; // 传参时拷贝的实参,实际值只有在函数内有效,不允许返回
return b; // 通过引用形参 传递的是外部实参的引用,可以传递
}
// 返回指针
int *funct(int a, int *b)
{
int c = 1;
return *c; // 标准局部对象 不允许返回
return *a; // 传参时拷贝的实参,实际值只有在函数内有效, 不允许返回
return b; // 传参时拷贝的实参,虽然指针是局部对象但他指向的值是外部数据,可以传递
}
总之判断返回是否是局部变量可以看返回值的实际值是作用范围否只在函数体内。
7.4 函数声明
函数调用时还未定义则需要声明。函数声明和变量声明类似可查看相关内容。
函数形参可以定义默认值,调用函数时如果不提供参数值则使用默认值
{
}
funct() // 等同于 funct(1,"val", " ")
funct(3) // 等同于 funct(3,"val", " ")
funct(3,"new") // 等同于 funct(3, "new", " ")
7.5 局部对象
局部对象的作用范围限制在函数内,函数执行完毕局部变量会被系统自动销毁。
不过静态局部对象不会销毁,所有静态对象一旦创建就会一直存在直到应用程序关闭。
静态对象初始化只会执行一次:
{
static int a(0); // 只会执行一次,第二次执行到代码处会检查静态变量 a 是否存在若存在则跳过
a++;
cout << a << endl;
} // 函数到此执行完毕并清理局部对象,但不会清理静态对象,所以a依然存在,值为最后操作结果。 a虽然不会被清理但只限在定义的函数体内访问
funct() ; // 输出 1
funct() ; // 输出 2
funct() ; // 输出 2
7.6 内联函数
内联函数是指编译时将调用函数的地方用实际函数体语句替代的一类函数,函数定义时返回值前加上 inline 就表示函数内联
inline bool funct(int a, int`b)
{
return a > b;
}
bool c = funct(a, b) ; // 编译时实际会被替换为 bool c = (a > b);
内联函数有较好的性能,因为函数在调用时系统刚要分配栈空间,内联函数会直接展开代码,所以不会有栈空间分配步骤 (内联函数体内数据需要的空间会在调用它的函数执行时一并分配)。
内联函数的特点决定了如果修改函数则所有用到内联函数的地方都要重新编译。
7.7 类的成员函数
类成员函数和普通该函数没有本质区别。
类成员函数包含一个隐藏参数 this 指针。它指向成员函数所属的当前类对象。
类成员函数名后面可以用const修饰,其作用是表示 this 指针指向常量,也就是说不允许修改当前类的任何成员
{
public:
int a ;
void funct1() const
{
a = 1; // 错误,等价于 this->a = 1 由于函数后面const修饰 this 指针指向了常量所以无法修改
}
void funct2()
{
a = 1; // 允许,等价于 this->a = 1
}
}
类成员函数比较多,比如普通成员函数,构造函数,拷贝函数,析构函数等等,会在后面详解。
7.8 函数重载
函数重载是指返回类型相同,函数名相同但参数不完全相同的多个函数。函数调用时会根据传递的参数类型和个数寻找最合适的重载函数。
不但参数类型和个数可以作为重载依据,当形参数是引用或指针时 const 可用作重载依据。
// 引用形参时 是否是const
const int b;
通常来说函数以参数类型不同或参数数量来重载。
7.9 指向函数的指针
指针不但可以指向内置类型,类类型,数组,还可以指向函数。函数指针的最大作用是将函数作为一种类型传递。
定义语法是:
void (*pr)(int i, string s) ; // 定义了一个函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型
pr = funct ; // 为定义的指针赋值(funct是函数名称)
pr(1,"str") ; // 通过指针调用函数(不用解引)
可以用 typedef 可简化定义,将定义一个变为定义一类
typedef void (*pr)(int i, string s) ; // 定义了一类函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型
pr f1 = funct ; // 定义类型变量
f1(1,"str") ; // 通过指针调用函数
在这两种方式中,指针都可以赋0值,表示还未指向任何函数。
最后我们看看实际中函数指针的应用
// 形参是函数指针 返回函数指针
pr getfunction(pr f,int`a)
{
f(a); // 执行
return f; // 返回函数指针
}
pr f1 = getfunction(funct, 1);
f1(2);
定义参数时用了typedef的类型pr,如果不使用则定义语句相当复杂。
对于有重载的函数,定义的指针一定要有对应的重载版本。 初始化指针变量时系统也会查找对应的重载函数
void fun(string a);
typedef void (*pr)(string); // 定义了string形参版的函数指针
pr p = fun; // 定义函数指针变量,系统会用 void fun(string) 版函数来初始化p
C++ Primer 第八章 标准IO库
学习本章内容之前有必要对缓冲区的概念做一个基本了解,我引用了网上一片文章《C++编程对缓冲区的理解》,内容如下:
什么是缓冲区
缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。
缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
为什么要引入缓冲区
比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。
现在您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。
缓冲区的类型
缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。
1、全缓冲
在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。
2、行缓冲
在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。
3、不带缓冲
也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。
缓冲区的刷新
下列情况会引发缓冲区的刷新:
1、缓冲区满时;
2、执行flush语句;
3、执行endl语句;
4、关闭文件。
可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区),如:
cout<<flush ; // 将显存的内容立即输出到显示器上进行显示
cout<<endl ; // endl控制符的作用是将光标移动到输出设备中下一行开头处,并且清空缓冲区
相当于
cout<<”\n” <<flush ;
通过实例演示说明
1、文件操作演示全缓冲
创建一个控制台工程,输入如下代码:
using namespace std;
int main()
{
//创建文件test.txt并打开
ofstream outfile("test.txt");
//向test.txt文件中写入4096个字符’a’
for(int n=0; n < 4096; n++)
{
outfile << 'a';
}
//暂停,按任意键继续
system("PAUSE");
//继续向test.txt文件中写入字符’b’,也就是说,第4097个字符是’b’
outfile << 'b';
//暂停,按任意键继续
system("PAUSE");
return 0;
}
上面这段代码很容易理解,已经在代码内部作了注释。
编写这段小代码的目的是验证WindowsXP下全缓冲的大小是4096个字节,并验证缓冲区满后会刷新缓冲区,执行真正的I/O操作。
编译并执行,运行结果如下:
此时打开工程所在文件夹下的test.txt文件,您会发现该文件是空的,这说明4096个字符“a”还在缓冲区,并没有真正执行I/O操作。敲一下回车键,窗口变为如下:
此时再打开test.txt文件,您就会发下该文件中已经有了4096个字符“a”。这说明全缓冲区的大小是4K(4096),缓冲区满后执行了I/O操作,而字符“b”还在缓冲区。
再次敲一下回车键,窗口变为如下:
此时再打开test.txt文件,您就会发现字符“b”也在其中了。这一步验证了文件关闭时刷新了缓冲区。
2、键盘操作演示行缓冲
先介绍getchar()函数。
函数原型:int getchar(void) ;
说明:当程序调用getchar()函数时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()函数才开始从键盘缓冲区中每次读入一个字符。也就是说,后续的getchar()函数调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才重新等待用户按键。
不知道您明白了没有,再通俗一点讲,当程序调用getchar()函数时,程序就等着用户按键,并等用户按下回车键返回。期间按下的字符存放在缓冲区,第一个字符作为函数返回值。继续调用getchar()函数,将不再等用户按键,而是返回您刚才输入的第2个字符;继续调用,返回第3个字符,直到缓冲区中的字符读完后,才等待用户按键。
如果您还没有明白,只能怨我表达能力有限,您可以结合以下实例体会。
创建一个控制台工程,输入如下代码:
using namespace std;
int main()
{
char c;
//第一次调用getchar()函数,程序执行时,您可以输入一串字符并按下回车键,按下回车键后该函数返回。返回值是用户输入的第一个字符 (假设用户输入了 abcdef,函数返回a)
c = getchar();
//显示getchar()函数的返回值
cout<< c << endl; // 输出 a
// 循环多次调用getchar()函数,将每次调用getchar()函数的返回值显示出来,直到遇到回车符才结束。 这时函数执行不会让用户输入而是顺序读取缓冲区字符内容。第一个字符用户输入结束后已经读取,所以会从第二个字符开始读
while((c = getchar())!='\n')
{
cout<< "," << c <<endl
}
return 0;
}
这段小代码也很简单,同样在代码内部都有注释。最后输出结果是
a
,b
,c
,d
,e
,f
getchar()函数的执行就是采用了行缓冲。第一次调用getchar()函数,会让程序使用者(用户)输入一行字符并直至按下回车键 函数才返回。此时用户输入的字符和回车符都存放在行缓冲区。
再次调用getchar()函数,会逐步输出行缓冲区的内容。
3、标准错误输出不带缓冲
如错误输出时使用:
cerr<<”错误,请检查输入的参数!” ;
这条语句等效于:
fprintf(stderr, ”错误,请检查输入的参数!”) ;
看完了缓冲区内容我们来学习C++标准IO库。
8.1 面向对象的标注库
先来看IO操作类图:
IO库大致可操作三类数据: 控制台流(stream) , 文件(file) , 字符串 (string)。
操作类型又可分三类:输入(in), 输出(out) ,输入与输出(in/out)。
ostream 是所有类型输出操作的基类
它扩展出两个子类: ofstream针对文件的输出操作类;ostringstream针对string的输出操作类
istream 是所有类型输入操作的基类
它扩展出两个子类: ifstream针对文件的输入操作类;istringstream针对string的输入操作类
ostream 和 istream共同扩展出类 iostream 它负责处理控制台stream的输入和输出
iostream 扩展出两个子类:stringstream专门处理string的输入输出; fstream专门处理文件的输入输出
所有类定义在不同头文件,正确使用类必须引入对应的头文件
iosteam头文件中定义了:
istream(从流中读取);
ostream(写到流中);
iostream(对流进行读写,从istream和osteam派生而来)
fsteam头文件中定义了:
ifstream(从文件中读取,由istream派生而来);
ofstream(写到文件中去,由ostream派生而来);
fstream(对文件进行读写,由iostream派生而来);
ssteam头文件中定义了:
istringstream(从string对象中读取,由istream派生而来);
ostringstream(写到string对象中去,由ostream派生而来);
stringstream(对string对象进行读写,由iostream派生而来);
要想正确使用IO类一定要理解他们的对应关系和每个类的职责。
c++的标准输入输出库iostream 是一个类库,以类的形式组织,使用该库中的类要先引用命名空间:using std;
最常使用的是cin和cout,这两个都是对象,cin是istream类的对象,cout是ostream类的对象,而输入的 cin>> 与输出时的 cout<< 中的左移 << 与右移 >> 分别是 istream 类与 ostream 类的操作符重载。
iostream库里面创建了3个标准流对象:
cin 表示标准输入的istream对象,cin可以使我们从设备读取数据。
cout 表示标准输出的ostream对象,cout可以使我们向设备写入数据。
cerr 表示标准错误的ostream对象,cerr是导出程序错误消息的地方,只能向屏幕设备写数据。
标准的流对象都有默认的设备:
cout << da
cin >> da
不管是输出到屏幕还是写入string或者保存到txt文本,字符都是不可获取的。通常我们不仅仅使用英文标准字符,我们还可能输入输出中文字符或其他非英文字符。这时候需要国际字符支持。
例如我们要保存一个字符 'a' 可以定义 char, 但是我们要保存字符 '家' 就无法按使用char 而要使用 wchar_t 了。IO类也有这样的区分,例如我们要在控制台输出中文字符就只能用 wcout << "你好" << endl想要将中文保存到文本就要用wofstream 或 wfstream 。要正确读取包含中文字符文件要使用 wifstream 类。
IO对象无法复制或者赋值,所以io对象作为函数形参或返回值时只能使用指针或引用。
iostream &Getio(iostream &io, fstream *fs){ ...} // 正确,参数和返回以引用或指针形式传递
iostream Getio(iostream io){ ...} // 错误,参数和返回以拷贝方式传递会发生复制和赋值操作
8.2 条件状态
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值,则该函数返回true
s.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
这么多操作我们该如何理解呢。
上面说过,任何一个IO对象在任意时刻都有一种状态。iostate 就是代表状态的枚举。badbit,failbit,eofbit,goodbit是iostate的一个具体值,看下面代码
{
cout << std::iostream::good << std::iostream::badbit << std::iostream::eofbit << std::iostream::failbit << endl; // 输出 0 1 2 4
std::iostream::iostate coutstate = cin.rdstate(); // 得到cin对象的原始状态值
cout << coutstate << endl; // 输出 0 cin的状态值是 std::iostream::good
int i;
cin >> i; // 输入"123"
cout << cin.rdstate() << endl; // 输出 0 因为“123”可以被正确转成int并被存入i 所以cin的状态置为 std::iostream::good
cout << cin.good() << cin.eof() << cin.fail() << cin.bad() << end; // 输出 1 0 0 0
cin >> i; // 输入"abd"
cout << cin.rdstate() << endl; // 输出 4 因为“abc”无法转成int存入i 所以cin的状态置为 std::iostream::failbit
cout << cin.good() << cin.eof() << cin.fail() << cin.bad() << end; // 输出 0 0 1 0
cin.clear(); // 重置cin状态为std::iostream::good 否则下面的cin << i 不会执行, 或者这样设置cin.clear(std::iostream::failbit)
cin >> i; // 输入"568"
cout << cin.rdstate() << endl; // 输出 0 因为“568”可以被正确转成int并被存入i 所以cin的状态置为 std::iostream::good
cout << cin.good() << cin.eof() << cin.fail() << cin.bad() << end; // 输出 1 0 0 0
}
8.3 输出缓冲区的管理
关于缓冲区内容本章开头有介绍。
这里介绍tie()函数用法,函数可将输出流与输入流关联起来,在这种情况下在读取输入流时将刷新关联的输出流,下面代码解释了含义
using namespace std;
int main()
{
//创建文件test.txt并打开
ofstream outfile("test.txt");
//将输出流对象 outfile 和输入cin关联起来
cin.tie(&outfile);
//向test.txt文件中写入字符串
// 执行输入语句时会立刻刷新关联的输出流,字符串被写到文件中
cin >> in;
return 0;
}
如果不用tie()函数做关联,main 方法执行完毕字符串"abcedf"才会被刷新到test.txt文本文件(执行完毕后系统会自动关闭文件从而刷新缓冲区内容到文件)。
tie()关联之后第一次执行到 cin 语句系统会立刻刷新关联的输出流,所以内容会立刻写入文件不必等到main执行完毕。
8.4 文件流对象的使用
C++ 通过以下几个类支持文件的输入输出:
ofstream: 写操作(输出)的文件类 (由ostream引申而来)
ifstream: 读操作(输入)的文件类(由istream引申而来)
fstream: 可同时读写操作的文件类 (由iostream引申而来)
打开文件(Open a file)
对这些类的一个对象所做的第一个操作通常就是将它和一个真正的文件联系起来,也就是说打开一个文件。被打开的文件在程序中由一个流对象(stream object)来表示 (这些类的一个实例) ,而对这个流对象所做的任何输入输出操作实际就是对该文件所做的操作。
要通过一个流对象打开一个文件,我们使用它的成员函数open() :
void open (const char *filename, openmode mode) ;
这里filename 是一个字符串,代表要打开的文件名,mode 是以下标志符的一个组合:
ios::in | 为输入(读)而打开文件 |
ios::out | 为输出(写)而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 所有输出附加在文件末尾 |
ios::trunc | 如果文件已存在则先删除该文件 |
ios::binary | 二进制方式 |
这些标识符可以被组合使用,中间以”或”操作符(|)间隔。例如,如果我们想要以二进制方式打开文件"example.bin" 来写入一些数据,我们可以通过以下方式调用成员函数open() 来实现:
ofstream file ;
file.open ("example.bin", ios::out | ios::app | ios::binary) ;
ofstream, ifstream 和 fstream所有这些类的成员函数open 都包含了一个默认打开文件的方式xxstream.open("filepath") ,这三个类的默认方式各不相同:
类 | 参数的默认方式 |
---|---|
ofstream | ios::out | ios::trunc |
ifstream | ios::in |
fstream | ios::in | ios::out |
只有当函数被调用时没有声明方式参数的情况下,默认值才会被采用。如果函数被调用时声明了任何参数,默认值将被完全改写,而不会与调用参数组合。
由于对类ofstream, ifstream 和 fstream 的对象所进行的第一个操作通常都是打开文件,这些类都有一个构造函数可以直接调用open 函数,并拥有同样的参数。这样,我们就可以通过以下方式进行与上面同样的定义对象和打开文件的操作:
ofstream file("example.bin", ios::out | ios::app | ios::binary);
// 等价于
ofstream file ;
file.open("example.bin",ios::out | ios::app | ios::binary) ;
两种打开文件的方式都是正确的。你可以通过调用成员函数is_open()来检查一个文件是否已经被顺利的打开了:
bool is_open() ;
它返回一个布尔(bool)值,为真(true)代表文件已经被顺利打开,假( false )则相反。
关闭文件(Closing a file)
当文件读写操作完成之后,我们必须将文件关闭以使文件重新变为可访问的。关闭文件需要调用成员函数close(),它负责将缓存中的数据排放出来并关闭文件。它的格式很简单:void close();
这个函数一旦被调用,原先的流对象(stream object)就可以被用来打开其它的文件了,这个文件也就可以重新被其它的进程(process)所有访问了。
为防止流对象被销毁时还联系着打开的文件,析构函数(destructor)将会自动调用关闭函数close。
文本文件(Text mode files)
类ofstream, ifstream 和fstream 是分别从ostream, istream 和iostream 中引申而来的。这就是为什么 fstream 的对象可以使用其父类的成员来访问数据。
一般来说,我们将使用这些类与同控制台(console)交互同样的成员函数(cin 和 cout)来进行输入输出。如下面的例题所示,我们使用重载的插入操作符<<
int main ()
{
ofstream examplefile ("example.txt");
if (examplefile.is_open())
{
// 输出到(写入)文件
examplefile << "This is a line.\n";
examplefile << "This is another line.\n";
examplefile.close();
}
return 0;
}
文件内容是:
This is a line.
This is another line.
从文件中读入数据也可以用与 cin的使用同样的方法:
#include <fstream.h>
#include <stdlib.h>
int main ()
{
char buffer[256];
ifstream examplefile ("example.txt");
if (! examplefile.is_open())
{
cout << "Error opening file"; exit (1);
}
while (! examplefile.eof() )
// 从文件输输入(读出)一行内容到 buffer
cout << buffer << endl;
}
return 0;
}
exit()退出程序,把控制权交给OS
return结束当前函数,返回函数值,把控制权交给调用函数
在main函数中return 与exit用法差不多
上面的例子读入一个文本文件的内容,然后将它打印到屏幕上。注意我们使用了一个新的成员函数叫做eof ,它是ifstream 从类 ios 中继承过来的,当到达文件末尾时返回true 。
状态标志符的验证(Verification of state flags)
除了eof()以外,还有一些验证流的状态的成员函数(所有都返回bool型返回值):
- bad()
如果在读写过程中出错,返回 true 。例如:当我们要对一个不是打开为写状态的文件进行写入时,或者我们要写入的设备没有剩余空间的时候。
- fail()
除了与bad() 同样的情况下会返回 true 以外,加上格式错误时也返回true ,例如当想要读入一个整数,而获得了一个字母的时候。
- eof()
如果读文件到达文件末尾,返回true。
- good()
这是最通用的:如果调用以上任何一个函数返回true 的话,此函数返回 false 。
要想重置以上成员函数所检查的状态标志,你可以使用成员函数clear(),没有参数。
获得和设置流指针(get and put stream pointers)
所有输入/输出流对象(i/o streams objects)都有至少一个流指针:
- ifstream, 类似istream, 有一个被称为get pointer的指针,指向下一个将被读取的元素。
- ofstream, 类似 ostream, 有一个指针 put pointer ,指向写入下一个元素的位置。
- fstream, 类似 iostream, 同时继承了get 和 put
我们可以通过使用以下成员函数来读出或配置这些指向流中读写位置的流指针:
- tellg() 和 tellp()
这两个成员函数不用传入参数,返回pos_type 类型的值(根据ANSI-C++ 标准) ,就是一个整数,代表当前get 流指针的位置 (用tellg) 或 put 流指针的位置(用tellp).
- seekg() 和seekp()
这对函数分别用来改变流指针get 和put的位置。两个函数都被重载为两种不同的原型:
seekg ( pos_type position ) ;
seekp ( pos_type position ) ;使用这个原型,流指针被改变为指向从文件开始计算的一个绝对位置。要求传入的参数类型与函数 tellg 和tellp 的返回值类型相同。
seekg ( off_type offset, seekdir direction ) ;
seekp ( off_type offset, seekdir direction ) ;
使用这个原型可以指定由参数direction决定的一个具体的指针开始计算的一个位移(offset)。它可以是:ios::beg 从流开始位置计算的位移 ios::cur 从流指针当前位置开始计算的位移 ios::end 从流末尾处开始计算的位移
流指针 get 和 put 的值对文本文件(text file)和二进制文件(binary file)的计算方法都是不同的,因为文本模式的文件中某些特殊字符可能被修改。由于这个原因,建议对以文本文件模式打开的文件总是使用seekg 和 seekp的第一种原型,而且不要对tellg 或 tellp 的返回值进行修改。对二进制文件,你可以任意使用这些函数,应该不会有任何意外的行为产生。
以下例子使用这些函数来获得一个二进制文件的大小:
#include <fstream.h>
const char * filename = "example.txt";
int main ()
{
long l,m;
ifstream file(filename, ios::in|ios::binary);
l = file.tellg();
file.seekg(0, ios::end);
m = file.tellg();
file.close();
cout << "size of " << filename << " is " << (m-l) << " bytes.\n"; // 输出 size of example.txt is 40 bytes.
return 0;
}
二进制文件(Binary files)
在二进制文件中,使用<< 和 >>,以及函数(如getline)来操作符输入和输出数据,没有什么实际意义,虽然它们是符合语法的。
文件流包括两个为顺序读写数据特殊设计的成员函数:write 和 read。第一个函数 (write) 是ostream 的一个成员函数,都是被ofstream所继承。而read 是istream 的一个成员函数,被ifstream 所继承。类 fstream 的对象同时拥有这两个函数。它们的原型是:
read ( char * buffer, streamsize size ) ;
这里 buffer 是一块内存的地址,用来存储或读出数据。参数size 是一个整数值,表示要从缓存(buffer)中读出或写入的字符数。
#include <fstream.h>
const char * filename = "example.txt";
int main ()
{
char * buffer;
long size;
ifstream file (filename, ios::in|ios::binary|ios::ate); // ios::ate 表示指向文件末尾
size = file.tellg(); // 取得文件大小
file.seekg (0, ios::beg);
buffer = new char[size];
file.read(buffer, size);
file.close();
cout << "the complete file is in a buffer";
delete[] buffer;
return 0;
}
缓存和同步(Buffers and Synchronization)
当我们对文件流进行操作的时候,它们与一个streambuf 类型的缓存(buffer)联系在一起。这个缓存(buffer)实际是一块内存空间,作为流(stream)和物理文件的媒介。例如,对于一个输出流, 每次成员函数put (写一个单个字符)被调用,这个字符不是直接被写入该输出流所对应的物理文件中的,而是首先被插入到该流的缓存(buffer)中。
当缓存被排放出来(flush)时,它里面的所有数据或者被写入物理媒质中(如果是一个输出流的话),或者简单的被抹掉(如果是一个输入流的话)。这个过程称为同步(synchronization),它会在以下任一情况下发生:
- 当文件被关闭时: 在文件被关闭之前,所有还没有被完全写出或读取的缓存都将被同步。
- 当缓存buffer 满时:缓存Buffers 有一定的空间限制。当缓存满时,它会被自动同步。
- 控制符明确指明:当遇到流中某些特定的控制符时,同步会发生。这些控制符包括:flush 和endl。
- 明确调用函数sync(): 调用成员函数sync() (无参数)可以引发立即同步。这个函数返回一个int 值,等于-1 表示流没有联系的缓存或操作失败。
在C++中,有一个stream这个类,所有的I/O都以这个“流”类为基础的,包括我们要认识的文件I/O,stream这个类有两个重要的运算符:
1、插入器 (<<)
向流输出数据。比如说系统有一个默认的标准输出流(cout),一般情况下就是指的显示器,所以,cout<<"Write Stdout"<<'n';就表示把字符串"Write Stdout"和换行字符('n')输出到标准输出流。
2、析取器 (>>)
从流中输入数据。比如说系统有一个默认的标准输入流(cin),一般情况下就是指的键盘,所以,cin>>x;就表示从标准输入流中读取一个指定类型(即变量x的类型)的数据。
在C++中,对文件的操作是通过stream的子类fstream(file stream)来实现的,所以,要用这种方式操作文件,就必须加入头文件fstream.h。下面就把此类的文件操作过程一一道来。
一、打开文件
在fstream类中,有一个成员函数open(),就是用来打开文件的,其原型是:
void open(const char* filename, int mode, int access) ;
参数:
filename: 要打开的文件名
mode: 要打开文件的方式
access: 打开文件的属性
打开文件的方式在类ios(是所有流式I/O类的基类)中定义,常用的值如下:
ios::app: 以追加的方式打开文件
ios::ate: 文件打开后定位到文件尾,ios:app就包含有此属性
ios::binary: 以二进制方式打开文件,缺省的方式是文本方式。两种方式的区别见前文
ios::in: 文件以输入方式打开
ios::out: 文件以输出方式打开
ios::nocreate: 不建立文件,所以文件不存在时打开失败
ios::noreplace:不覆盖文件,所以打开文件时如果文件存在失败
ios::trunc: 如果文件存在,把文件长度设为0
可以用“或”把以上属性连接起来,如 ios::out| ios::binary
打开文件的属性取值是:
0:普通文件,打开访问
1:只读文件
2:隐含文件
4:系统文件
可以用“或”或者“+”把以上属性连接起来 ,如3或 1|2 就是以只读和隐含属性打开文件。
例如:以二进制输入方式打开文件c:config.sys
fstream file1 ;
file1.open("config.sys", ios::binary | ios::in,0) ;
如果open函数只有文件名一个参数,则是以读/写普通文件打开,即:
file1.open("config.sys") ; 等价于 file1.open("config.sys",ios::in|ios::out,0) ;
另外,fstream还有和open()一样的构造函数,对于上例,在定义的时侯就可以打开文件了:
fstream file1("config.sys") ;
特别提出的是,fstream有两个子类:ifstream(input file stream)和ofstream(outpu file stream),ifstream默认以输入方式打开文件,而ofstream默认以输出方式打开文件。
ifstream file2("dos.def") ; //以输入方式打开文件
ofstream file3("x.123") ; //以输出方式打开文件
所以,在实际应用中,根据需要的不同,选择不同的类来定义:如果想以输入方式打开,就用ifstream来定义;如果想以输出方式打开,就用ofstream来定义;如果想以输入/输出方式来打开,就用fstream来定义。
二、关闭文件
打开的文件使用完成后一定要关闭,fstream提供了成员函数close()来完成此操作,如:file1.close();就把file1相连的文件关闭。
三、读写文件
读写文件分为文本文件和二进制文件的读取,对于文本文件的读取比较简单,用插入器和析取器就可以了;而对于二进制的读取就要复杂些,下要就详细的介绍这两种方式
1、文本文件的读写
文本文件的读写很简单:用插入器(<<)向文件输出;用析取器(>>)从文件输入。假设file1是以输入方式打开,file2以输出打开。示例如下:
file2<<"I Love You" ; // 向文件写入字符串"I Love You"
int i ;
file1 >> i ; // 从文件输入一个整数值。
这种方式还有一种简单的格式化能力,比如可以指定输出为16进制等等,具体的格式有以下一些
操纵符 功能 输入/输出
dec 格式化为十进制数值数据 输入和输出
endl 输出一个换行符并刷新此流 输出
ends 输出一个空字符 输出
hex 格式化为十六进制数值数据 输入和输出
oct 格式化为八进制数值数据 输入和输出
setpxecision(int p) 设置浮点数的精度位数 输出
比如要把123当作十六进制输出:file1<<hex<<123;要把3.1415926以5位精度输出:file1<<setpxecision(5)<<3.1415926。
2、二进制文件的读写
①put()
put()函数向流写入一个字符,其原型是ofstream &put(char ch),使用也比较简单,如file1.put('c');就是向流写一个字符'c'。
②get()
get()函数比较灵活,有3种常用的重载形式:
一种就是和put()对应的形式:ifstream &get(char &ch) ;功能是从流中读取一个字符,结果保存在引用ch中,如果到文件尾,返回空字符。如file2.get(x);表示从文件中读取一个字符,并把读取的字符保存在x中。
另一种重载形式的原型是: int get() ;这种形式是从流中返回一个字符,如果到达文件尾,返回EOF,如x=file2.get();和上例功能是一样的。
还有一种形式的原型是:ifstream &get(char *buf,int num,char delim='n') ;这种形式把字符读入由 buf 指向的数组,直到读入了 num 个字符或遇到了由 delim 指定的字符,如果没使用 delim 这个参数,将使用缺省值换行符'n'。例如:
file2.get(str1,127,'A') ; // 从文件中读取字符到字符串str1,当遇到字符'A'或读取了127个字符时终止。
③读写数据块
要读写二进制数据块,使用成员函数read()和write()成员函数,它们原型如下:
read(unsigned char *buf,int num) ;
write(const unsigned char *buf,int num) ;
read() 从文件中读取 num 个字符到 buf 指向的缓存中,如果在还未读入 num 个字符时就到了文件尾,可以用成员函数 int gcount();来取得实际读取的字符数;而 write() 从buf 指向的缓存写 num 个字符到文件中,值得注意的是缓存的类型是 unsigned char *,有时可能需要类型转换。
例:
unsigned char str1[]="I Love You";
int n[5];
ifstream in("xxx.xxx");
ofstream out("yyy.yyy");
out.write(str1,strlen(str1));//把字符串str1全部写到yyy.yyy中
in.read((unsigned char*)n,sizeof(n));//从xxx.xxx中读取指定个整数,注意类型转换
in.close(); out.close();
四、检测EOF
成员函数eof()用来检测是否到达文件尾,如果到达文件尾返回非0值,否则返回0。原型是int eof();
例: if(in.eof()) ShowMessage("已经到达文件尾!") ;
五、文件定位
和C的文件操作方式不同的是,C++ I/O系统管理两个与一个文件相联系的指针。一个是读指针,它说明输入操作在文件中的位置;另一个是写指针,它下次写操作的位置。每次执行输入或输出时相应的指针自动变化。
所以,C++的文件定位分为读位置和写位置的定位,对应的成员函数是 seekg()和 seekp(),seekg()是设置读位置,seekp是设置写位置。它们最通用的形式如下:
istream &seekg(streamoff offset,seek_dir origin);
ostream &seekp(streamoff offset,seek_dir origin);
streamoff定义于 iostream.h 中,定义有偏移量 offset 所能取得的最大值,seek_dir 表示移动的基准位置,是一个有以下值的枚举:
ios::beg: 文件开头
ios::cur: 文件当前位置
ios::end: 文件结尾
这两个函数一般用于二进制文件,因为文本文件会因为系统对字符的解释而可能与预想的值不同。
例:
file1.seekg(1234,ios::cur) ; //把文件的读指针从当前位置向后移1234个字节
file2.seekp(1234,ios::beg) ; //把文件的写指针从文件开头向后移1234个字节
C++ Primer 第九章 顺序容器
标准库定义了三种顺序容器类型:vector、list
它们的差别在于访问元素的方式,以及添加或删除元素相关操作的运行代价。
标准库还提供了三种容器适配器(adaptors)。
实际上,适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口,来适应基础的容器类型。顺序容器适配器包括
顺序容器 | |
vector<T> | 支持快速随机访问 |
list<T> | 支持快速插入/删除 |
deque<T> | 双端队列 |
顺序容器适配器 | |
stack | 后进先出(LIFO)堆栈 |
queue | 先进先出(FIFO)队列 |
priority_queue | 有优先级管理的队列 |
), resieze()(重新划分容器容量)等操作; vector不用担心越界当空间不够用的时候,系统会自动按照一定的比例(对capacity( )大小)进行扩充。在vector序列末尾添加(push_back( ))或者删除(pop_back(
))对象效率高,在中间进行插入或删除效率很低,主要是要进行元素的移动和内存的拷贝,原因就在于当内存不够用的时候要执行重新分配内存,拷贝对象到新存储区,销毁old对象,释放内存等操作,如果对象很多的话,这种操作代价是相当高的。为了减少这种代价,使用vector最理想的情况就是事先知道所要装入的对象数目,用成员函式 reserve( ) 预定下来;vector最大的优点莫过于是检索(用operator[ ])速度在这三个容器中是最快的。
deque : deque是一个double-ended。queue是由多个连续内存块构成,deque是list和vector的兼容,分为多个块,每一个块大小是512字节,块通过map块管理,map块里保存每个块得首地址。因此该容器也有索引操作operator[ ],效率没vector高。另外,deque比vector多了push_front( ) & pop_front( )操作。在两端进行此操作时与list的效率 差不多。
学习顺序容器最重要的是了解每种容器的结构原理进而在实际应用中选择最合适的容器。
三种容器都是范型类型,可以定义不同的类型容器。
容器元素类型必须满足以下两个约束:
- 元素类型必须支持赋值运算。
- 元素类型的对象必须可以复制。
顺序容器有大量的属性和操作,最常用的是迭代器,增加,删除元素。
常用迭代器运算
*iter | 返回迭代器 iter 所指向的元素的引用 |
iter->mem | 对 iter 进行解引用,获取指定元素中名为 mem 的成员。等效于 (*iter).mem |
++iter iter++ | 给 iter 加 1,使其指向容器里的下一个元素 |
--iter iter-- | 给 iter 减 1,使其指向容器里的前一个元素 |
iter1 == iter2 | 比较两个迭代器是否相等(或不等)。当两个迭代器指向同一个容器中的同一个元素,或者当它们都指向同一个容器的超出末端的下一位置时,两个迭代器相等 |
iter + n | 在迭代器上加(减)整数值 n,将产生指向容器中前面(后面)第 n 个元素的迭代器。新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一位置 |
iter1 += iter2 | 这里迭代器加减法的复合赋值运算:将 iter1 加上或减去 iter2 的运算结果赋给 iter1 |
iter1 - iter2 | 两个迭代器的减法,其运算结果加上右边的迭代器即得左边的迭代器。这两个迭代器必须指向同一个容器中的元素或超出容器末端的下一位置 只适用于 vector 和 deque 容器 |
>, >=, <, <= | 迭代器的关系操作符。当一个迭代器指向的元素在容器中位于另一个迭代器指向的元素之前,则前一个迭代器小于后一个迭代器。关系操作符的两个迭代器必须指向同一个容器中的元素或超出容器末端的下一位置 只适用于 vector 和 deque 容器 |
最重要的迭代器begin() 和end() 。
顺序容器添加元素操作
c.push_back(t) | 在容器 c 的尾部添加值为 t 的元素。返回 void 类型 |
c.push_front(t) | 在容器 c 的前端添加值为 t 的元素。返回 void 类型 只适用于 vector 和 deque 容器 |
c.insert(p,t) | 在迭代器 p 所指向的元素前面插入值为 t 的新元素。返回指向新添加元素的迭代器 |
c.insert(p,n,t) | 在迭代器 p 所指向的元素前面插入 n 个值为 t 的新元素。返回 void 类型 |
c.insert(p,b,e) | 在迭代器 p 所指向的元素前面插入由迭代器 b 和 e 标记的范围内的元素。返回 void 类型 |
顺序容器的大小操作
c.size() | 返回容器 c 中的元素个数。返回类型为 c::size_type |
c.max_size() | 返回容器 c 可容纳的最多元素个数,返回类型为 c::size_type |
c.empty() | 返回标记容器大小是否为 0 的布尔值 |
c.resize(n) | 调整容器 c 的长度大小,使其能容纳 n 个元素,如果 n < c.size(),则删除多出来的元素;否则,添加采用值初始化的新元素 |
c.resize(n,t) | 调整容器 c 的长度大小,使其能容纳 n 个元素。所有新添加的元素值都为 t |
看下面例子:
list<int> ilist(10, 42);
ilist.resize(15);
ilist.resize(25, -1);
ilist.resize(5);
resize 操作可能会使迭代器失效。在 vector 或 deque 容器上做 resize 操作有可能会使其所有的迭代器都失效。
对于所有的容器类型,如果 resize 操作压缩了容器,则指向已删除的元素迭代器失效。
vector 预分配机制: 可以在元素不存在的情况下预分配一段空间,为以后的存储做准备。这段空间可以用reserve()调节。capacity()返回的值就是可以存放元素的个数。capacity() - size()就是下次重新进行空间分配前的预留元素个数。至于max_size() 指的是一个vector结构可供储存元素的个数的上线,通常是由于寻址空间决定的。
访问顺序容器内元素的操作
c.back() | 返回容器 c 的最后一个元素的引用。如果 c 为空,则该操作未定义 |
c.front() | 返回容器 c 的第一个元素的引用。如果 c 为空,则该操作未定义 |
c[n] | 返回下标为 n 的元素的引用 如果 n <0 或 n >= c.size(),则该操作未定义 只适用于 vector 和 deque 容器 |
c.at(n) | 返回下标为 n 的元素的引用。如果下标越界,则该操作未定义 只适用于 vector 和 deque 容器 |
使用下标运算的另一个可选方案是 at 成员函数。这个函数的行为和下标运算相似,但是如果给出的下标无效,at 函数将会抛出 out_of_range 异常:
vector<string> svec;
cout << svec[0]; // run-time error: There are no elements in svec!
顺序容器有大量的成员可用,学习容器不需要记住所有成员操作时可查阅相关文档。
顺序容器还有三种适配器,主要作用是包装其它容器使之具有某种操作特征。
1 stack 堆栈适配器 ( 可用的容器类型 vector deque list)
2 queue 队列适配器 ( 可用的容器类型 deque list)
3 priority_queue 优先级队列 (可用的容器类型 deque vector)
#include <iostream>
#include <stack> // 堆栈适配器头文件
#include <list>
using namespace std;
int main( void)
{
std::stack< int,std::list< int> > charStack;
cout << " 入栈: " << endl;
for( int i= 0;i< 10;i++)
{
cout<<i+ 66<<endl;
charStack.push( 66+i);
}
cout<< " 出栈: "<<endl;
int size=charStack.size();
for (i= 0;i<size;i++)
{
cout<<charStack.top()<<endl;
charStack.pop();
}
cout<<endl;
return 0;
}
queue用法:
#include <iostream>
#include <queue>
#include <list>
using namespace std;
int main()
{
std::queue< int,list< int> > intQueue;
cout << " 入队: " << endl;
for( int i= 1;i<= 10;i++)
{
intQueue.push(i* 100);
cout<<i* 100<<endl;
}
cout<< " 出队: "<<endl;
int size=intQueue.size();
for(i= 0;i<size;i++)
{
cout<<intQueue.front()<<endl;
intQueue.pop();
}
return 0;
}
priority_queue 用法:
#include <iostream>
#include <queue>
#include <list>
using namespace std;
int main()
{
std::priority_queue< int,vector< int>,std::greater< int> > intPQueue; // 优先级greater<type> 可换成 less<type>
intPQueue.push( 100);
intPQueue.push( 500);
intPQueue.push( 600);
intPQueue.push( 200);
intPQueue.push( 300);
intPQueue.push( 400);
int size=intPQueue.size();
for( int i= 0;i<size;i++)
{
cout<<intPQueue.top()<<endl;
intPQueue.pop();
}
return 0;
}
C++ Primer 第十章 关联容器
关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。
关联容器(Associative containers)支持通过键来高效地查找和读取元素。两个基本的关联容器类型是 map 和 set。
map 的元素以键-值(key-value)对的形式组织:键用作元素在 map 中的索引,而值则表示所存储和读取的数据。set 仅包含一个键,并有效地支持关于某个键是否存在的查询。
关联容器类型
map | 关联数组:元素通过键来存储和读取 |
set | 大小可变的集合,支持通过键实现的快速读取 |
multimap | 支持同一个键多次出现的 map 类型 |
multiset | 支持同一个键多次出现的 set 类型 |
一般来说,如果希望有效地存储不同值的集合,那么使用 set 容器比较合适,而 map 容器则更适用于需要存储(乃至修改)每个键所关联的值的情况。在做某种文本处理时,可使用 set 保存要忽略的单词。而字典则是 map 的一种很好的应用:单词本身是键,而它的解释说明则是值。
set 和 map 类型的对象所包含的元素都具有不同的键,不允许为同一个键添加第二个元素。如果一个键必须对应多个实例,则需使用 multimap 或 multi set,这两种类型允许多个元素拥有相同的键。
在开始介绍关联容器之前,必须先了解一种与之相关的简单的标准库类型—— pair,该类型在 utility 头文件中定义。
pairs 类型提供的操作
pair<T1, T2> p1; | 创建一个空的 pair 对象,它的两个元素分别是T1 和 T2 类型,采用值初始化 |
pair<T1, T2> p1(v1, v2); | 创建一个 pair 对象,它的两个元素分别是 T1 和 T2 ,其中 first 成员初始化为 v1,而 second 成员初始化为 v2 |
make_pair(v1, v2) | 以 v1 和 v2 值创建一个新 pair 对象,其元素类型分别是 v1 和 v2 的类型 |
p1 < p2 | 两个 pair 对象之间的小于运算,其定义遵循字典次序:如果 p1.first < p2.first 或者 !(p2.first < p1.first) && p1.second < p2.second,则返回 |
p1 == p2 | 如果两个 pair 对象的 first 和 second 成员依次相等,则这两个对象相等。该运算使用其元素的 == 操作符 |
p.first | 返回 p 中名为 first 的(公有)数据成员 |
p.second | 返回 p 的名为 second 的(公有)数据成员 |
pair 的创建和初始化
如果在创建 pair 对象时不提供初始化式,则调用默认构造函数对其成员采用值初始化。
strings pair<string, int> word_count; // holds a string and an int
pair<string, vector<int> > line; // holds string and vector<int>
pair<string, string> author("James", "Joyce");
pair 类型的使用相当繁琐,因此,如果需要定义多个相同的 pair 类型对象,可考虑利用 typedef 简化其声明:
与其他标准库类型不同,对于 pair 类,可以直接访问其数据成员:其成员都是公有的,分别命名为 first 和 second。
if (author.first == "James" && author.second == "Joyce")
{
firstBook = "Stephen Hero";
}
除了构造函数,标准库还定义了一个 make_pair 函数,由传递给它的两个实参生成一个新的 pair 对象。可如下使用该函数创建新的 pair 对象,并赋给已存在的 pair对象:
string first, last;
while (cin >> first >> last)
{
// generate a pair from first and last
next_auth = make_pair(first, last);
// process next_auth...
}
make_pair 函数生成一个新的 pair 对象,此操作等价于下面更复杂的操作:
next_auth = pair<string, string > (first, last);
由于 pair 的数据成员是公有的,因而可如下直接地读取输入:
// read directly into the members of next_auth
while (cin >> next_auth.first >> next_auth.second)
{
// process next_auth...
}
“关联容器元素根据键的次序排列”这一事实就是一个重要的结论:在迭代遍历关联容器时,我们可确保按键的顺序的访问元素,而与元素在容器中的存放位置完全无关。
map 是键-值对的集合。map 类型通常可理解为关联数组(associative array):可使用键作为下标来获取一个值,正如内置数组类型一样。而关联的本质在于元素的值与某个特定的键相关联,而并非通过元素在数组中的位置来获取。
map 对象的定义
map<k, v> m; | 创建一个名为 m 的空 map 对象,其键和值的类型分别为 k 和 v |
map<k, v> m(m2); | 创建 m2 的副本 m,m 与 m2 必须有相同的键类型和值类型 |
map<k, v> m(b, e); | 创建 map 类型的对象 m,存储迭代器 b 和 e 标记的范围内所有元素的副本。元素的类型必须能转换为 pair<const k, v> |
在实际应用中,键类型必须定义 < 操作符,而且该操作符应能“正确地工作”,这一点很重要。
在使用关联容器时,它的键不但有一个类型,而且还有一个相关的比较函数。
对于键类型,唯一的约束就是必须支持 < 操作符,至于是否支持其他的关系或相等运算,则不作要求。
map 对象的元素是键-值对,其 value_type 是存储元素的键以及值的 pair 类型,而且键为 const。
在学习 map 的接口时,需谨记 value_type 是 pair 类型,它的值成员可以修改,但键成员不能修改。
map 类定义的类型
map<K, V>::key_type | 在 map 容器中,用做索引的键的类型 |
map<K, V>::mapped_type | 在 map 容器中,键所关联的值的类型 |
map<K, V>::value_type | 一个 pair 类型,它的 first 元素具有 const map<K, V>::key_type 类型,而 second 元素则为 map<K, V>::mapped_type 类型 |
map<string, int> word_count; // empty map from string to int
map<string, int>::iterator map_it = word_count.begin(); // *map_it is a reference to a pair<const string, int> object
cout << " " << map_it->second; // prints the value of the element
map_it->first = "new key"; // error: key is const
++map_it->second; // ok: we can change value through an iterator
map 类额外定义了两种类型:key_type 和 mapped_type,以获得键或值的类型。
给 map 添加元素
可使用 insert 成员实现;或者,先用下标操作符获取元素,然后给获取的元素赋值。在这两种情况下,一个给定的键只能对应于一个元素这一事实影响了这些操作的行为。 如下编写程序时:
// insert default initialzed element with key Anna; then assign 1 to its value
word_count["Anna"] = 1;
使用下标访问 map 与使用下标访问数组或 vector 的行为截然不同:用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值。
下标操作符返回值的使用
通常来说,下标操作符返回左值。它返回的左值是特定键所关联的值。可如下读或写元素:
++word_count["Anna"]; // fetch the element and add one to it
cout << word_count["Anna"]; // fetch the element and print it; prints 2
有别于 vector 或 string 类型,map 下标操作符返回的类型与对 map 迭代器进行解引用获得的类型不相同。显然,map 迭代器返回 value_type 类型的值——包含const key_type 和 mapped_type 类型成员的 pair 对象;下标操作符则返回一个 mapped_type 类型的值。
对于 map 容器,如果下标所表示的键在容器中不存在,则添加新元素,这一特性可使程序惊人地简练:这段程序创建一个 map 对象,用来记录每个单词出现的次数。
map<string, int> word_count; // empty map from string to int
string word;
while (cin >> word)
++word_count[word];
容器提供的 insert 操作
m.insert(e) | e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则插入一个值为 e.second 的新元素;如果该键在 m 中已存在,则保持 m 不变。该函数返回一个 pair 类型对象,包含指向键为 e.first 的元素的 map 迭代器,以及一个 bool 类型的对象,表示是否插入了该元素 |
m.insert(beg,end) | beg 和 end 是标记元素范围的迭代器,其中的元素必须为 m.value_type 类型的键-值对。对于该范围内的所有元素,如果它的键在 m 中不存在,则将该键及其关联的值插入到 m。返回void 类型 |
m.insert(iter, e) | e 是一个用在 m 上的 value_type 类型的值。如果键(e.first)不在 m 中,则创建新元素,并以迭代器 iter 为起点搜索新元素存储的位置。返回一个迭代器,指向 m 中具有给定键的元素 |
以 insert 代替下表运算
插入元素的另一个方法是:直接使用 insert 成员,其语法更紧凑:
word_count.insert(map<string, int>::value_type("Anna", 1));
传递给 insert 的实参相当笨拙。可用两种方法简化:使用 make_pair:
word_count.insert(make_pair("Anna", 1));
或使用 typedef
typedef map<string,int>::value_type valType;
带有一个键-值 pair 形参的 insert 版本将返回一个值:包含一个迭代器和一个 bool 值的 pair 对象,其中迭代器指向 map 中具有相应键的元素,而 bool值则表示是否插入了该元素。如果该键已在容器中,则其关联的值保持不变,返回的 bool 值为 true。在这两种情况下,迭代器都将指向具有给定键的元素。下面是使用 insert 重写的单词统计程序:
map<string, int> word_count; // empty map from string to int
string word;
while (cin >> word)
{
// inserts element with key equal to word and value 1;
// if word already in word_count, insert does nothing
pair<map<string, int>::iterator, bool> ret = word_count.insert(make_pair(word, 1));
if (!ret.second)
// word already in word_count
++ret.first->second;
// increment
counter
对于每个单词,都尝试 insert 它,并将它的值赋 1。
if 语句检测 insert 函数返回值中的 bool 值。如果该值为 false,则表示没有做插入操作,按 word 索引的元素已在word_count 中存在。此时,将该元素所关联的值加 1。
使用下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素。map 容器提供了两个操作:count 和 find,用于检查某个键是否存在而不会插入该键。
不修改 map 对象的查询操作
m.count(k) | 返回 m 中 k 的出现次数 |
m.find(k) | 如果 m 容器中存在按 k索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器 |
对于 map 对象,count 成员的返回值只能是 0 或 1。map 容器只允许一个键对应一个实例,所以 count 可有效地表明一个键是否存在。
而对于 multimaps 容器,count 的返回值将有更多的用途。
如果返回值非 0,则可以使用下标操作符来获取该键所关联的值,而不必担心这样做会在 map 中插入新元素:
if (word_count.count("foobar"))
occurs = word_count["foobar"];
当然,在执行 count 后再使用下标操作符,实际上是对元素作了两次查找。如果希望当元素存在时就使用它,则应该用find 操作。find 操作返回指向元素的迭代器,如果元素不存在,则返回 end 迭代器:
map<string,int>::iterator it = word_count.find("foobar");
if (it != word_count.end())
occurs = it->second;
如果希望当具有指定键的元素存在时,就获取该元素的引用,否则就不在容器中创建新元素,那么应该使用 find。
有一点不同:map 容器的 erase 操作返回 void,而顺序容器的erase 操作则返回一个迭代器,指向被删除元素后面的元素。
从 map 对象中删除元素
m.erase(k) | 删除 m 中键为 k 的元素。返回 size_type 类型的值,表示删除的元素个数 |
m.erase(p) | 从 m 中删除迭代器 p 所指向的元素。p 必须指向 m 中确实存在的元素,而且不能等于 m.end()。返回 void |
m.erase(b,e) | 从 m 中删除一段范围内的元素,该范围由迭代器对 b 和 e 标记。b 和 e 必须标记 m 中的一段有效范围:即 b 和 e 都必须指向 m 中的元素或最后一个元素的下一个位置。而且,b 和 e 要么相等(此时删除的范围为空),要么 b 所指向的元素必须出现在 e 所指向的元素之前。返回 void 类型 |
if (word_count.erase(removal_word))
cout << "ok: " << removal_word << " removed\n";
else
cout << "oops: " << removal_word << " not found!\n";
m.erase(k)函数返回被删除元素的个数。 对于map 容器,该值必然是 0 或 1。如果返回 0,则表示欲删除的元素在 map 不存在。
map 对象的迭代遍历
与其他容器一样,map 同样提供 begin 和 end 运算,以生成用于遍历整个容器的迭代器。例如,可如下将 map 容器 word_count 的内容输出:
map<string, int>::const_iterator map_it = word_count.begin();
// for each element in the map
while (map_it != word_count.end())
{
// print the element key, value pairs
cout << map_it->first << " occurs " << map_it->second << " times" << endl;
++map_it; // increment iterator to denote the next element
}
这个单词统计程序依据字典顺序输出单词。在使用迭代器遍历 map 容器时,迭代器指向的元素按键的升序排列。
set 类型
当只想知道一个值是否存在时,使用 set 容器是最适合的。
两种例外包括:set 不支持下标操作符,而且没有定义 mapped_type 类型。在 set 容器中,value_type 不是 pair 类型,而是与 key_type 相同的类型。它们指的都是 set 中存储的元素类型。这一差别也体现了 set 存储的元素仅仅是键,而没有所关联的值。与 map 一样,set 容器存储的键也必须唯一,而且不能修改。
set 容器的定义和使用
在 set 对象中插入一组元素时,对于每个键,事实上都只添加了一个元素:
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i)
{
ivec.push_back(i);
ivec.push_back(i); // duplicate copies of each number
}
// iset holds unique elements from ivec
set<int> iset(ivec.begin(), ivec.end());
cout << ivec.size() << endl; // prints 20
cout << iset.size() << endl; // prints 10
可使用 insert 操作在 set 中添加元素:
set1.insert("the"); // set1 now has one element
set1.insert("and"); // set1 now has two elements
另一种用法是,调用 insert 函数时,提供一对迭代器实参,插入其标记范围内所有的元素。该版本的 insert 函数类似于形参为一对迭代器的构造函数——对于一个键,仅插入一个元素:
iset2.insert(ivec.begin(), ivec.end()); // iset2 has 10 elements
与 map 容器的操作一样,带有一个键参数的 insert 版本返回 pair类型对象,包含一个迭代器和一个 bool 值,迭代器指向拥有该键的元素,而 bool 值表明是否添加了元素。使用迭代器对的insert 版本返回 void 类型。
正如不能修改 map 中元素的键部分一样,set 中的键也为 const。
set<int>::iterator set_it = iset.find(1);
*set_it = 11; // error: keys in a set are read-only
cout << *set_it << endl; // ok: can read the key
删除指定文件中所有的单词(即该文件记录的是排除集)。也即,我们的单词统计程序只对那些不在排除集中的单词进行统计。使用 set 和 map 容器,可以简单而直接地实现该功能:
{
set<string> excluded; // set to hold words we'll ignore
string remove_word;
while (remove_file >> remove_word)
excluded.insert(remove_word);
string word;
while (cin >> word)
// increment counter only if the word is not in excluded
if (!excluded.count(word))
++word_count[word];
}
map 和 set 容器中,一个键只能对应一个实例。而 multiset 和 multimap 类型则允许一个键对应多个实例。 注意到,关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和 multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放。 迭代遍历 multimap 或multiset 容器时,可保证依次返回特定键所关联的所有元素。
基于一个事实——在 multimap 中,同一个键所关联的元素必然相邻存放。
使用 find 和 count 操作
string search_item("Alain de Botton");
// how many entries are there for this author
typedef multimap<string, string>::size_type sz_type;
sz_type entries = authors.count(search_item);
// get iterator to the first entry for this author
multimap<string,string>::iterator iter = authors.find(search_item);
// loop through the number of entries there are for this author
for (sz_type cnt = 0; cnt != entries; ++cnt, ++iter)
cout << iter->second << endl; // print each title
返回迭代器的关联容器操作
m.lower_bound(k) | 返回一个迭代器,指向键不小于 k 的第一个元素 |
m.upper_bound(k) | 返回一个迭代器,指向键大于 k 的第一个元素 |
m.equal_range(k) | 返回一个迭代器的 pair 对象它的 first 成员等价于 m.lower_bound(k)。而second 成员则等价m.upper_bound(k) |
使用这些操作,可如下重写程序:
// beg and end denote range of elements for this author
typedef multimap<string, string>::iterator authors_it;
authors_it beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item);
// loop through the number of entries there are for this author
while (beg != end)
{
cout << beg->second << endl; // print each title
++beg;
}
如果键 search_item 在容器中存在,则使 beg 指向第一个与之匹配的元素。如果容器中没有这样的元素,那么beg 将指向第一个键比 search_item 大的元素。
若该键没有关联的元素,则 lower_bound 和 upper_bound返回相同的迭代器:都指向同一个元素或同时指向 multimap的超出末端位置。它们都指向在保持容器元素顺序的前提下该键应被插入的位置。
如果该键所关联的元素存在,那么 beg 将指向满足条件的元素中的第一个。可对 beg做自增运算遍历拥有该键的所有元素。当迭代器累加至 end 标志时,表示已遍历了所有这些元素。当 beg 等于end 时,表示已访问所有与该键关联的元素。
equal_range 函数返回存储一对迭代器的 pair 对象。如果该值存在,则 pair 对象中的第一个迭代器指向该键关联的第一个实例,第二个迭代器指向该键关联的最后一个实例的下一位置。如果找不到匹配的元素,则 pair 对象中的两个迭代器都将指向此键应该插入的位置。
使用 equal_range 函数再次修改程序:
// pos holds iterators that denote range of elements for this key
pair<authors_it, authors_it> pos = authors.equal_range(search_item);
// loop through the number of entries there are for this author
while (pos.first != pos.second)
{
cout << pos.first->second << endl; // print each title
++pos.first;
}
本程序的 pos.first 等价于前一方法中的 beg,而 pos.second 等价于 end。
小结
关联容器的元素按键排序和访问。关联容器支持通过键高效地查找和读取元素。键的使用,使关联容器区别于顺序容器,顺序容器的元素是根据位置访问的。
map 和 multimap 类型存储的元素是键-值对。它们使用在 utility 头文件中定义的标准库 pair 类,来表示这些键-值对元素。对 map 或 multimap 迭代器进行解引用将获得 pair类型的值。pair 对象的first 成员是一个 const 键,而 second 成员则是该键所关联的值。set 和 multiset 类型则专门用于存储键。在 map 和set 类型中,一个键只能关联一个元素。而multimap 和 multiset 类型则允许多个元素拥有相同的键。
关联容器共享了顺序容器的许多操作。除此之外,关联容器还定义一些新操作,并对某些顺序容器同样提供的操作重新定义了其含义或返回类型,这些操作的差别体现了关联容器中键的使用。
关联容器的元素可用迭代器访问。标准库保证迭代器按照键的次序访问元素。begin操作将获得拥有最小键的元素,对此迭代器作自增运算则可以按非降序依次访问各个元素。
C++ Primer 第十一章 范型算法
标准库提供的
vector<int>::const_iterator result =
只要找到与给定值相等的元素,find
由于
类似地,由于指针的行为与作用在内置数组上的迭代器一样,因此也可以使用
int ia[6] = {27, 210, 12, 47, 109, 83};
int *result = find(ia, ia + 6, 12);
如果需要传递一个子区间,则传递指向这个子区间的第一个元素以及最后一个元素的下一位置的迭代器(或指针)。
元素值的比较,有两种解决方法。默认情况下,find
accumulate 算法
该算法在
将
用于指定累加起始值的第三个实参是必要的,因为
这个事实有两层含义。首先,调用该函数时必须传递一个起始值,否则,accumulate
考虑下面的例子,可以使用
string sum = accumulate(v.begin(), v.end(), string(""));
这个函数调用的效果是:从空字符串开始,把
传递一个字符串字面值,将会导致编译时错误。因为此时,累加和的类型将是
find_first_of
list<string>::iterator it = find_first_of(roster1.begin(), roster1.end(),roster2.begin(), roster2.end());
roster1
fill
fill(vec.begin(), vec.end(), 0); // 将迭代器范围内元素值设置为0
如果输入范围有效,则可安全写入。这个算法只会对输入范围内已存在的元素进行写入操作。
fill_n
带有的参数包括:一个迭代器、一个计数器以及一个值。该函数从迭代器指向的元素开始,将指定数量的元素设置为给定的值。fill_n
初学者常犯的错误的是:在没有元素的空容器上调用
fill_n(vec.begin(), 10, 0);
这个
back_inserter
使用
fill_n (back_inserter(vec), 10, 0);
现在,fill_n
copy 函数
copy (ilst.begin(), ilst.end(), back_inserter(ivec));
copy
当然,这个例子的效率比较差:通常,如果要以一个已存在的容器为副本创建新容器,更好的方法是直接用输入范围作为新构造容器的初始化式:
vector<int> ivec(ilst.begin(), ilst.end());
replace 函数
replace(ilst.begin(), ilst.end(), 0, 42);
这个调用将所有值为
replace_copy。
这个算法接受第三个迭代器实参,指定保存调整后序列的目标位置。
vector<int> ivec;
replace_copy (ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42);
调用该函数后,ilst
对容器元素重新排序的算法(sort,unique
假设我们要分析一组儿童故事中所使用的单词。例如,可能想知道它们使用了多少个由六个或以上字母组成的单词。每个单词只统计一次,不考虑它出现的次数,也不考虑它是否在多个故事中出现。要求以长度的大小输出这些单词,对于同样长的单词,则以字典顺序输出。
1. 去除重复
为了说清楚,使用下面这个简单的故事作为我们的输入:
对于这个输入,我们的程序应该产生如下输出:
- 1.1 调用
fox jumps over quick red red slow the the turtle
- 1.2 调用
fox jumps over quick red slow the turtle red the
注意,words
- 1.3 调用
值得注意的是,对没有重复元素的
2. 定义需要的实用函数
谓词是做某些检测的函数,返回用于条件判断的类型,指出条件是否成立。
我们需要的第一个谓词将用在基于大小的元素排序中,指出第一个字符串是否比第二个短:
{
另一个所需的谓词函数将判断给出的
bool GT6(const string &s)
{
3. 排序算法
stable_sort(words.begin(), words.end(), isShorter);
调用后,words
4. 统计长度不小于 6 的单词
现在此
bool GT6(const string &s){return s.size() >= 6;}
执行
insert iterators(插入迭代器):这类迭代器与容器绑定在一起,实现在容器中插入元素的功能。
iostream 迭代器:这类迭代器可与输入或输出流绑定在一起,用于迭代遍历所关联的 IO 流。
reverse iterator(反向迭代器):所有容器类型都定义了自己的 reverse_iterator 类型,由rbegin 和 rend 成员函数返回。
C++ Primer 第十二章 类
简单地说,类就是定义了一个新的类型和一个新作用域。
12.1 类的定义和声明
类由类成员组成。类成员包括属性,字段,成员函数,构造函数,析构函数等组成。
类设计应该遵从抽象封装性。
类抽象性指对于类的使用者来说只需知道类接口即可使用类功能。类的具体实现由设计者负责。即使某个功能发生了变更但由于使用者是以接口方式调用类所以用户代码无需做任何修改。
类封装性指类用户只需知道类的功能无需了解具体实现。实现代码对用户来说不可见。
C++类没有访问级别限限制,定义类时不能用public 或 private 做修饰。类成员有访问级别,可以定义 public protect private
public:
string name;
// 给类定义别名类型成员 index 由于别名要在外部访问所以一定要定义在 public
typedef std::string::size_type index;
// 内部定义的函数,等价于inline
char get() const { return contents[cursor]; }
// 内部声明一个成员函数(无定义),且函数是内联的inline表示在编译时该声明会被替换成定义语句
// 内部声明一个成员函数(无定义)
index get_cursor() const;
};
// 定义类 Screen 的成员函数 get 具体实现
char Screen::get(index r, index c) const
{
index row = r * width; // compute the row location
return contents[row + c]; // offset by c to fetch specified character
}
// 定义类 Screen 的成员函数 get_cursor 具体实现,且是内联的
{
return cursor;
}
注意:类的inline修饰符可以放在类内部申明也可以放在外部定义。一般放在内部声明便于理解。
类定义完毕后一定要加上封号结束符 ;。
类数据成员只允许声明不允许定义;
可以声明类而不定义它。成为前向声明又叫不完全类,这样的类无法定义实例也无法使用成员。一般用来处理类相互依赖的情况。定义了类就能定义类对象:myclass obj; 一定要注意不能是 myclass obj() ; 类对象定义时会分配内存空间,每个类都有自己的空间相互间不受影响。
12.2 隐含的this指针
类对象包含一个 this 指针指向自身(当前的实例对象)且无法更改指针指向。在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const 指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向 const 类类型对象的 const 指针。既不能改变 this 所指向的对象,也不能改变 this 所保存的地址。
基于成员函数是否为 const,可以重载一个成员函数;同样地,基于一个指针形参是否指向 const可以重载一个函数。
{
public:
mycls(){}; // 想要定义 const mycls a; 必须要显示定义默认构造函数
mycls &Get(){ return *this; };
const mycls &Get() const { return *this; }; // 想如果const函数返回this引用或指针; 必须要返回const指针或引用,因为无法用const对象(this)初始化非const对象。
};
const mycls b;
mycls b1 = c.Get(); // 调用const版Get函数
const mycls b2 = c.Get(); // 调用const版Get函数
// b1 b2 定义时会调用类的拷贝函数。b1,b2是Get返回值的副本,b1还会将常量副本转变成变量
mycls &b3 = c.Get(); // 错误,不能用 const &mycls 初始化 &mycls (指针或者引用类型不能用常量初始化变量)
const mycls &b4 = c.Get();
mycls a;
mycls a1 = a.Get(); // 调用非const版Get函数
const mycls a2 = a.Get(); // 调用非const版Get函数
由此可见调用那个版本和调用对象是否const有关系,const对象会调用const版本,非const对象会调用非const版本。
引用网上的总结:
成员函数具有const重载时,类的const对象将调用类的const版本成员函数,类的非const对象将调用非const版本成员函数。
如果只有const成员函数,类的非const对象也可以调用const成员函数。 ——这个思路来描述很囧。下同。
如果只有非const成员函数,类的const对象…额,不能调用非const成员函数。 ——其实跟上一句的意思是一样的:const对象只能调用它的const成员函数。
总的来说,就是当我们调用一个成员函数时,编译器会先检查函数是否有const重载,如果有,将根据对象的const属性来决定应该调用哪一个函数。如果没有const重载,只此一家,那当然就调用这一个了。这时编译器亦要检查函数是不是没有const属性而调用函数的对象又有const属性,若如此,亦无法通过编译。
还有一点非常重要,想要定义类的const对象必须显示定义对应构造函数,无法依赖系统自动分配的构造函数。
12.3 类作用域
每个类对象独有独立的作用域。
C++类定义一般分两部分,类成员申明(类定义内部)和类成员定义(类定义外部)。虽然成员定义在类外部但还是可以像类内部定义一样使用类所有成员。
12.4 构造函数
构造函数是特殊的成员函数。在类对象定义时被调用。 不能通过定义的类对象调用构造函数,构造函数可以定义多个或者说构造函数允许重载。
如果没有定义任何构造函数,系统就会给类分配一个无参的默认构造函数,类只要定义了一个构造函数,编译器也不会再生成默认构造函数。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数
定义类对象时不能写成 Sales_item myobj(); 编译器会理解成:一个返回 Sales_item 类型叫 myobj的函数声明。 正确写法是去掉后面的括号。
构造函数后面不允许定义成 const,这样定义会产生语法错误: Sales_item() const {};
构造函数在执行时会做类数据成员的初始化工作。从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。
{
public:
mycls()
{
age = 12; name = "tom";
};
mycls(int i):age(i)
{
age = 12 + i; name = "tom";
};
private:
int age;
string name;
};
mycls obj1 ;使用无参构造函数,虽然构造函数并没有显示初始化数据成员但类类型name还是会被初始化成默认值name初始化为"" age未初始化(其值是个随机数),初始化后构造函数重新赋值,最终age=12, ame = "tom" ;
mycls obj2(4) ; 用构造函数参数初始化 age = 4, name = "",构造函数重新赋值,最终age=16, name = "tom" ;
如果数据成员是自定义类类型,如果不显示初始化则类一定要有默认构造函数否则编译错误,成员被初始化的次序就是定义成员的次序。第一个成员首先被初始化,然后是第二个,依次类推。
默认情况下可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
{
public:
int i;
mycls(int i){ };
explicit mycls(string s){ };
};
mycls obj(2) ; 也可以这样使用这个构造函数 mycls obj = 2; 这里做了一个类型转换,但是这样的写法很不直观。
可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数:mycls obj("tom"), 无法用 mycls obj = "tom" 因为转换被禁止,通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。
12.5 友元
友元机制允许一个类将其非公有成员的访问权限授予指定的函数或类。
将类作为自己的友元类如下定义
class me
{
friend class he;
private:
int i;
string s;
};
class he
{
public:
void show(me &it)
{
cout << it.i << it.s << endl;
};
};
类he是me的友元类,所以he中可以访问me的私有成员i和s;
将类成员作为另一个类的友元函数情况比较复杂,需要用到前面讲过的前向声明(两个类之间有互相依赖关系)
class me; // 先要前向声明类
class he // 友元类需要目标类做参数由于目标类已声明所以可以使用类引用或者指针--show(me &it)方法中的参数
{
public:
void show(me &it);
};
class me // 目标类需要声明类的的成员show作为自己的友元函数,he在上面做了成员声明所以成员show(me &it)可用
{
friend void he::show(me &it);
private:
int i;
string s;
};
void he::show(me &it) // 友元方法中使用目标类私有成员,目标类上一步定义了私有成员因此这里成员可用
{
cout << it.i << it.s << endl;
};
声明定义的顺序非常重要,一定要理解否则会产生各种未定义类编译错误。
12.6 static 成员
类的静态成员不属于任何一个类对象所以静态成员中(主要是静态方法)不包含this指针因此也无法声明成const函数,它是所有类对象共享数据。
不同于其他语言的访问方式,静态成员既可以通过类型访问:myclass::staticname() 也可以通过类对象(对象,指针或者引用)访问:obj.staticname()。
一般来说类数据成员在类定义体内不能初始化化,但有个特例 const static 数据成员就可以在类的定义体中进行初始化 。
类非 static 数据成员在类体内声明,必须要在类体外定义。
{
public:
void show()
{
cout << i << j << endl;
};
private:
static int i;
const static int j = 1;
};
int me::i = 1; //这一步不能少,否则编译器检查到show()方法中使用i类体外又没有定义会产生编译错误
需要强调const static 体内初始化但体外定义也不能少,但是如果体外不作定义在定义类对象时会产生编译异常。
最后补充一点关于类成员函数的重载。函数重载不但可以用参数类型和参数个数不同来重载,还可以通过const修饰变量来实现函数重载,即函数名称、参数个数、参数类别都一样,唯一的区别在于变量是否为const修饰。用 const做重载依据有两种类型:const参数,const函数:
{
public:
A() {}
void func(int *a) //相当于void func(int *a, A *this)
{
std::cout << "_func_int_ptr_" << std::endl;
}
void func(const int *a) //相当于void func(const int *a, A *this)
{
std::cout << "_func_const_int_ptr_" << std::endl;
}
void func(int *a) const //相当于void func(int *a, const A *this)
{
std::cout << "_const_func_int_ptr_" << std::endl;
}
void func(const int *a) const //相当于void func(const int *a, const A *this)
{
std::cout << "_const_func_const_int_ptr_" << std::endl;
}
};
int main(int argc, char* argv[])
{
A a;
int nValue = 3;
const int nValueCnst = 3;
a.func(&nValue);
a.func(&nValueCnst);
const A aa;
aa.func(&nValue);
aa.func(&nValueCnst);
return 0;
}
其输出为:
_func_int_ptr_
_func_const_int_ptr_
_const_func_int_ptr_
_const_func_const_int_ptr_
从这里可以看出,通过const修饰一个变量可以实现同名称函数的重载。另外,一个类的非const对象可以调用其const函数(如果只定义了const函数版本,非const对象就可以调用const成员函数)。但const 对象无法调用非 const 函数(非const函数可能会修改 this 而 this 是 const对象,有潜在BUG)。
总结起来,可以初始化的情况有如下几个地方:
1. 类型为const 且 static 的整型变量可以在定义时直接初始化值(只能用赋值初始化不能用直接初始化) 也可以在体外。
2. 普通const常量(不包含第一种情况)必须要在构造函数初始化列表中初始化值。
3. 只要有static修饰,必须要在类定义体外定义并给值(第一种情况时也需要这么做,不过只能定义不能再给值) static数据不属于任何对象所以不能出现在构造函数初始化列表。
4. 普通的变量可以在构造函数的内部,通过赋值方式进行。当然这样效率不高。
5. 数组成员不能在初始化列表里初始化的。只能自动调用数组的无参构造函数(可以在构造函数内操作数组)。
{
public:
int a;
const int b;
static int c;
static const d = 1; // 体内定义时不能用直接初始化给值
static const e;
obj():a(0),b(0){};
};
int obj::c = 2 ; // 体外定义(不能出现statci关键字)
const int obj::d ; // 体外定义,d已经在体内给只所以只需定义不能给值(const 必须)
const int obj::e = 1 ; // 定义并给值
C++ Primer 第十三章 复制控制
复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。不管类是否定义了自己的析构函数,编译器都会自动为类中非 static 数据成员执行析构函数。
赋值操作符与构造函数一样,赋值操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,编译器将为我们合成一个。
{
public:
myclass(const myclass &obj){}; // 复制构造函数
~myclass(){}; // 析构函数
myclass& operator=(const myclass &obj){}; // 赋值操作符
private:
int age;
string name;
}
13.1 复制构造函数
编译器合成的复制控制函数是非常精练的——它们只做必需的工作。但对某些类而言,依赖于默认定义会导致灾难。实现复制控制操作最困难的部分,往往在于识别何时需要覆盖默认版本。有一种特别常见的情况需要类定义自己的复制控制成员的:类具有指针成员。
复制构造函数在下列情况下会被调用:
myclass obj2 = obj1; // 根据另一个同类型的对象显式或隐式初始化一个对象
myclass fun(myclass par)
{
// ...
return par; // 从函数返回时复制一个对象
}
fun(obj1); // 复制一个对象,将它作为实参传给一个函数
vector<string> svec(5);; // 编译器首先使用 string 默认构造函数创建一个临时值来初始化 svec,然后使用复制构造函数将临时值复制到 svec 的每个元素
myclass ls[]{obj1,obj1,obj1,obj1};// 根据对象初始化数组
myclass ls[]{myclass(),myclass(),myclass()}; // 按照书上说是会调用复制构造函数但实际不会调用,据说是做了优化
如果不提供显示的复制构造函数系统会合成一个。合成的构造函数会在上述情况发生时会赋值对象副本并将对象数据成员逐一初始化成与原对象相同的值。
有个有趣的现象:数组是不能复制的,但如果对象数据成员是个数组类型却可以复制数组给对象副本的对应成员,合成复制构造函数模型如下
{
// obj是源对象,用它来复制副本
}
如果想禁止复制可以显示声明私有的复制构造函数(最好不要这么做否则类只能作为指针或引用传递),复制构造函数属于构造函数,一旦定义复制构造函数应该给类显示同时定义一个默认构造函数。
13.2 赋值操作符
类的赋值操作符实际上是操作符重载(operator=)
赋值操作结果和拷贝构造函数类似,它会执行逐个成员赋值(复制构造是逐个成员初始化,然后也允许重新赋值)
{
// obj是源对象,用它来为操作符左面对象赋值
age = obj.age;
name = obj.name;
return *this;
}
赋值操作符和复制构造函数几乎可以看做一个整体,如果需要其中一个几乎肯定也需要另外一个。
关于操作符重载会在后续章节做详细介绍。
13.3 析构函数
析构函数一个用途是对象在销毁之前做一些相关操作,比如清理资源,刷新缓冲区等。析构函数在对象即将销毁前执行
{
public:
string name;
~he(){cout << name << " is delete!" << endl;};
};
int main()
{
he cls;
cls.name = "zhang san";
he *ls = new he[4];
ls[0].name = "item0";
ls[1].name = "item1";
ls[2].name = "item2";
ls[3].name = "item3";
delete [] ls;
cout << "delete list" << endl;
he *pr = new he();
pr->name = "li si"
cout << "delete li si" << endl;
}
// 输出:
item3 is delete!
item2 is delete!
item1 is delete!
delete list
li si is delete!
delete li si
zhang san is delete!
赋值操作和复制(拷贝)构造函数效果类似,在使用=号操作时有时候会调用赋值有时候会调用复制构造函数,怎么区分调用方式呢?
复制(拷贝)构造函数,是用一个已知的对象去初始化另一个正在创建的对象;赋值操作,是用一个已经存在的对象去更新另一个已经存在的对象。
myclass a ;
myclass b = a ; // 用一个已知的对象去初始化另一个正在创建的对象,调用复制构造函数
b = a ; // 用一个已经存在的对象去更新另一个已经存在的对象,调用赋值操作
赋值操作符可以通过指定不同类型的右操作数而重载,看代码
{
public:
myclass& operator=(const myclass &obj){ name = obj.name; return *this;}; // 赋值操作符
myclass& operator=(string str){ name = str; return *this;}; // 赋值操作符重载
private:
string name;
}
myclass a;
myclass b;
b = a; // 调用 operator=(const myclass &obj)版
b = "tom"; // 调用 operator=(string str)版
本章最后介绍了智能指针的概念。它不是c++具体技术而是解决拷贝对象时指针字段会可能会引发错误的解决方案
class myclass
{
public:
string name;
int age;
};
// 智能指针
class curr
{
// 将具体类设置成智能指针的友元类
friend class test;
private:
curr(myclass &ip):cur(&ip),used(1) {};
// 最后一个拥有指针成员的对象消亡时会删除智能指针对象,析构函数执行删除真正指向的类对象
~curr()
{
cout << "已经没有任何指针指向myclass对象!"<< endl;
delete cur; // 构造函数参数*ip必须是动态创建的对象指针 delete才能正确删除,否则会产生无法预知的运行时错误
};
myclass *cur;
int used;
};
// 具体类
class test
{
public:
test(myclass &ip, string stname,int stage): pro(new curr(ip)) ,name(stname) ,age(stage) {};
test(const test &t): pro(t.pro) ,name(t.name) ,age(t.age)
{
++pro->used;
};
~test()
{
--pro->used;
// 最后一个引用对象消失,智能指针计数器等于0,删除智能指针动态对象(智能指针删除时会出发自身的析构函数,析构函数中负责删除类成员)
if(pro->used == 0)
{
cout << "over" << endl;
delete pro;
}
};
void show(){cout << pro->used << endl;};
private:
curr *pro;
string name;
int age;
};
int main()
{
myclass *pr = new myclass();
test *t1 = new test(*pr,"tom",21) ;
t1->show(); // 1
test *t2 = new test(*t1);
t1->show(); // 2
t2->show(); // 2
delete t1;
t2->show(); // 1
delete t2; // over 已经没有任何指针指向myclass对象!
}
智能指针基本思路是用智能指针对象替换数据成员类对象指针,由智能指针维护对象指向。当具体类发生拷贝或删除时更新智能指针维护的计数器。如果计数器==0说明所有具体类都消亡,删除智能指针。智能指针再负责删除数据成员对象。
C++ Primer 第十四章 重载操作符与转换
14.1 重载操作符的定义
操作符(+ ,- , * , / , = , < , >)可以被内置类型使用,比如两个整数相加或相减,两个字符串相加,两个数组比较大小等等。自定义类默认是不能使用大多数操作符的。自定义类是复合类型,相加或想减或比较大小并没有相应的规则匹配:两个类相加等于什么? 两个类如何确定谁大谁小? C++允许我们通过重载运算符的技术让自定义对象支持这些操作。我们可以定义重载规则。
操作符重载语法很简单: 关键字 operator 后接操作符 比如 operator+
可以重载的操作符:
+ | - | * | / | % |
^ | & | | | ~ | ! |
= | > | < | += | -= |
*= | /= | %= | ^= | &= |
|= | >> | << | >>= | <<= |
== | != | >= | <= | && |
|| | ++ | -- | ->* | , |
-> | [] | () | operator new | operator new[] |
operator delete | operator delete [] |
不能重载的操作符:
:: | . | .* | ? : |
sizeof | typeid | new | delete |
static_cast | dynamic_cast | const_cast | reinterpret_cast |
重载操作符可以定义成类成员函数,也可以定义成非成员函数
{
public:
myclass() : age(0){};
// 成员函数定义
myclass operator+(const myclass &obj) const
{
myclass cls;
cls.age = age + obj.age;
return cls;
};
int age;
}
// 等价的非成员函数定义
myclass operator+(const myclass &obj1, const myclass &obj2)
{
myclass cls;
cls.age = obj1.age + obj2.age;
return cls;
}
成员函数定义看起来少了一个参数,实际上语法将this限定为第一个操作数。大部分操作符允许定义为成员或非成员函数,具体如何定义看个人喜好。
+ 有些操作符只能定义为成员,如果定义成非成员会产生编译错误 赋值= 下标[] 调用() 箭头访问-> 都不允许定义成非成员函数。
+ 有些操作符只能定义为非成员,如 输入 << 输出 >> 操作符
+ 改变自身状态的建议定义为成员函数,例如 自增++ 自减-- 解引 复合操作+= -= 等
+ 对称操作符建议定义为非成员函数,例如加减 + - 比较 < == >
+ 成员函数中可以使用 this 而非成员函数中无法使用,因为函数不属于任何对象。
重载操作符至少要包含一个类类型操作数
myclass operator+(myclass *obj1, const myclass *obj2) // 操作数至少要包含一个类类型,防止用户修改内置类型的操作符,如果用户定义 int operator+(int a,int b) 意味着用户要修改int类型的加法操作符。
输入操作符:
{
temp << obj.size;
return temp;
}
输出操作符:
{
tmp >> obj.size;
return temp;
}
输入输出操作符返回都必须是引用,且第一个形参也是引用,前面章节中已说明 IO对象无法复制或者赋值。
这两个操作符只能定义成非成员函数,原因在于第一形参必须是IO对象引用,而定义为成员函数时第一个参数被省略且被限定为 this 所以只能定义成非成员函数。
算数运算符:
{
myclass cls;
cls.age = obj1.age + obj2.age;
return cls;
}
myclass operator-( const myclass &obj1, const myclass &obj2)
{
myclass cls;
cls.age = obj1.age - obj2.age;
return cls;
}
bool operator==( const myclass &obj1, const myclass &obj2)
{
return obj1.age == obj2.age;
}
bool operator>( const myclass &obj1, const myclass &obj2)
{
return obj1.age > obj2.age;
}
赋值=操作符:
{
age = obj.age;
return * this;
};
下标操作符:
string& operator[] ( const size_t index) // 返回左值(引用) 下标操作可读可写
下标操作可实现const重载
string& operator[] ( const size_t index) // 下标操作可读可写
const string& operator[] ( const size_t index) const // 下标操作只读
调用规则是 const对象调用const版,非const调用非const版
myclass a ;
a[ 1] ; // 调用非const版
const myclass b ;
b[ 1] ; // 调用const版
解引操作符:
{
public:
void show( int p){cout << p << endl;};
}
class myclass
{
public:
myclass:sp(p) (cls *p)
cls & operator*(){ return *sp;}; // 解引返回具体类
cls * operator->(){ return sp;}; // 箭头返回类指针,实际使用时返回的指针会立刻再做系统的箭头操作
const cls & operator*() const { return *sp;}; // const重载版本
const cls * operator->() const { return sp;}; // const重载版本
private:
cls *sp;
}
cls a;
myclass t(&a) ;
cls b = *t ; // 调用解引操作符,返回cls类对象
t->show( 3) ; // 调用箭头操作符,调用cls对象的show()方法
此时t是对象而不是指针,如果t是指针,则会调用系统解引和箭头操作:
cls *j = &t;
*j ; // 返回 myclass对象
j->show( 3) ; // 调用myclass对象的show方法,本例中myclass没有定义方法,运行时报错
自增操作符:
{
public:
int ls[ 4];
int *cur;
myclass:ls{ 2, 3, 1, 5},cur(ls){};
myclass & operator++(){ cur ++; return * this;}; // ++myclass重载,返回引用或对象
myclass operator++( int){ myclass tmp(* this); cur ++; return tmp;}; // myclass++重载,只能返回对象(不允许返回局部对象的引用)
}
myclass++重载多了形参int,只起到标识作用。
使用和内置类自增没审美区别,自减和自增类似。
调用操作符:
{
public:
int operator() ( int i){ return i + 2;};
}
定义了调用操作符的类对象叫做 函数对象 ,因为他们的对象行为类似函数
myclass obj;
int j = obj( 5) ; // 使用调用操作符重载函数 j = 7
转换操作符:
{
public:
operator int() const (){ return a;};
在下列情况下函数执行:
1. 表达式中 1 + obj; 2 < obj;
2. 条件中 if(obj) 先转换成int 在转成bool
3. 传参或返回值时
C++ Primer 第十五章 面向对象编程
15.1 面向对象编程:概述
继承:
虚函数:virtual
动态绑定:
15.2 定义基类和派生类
成员限制符:public private protected
protected:在子类中可访问,派生类内部可以访问本类对象protected成员,不能访问基类对象protected成员
class item : public base
{
void test(item &a , base &b)
{
a.name; // 可以访问本类对象protected成员
b.name; // 错误,不能访问基类对象protected成员
}
}
C++允许多重继承,例如 class item : public base1, base2...
原则上子类重写父类虚函数时声明和定义要于父类完全一致,但有一个例外:虚函数返回值是父类的指针或引用 可以在子类中将返回改成子类的指针或引用:比如父类有虚函数:base *test(); 子类可重写成: item *test();
声明一个包含派生列表的类(而不实现)是错误的。
class item: public base;
动态绑定需要符合两个条件:调用函数必须是virtual ;必须要通过指针或引用调用虚函数。 动态绑定时执行函数取决于实际执行的类型,而不取决于指针或引用变量类型。
item b;
base a = b;
a.show(); // 不会动态绑定,a不是指针也不是引用。调用变量a的类方法(a类是base 所以调用base版方法)
base *a = &b;
a->show(); // 动态绑定,调用item版方法,调用实际数据的类方法(实际数据b是item类)
base &a = b;
a.show(); // 动态绑定,调用item版方法,调用实际数据的类方法(实际数据b是item类)
virtual函数版本是在运行时确定,非virtual函数是在编译时确定。
也可以指定执行virtual函数版本如:
base *a = &b;
a->base::show(); // 指针指定调用base版方法
base &a = b;
a.base::show(); // 引用指定调用base版方法
函数可以设定默认默认参数,默认参数定义的顺序为自右到左。即如果一个参数设定了缺省值时,其右边的参数都要有缺省值
c++三种继承方式:public, private, protected 假设B类继承A类,即B类是A类的直接子类。
public继承:A的访问属性在B类保持不变。
A的public-----------B仍是public;
A的protected-------B仍是protected;
A的private----------B无法访问(仍是private);
protected继承:
A的public-----------B变成protected;
A的protected-------B仍是protected;
A的private----------B无法访问(仍是private);
private继承:
A的public-----------B无法访问(变成private);
A的protected-------B无法访问(变成private);
A的private----------B无法访问(仍是private);
派生类可以恢复继承的成员访问级别(只能恢复子类可访问的成员级别),但不能使被恢复成员的级别比他原来的还大。
{
public:
void show(){};
void show(int i){};
protected:
void log(){};
};
class item : private base
{
public:
using base::show; // 可以恢复所有重载版本到子类
using base::log; // 错误不能使被恢复成员的级别比他原来的还大
}
派生类继承基类默认级别是由派生类决定,如果派生类是struct则默认是public,若是class则是private。
class a : b // prvate 继承
struct a : b // public 继承
基类的友元关系是无法被子类继承的,所以要想基类的友元类访问子类的私有成员需要在子类中定义友元关系。
15.3 基类到派生类的转换
基类对象和派生类之间有单向转换关系。派生类可以转换成基类反过来则不允许。因为基类里的成员派生类中都包含所以转换无错,但派生类中所有对象基类并不全部包含所以转化会失败。
一个基类引用或指针指向派生类时实际执行的是派生类的代码。
一个基类对象指向派生类时会发生拷贝赋值操作,用派生类中数据成员初始化或赋值基类对应成员,而方法成员还是使用基类版本。所以这种情况下不会发生动态绑定virtual函数。
15.4 构造函数和复制控制
缺省情况下派生类创建对象时会先调用基类的默认构造函数,然后再调用自己的构造函数。
也可以在派生类构造函数中显示调用基类某个构造函数,甚至给基类构造函数传参。调用语法是
{
public:
item (int age,string name) : base(age,name),prage(age),prname(name) {}; // 调用基类构造函数并传参, 初始化本类成员
}
派生类只能调用直接基类构造函数。 如果不显示调用基类构造函数则基类一定要有默认构造函数否则会产生编译错误。
复制构造函数有点不同:子类使用合成复制构造函数则先调用基类默认构造函数再调用子类合成复制构造函数。如果定义了子类的复制构造则一定要显示调用基类赋值构造函数。否则会出现 子类成员是被复制对象副本,而基类成员却未初始化。
{
public:
item (cosnt item &it) : base(it) ... {}; // 一定要调用基类复制构造函数base(it)
}
赋值操作同复制类似,如果派生类定义了自己的赋值操作一定要显示为基类进行赋值
{
public:
item &operator=(const item &it)
{
base:: operator=(it); // 显示调用基类赋值操作
//...
};
}
析构函数无论如何总是会调用父类的析构函数。析构函数运行顺序和构造函数相反,总是先运行子类析构函数再运行父类析构函数。
{
public: ~one(){ cout << "end one" << endl;}; one{ cout << "init one" << endl;};
}
class two : public one
{
public: ~tow(){ cout << "end two" << endl;}; tow(){ cout << "init two" << endl;};
}
class three : public two
{
public: ~three(){ cout << "end three" << endl;}; three(){ cout << "init three" << endl;};
}
three b; // 此时依次输出 "init one" "init two" "init three"
one *a = &b;
// 当超过作用域时对象 b 被释放,依次输出 "end three" "end two" "end one"
当定义three *a =&b, a在回收时不会调用任何方法因为它是指针,只有释放对象b析构才能执行。
但是有一种情况输出层级和指针有直接关系:动态对象,下面代码只会执行指针对象的析构函数。
one *a = new three() ;
delete a ; // 只输出"end one"
如何才能输出 "end three" "end two" "end one"呢? 只要将类 one 中析构函数设置成虚析构函数即可 virtual ~one(){...} 。
构造函数和赋值函数不要定义成虚函数,因为会让人混淆且没有什么用处。
15.5 继承情况下的类作用域
子类可以定义和父类一样的非虚函数,此时子类会覆盖父类函数。
和虚函数动态绑定不同,调用版本并不是由指向的数据类型决定,而是由申明变量类型决定。
如果想调用父类成员需要如此调用
a.base::show(); // 调用base类的show方法
item *b =&a;
b->base::show(); // 调用base类的show方法
base *k = &a;
k->base::show(); // 变量类型是base,但实际对象是item 所以需要b->base::show();
15.6 纯虚函数
纯虚函数申明很简单 void show()=0;拥有纯虚函数的类无法定义对象,但可以定义指针或引用。假设基类 base 定义了纯虚函数。
base c ; // 错误
base *c = &b ; // 正确
base &c = b ; // 正确
15.7 容器与继承
容器对象可以定义成存放基类对象,但可以给容器加入子类对象,这时候子类会被转换成基类对象,或者说基类部分会被系统删除。
可以定义基类指针或引用类型容器,再增加子类指针活引用,这时候会更具实际内容不同执行不同代码(动态绑定)。
15.8 句柄类与继承
我们知道C++中最令人头疼的当属指针,如果您申请了对象却没有释放它,时间一长就会造成系统崩溃,大量的内存溢出使得您的程序的健壮性出现问题而句柄类就是为了能够解决这一问题而出现的,句柄类有点类似于智能指针。
好了,废话不多说,我们来看代码,首先我们来看 head.h文件的代码:
#define HEAD_H
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
//基类
class Item_base
{
public:
//基类的虚函数,用于智能地复制对象
virtual Item_base* clone() const
{
return new Item_base(*this);
}
};
//子类
class Bulk_item: public Item_base
{
//子类的虚函数的重载,用于智能地复制对象
virtual Bulk_item* clone() const
{
return new Bulk_item(*this);
}
};
//句柄类
class Sales_item
{
public:
//默认构造函数,用来初始化一个引用计数器(句柄类未绑定任何对象)
Sales_item(): p(0), use(new size_t(0)) { cout << "Sales_item定义了空句柄" << endl;};
//带有一个参数的,且该参数为基类引用的构造函数
Sales_item( const Item_base &i): p(i.clone()), use(new size_t( 1 )) { cout << "Sales_item的引用计数器初始化为1" << endl; };
//复制构造函数,需要注意的是,每复制一次就需要增加引用计数一次
Sales_item( const Sales_item &i ): p(i.p), use(i.use) { ++*use;};
void show(){cout<< "user: " << *use << endl;};
//析构函数,析构的时候会判断是否能够释放指针所指向的数据
~Sales_item() { decr_use();};
//赋值操作符重载
Sales_item& operator= ( const Sales_item& );
//访问操作符重载
const Item_base* operator-> () const
{
if( p )
{
return p;
}
else
{
cout << "p指针错误" << endl;
}
};
//解引用操作符重载
const Item_base& operator* () const
{
if( p )
{
return *p;
}
else
{
//重载虚函数,用于智能地复制对象
cout << "p指针错误" << endl;
}
};
private:
//两个指针存储着引用计数器以及数据的指针
Item_base *p;
size_t *use;
//减少引用
void decr_use()
{
if(*use == 0 && p == 0)
{
cout << "空句柄无需释放任何资源"<<endl;
return;
}
cout << "在 dec_use函数中引用计数减少了,当前计数值为:" << *use - 1 << endl;
if( --*use == 0 )
{
delete p;
delete use;
cout << "在 dec_use函数中计数器减为0,释放对象" << endl;
}
};
};
//赋值操作符重载,每次复制都会增加引用计数
Sales_item& Sales_item::operator= ( const Sales_item &si )
{
//这里需要特别注意的就是待复制的对象的计数器需要加1而被赋值的对象需要减1
//增加被复制对象的引用计数
++*si.use;
//将即将被赋值的对象的引用计数减1
decr_use();
//复制指针
p = si.p;
use = si.use;
//返回
return *this;
};
#endif //HEAD_H
接下来我们来看mail.cc的代码:
int main()
{
// 被包装类(实际上包装的是这个对象的副本)
Bulk_item item;
Sales_item a(item); // 输出 : Sales_item的引用计数器初始化为1
a.show(); // 输出 : user:1
Sales_item b(a);
a.show(); // 输出 : user:2
b.show(); // 输出 : user:2
Sales_item c; // 输出 : Sales_item定义了空句柄
c.show(); // 输出 : user:0
c = b; // 输出 : 空句柄无需释放任何资源
c.show(); // 输出 : user:3
b.show(); // 输出 : user:3
a.show(); // 输出 : user:3
}
当main函数执行完毕,c最先被释放:
// 输出 : 在 dec_use函数中引用计数减少了,当前计数值为: 2
b被释放:
// 输出 : 在 dec_use函数中引用计数减少了,当前计数值为: 1
a被释放:
// 输出 : 在 dec_use函数中引用计数减少了,当前计数值为: 0
此时已经删除了被包装对象(item的副本)
最后item 对象被释放
结论:我们可以看到,句柄类能够很方便并且能够很安全地释放内存,不会导致内存的泄露。
C++ Primer 第十六章 模板与范型编程
16.1 模板定义
模板和c#范型一样,建立一个通用的类或函数,其参数类型和返回类型不具体指定,用一个虚拟的类型来代表,通过模板化函数或类实现代码在的重用。
定义语法是:
template<typename 类型参数>
返回类型 函数名(模板形参表)
{
函数体
}
或 :
template<class 类型参数>
返回类型 函数名(模板形参表)
{
函数体
}
template是一个声明模板的关键字,类型参数一般用T这样的标识符来代表一个虚拟的类型,当使用函数模板时,会将类型参数具体化。typename和class关键字作用都是用来表示它们之后的参数是一个类型的参数。只不过class是早期C++版本中所使用的,后来为了不与类产生混淆,所以增加个关键字typename。
函数模板:
T Add(T x,T y)
{
return x+y;
};
int main()
{
int x=10,y=10;
std::cout<<Add(x,y)<<std::endl;//相当于调用函数int Add(int,int)
double x1=10.10,y1=10.10;
std::cout<<Add(x1,y1)<<std::endl;//相当于调用函数double Add(double,double)
long x2=9999,y2=9999;
std::cout<<Add(x2,y2)<<std::endl;//相当于调用函数long Add(long,long)
}
template内可以定义多个类型形参,每个形参用,分割并且所有类型前面都要用typename修饰。
template <typename T,typename Y> T Add(T x,Y y) ; // ok
template <typename T,Y> T Add(T x,Y y) ; // 错误,Y之前缺少修饰符
函数模板也可以声明inline 语法是 template <typename T,typename Y> inline T Add(T x,Y y) ;
类模板:
class base
{
public:
base(T a);
Y Get();
private:
T s1
T s2
};
int main()
{
base<int,string> it(1,"name"); // 类后面的类型参数不能缺省
}
和函数模板不一样,类模板无法使用类型推断,所以定义对象时一定要显示传递类型参数。
类型形参名称有自己的作用域:
template <typename T>
T Add(T x,T y)
{
typedef stirng T; // 错误,内部定义会产生名字冲突
//...
};
可以像申明一般函数或类一样声明(而不定义)。但类型形参不能省略 template <typename T,typename Y> class base ; 声明了一个类模板。
模板类型参数可以用typename 或者class 来修饰,大部分情况下二者可以互换。但有一种特殊用方法时需要typename
{
public:
class inbase{}; // 内部类
};
template <typename T>
void test()
{
typename T::inbase p; // 这时候必须要在前面加上typename,表示要定义一个类型为T类(T是类型参数)内部定义的inbase类对象
T::inbase p; // 如果不加编译会报错,因为编译器认为T::inbase表示T类的静态成员inbase,所以这样书写语法是错误的
}
要注意,这种用法需要满足条件:类型形参T必须要定义内部类inbase 否则会编译错误。
模板编程中还可以在类型形参列表中定义非类型形参,这时非类型形参会被当成常量
T Add(T x) // Add(T x,int i) 这样定义编译错误,i 和非形参i名称冲突
{
return x + i;
};
int main()
{
Add<int,10>(5);
}
范型编程有两个重要原则:形参尽量使用const引用(防止拷贝),形参本身操作尽量少(传递一个不支持函数形参体操作的类型会报错)
16.2 实例化
函数模板可以定义函数指针并予以赋值
int(*pr) (int,string) = Get ; // 定义函数指针并赋值
pr(5,"str") ; // 用函数指针调用函数无需解引,或者(*pr)(5,"str") ;
函数模板指针作为形参时需注意重载情况。对二义性的调用要指定类型来消除
template <typename T> T Get(T x) ; // 声明函数
void fun(int (*) (int));
void fun(string (*) (string));
fun(Get); // 错误,有二义性,类型推断后重载的两个fun函数都能通过。
fun(Get<int>); // 指定类型,消除了二义性
16.3 模板编译模型
[1] 当编译器看到模板定义的时候,它不立即产生代码。 只有在看到用到模板时 ,如调用了函数模板或定义了类模板的对象的时候,编译器才产生特定类型的模板实例 。
[2] 一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
[3] 模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。 当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要哪些通常放在源文件中的代码。
[4] 标准C++为编译模板代码定义了两种模型。 所有编译器都支持第一种模型,称为“包含”模型( inclusion compilation model) ;只有一些编译器支持第二种模型,“分别编译”模型( separate compilation model) 。
[5] 在两种模型中,构造程序的方式很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义 。
[6] 在包含编译模型,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条#include指示使定义可用,该#include引入了包含相关定义的源文件 。
[7] 在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用export关键字来做这件事 。export关键字能够指明给定的定义可能会需要在其他文件中产生实例化 。
[8] 在一个程序中,一个模板只能定义为导出一次。 一般我们在函数模板的定义中指明函数模板为导出的 ,这是通过在关键字template之前包含export关键字而实现的。对类模板使用export更复杂一些 ,记得应该在类的实现文件中使用export,否者如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。
[9] 导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字export不在类模板本身指定,而是只在被导出的特定成员定义上指定。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。
16.4 类模板成员
普通类不但定义非模板函数成员,也能定义模板函数成员:
{
public:
template<typename T> T Get(T a); // 模板函数成员申明
};
template<typename T> T base::Get(T a) //成员函数类外部定义
{
return a;
}
可这样调用:
base obj ;
obj.Get<int>(20) ;
obj.Get("str") ; // 类型推断,等价于obj.Get<string>("str") ;
如果是模板类
class base
{
public:
template<typename Y> Y Get(Y a); // 模板函数成员申明
};
template<typename T> // 这一步不可少,确定T也是个模板类型参数
template<typename Y> Y base<T>::Get(Y a)
{
return a;
}
可这样调用:
base<string> obj ;
obj.Get<int>(20) ;
obj.Get("str") ; // 类型推断,等价于obj.Get<string>("str") ;
类模板或函数模板可以作为其他类的友元,不过由于其特殊性可以做一些限制。
class he
{
// ...
}
template<typename T>
class base
{
template<typename Y> friend class he; // 表示所有类型的模板类对象都是友元
friend class he<int>; // 表示只有int类型形参的模板类对象才是友元
friend class he<T>; // 表示只有类型形参和base类型参数一致的模板类对象才是友元
}
友元函数和模板类情况相似。 第一种友元可以看做是完全申明,第二种和第三种友元则需要至少在base定以前有完全申明,否则会编译错误。
16.5 一个范型句柄类
如果对上一章句柄类有充分理解范型句柄类应该非常容易掌握。
16.6 模板特化
模板的特化(template specialization)分为两类:函数模板的特化和类模板的特化。
函数模板的特化:当函数模板需要对某些类型进行特别处理,称为函数模板的特化。例如:
{
return t1 == t2;
};
int main()
{
char str1[] = "Hello";
char str2[] = "Hello";
cout << IsEqual(1, 1) << endl;
cout << IsEqual(str1, str2) << endl; //输出0
return 0;
}
最后一行比较字符串是否相等。由于对于传入的参数是char *类型的,IsEqual函数模板只是简单的比较了传入参数的值,即两个指针是否相等,因此这里打印0。显然,这与我们的初衷不符。因此,sEqual函数模板需要对char *类型进行特别处理,即特化:
{
return strcmp(t1, t2) == 0;
}
这样,当IsEqual函数的参数类型为char* 时,就会调用IsEqual特化的版本,而不会再由函数模板实例化。
类模板的特化:与函数模板类似,当类模板内需要对某些类型进行特别处理时,使用类模板的特化。例如:
class compare
{
public:
bool IsEqual(T t1, T t2)
{
return t1 == t2;
}
};
int main()
{
char str1[] = "Hello";
char str2[] = "Hello";
compare<int> c1;
compare<char *> c2;
cout << c1.IsEqual(1, 1) << endl; //比较两个int类型的参数
cout << c2.IsEqual(str1, str2) << endl; //比较两个char *类型的参数
return 0;
}
这里最后一行也是调用模板类compare<char*>的IsEqual进行两个字符串比较,显然这里存在的问题和上面函数模板中的一样,我们需要比较两个字符串的内容,而
不是仅仅比较两个字符指针。因此,需要使用类模板的特化:
{
public:
bool IsEqual(char* t1, char* t2)
{
return strcmp(t1, t2) == 0; //使用strcmp比较字符串
}
};
注意:进行类模板的特化时,需要特化所有的成员变量及成员函数。