0%

动态内存

我们的程序到目前为止只使用过静态内存或栈内存。静态内存用来保存局部static对象(就是局部对象加上static)、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,在其定义的程序块运行时才存在: static 对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间( free store)或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象一即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

动态内存与智能指针

在C++中,动态内存的管理是通过一对运算符来完成的: new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化; delete, 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

新标准提供了两个智能指针类型类管理动态对象。他们行为类似常规指针,却可以自动的释放锁指向的对象,这两种指针的区别在于管理底层指针的方式:

shared_ ptr允许多个指针指向同-一个对象; unique_ ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ ptr的伴随类,它是一种弱引用,指向shared_ ptr所管理的对象。这三种类型都定义在memory头文件中。

shared_ptr类

创建时我们也需要提供指向的类型:

1
2
shared_ptr<string> p1;				// shared_ _ptr, 可以指向string
shared_ptr<list<int>> p2; // shared_ ptr, 可以指向int的list

默认初始化的指针中保存着一个空指针,

智能之后着呢使用方式与普通指针类似,解引用返回指向的对象,在if使用,是检测它是否为空:

1
2
3
//如果p1不为空,检查它是否指向一个空string
if (p1 && pl->empty())
*p1 = "hi"; //如果p1指向一个空string,解引用p1,将一个新值赋予string

image.png

image.png

make_shared函数

他是最安全的分配和使用动态内存的方法,函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr,一样要给出创建对象类型:

1
2
3
4
5
6
//指向一个值为42的int的shared_ ptr
shared_ptr<int> p3 = make_shared<int> (42) ;
// p4指向一个值为"999999999"的string
shared_ptr<string> p4 = make_shared<string>(10'9') ;
// p5指向一个值初始化的(参见3.3.1节,第88页)int,即,值为0
shared_ptr<int> p5 = make_shared<int>() ;

通常使用auto来指向它

1
2
// p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>() ;

shared_ptr拷贝和赋值

1
2
auto p = make shared<int>(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向相同对象,此对象有两个引用者

每一个shared_ptr都会有一个关联的计数器,为引用计数。拷贝一个shared_ptr、作为参数传递给函数或者作为返回值就会递增,给shared_ptr赋予新值或者它被销毁计数器会递减。

1
2
3
4
5
auto r = make_shared<int>(42); // r指向的int只有一个引用者
r = q; //给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
// r原来指向的对象已没有引用者,会自动释放

shared_ptr销毁管理对象

当指向一个对象的最后一个智能指针被销毁,指针的析构函数会递减指向对象的析构函数的引用计数,计数为0,指针的析构函数会销毁对象,释放内存。

且动态对象不再被使用时,shared_ptr类会自动的释放对象,特性使得动态内存的使用变得容易,例如在函数创建智能指针在离开作用域后会自动的释放掉

1
2
3
4
5
6
7
8
9
10
// factory 返回一个shared_ ptr, 指向一个动态分配的对象
shared_ptr<Foo> factory(T arg)
//恰当地处理arg
// shared_ ptr负责释放内存
return make_shared<Foo> (arg) ;
}
void use_factory(T arg)
shared_ptr<Foo> P = factory(arg) ;
//使用p
} // p离开了作用城,它指向的内存会被自动释放掉

使用了动态生存期的资源的类

程序使用动态内存出于以下三种原因之一:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

目前使用的类分配资源都与对应对象生存期一致。例如每个vector拥有自己的元素,当拷贝一个vector时,原vector和副本vector是相互分离的。

如果我们希望有一个类,当它进行拷贝时,不是拷贝其中成员,而是不同对象之间共享相同的元素。所以当两个对象共享底层数据,当其中一个被销毁,我们不能单方面的销毁底层数据:

1
2
3
4
5
6
Blob<string> bl; // 空Blob
{ //新作用域
Blob<string> b2 = {"a", "an", "the"};
bl = b2;//bl和b2共享相同的元素
} // b2被销毁了,但b2中的元素不能销毁
// bl指向最初由b2创建的元素

定义StrBlob

这里想要实现一个StrBlob类管理string元素,如果我们在类内直接使用一个vector来保存元素,那么当多个对象中的一个被销毁时就会把底层vector销毁,所以这里使用vector保存在动态内存中。

为了实现数据共享,我们为StrBlob设置一个shared_ptr来管理动态内存分配的vector。该指针可以记录有多少个StrBlob共享相同的vector。

还需要提供一些操作,当访问一个不存在的元素,会抛出异常,且有一个默认构造和单一构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class StrB1ob {
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob() ;
StrBlob(std::initializer_list<std::string> il) ;
size_type size() const { return data->size(); }
bool empty() const { return data->empty() ; }
//添加和删除元素
void push_back (const std: :string &t) {data->push_ back(t); }
void pop_back() ;
//元素访问
std::string& front () ;
std::string& back() ;
private:
std::shared_ptr<std::vector<std::string>> data;
//如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};

StrBlob构造函数

1
2
3
StrBlob::StrB1ob (): data (make_shared<vector<string>>()) { }
StrBlob::StrBlob (initializer_list<string> il) :
data (make_shared<vector<string>>(il)) {}

元素访问成员函数

由于操作访问函数需要先检查存不存在,所以定义一个私有的工具函数check:

1
2
3
4
void StrBlob::check(size_type i, const string &msg) const
if (i >= data->size())
throw out_of_range(msg) ;
}

其他操作首先调用check,如成功则继续下一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string& StrBlob::front ()
{
//如果vector为空,check 会抛出一个异常
check(0"front on empty StrB1ob") ;
return data->front () ;
}
string& StrBlob::back()
{
check(0"back on empty StrB1ob") ;
return data->back() ;
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob") ;
data->pop_back() ;
}

最后还应对front和back的const版本进行重载:

1
2
3
4
5
6
7
8
9
10
11
const string& StrBlob::front ()
{
//如果vector为空,check 会抛出一个异常
check(0"front on empty StrB1ob") ;
return data->front () ;
}
const string& StrBlob::back()
{
check(0"back on empty StrB1ob") ;
return data->back() ;
}

StrBlob的拷贝、赋值和销毁

该类型对象被拷贝’赋值或者销毁时,执行相应操作的是shared_ptr成员而不是vector,直到最后一个指向vector的指针对象被销毁。

直接管理内存

还可以使用new和delete来分配内存,但非常容出错。

使用new动态分配内存和初始化对象

new分配的内存是无名的,返回一个指向该对象的指针:

1
2
3
4
int *pi = new int; 			// pi指向一个动态分配的、未初始化的无名对象
// 默认情况下,动态分配内存是默认初始化的,意味着内置类型或组合类型的值是未定义的。
string *ps = new string; //初始化为空string
int *pi = new int; // pi指向一个未初始化的int

也可以使用列表初始化,或值初始化:

1
2
3
4
5
6
7
8
int *pi = new int(1024);			// pi指向的对象的值为1024
string *ps = new string(10'9'); // *ps 为"999999999"
// vector 有10个元素,值依次从0到9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
string *ps1 = new string; //默认初始化为空string
string *ps = new string() ; //值初始化为空string
int *pil = new int; //默认初始化; *pi1 的值未定义
int *pi2 = new int(); //值初始化为0; *pi2为0

建议对动态分配的对象进行初始化操作。

如果提供了一个括号包围的初始化器,可以使用auto自动接管动态内存,但括号内必须仅有单一初始化器才可以使用:

1
2
3
auto p1 = new auto (obj) ;				// p指向一个与obj类型相同的对象
//该对象用obj进行初始化
auto p2 = new auto{a,b,c} ; // 错误:括号中只能有单个初始化器

动态分配const对象

一个动态内存的const对象必须进行初始化,对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显示初始化。new返回的也是一个const指针。

内存耗尽

当程序用光了所有可用内存,new就会失败,会抛出一个bad_alloc的异常,可以改变new的方式来阻止异常:

1
2
3
//如果分配失败,new返回一个空指针
int *p1 = new int; //如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int; //如果分配失败,new返回一个空指针

这种new为定位new,这种形式允许我们传递额外参数,nothow就是告诉它不能抛出异常。以上类型都在头文件new中。

释放动态内存

我们使用delete来释放内存:

1
delete p; // p必须指向一个动态分配的对象或是一个空指针

但传递给delete的指针必须是指向动态分配的内存或空指针,其他行为是未定义的。

const对象的值不能被改变,但是本身可以销毁,同样delete指向它的指针。

动态对象的生存期直到被释放时为止

如果不使用智能指针,那么必须显示的释放它。

1
2
3
4
5
6
7
8
9
10
// factory 返回一个指针,指向一个动态分配的对象
Foo* factory(T arg)
{
//视情况处理arg
return new Foo(arg); // 调用者负责释放此内存
}
void use_factory(T arg)
{
Foo *p = factory(arg) ; //使用p但不delete它
}//p离开了它的作用域,但它所指向的内存没有被释放!

所以必须在use_factory中delete掉这个p,或者return出去让外部释放。

坚持使用智能指针,避免所有这些问题。

delete之后重置指针

delete指针之后,指针值就无效了,虽然指针已经无效,但有些仍保存着地址,为空悬指针:即指向一块曾经保存数据对象但现在已经无效的内存指针。

它和未初始化指针很像,解决办法是,在指针即将离开其作用域之前释放它所关联的内存,这样没有机会继续使用,也可以在delete之后给其赋值为nullptr。

shared_ptr和new结合使用

我们可以用new返回的指针来初始化智能指针:

1
2
shared_ptr<double> p1; 			//shared_ ptr可以指向一个double
shared_ptr<int> p2 (new int(42)); //p2指向一个值为42的int

接受参数的智能指针是explicit的,因此我们不能将一个内置指针隐式转换为智能指针,必须使用直接初始化形式:

1
2
3
4
5
6
7
8
shared_ptr<int> p1 = new int (1024);		// 错误:必须使用直接初始化形式
shared_ptr<int> p2 (new int(1024)); // 正确:使用了直接初始化形式
shared_ptr<int> clone (int p) {
return new int(p) ; // 错误:隐式转换为shared ptr<int>
}
shared_ ptr<int> clone(int p) {
return shared_ ptr<int> (new int(p)) ;//正确:显式地用int*创建shared_ ptr<int>
}

不可混用普通与智能指针

1
2
3
4
5
int *x(new int (1024)) ;
//危险: x是一个普通指针,不是一个智能指针
process(x); //错误:不能将int*转换为一个shared_ ptr<int>
process(shared_ptr<int>(x)); // 合法的,但内存会被释放!
int j = *x;//未定义的:x是一个空悬指针!

将临时的shared_ptr传递给函数,在调用结束后就会被销毁,则x变为空悬指针。

当将一个shared_ptr绑定到一个 普通指针时,我们就将内存的管理责任交给了这个shared_ptr.-旦这样做了 ,我们就不应该再使用内置指针来访问shared__ptr所指向的内存了。

也不要使用get初始化另一个智能指针或者为智能指针赋值

智能指针定义了名为get的函数,返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况二设计的:是为了不能使用智能指针的代码使用,但此指针不能delete。

1
2
3
4
5
6
7
shared_ptr<int> p(new int(42)); //引用计数为1
int *q = p.get(); //正确:但使用q时要注意,不要让它管理的指针被释放
{ //新程序块
//未定义:两个独立的shared_ptr指向相同的内存
shared_ptr<int> (q) ;
} //程序块结束,q被销毁,它指向的内存被释放
int foo = *p; //未定义: p指向的内存已经被释放了

其他shared_ptr操作

使用reset将一个新指针赋予它:

1
2
p = new int(1024) ;		//错误:不能将一个指针赋予shared_ ptr
p.reset(new int (1024)) ; //正确: p指向一个新对象

通常与unique一起使用,控制多个shared_ptr共享的对象,检查自己是当前对象仅有的用户,如果不是,在改变之前要做一次新的拷贝:

1
2
3
if (!p.unique())
p.reset (new string(*p)); // 我们不是唯一用户;分配新的拷贝
*p += newVal; //现在我们知道自己是唯一的用户,可以改变对象的值

智能指针和异常

在函数中使用智能指针,即使函数发生了异常,局部对象也会被销毁,而如果使用new,则在delete之前出现异常不会自动释放。

智能指针指针和哑类

有一些为C和C++两种语言设计的类,通常要求用户显示的释放所使用的任何资源。我们可以使用管理动态内存类似的技术管理不具有良好定义的析构函数,例如:

1
2
3
4
5
6
7
8
9
10
11
struct destination;						//表示我们正在连接什么
struct connection; //使用连接所需的信息
connection connect (destination*) ; //打开连接
void disconnect(connection) ; //关闭给定的连接
void f(destination &d /*其他参数*/)
{
//获得一个连接;记住使用完后要关闭它
connection C = connect(&d) ;
//使用连接
//如果我们在f退出前忘记调用disconnect,就无法关闭c了
}

如果connection没有析构函数,就会造成内存泄漏,可以使用shared_ptr保证connection被正确关闭。

使用自己的释放操作

首先定义一个函数来代替delete,这个删除器函数必须能够完成对shared_ptr保存的指针进行释放的操作。

1
2
3
4
5
6
7
8
void end_connection (connection *p) { disconnect(*p); }
void f (destination &d /*其他参数*/)
{
connection C = connect (&d) ;
shared_ptr<connection> P(&C, end_connection) ;
//使用连接
//当f退出时(即使是由于异常而退出), connection会被正确关闭
}

当p被销毁时,他会使用end_connection来代替delste,从而确保链接关闭。

智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的
前提下。为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针。
  • 不delete get()返回的指针。
  • 不使用get()初始化或reset另一个智能指针。
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

unique_ptr

一个unique_ptr“拥有”它所指的对象,且只能有一个unique_ptr指向给定对象,指针被销毁时对象也会被销毁。定义它时,没有make_shared类似的函数,需要绑定一个new返回的指针。

1
2
unique_ ptr <double> p1; //可以指向一个double的unique_ ptr
unique_ ptr<int> p2 (new int(42)); // p2指向一个值为42的int

且不支持拷贝或赋值操作

image.png

但可以通过调用release或reset将指针转移所有权:

1
2
3
4
5
//将所有权从p1 (指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1.release()); // release 将p1置为空
unique_ptr<string> p3(new string ("Trex"));
//将所有权从p3转移给p2
p2.reset (p3.release()); // reset 释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针并置空,并且切断了它和原指针的联系,如果不移交给智能指针,一定要delete。

reset成员接受可选指针,然后重新指向给定指针。

函数中的unique_ptr

我们可以拷贝或赋值一个精要呗销毁的unique_ptr,如函数返回它。

1
2
3
4
5
6
7
8
9
unique_ptr<int> clone(int p) {
//正确:从int*创建一个unique_ ptr<int>
return unique_ ptr<int> (new int(p) ) ;
}
//还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p) {
unique_ptr<int> ret (new int (p) ) ;
return ret;
}

这是一种特殊的拷贝,将在之后介绍它。

传递删除器

与shared_ptr类似,可以重载删除器,一样需要提供删除器类型,在创建或reset时提供指定类型的可调用的删除器。

1
2
3
// P指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT, delT> P (new objT, fcn) ;

用unique_ptr代替shared_ptr:

1
2
3
4
5
6
7
8
void f(destination &d /*其他需要的参数*/)
{
connection c = connect (&d); //打开连接
//当p被销毁时,连接将会关闭
unique_ptr<connection, decltype(end_connection)*> P(&C, end_connection) ;
//使用连接
//当f退出时(即使是由于异常而退出), connection会被正确关闭
}

在本例中我们使用了decltype来指明函数指针类型。由于decltype (end_ connection) 返回一个函数类型,所以我们必须添加一个*来指出我们正在使用该类型的一个指针。

weak_ptr

它是一种不控制所指向对象生存期的智能指针,它指向由一个shared(后面都简写)管理的对象将weak绑定到shared不会增加shared的引用计数,计数归0,即使有weak对象也会被释放。weak名字意为这种指针“弱”共享对象。

image.png

创建weak时需要用shared初始化它:

1
2
auto P = make_shared<int> (42) ;
weak_ptr<int> wp(p); // wp弱共享p; p的引用计数未改变

因为weak若共享特性,它指向的对象可能不存在,所以在访问时必须调用lock判断,它返回一个指向共享对象的shared:

1
2
3
if (shared_ptr<int> np = wp.lock()) { //如果np不为空则条件成立
//在if中,np与p共享对象
}

核查指针类

如果将StrBolb类定义一个伴随指针,保存一个weak_ptr,指向StrBolob的data成员,使用weak不会影响StrBlob指向vector的生存期,但可以阻止用户访问不存在的vector。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//对于访问一个不存在元素的尝试,StrBlobPtr抛出一个异常
class StrBlobPtr {
public:
StrBlobPtr():curr (0) { }
StrBlobPtr(StrB1ob &a, size_t sz = 0) :
wptr(a.data),curr(sz) { }
std::string& deref () const;
StrBlobPtr& incr(); // 前缀递增
private:
//若检查成功,check返回一个指向vector的shared_ptr
std::shared_ptr<std::vector<std::string>>
check(std::size_tconst std::string&) const ;
//保存一个weak_ptr,意味着底层vector可能会被销毁
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; // 在数组中的当前位置
};

此类需要注意不能将StrBlobPtr绑定到一个const StrBlob对象是因为构造函数只接受非const对象的引用

check函数也与之前不同需要检查指向的vector是否还存在:

1
2
3
4
5
6
7
8
9
10
std::shared_ptr<std: :vector<std: :string>>
StrBlobPtr::check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock(); // vector还存在吗?
if (!ret)
throw std::runtime_error ("unbound StrBlobPtr") ;
if (i >= ret->size())
throw std::out_of_range (msg) ;
return ret; // 否则,返回指向vector的shared_ptr
}

指针操作

现在我们将定义deref和incr的函数来解引用和递增StrBlobPtr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end") ;
return (*p)[curr]; // (*p) 是对象所指向的vector
}

//前缀递增:返回递增后的对象的引用
StrBlobPtr& StrBlobPtr::incr ()
{
// 如果curr已经指向容器的尾后位置,就不能递增它
check(curr, " increment past end of StrBlobPtr") ;
++curr; //推进当前位置
return *this;
}

//此外为了访问data成员需要声明StrBlob的friend
//对于StrBlob 中的友元声明来说, 此前置声明是必要的
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
//其他成员与12.1.1节(第405页)中声明相同
//返回指向首元素和尾后元素的StrBlobPtr
StrBlobPtr begin() { return StrBlobPtr(*this) ; }
StrBlobPtr end ()
{ auto ret = StrBlobPtr(*this, data->size()); return ret;}
//这里就是
};

动态数组

如果需要可变数量的对象时,可以使用在StraBlob中采取的方法。

new和数组

定义:

1
2
3
4
5
6
//调用get_size确定分配多少个int
int *pia = new int[get_size()]; // pia指向第一个int

//也可以用一个表示数组类型的类型别名分配:
typedef int arrT[42]; // arrT表示42个int的数组类型
int *p = new arrT; //分配一个42个int的数组; p指向第一个int

最后的代码等于int *P new int[42];

在分配后得到元素类型的指针,所以不能使用begin或end,不可以用范围for来处理动态数组的元素

要记住我们所说的动态数组并不是数组类型,这是很重要的。

初始化动态分配的数组

可以使用默认初始化或者值初始化(跟一对空括号)

1
2
3
4
int *pia = new int[10];				// 10 个未初始化的int
int *pia2 = new int[10]() ; // 10个值初始化为0的int
string *psa = new string[10] ; // 10个空string .
string *psa2 = new string[10](); // 10 个空string

还可以提供初始化器:

1
2
3
4
//10个int分别用列表中对应的初始化器初始化
int *pia3 = new int[10]{0,1,2,3,4,5,6, 7,8,9};
//10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
string *psa3 = new string[10]{"a""an", "the", string(3,'x') };

与内置初始化一样,初始化器会初始化开始部分的元素,剩余执行值初始化。

我们不可以在括号内给出初始化器,且不能用auto分配数组。

动态分配空数组

可以用任意表达式唉确定分配相对数目

1
2
3
4
5
6
7
8
size_t n = get_size(); 	//get_size 返回需要的元素的数目
int* P = new int[n]; //分配数组保存元素
for (int* q = p; q != p + n; ++q) .
/*处理数组*/ ;

//即使为0,也能够正常运作
char arr[0] ; //错误:不能定义长度为0的数组.
char *cp = new char[0]; // 正确:但cp不能解引用

cp可以就像尾后迭代器一样使用

释放动态数组

1
2
delete P;		// p必须指向一个动态分配的对象或为空
delete [] pa; //pa必须指向一个动态分配的数组或为空

释放元素是按逆序销毁,且方括号是必须的

1
2
3
typedef int arrT[42] ;		// arrT是42个int的数组的类型别名
int *p = new arrT; //分配一个42个int的数组; p指向第一个元素
delete [] p; //方括号是必需的,因为我们当初分配的是一个数组

智能指针和动态数组

标准库提供了一个可以管理new分配的数组的unique版本。但必须在对象后跟一对空方括号

1
2
3
//up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up (new int[10]) ;
up.release(); //自动用delete[]销毁其指针

当一个unique指向一个数组时,我们可以使用下标运算来访问数组中的元素:

1
2
for (size_t i = 0;i != 10; ++i)
up[i] = i; //为每个元素赋予一个新值

image.png

与unique不同的是shared不支持管理动态数组。如果希望使用shared管理动态数组需要自定义删除器:

1
2
3
//为了使用shared_ptr,必须提供一个删除器
shared_ptr<int> sp(new int[10], [](int *p) { delete[] P; }) ;
sp.reset(); //使用我们提供的lambda释放数组,它使用delete []

这里直接传递一个lambda表达式作为删除器。如果不提供删除器,则后果与delete不加[]一样。此外他也不支持下标运算:

1
2
3
// shared_ ptr未定义下标运算符,并且不支持指针的算术运算
for (size_t i = 0;i != 10; ++i)
* (sp.get() + i) = i; //使用get获取一个内置指针

所以只能使用get获取内置指针来访问数组元素

allocator类

new有一些缺陷:因为它将内存分配与对象构造组合在一起,所以会导致不必要的浪费:

1
2
3
4
5
6
7
8
string *const P = new string[n]; // 构造n个空string
string s;
string *q = P; // q指向第一个string
while(cin >> s && q != P + n)
*q++ = s; //赋予*q一个新值
const size_t size = q - P; //记住我们读取了多少个string
//使用数组
delete[] p; // P指向一个数组;记得用delete[]来释放

这里创建了n个string,但可能并不需要这么多,所以造成了浪费。

新的方法allocalltor

它定义在头文件memory中,帮助我们将内存分配和对象构造分开。分配时需要给出类型:

1
2
allocator<string> alloc;				//可以分配string的allocator对象
auto const P = alloc.allocate (n) ; // 分配n个未初始化的string

image.png

分配未构造的内存

使用alloc.construct构造对象,额外的参数用于调用对象的构造函数。

1
2
3
4
auto q = p; 						//q指向最后构造的元素之后的位置
alloc.construct(q++); //*q为空字符串
alloc.construct(q++,10, 'c'); //*q为cccccccccc
alloc.construct(q++, "hi"); //*q为hi !

在没有构造的情况下访问内存试错误的:

1
2
cout << *p <<endl; //正确:使用string的输出运算符
cout << *q <<endl; //灾难:q指向未构造的内存!

当用完对象后,必须对每个构造元素调用destroy来销毁它们。接受一个指针对指向对象执行析构:

1
2
while (q != p)
alloc.destroy(--q);//释放我们真正构造的string

销毁元素后可以重新使用内存,也可以归还系统

1
alloc.deallocate(p, n) ;

需要注意的是,第二个大小参数必须与调用allocate时一样。

拷贝和填充未初始化内存

1
2
3
4
5
6
//分配比vi中元素所占用空间大一倍的动态内存
auto p = alloc.allocate(vi.size () * 2);
//通过拷贝vi中的元素来构造从p开始的元素
auto q = uninitialized_copy (vi.begin(), vi.end(), p);
//将剩余元素初始化为42
uninitialized_fill_n(g, vi.size() , 42);

使用标准库:文本查询程序

此部分将单独作为一章。

关联容器

使用关联容器

关联容器

使用关联容器

map

1
2
3
4
5
6
7
8
//统计每个单词在输入中出现的次数
map<string,size_t > word_count; // string到size_t 的空map
string word;
while (cin >> word)
++word_count[word] ; //提取word的计数器并将其加1
for (const auto &w : word_ count) // 对map中的每个元素
//打印结果
cout << w.first << " occurs”<< w.second << ((w.second>1)?”times":”time") << endl;

set

1
2
3
4
5
6
7
8
9
//统计输入中每个单词出现的次数
map<string,size_t > word_count; // string 到size_ t的空map .
set<string> exclude = { "The", "But", "And""Or""An", "A",
"the""but", "and""or""an""a"};
string word;
while (cin >> word)
//只统计不在exclude中的单词
if (exclude. find (word) == exclude.end() )
++word_ count [word]; // 获取并递增word的计数器

关联容器概述

定义关联容器

1
2
3
4
5
6
7
8
map<string,size_ t> word_ count; //空容器
//列表初始化
set<string> exclude = { "the", "but", "and", "or", "an", "a",
"The", "But""And""Or", "An""A"};
//三个元素; authors将姓映射为名
map<string,string> authors = { {"Joyce", "James"},
{ "Austen", "Jane"},
{"Dickens", "Charles"} };

初始化multimap或multiset

multi容器允许多个元素具有相同的关键字:

1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个有20个元素的vector,保存0到9每个整数的两个拷贝
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i) {
ivec.push_ back(i) ;
ivec.push_ back(i); // 每个数重复保存一次
}
// iset包含来自ivec的不重复的元素; miset包含所有20个元素
set<int> iset (ivec. cbegin(),ivec.cend()) ;
multiset<int> miset (ivec.cbegin(),ivec.cend()) ;
cout << ivec.size() << endl; //打印出20
cout << iset.size() << endl; //打印出10
cout << miset.size() << endl; // 打印出 20

关键字类型的要求

对于有序容器中的关键字类型必须定义比较元素的方法。

使用关键字类型比较函数

可以指定一个比较函数来进行比较,需要在容器定义时紧跟着关键字类型给出。

当使用Sales_data类时因为没有<运算符,所以我们需要自己定义:

1
2
3
4
5
6
7
8
9
bool compareIsbn (const Sales_data &1hs, const Sales_data &rhs)
{
return lhs.isbn() < rhs.isbn() ;
}
//然后定义容器时传入该函数,需要提供想要使用的操作的指针:
//bookstore中多条记录可以有相同的ISBN
// bookstore中的元素以ISBN的顺序进行排列
multiset<Sales_data, dec1type(compareIsbn)*>
bookstore (compareIsbn);

使用decltype指出自定义操作的类型,必须加上*指出给定的函数指针。

pair类型

在头文件utility中,一个pair保存两个数据成员:

1
2
3
pair<string, string> anon;				//保存两个string
pair<string, size_ t> word_ count; //保存一个string和一个size_ t
pair<string, vector<int>> line; //保存string和vector<int>

也可以使用列表初始化

1
pair<string, string> author{"James", "Joyce"};

成员均为public使用first和second进行访问

1
2
//打印结果
cout << w.first << "occurs" << w.second<< ( (w.second > 1)? ”time s: ”time" ) << endl ;

返回pair的函数

1
2
3
4
5
6
7
8
pair<string,int> process (vector<string> &V)
{
//处理v
if (!v.empty())
return {v. back(), v.back().size()}; // 列表初始化
else
return pair<string, int>(); //隐式构造返回值
}

else中返回的是一个空pair

早期版本中不可以列表初始化返回则必须显示构造或使用make_pair来生成:

1
2
3
4
if (!v.empty())
return pair<string,int> (v.back(),v.back().size());
if (!v.empty())
return make_ pair (V.back(),v.back().size());

关联容器的操作

关联容器迭代器

解引用关联容器得到迭代器得到一个value_type,对于map得到一个pair,first成员中保存的是const关键字,它是不可以更改的,second保存值。而set中value_type与value_type是一样的。

遍历关联容器

使用迭代器遍历容器

1
2
3
4
5
6
7
8
//获得一个指向首元素的迭代器
auto map_it = word_count.cbegin() ;
//比较当前迭代器和尾后迭代器.
while (map_it != word_count.cend() ) {
//解引用迭代器,打印关键字-值对
cout << map_it->first << ”occurs” << map_it->second << " times" << endl;
++map_it; // 递增迭代器,移动到下一个元素
}

打印出来的函数为升序排列。

关联容器和算法

通常不对关联容器使用泛型算法,关键字的const特性意味着不可以重排或修改元素,set中的元素时const的,所以只可以使用只读的算法,推荐使用容器内部的find,它会比泛型算法快得多,泛型find使用的是顺序搜索。

添加元素

使用insert添加一个元素或元素范围,

1
2
3
4
5
6
7
8
9
10
11
12
13
//set容器
vector<int> ivec = {2,4,6,8,2,4,6,8}; // ivec有8个元素
set<int> set2 //空集合
set2.insert(ivec.cbegin(),ivec.cend()); //set2有4个元素
set2.insert({1,3,5,7,1,3,5,7}); // set2现在有8个元素
//insert可以接受一对迭代器,也可以接受一个初始化列表。

//map容器
//向word_count插入word的4种方法
word_count.insert ({word, 1}) ;
word_count.insert (make_pair(word, 1)) ;
word_count.insert (pair<string, size_t>(word, 1)) ;
word_count.insert (map<string,size_t>::value_type (word, 1));

检测insert返回值

insert返回值依赖容器类型和参数,对于map和set,添加单一元素返回的是一个pair,first指向给定的关键字元素,second是bool值,指出插入成功还是失败(已存在)。

1
2
3
4
5
6
7
8
9
10
//统计每个单词在输入中出现次数的一种更烦琐的方法
map<string,size_t> word_count; // 从string到size_t的空map.
string word;
while (cin >> word) {
//插入一个元素,关键字等于word, 值为1;
//若word已在word_ count中,insert什么也不做
auto ret = word_count.insert ({word, 1}) ;
if (!ret.second) // word已在word_ count中
++ret.first->second; //递增计数器
}

展开递增语句

++ret.first->second;这句话就代表增加插入的那个元素中的second++。

multi容器添加元素

这种容器可以包含多个相同的关键字

1
2
3
4
5
mul timap<string, string> authors;
//插入第一个元素,关键字为Barth, John
authors.insert ({"Barth,John", "Sot-Weed Factor"});
//正确:添加第二个元素,关键字也是Barth, John
authors.insert({"Barth, John", "Lost in the Funhouse"}) ;

删除元素

image.png

erase可以接受一个迭代器或者是一对迭代器来删除一个或一个范围的元素。还可以接受一个key_type,删除所匹配的关键字元素。

1
2
3
4
//删除一个关键字,返回删除的元素数量
if (word_count.erase (removal_word) )
cout << "ok:”<< removal_word << ”removed\n";
else cout << "oops: ”<< removal_ word << ”not found! \n";

如果是multi容器,可能删除多个元素

1
auto cnt = authors.erase ("Barth,John") ;

上面authors中添加了两个相同的元素,则cnt等于2。

map的下标操作

map和unordered_map具有下标运算和at函数,set中不可以使用。

map通过下标运算,接受一个索引(即关键字)获取与之关联的值,但不同的是,如果没有此关键字,会创建一个关键字,并将对应的值初始化。

1
2
3
map <string, size_t> word_count; / / empty map
//插入一个关键字为Anna的元素,关联值进行值初始化;然后将1赋予它
word_count ["Anna"] = 1;
  • 在word_ _count中搜索关键字为Anna的元素,未找到。
  • 将一个新的关键字-值对插入到word _count中。关键字是-一个const string,保存Anna。值进行值初始化,在本例中意味着值为0。
  • 提取出新插入的元素,并将值1赋予它。

image.png

使用下标操作的返回值

通常解引用的一个迭代器的类型与下标运算返回的类型是一样的,但map不同,会得到有个mappped_type对象,且为左值。

访问元素

count可以统计元素个数,对于不允许重复元素存在的容器,推荐使用find。

map使用find代替下标操作

map容器的下标操作如果关键字不存在,则会插入这个新的关键字,所以在只是想知道有没有这个关键字时可以用find代替。

multi容器查找元素

方法一:

1
2
3
4
5
6
7
8
9
string search_ item("Alain de Botton") ;		//要查找的作者
auto entries = authors. count (search_ item) ; //元素的数量
auto iter = authors. find (search_ item) ; //此作者的第一本书
//用一个循环查找此作者的所有著作
while (entries) {
cout << iter->second << endl ; //打印每个题目
++iter; //前进到下一本书
--entries; //记录已经打印了多少本书
}

方法二

lower_bound和up_bound分别返回匹配的第一个位置和最后一个位置的后一个位置。不存在则返回可插入位置

1
2
3
4
5
6
// authors 和search_ item 的定义,与前面的程序一样
//beg和end表示对应此作者的元素的范围.
for (auto beg = authors. lower_bound (search_item) ,
end = authors.upper_bound (search_item) ;
beg != end; ++beg)
cout << beg->second << endl; //打印每个题目

方法三

equal_range函数接受关键字,返回pair。若关键字存在则第一个指向第一个与之匹配的位置,第二个是最后一个与之匹配位置的后一个位置。若不存在则返回可插入位置。

1
2
3
4
5
// authors 和search_item的定义, 与前面的程序一样
// pos保存迭代器对,表示与关键字匹配的元素范围
for (auto pos = authors.equal_range (search_item) ;
pos.first != pos.second; ++pos.first)
cout << pos.first->second << endl; //打印每个题目

单词转换map(跳)

无序容器

使用无序容器

unordered_map或者unordered_set,他们都有与前面类似的操作,通常可以用一个无序容器替换对应的有序容器,但顺序会与有序容器不同。

管理桶

无序容器在储存上为一组桶,无序容器使用一个哈希函数将所有元素映射到桶中,容器将具有一个特定的哈希值的所有元素保存在相同的桶中,访问时先按照哈希值找到对应的桶,再在桶中找对应的元素。因此容器性能依赖哈希函数的质量和桶的数量和大小。

最理想的情况应该是哈希函数将所有元素尽可能的均匀的分配到每个桶中。

image.png

无序容器对关键字类型的要求

默认情况下,无序容器使用关键字类型的==运算符来比较元素,还使用一个hash 类型的对象来生成每个元素的哈希值。例如当我们想要将一个int值使用哈希函数,就是hash,

但是我们不能直接定义关键字类型为自定义类类型的无序容器。可以不直接使用哈希模板而是使用自己的hash版本。例如Sale_data用作关键字,我们需要提供函数来代替==运算符和哈希计算函数。

1
2
3
4
5
6
7
8
size_t hasher(const Sales_ data &sd)
{
return hash<string>() (sd. isbn()) ;
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() ;
}
1
2
3
4
using SD_multiset = unordered multiset<Sales_data,
decltype(hasher)*, decltype(eqOp)*>;
//参数是桶大小、哈希函数指针和相等性判断运算符指针
SD_multiset bookstore(42, hasher, eqOp) ;

如果类内具有==运算符,可以只重载哈希函数

1
2
//使用FooHash生成哈希值; Foo必须有==运算符
unordered_set<Foo, decltype (FooHash)*> fooSet (10,FooHash);

泛型算法

概述

大多数算法定义在头文件algorithm,标准库还在numeric中定义了一些。且一般情况不直接操作容器而是需要一个迭代器指定的范围。

如find:

1
2
3
4
5
6
7
8
9
string val = "a value"; //我们要查找的值
//此调用在list中查找string元素
auto result = find(lst. cbegin(),lst.cend(), val) ;
//类似的,由于指针就像内置数组上的迭代器一样, 我们可以用find在数组中查找值,这里使用到了begin和end:
int ia[] = {2721012, 4710983};
int val = 83;
int* result = find (begin(ia), end(ia), val);
//还可以在子序列中查找,如从ia[1]开始,直至(但不包含) ia[4]的范围内查找元素
auto result = find(ia + 1,ia+4,val);

迭代器算法不依赖容器但依赖元素类型

例如find用元素类型==运算符,例如还会使用到<运算符等,所以需要在使用时确保元素类型定义了该操作,或者自定义操作。

算法永远不会改变容器大小(即不会添加或删除元素)

初识泛型算法

只读算法

意思是:只会读取输入范围不会改变元素。如:find、count、accumulate:

1
2
3
//对vec中的元素求和,和的初值是0
int sum = accumulate (vec. cbegin(),vec.cend() ,0);
//第三个参数类型决定了函数使用的加法运算符及返回值类型

算法和元素类型

accumulate将给定的元素范围加到第三个参数上。所以必须保证容器元素类型能够转换成和的元素类型,且和的类型定义了+操作:string sum = accumulate (v.cbegin(),v.cend() ,string("")) ;

注意这里必须显示的创建一个string,不可以使用字符串字面值:

1
2
//错误: const char*.上没有定义+运算符
string sum = accumulate (v.cbegin(),v.cend(), "") ;

对于只读算法,最好只采用cbegin和cend

操作两个序列的算法

equal用于确定是否保存相同的值。

1
2
// roster2中的元素数目应该至少与rosterl一样多
equal (roster1 .cbegin(),rosterl.cend() ,roster2. cbegin() ) ;

由于使用的是迭代器,所以不同类型的容器可以比较。而且,元素类型也不必一一样,只要我们能用=来比较两个元素类型即可。例如,在此例中,rosterl 可以是vector,而roster2 是list<const char*>。

那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。

写容器元素的算法

-些算法将新值赋予序列中的元素。当我们使用这类算法时,必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。记住,算法不会执行容器操作,因此它们自身不可能改变容器的大小。一些算法会自己向输入范围写入元素。这些算法本质上并不危险,它们最多写入与给定序列一样多的元素。

1
2
3
fill(vec.begin(), vec.end ( ),0);//将每个元素重置为0
//将容器的一个子序列设置为10
fill(vec.begin(), vec.begin () + vec.size ()/210);

关键概念:迭代器参数
一些算法从两个序列中读取元素。构成这两个序列的元素可以来自于不同类型的容器。例如,第一个序列可能保存于一个vector中,而第二个序列可能保存于一个list.deque、内置数组或其他容器中。而且,两个序列中元素的类型也不要求严格匹配。算法要求的只是能够比较两个序列中的元素。例如,对equal算法,元素类型不要求相同,但是我们必须能使用一来比较来自两个序列中的元素。
操作两个序列的算法之间的区别在于我们如何传递第二个序列。一些算法,例如equal,接受三个迭代器:前两个表示第一个序列的范围,第三个表示第二个序列中的首元素。其他算法接受四个迭代器:前两个表示第一个序列的元素范围,后两个表示第二个序列的范围。
用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。确保算法不会试图访问第二个序列中不存在的元素是程序员的责任。例如,算法 equal会将其第一个序列中的每个元素与第二个序列中的对应元素进行比较。如果第二个序列是第一个序列的一个子集,则程序会产生一个严重错误———equal会试图访问第二个序列中末尾之后(不存在)的元素。

算法不检查写操作

一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,该序列从目的位置迭代器指向的元素开始。例如,函数fill_n:

1
2
3
4
5
6
7
8
vector<int> vec; //空vector
//使用vec,赋予它不同值
fill_n(vec.begin (), vec.size ( ),0);//将所有元素重置为0
//函数fill_n假定写入指定个元素是安全的。即,如下形式的调用
fill_n(dest, n, val);
//一定要保证写入后大小不能超过容器大小,因为它不会重新开辟空间
vector<int> vec; //空向量
//灾难:修改vec中的10个(不存在)元素fill_n (vec.begin (),10,0);

back_inserter

头文件为iterator,它接受一个引用,返回一个与容器绑定的插入迭代器,当通过此迭代器赋值会调用push_back插入给定元素:

1
2
3
4
5
6
7
vector<int> vec; // 空向量
auto it = back_inserter(vec);//通过它赋值会将元素添加到vec中
*it = 42; // vec中现在有一个元素,值为42
//我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用。例如:
vector<int> vec; //空向量
//正确:back_inserter创建一个插入迭代器,可用来向vec添加元素
fill_n(back_inserter(vec),100); //添加10个元素到vec

由于每次赋值都会使用push_back,所以可以添加元素。

重排容器元素

先看一段代码,用于消除重复的元素:

1
2
3
4
5
6
7
8
9
10
void elimDups (vector<string> &words)
{
//按字典序排序words,以便查找重复单词
sort (words .begin(),words.end() ) ;
//unique 重排输入范围,使得每个单词只出现一次
//排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
auto end_ _unique = unique (words .begin(),words.end()) ;
//使用向量操作erase删除重复单词
words. erase (end_ _unique, words.end()) ;
}

这里首先使用sort进行排序,这里使用的是string中的<成员,这样相同的元素就会相邻,然后使用unique算法“删除”重复元素,还记得其实我们并不能真的删除容器中的元素,它只是将将重复元素覆盖,然后返回一个“删除”后容器的end位置,所以最后一句代码用erase真正的删除掉它们。

image.png

标准库算法对迭代器而不是容器进行操作。因此,算法不能(直接)添加或删除元素。

定制操作

很多算法允许我们自定义元素的运算符,如sort中的<。

向算法传递函数

如果希望排序是按照单词长度排序,可添加一个参数,称谓词

谓词

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类: 一元谓词(unary predicate, 意味着它们只接受单一参数)和二元谓词( binary predicate, 意味着它们有两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型,这里就是用它来代替<来比较参数。

1
2
3
4
5
6
7
//比较函数,用来按长度排序单词
bool isShorter (const string &s1, const string &s2)
{
return sl.size() < s2.size() ;
}
//按长度由短至长排序words
sort (words.begin(), words.end(), isShorter);

如果还希望当长度相等时,按字典排序:

1
2
3
4
5
6
elimDups (words); // 将words按字典序重排,并消除重复单词
//按长度重新排序,长度相同的单词维持字典序
stable_ sort (words .begin(),words.end() ,isShorter) ;
for (const auto &s : words) // 无须拷贝字符串
cout << s <<”"; //打印每个元素,以空格分隔
cout << endl;

lambda表达式

求大于等于一个给定长度的单词有多少,框架如下:

1
2
3
4
5
6
7
8
9
void biggies (vector<string> &words, vector<string>::size_ type sz)
{
elimDups (words); // 将words按字典序排序,删除重复单词.
//按长度排序, 长度相同的单词维持字 典序
stable_ sort (words .begin(), words.end(), isShorter) ;
//获取一个迭代器,指向第一个满足size()>= sz的元素
//计算满足size >= sz的元素的数目
//打印长度大于等于给定值的单词,每个单词后面接一个空格
}

可以使用find_if算法来查找特定大小的元素,第三个参数为谓词,它将对每个元素使用谓词,返回第一个使谓词返回非0值的元素。不存在返回尾迭代器。

我们的想法是编写一个接受string和长度两个参数返回bool值, 表示string长度是否大于给定长度,但find_if值接受一元谓词,所以使用lambda表达式

介绍

与任何函数类似,一个 lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式:
[ capture list ] (parameter list) -> return type { function body }
其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、parameter list和 function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda必须使用尾置返回来指定返回类型:

1
2
auto f = []{ return 42; };
cout << f() << endl; //打印42

如果 lambda的函数体包含任何单一return语句之外的内容,且未指定返回类型,则返回void。

传参

lambda不能有默认参数,所以实参形参数目必须相等且匹配,编写一个isShorter类型的lambda:

1
2
[](const string &a, const string &b)
{return a.size()<b.size ();}
1
2
3
4
//按长度排序,长度相同的单词维持字典序
stable_sort (words.begin ( ), words.end () ,
[](const string &a,const string &b)
{ return a.size () < b.size();});

当比较元素长度,就会使用lambda。

使用捕获列表

在例子中,我们需要捕获用户传进来的边界长度用来查找:

1
[sz] (const string &a) { return a.size() >= SZ; }

捕获了sz,所以我们才可以使用它,没有捕获的不可以使用。

一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。

调用find_if

1
2
3
4
5
//获取一个迭代器,指向第一个满足size () >= sz的元素
auto wc = find_if(words.begin(), words.end () ,
[sz] (const string &a)
{ return a.size() >=sz; });
auto count = words.end() - wc; //它表示size >= sz 元素的数目

for_each算法

我们还可以打印出>给定长度的单词:

1
2
3
//打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each(wc, words.end (), [] (const string &s ) { cout << s <<" "; });
cout << endl;

捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字,所以可以使用cout。

完成程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void biggies(vector<string> &words, vector<string>::size_type sz)
{
elimDups (words);// 将words按字典序排序,删除重复单词
//按长度排序,长度相同的单词维持字典序
stable_sort (words.begin (), words.end(), [](const string &a, const string &b)
{return a.size() < b.size();});
//获取一个迭代器,指向第一个满足size () >= sz的元素
auto wc = find_if (words.begin(), words.end (), [sz] (const string &a)
{ return a.size () >=sz; });
//计算满足size >= sz的元素的数目
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s")
<<" of length " <<sz<<" or longer" << endl;
//打印长度大于等于给定值的单词,每个单词后面接一个空格
for_each (wc, words.end(), [] (const string &s) {cout << s << " ";});
cout <<endl;
}

lambda捕获和返回

当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。目前,可以这样理解,它就是一个未命名的类类型的对象,捕获列表里是他的数据成员,使用auto定义一个lambda初始值变量时,就定义了一个从lambda生成的类型的对象。

值捕获与引用捕获

类似参数传递,变量的捕获方式也可以是值或引用。到目前为止,我们的 lambda采用值捕获的方式。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda创建时拷贝,而不是调用时拷贝:

1
2
3
4
5
6
void fcn1 ()
size_t v1 =42; //局部变量
//将v1拷贝到名为f的可调用对象
auto f =[v1] { return vl; };
v1 = 0;
auto j = f(); //j为42;f保存了我们创建它时v1的拷贝

采用引用则改变该值会同时改变:

1
2
3
4
5
6
void fcn2(){
size_t v1 = 42; //局部变量
//对象2包含v1的引用
auto f2 =[ &v1] { return vl; };
v1 = 0;
autoj = f2(); //j为0;f2保存v1的引用,而非拷贝

但捕获引用返回引用有一个问题是,必须保证使用lambda时该引用对象存在

1
2
3
4
5
6
7
void biggies(vector<string> &words, vector<string>::size_type sz, 
ostream &os= cout,char c = ' ')
{
//与之前例子一样的重排words的代码
//打印count的语句改为打印到os
for_each (words.begin (), words.end ( ), [ &os,c] (const string &s){ os << s<< c; });
}

由于我们不能拷贝ostream对象,所以拷贝os的唯一方式就时捕获引用(或指向os的指针)

我们可以从函数返回lambda,但该lambda不能捕获引用,因为局部变量消失会使lambda数据成员不可用。

我们应尽量减少引用或指针捕获

隐式捕获

使用=(值捕获)&(引用捕获)告诉编译器接下来我要使用的变量都采用该捕获方式:

1
2
3
// sz为隐式捕获,值捕获方式
wc = find_if (words.begin(), words.end(),
[=](const string &s) {return s.size() >= sz; });

也可以一部分值捕获,一部分引用捕获:

1
2
3
4
// os隐式捕获,引用捕获方式;c显式捕获,值捕获方式
for_each ( words.begin(), words.end(), [&, c](const string &s){ os<< s<< c; });
// os显式捕获,引用捕获方式;c隐式捕获,值捕获方式
for_each (words.begin(), words.end(), [=, &os](const string &s){ os << s << c; });

混合捕获必须把默认捕获方式写在前面(只能是=或者&),且显示捕获方式必须与默认不同。

image.png

可变lambda

默认情况是如果一个lambda包含除return以外的任何语句,则假定此lambda返回void,则不能返回值,这里使用位置返回类型来返回:

1
2
transform (vi.begin(), vi.end(), vi.begin(), 
[](int i)->int { if (i < 0) return -i; else return i; });

transform接受一对迭代器范围,和一个目的地,将由第四个参数调用后放入目的地。

参数绑定bind(跳过)

对于只在一两个地方用到的简单操作可以使用lambda,而很多地方,且操作更多,我们应该使用函数。

再探迭代器

  • 插入迭代器(insert iterator):这些迭代器被绑定到一个容器上,可用来向容器插入元素。
  • 流迭代器(stream iterator):这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
  • 反向迭代器( reverse iterator ):这些迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器。
  • 移动迭代器(move iterator):这些专用的迭代器不是拷贝其中的元素,而是移动它们。

插入迭代器

it=t 在it指定的当前位置插入值t。假定c是it绑定的容器,依赖于插入迭代器的不同种类,此赋值会分别调用c.push_back(t) 、c.push_front(t)或c.insert (t,p),其中p 为传递给inserter的迭代器位置

*it,++it,it++ 这些操作虽然存在,但不会对it做任何事情。每个操作都返回it插入器有三种类型。

差异在于元素插入的位置:

  • back_inserter创建一个使用push_back的迭代器。
  • front inserter创建一个使用push_front的迭代器。
  • inserter创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

插入迭代器还是基于容器自身的push操作,所以必须确保有该操作才可以使用对应的插入迭代器。

1
2
3
4
*it = val;
//其效果与下面代码一样
it = c.insert(it, val) ; // it指向新加入的元素
++it; //递增it使它指向原来的元素

front_inserter生成的迭代器的行为与inserter生成的迭代器完全不一样。当我们使用front_inserter时,元素总是插入到容器第一个元素之前。即使我们传递给inserter的位置原来指向第一个元素,只要我们在此元素之前插入一个新元素,此元素就不再是容器的首元素了:

1
2
3
4
5
6
list<int> lst = {1,2,3,4 };
list<int> lst2,lst3; //空list
//拷贝完成之后,lst2包含4 3 2 1
copy(lst.cbegin (), lst.cend(), front_inserter (lst2 ) ) ;
//拷贝完成之后,lst3包含1 2 3 4
copy (lst.cbegin(), lst.cend( ), inserter(lst3,lst3.begin () ) );

iostream迭代器(跳过)

反向迭代器

反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增(以及递减)操作的含义会颠倒过来。递增一个反向迭代器(++it)会移动到前一个元素;递减一个迭代器(–it)会移动到下一个元素。

除了forward_list之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、rend、crbegin 和 crend 成员函数来获得反向迭代器。这些成员函数返回指向容器尾元素和首元素之前一个位置的迭代器。与普通迭代器一样,反向迭代器也有 const和非const版本。

image.png

一些应用:

1
2
3
sort (vec.begin(), vec.end()); //按“正常序”排序vec
//按逆序排序:将最小元素放在vec的末尾
sort (vec.rbegin () , vec.rend () );
1
2
3
4
5
6
7
8
9
//在一个逗号分隔的列表中查找第一个元素
auto comma = find (line.cbegin(), line.cend(), ',' );
cout << string (line.cbegin() , comma) << endl;
//在一个逗号分隔的列表中查找最后一个元素
auto rcomma = find(line.crbegin(), line.crend(), ',');
//错误:将逆序输出单词的字符
cout << string(line.crbegin() , rcomma) << endl;
//正确:得到一个正向迭代器,从逗号开始读取字符直到line末尾
cout << string(rcomma.base(), line.cend()) << endl;

base函数将反向迭代器转换为正向迭代器。

泛型算法结构

算法所要求的迭代器可分为五类:

image.png

5类迭代器

输出迭代器之外,一个高层类别的迭代器支持底层类别迭代器的所有操作。

输入迭代器

他可以读取序列中的元素,必须支持:

  • 用于比较两个迭代器的相等和不相等运算符( =一、!=)
  • 用于推进迭代器的前置和后置递增运算(++)
  • 用于读取元素的解引用运算符(*);解引用只会出现在赋值运算符的右侧
  • 箭头运算符(->),等价于(*it) .member,即,解引用迭代器,并提取对象的成员

输入迭代器只用于顺序访问。对于一个输入迭代器,*it++保证是有效的,但递增它可能导致所有其他指向流的迭代器失效。其结果就是,不能保证输入迭代器的状态可以保存下来并用来访问元素。因此,输入迭代器只能用于单遍扫描算法。算法find和 accumulate要求输入迭代器;而istream_iterator是一种输入迭代器。

输出迭代器

可以看作输入迭代器功能上的补集——只写而不读元素。输出迭代器必须支持

  • 用于推进迭代器的前置和后置递增运算(++)
  • 解引用运算符(*),只出现在赋值运算符的左侧(向一个已经解引用的输出迭代器赋值,就是将值写入它所指向的元素)

我们只能向一个输出迭代器赋值一次。类似输入迭代器,输出迭代器只能用于单遍扫描算法。用作目的位置的迭代器通常都是输出迭代器。例如,copy函数的第三个参数就是输出迭代器。ostream_iterator类型也是输出迭代器。

前向迭代器

可以读写元素。这类迭代器只能在序列中沿一个方向移动。前向迭代器支持所有输入和输出迭代器的操作,而且可以多次读写同一个元素。因此,我们可以保存前向迭代器的状态,使用前向迭代器的算法可以对序列进行多遍扫描。算法replace要求前向迭代器,forward_list上的迭代器是前向迭代器。

双向迭代器

可以正向/反向读写序列中的元素。除了支持所有前向迭代器的操作之外,双向迭代器还支持前置和后置递减运算符(–)。算法 reverse要求双向迭代器,除了forward_list之外,其他标准库都提供符合双向迭代器要求的迭代器。

随机访问迭代器

提供在常量时间内访问序列中任意元素的能力。此类迭代器支持双向迭代器的所有功能

  • 用于比较两个迭代器相对位置的关系运算符(<、<=、>和>=)
  • 迭代器和一个整数值的加减运算(+、+=、-和-=),计算结果是迭代器在序列中前进(或后退)给定整数个元素后的位置
  • 用于两个迭代器上的减法运算符(-),得到两个迭代器的距离
  • 下标运算符(iter[n] ),与* (iter [n])等价

算法sort要求随机访问迭代器。array、deque、string和 vector的迭代器都是随机访问迭代器,用于访问内置数组元素的指针也是。

算法形参模式

alg ( beg, end, other args) ;

alg ( beg, end, dest, other args ) ;

alg (beg, end, beg2, other args) ;

alg(beg, end, beg2, end2, other args) ;

alg为算法名字,beg和end表示范围,dest为目的地,arg为算法。此外还有一些算法接受额外非迭代器参数。

接受单个目标迭代器的算法·

dest参数是一个表示算法可以写入的目的位置的迭代器。算法假定( assume):按其需要写入数据,不管写入多少个元素都是安全的。

向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。

常见情况是dest被绑定到一个插入迭代器或ostream_iterator。插入迭代器会将新元素添加到容器中,因而保证空间足够的,ostream_iterator会将数据写入到一个输出流,同样不管要写入多少个元素都没有问题。

接受第二个输入序列的算法

接受单独的 beg2或是接受beg2和 end2的算法用这些迭代器表示第二个输入范围。这些算法通常使用第二个范围中的元素与第一个输入范围结合来进行一些运算。

如果一个算法接受beg2和 end2,这两个迭代器表示第二个范围。这类算法接受两个完整指定的范围:[beg,end)表示的范围和[ beg2 end2)表示的第二个范围。

只接受单独的 beg2(不接受end2)的算法将beg2作为第二个输入范围中的首元素。此范围的结束位置未指定,这些算法假定从beg2开始的范围与 beg和 end所表示的范围至少一样大。

算法命名规范

一些算法使用重载形式传递一个谓词

1
2
unique (beg, end);					//使用==运算符比较元素
unique (beg, end,comp) ; //使用comp比较元素

_if版本算法

接受元素值的算法有一个不同名的版本,使用谓词代替元素值,接受谓词参数版本需要加_if:

1
2
find (beg, end,val) ;					//查找输入范围中val第一次出现的位置
find_if (beg, end,pred); //查找第一个令pred为真的元素

他们狗接受三个参数,因此不是重载。

拷贝与非拷贝版本

1
2
3
4
5
6
7
reverse(beg, end);					//反转输入范围中元素的顺序
reverse_copy(beg, end, dest); //将元素按逆序拷贝到dest
//一些算法同时提供_copy和_if版本。这些版本接受一个目的位置迭代器和一个谓词;
//从v1中删除奇数元素
remove_if(v1.begin (), v1.end(), [](int i){ return i % 2;});
//将偶数元素从v1拷贝到v2;v1不变
remove_copy_if(vl.begin(), v1.end(), back_inserter(v2), [](int i){ return i % 2;});

特定容器算法

因为如sort等一些通用算法要求随机访问迭代器,而list和forward_list提供的是前向和双向迭代器,所以只能使用他们自己的成员函数算法

image.png

image.png

链表还有一些splice算法,这些是链表独有的:

image.png

特有的操作会改变容器

多数链表特有的算法都与其通用版本很相似,但不完全相同。链表特有版本与通用版本间的一个至关重要的区别是链表版本会改变底层的容器。例如,remove的链表版本会删除指定的元素,非链表版本并不会真正删除,而是覆盖,unique的链表版本会删除第二个和后继的重复元素,非链表也是覆盖。

类似的,merge和splice会销毁其参数。例如,通用版本的merge将合并的序列写到一个给定的目的迭代器;两个输入序列是不变的。而链表版本的merge函数会销毁给定的链表——元素从参数指定的链表中删除,被合并到调用merge的链表对象中。在merge之后,来自两个链表中的元素仍然存在,但它们都已在同一个链表中。

顺序容器

顺序容器概述

vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快
forward_list 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快
array 固定大小数组。支持快速随机访问。不能添加或删除元素
string 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快

确定使用那种容器

  • 除非你有很好的理由选择其他容器,否则应使用vector。
  • 如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list。
  • 如果程序要求随机访问元素,应使用vector或deque。
  • 如果程序要求在容器的中间插入或删除元素,应使用list或forward_list。如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque。
  • 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则
    • 首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据,然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素。
    • 如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector 中。

如果程序兼具插入和访问,那么取决于哪一种操作更多。

容器库概览

首先定义时需要确定容器中的元素类型:

1
2
list<sales_data>					//保存Sales_data对象的list
deque<double> //保存double的deque

对容器可以保存的元素类型的限制

顺序几乎可以保存任意类型的元素,但若类型没有默认构造函数,虚特殊处理:

1
2
3
//假定noDefault是一个没有默认构造函数的类型
vector<noDefault> v1 (10, init); //正确:提供了元素初始化器
vector<noDefault> v2(10) ; //错误:必须提供一个元素初始化器

类型别名

iterator 此容器类型的迭代器类型
const_iterator 可以读取元素,但不能修改元素的迭代器类型
size_type 无符号整数类型,足够保存此种容器类型最大可能容器的大小
difference_type 带符号整数类型,足够保存两个迭代器之间的距离
value_type 元素类型
reference 元素的左值类型;与value_type&含义相同
const_reference 元素的const左值类型(即,const value_type&)

构造函数

c c; 默认构造函数,构造空容器(array,参见第301页)
c c1(c2); 构造c2的拷贝c1
cc(b,e); 构造c,将迭代器b 和e指定的范围内的元素拷贝到c(array不支持)
c c{a,b,c…}; 列表初始化c

赋值与swap

c1= c2 将c1中的元素替换为c2中元素
c1= {a,b,c… } 将c1中的元素替换为列表中元素(不适用于array)
a.swap(b) 交换a和 b的元素
swap(a,b) 与a.swap(b)等价

**大小 **

c.size () c中元素的数目(不支持forward_list)
c.max_size () c可保存的最大元素数目
c.empty() 若c中存储了元素,返回false,否则返回true

添加/删除元素(不适用于array)
注:在不同容器中,这些操作的接口都不同

c.insert(args) 将args 中的元素拷贝进c
c.emplace (inits) 使用inits构造c中的一个元素
c.erase (args) 删除args指定的元素
c.clear () 删除c中的所有元素,返回void

关系运算符

==,!= 所有容器都支持相等(不等)运算符
<,<=,>,>= 关系运算符(无序关联容器不支持)

获取迭代器

c.begin () , c.end () 返回指向c的首元素和尾元素之后位置的迭代器
c.cbegin(), c.cend () 返回const iterator

反向容器的额外成员(不支持forward_list)

reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator 不能修改元素的逆序迭代器
c.rbegin () , c.rend () 返回指向c的尾元素和首元素之前位置的迭代器
c.crbegin (), c.crend () 返回const_reverse_iterator

迭代器

一个迭代器范围有一对迭代器表示,通常是begin和end,[begin, end)。

他们应指向同一个容器中的元素或这最后一个容器的下一个位置,且begin<=end

左闭右合的范围

  • 如果begin 与 end相等,则范围为空
  • 如果begin与 end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
  • 我们可以对 begin递增若干次,使得begin==end

使用方法:

1
2
3
4
while (begin != end){
*begin = val; //正确:范围非空,因此begin指向一个元素
++begin; //移动迭代器,获取下一个元素
}

容器类型成员

如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用reference或const_reference。这些元素相关的类型别名在泛型编程中非常有用。为了使用这些类型,我们必须显式使用其类名:

1
2
3
4
// iter是通过list<string>定义的一个迭代器类型
list<string>::iterator iter;
// count是通过vector<int>定义的一个difference_type类型
vector<int>::difference_type count;

begin和end成员

1
2
3
4
5
list<string> a ={ "Milton","Shakespeare","Austen" };
auto it1 = a.begin (); //list<string>::iterator
auto it2 = a.rbegin (); //list<string>::reverse_iterator
auto it3 = a.cbegin (); //list<string>::const_iterator
auto it4 = a.crbegin(); //list<string>::const_reverse_iterator

C++新版本可以使用auto来定义类型:

1
2
3
4
5
6
//显式指定类型
list<string> : :iterator it5 = a.begin () ;
list<string> : :const_iterator it6 = a.begin ();
//是iterator还是const_iterator依赖于a的类型
auto it7 = a.begin(); //仅当a是const时, it7是const_iterator
auto it8 = a.cbegin (); //it8是const_iterator

当auto与begin或end结合使用时,获得的迭代器类型依赖于容器类型,与我们想要如何使用迭代器毫不相干。但以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么。

容器的定义和初始化

image.png

将一个容器初始化为另一个容器的拷贝

拷贝时容器类型及元素类型必须匹配。使用迭代器拷贝不需要容器类型相同,且元素类型只需要可以类型转换。

1
2
3
4
5
6
7
8
9
//每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = ( "Milton""Shakespeare""Austen"};
vector<const char*> articles = { "a", "an", "the" };

list<string> list2(authors) ; //正确:类型匹配
deque<string> authList (authors); //错误:容器类型不匹配
vector<string> words(articles); //错误:容器类型必须匹配
//正确:可以将const char*元素转换为string
forward_list<string> words (articles.begin ( ), articles.end ( ) );

初始化拷贝容器类型和元素类型都必须相同。

由于两个迭代器表示一个范围,因此可以使用这种构造函数来拷贝一个容器中的子序列。例如,假定迭代器it表示authors中的一个元素:

1
2
//拷贝元素,直到(但不包括)it指向的元素
deque<string> authList (authors.begin () , it);

列表初始化

1
2
3
//每个容器有三个元素,用给定的初始化器进行初始化
list<string> authors = {"Milton","Shakespeare""Austen"};
vector<const char*> articles = { "a", "an", "the" };

构造函数

接受一个容器大小,及初始值来创建容器:

1
2
3
4
vector<int> ivec ( 10,-1);				// 10个int元素,每个都初始化为-1
list<string> svec (10"hi! "); // 10个strings;每个都初始化为"hi !"
forward_list<int> ivec (10) ; // 10个元素,每个都初始化为0
deque<string> svec (10) ; // 10个元素,每个都是空string

如果元素类型具有没有默认构造函数,则必须提供初始值。

标准库array具有固定的大小

定义array,必须指定类型和容器大小:

1
2
3
4
array<int, 42>						//类型为:保存42个int的数组
array<string,10> //类型为:保存10个string的数组
array<int, 10>: :size_type i; //数组类型包括元素类型和大小
array<int> : :size_type j; //错误:array<int>不是一个类型

与其他容器不同点在于,默认构造的array是非空的,因为指定大小后,其中的元素都被默认初始化了。其次使用列表初始化元素数目必须小于等于容器大小,剩余部分用0填充:

1
2
3
array<int, 10> ia1;									//10个默认初始化的int
array<int,10> ia2 = {0,1,2,3,4,5,6,7,8,9}; //列表初始化
array<int,10> ia3 = {42}; // ia3[0]为42,剩余元素为О

与内置数组不同,array可以进行拷贝赋值操作:

1
2
3
4
int digs [10]= {0,1,2,3,4,5,6,7,8,9};
int cpy [10] = digs; //错误:内置数组不支持拷贝或赋值
array<int,10> digits = {0,1,2,3,4,5,6,7,8,9};
array<int,10> copy = digits; //正确:只要数组类型匹配即合法

但初始值类型,元素类型,大小都必须相同。

赋值和swap

1
2
c1 =c2;						//将c1的内容替换为c2中元素的拷贝
c1 ={ a,b,c}; //赋值后,c1大小为3

与内置数组不同,array类型允许赋值,但对象类型需要相等:

1
2
3
4
array<int,10> al = {0,1,2,3,4,5,6,7,8,9};
array<int,10> a2 = { 0 }; //所有元素值均为0
al = a2; //替换a1中的元素
a2 = {0}; //错误:不能将一个花括号列表赋予数组

c1=c2 将c1中的元素替换为c2中元素的拷贝。c1和c2必须具有相同的类型
c={a,b,c…} 将cl中元素替换为初始化列表中元素的拷贝(array不适用)
swap(c1,c2) 交换c1和 c2中的元素。c1和 c2必须具有相同的类型。swap通常
c1.swap (c2) 比从c2向c1拷贝元素快得多
assign 操作不适用于关联容器和array
seq.assign (b,e) 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素
seq.assign (il) 将seq中的元素替换为初始化列表i1中的元素
seq.assign (n,t) 将seq中的元素替换为n个值为t的元素

注意:赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)。

assign

在顺序容器(除array外)中可以assign成员赋值:

1
2
3
4
5
list<string> names;
vector<const char*>oldstyle;
names = oldstyle; //错误:容器类型不匹配
//正确:可以将const char*转换为string
names.assign (oldstyle.cbegin(), oldstyle.cend() );

由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器。

第二种用法接受整型值和一个元素值:

1
2
3
4
//等价于slist1.clear ();
//后跟slist1.insert(slist1.begin (),10,"Hiya ! ");
list<string> slist1 (1); // 1个元素,为空string
slist1.assign (10,"Hiya ! "); // 10个元素,每个都是“Hiya !”

swap

1
2
3
vector<string> svec1 (10); 		// 10个元素的vector
vector<string> svec2 (24); // 24个元素的vector
swap (svec1, svec2 );

除arraay,swap操作不对任何元素进行拷贝、删除、和插入,所以可以在常熟时间内完成,它只是交换了容器内部的数据结构(人话就是交换了头指针)。所以迭代器、引用、指针并不会失效,且所指向的元素也不会变,原本指向s1[1]的迭代器与s2交换后会指向s2[1]。但string调用swap会使迭代器、引用、指针失效。

array容器中使用swap会真正交换元素,所以时间会取决于元素数量,此外指针、引用、迭代器绑定的s1[1]现在还使s1[1],

容器大小操作

与大小相关的操作,size、empty、和max_size:返回容器所能容纳的最大元素数目。

关系运算符

除了无需关联容器外都支持关系运算符,但容器中的元素类型必须一样。比较容器实际上是逐对比较:

  • 如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。
  • 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
  • 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
1
2
3
4
5
6
7
8
vector<int> v1 = { 1357,912 };
vector<int> v2 = { 1,39 };
vector<int> v3 = { 1,35,7 };
vector<int> v4 = { 1,357912 };
vl < v2 // true; v1和v2在元素[2]处不同: v1[2]小于等于v2[2]
vl < v3 // false;所有元素都相等,但v3中元素数目更少
vl == v4 // true;每个元素都相等,且v1和v4大小相同
vl == v2 // false; v2元素数目比v1少

容器的关系运算符使用元素的关系运算符

容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符。如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。例如,我们在第7章中定义sales_data类型并未定义==-和<运算。

顺序容器操作

接下来介绍顺序容器的特有操作:

向顺序容器添加元素

image.png

当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。在一个vector或string 的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。

使用push_back

1
2
3
//从标准输入读取数据,将每个单词放到容器末尾string word;
while (cin >> word)
container.push back (word);

tip:当我们用对象初始化、或插入到容器时,实际上会先拷贝这个对象生成临时对象,再初始化或插入它。

在特定的位置添加元素

insert成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。vector.deque、list和string都支持insert成员。forward_list提供了特殊版本的insert成员。

slist.insert(iter,"Hello! ");//将"Hello ! "添加到iter之前的位置

有些容器(如vector)不支持push_front但可以使用insert来插入到开始的位置。

svec.insert (svec.end(), 10,"Anna" ); //将10个Anna插入到尾部

同时还有接受迭代器的版本:

1
2
3
4
5
6
vector<string> v = { "quasi","simba","frollo", "scar"};		
//将v的最后两个元素添加到slist的开始位置
slist.insert(slist.begin (), v.end () - 2, v.end() ) ;
slist.insert (slist.end(),{ "these""words","will""go", "at" , "the", "end" ) );
//运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin (), slist.begin (), slist.end () ) ;

不可以将一对指向自己的迭代器传入insert。

insert返回值

通过使用insert的返回值,可以在容器中一个特定位置反复插入元素:

1
2
3
4
list<string> lst;
auto iter = lst.begin ();
while (cin >> word)
iter = lst.insert (iter, word); //等价于调用push_front

insert返回的是第一个新加入元素的迭代器,如果不插入(即只有第一个参数),就返回传入的第一个参数。

使用emplace

新标准引入了三个新成员——emplace_front、emplace和 emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。

1
2
3
4
5
6
7
//在c的末尾构造一个sales_data对象
//使用三个参数的sales_data构造函数
c.emplace_back ( "978-0590353403",25,15.99);
//错误:没有接受三个参数的push_back 版本
c.push_back ("978-0590353403",25,15.99);
//正确:创建一个临时的Sales_data对象传递给push back
c.push_back(Sales_data("978-0590353403"25,15.99));

它与push或insert的区别在于它可以直接调用元素类型的构造函数在容器中直接创建,而push需要创建临时的对象,然后压入容器,但传入参数必须匹配。

1
2
3
4
5
// iter指向c中一个元素,其中保存了sales data元素
c.emplace_back(); //使用Sales_data的默认构造函数
c.emplace(iter,"999-999999999");//使用sales_data (string)
//使用sales_data的接受一个ISBN、一个 count和一个price的构造函数
c.emplace_front ( "978-0590353403"2515.99) ;

访问元素

包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用:

1
2
3
4
5
6
7
8
//在解引用一个迭代器或调用front或back之前检查是否有元素
if ( !c.empty()){
// val和val2是c中第一个元素值的拷贝
auto val = *c.begin () , val2 =c.front ();
// val3和val4是c中最后一个元素值的拷贝
auto last = c.end () ;
auto val3 = *(--last); //不能递减forward_ list迭代器
auto val4 = c.back (); //forward_list不支持

访问成员函数返回引用

const容器返回const引用,如果使用auto保存返回值,并希望改变元素的值,必须定义为引用类型:

auto &v = c.back();

下标操作和安全的随机访问

为保证使用下标访问不会越界,可以使用at,它会在下标越界时抛出异常:

1
2
3
vector<string> svec;					//空vector
cout << svec[0] ; //运行时错误: svec中没有元素!
cout << svec.at(0) ; //抛出一个out_ of_ range异常

删除元素

image.png

pop函数

这些操作返回void,如果需要弹出元素的值,就必须在执行弹出前保存它:

1
2
3
4
while (!ilist. empty()) {
process (ilist. front()) ; //对ilist的首元素进行一些处理
ilist.pop_ front() ; //完成处理后删除首元素.
}

特殊的forward_list

因为单向链表删除或者添加一个元素会改变前一个元素,但单向链表没办法访问前驱,所以我们可以添加或者删除给定元素之后的元素,

image.png

1
2
3
4
5
6
7
8
9
10
forward_ list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.before_ begin() ; //表示flst的“首前元素’
auto curr = flst.begin() ; //表示flst中的第一个元素
while (curr != flst.end()) { //仍有元素要处理
if(*curr % 2) //若元素为奇数
curr = flst.erase_ after (prev) ; //删除它并移动curr
else {
prev = curr; //移动迭代器curr,指向下 一个元素,prev指向
++curr ; // curr 之前的元素
}

改变容器大小

image.png

1
2
3
4
list<int> ilist(1042) ;					// 10个int:每个的值都是42
ilist. resize(15) ; //将5个值为0的元素添加到ilist的末尾.
ilist. resize(25-1) ; //将10个值为-1的元素添加到ilist的末尾
ilist. resize(5) ; //从ilist末尾删除20个元素

容器操作可能使迭代器失效

由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vector、string和deque尤为重要。

编写改变容器的循环程序

添加/删除vector、string 或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert或erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:

1
2
3
4
5
6
7
8
9
10
11
//傻瓜循环,删除偶数元素,复制每个奇数元素
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
auto iter = vi.begin(); //调用begin而不是cbegin,因为我们要改变vi
while (iter != vi.end()) {
if (*iter % 2) {
iter = vi.insert(iter, *iter); //复制当前元素
iter += 2; //向前移动迭代器,跳过当前元素以及插入到它之前的元素
} else
iter = vi.erase (iter) ; //删除偶数元素
//不应向前移动迭代器,iter 指向我们删除的元素之后的元素
}

不要保存end返回的迭代器

因为插入删除操作会使end迭代器失效。

vector对象如何增长

vector和string在添加元素时如果大小其容量,会开辟一块比预定容量更大的地方。

管理容量的成员函数

image.png

reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间,不过只有当需要的内存超过当前容量时,才会改变容量。一旦调用,会至少分配与需求一样大或更大的空间。

capacity和size

image.png

每个 vector实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。

额外的string操作

构造string的其他方法

image.png

1
2
3
4
5
6
7
8
9
10
const char *cp = "Hello world!!! ";		//以空字符结束的数组
char noNull[]={'H’, 'i'}; //不是以空字符结束
string s1(cp); // 拷贝cp中的字符直到遇到空字符;s1 == "Helloworld!!!"
string s2(noNull,2); //从noNull拷贝两个字符;s2 == "Hi"
string s3 (noNull); //未定义:noNull不是以空字符结束
string s4(cp + 65); //从cp [ 6]开始拷贝5个字符;s4 =="world"
string s5(s1,65) ; //从s1 [ 6]开始拷贝5个字符;s5 == "world"
string s6(s1,6); //从s1[ 6]开始拷贝,直至s1末尾;s6== "world! ! !"
string s7(s1,6,20); //正确,只拷贝到s1末尾;s7 == "world! ! ! "
string s8(s1,16); //抛出一个out_of_range异常

substr操作

1
2
3
4
5
string s("hello world");
string s2 = s.substr (0,5); // s2 = hello
string s3 = s.substr (6); // s3 = world
string s4 = s.substr (6,11); // s3 = world
string s5 = s.substr (12); //抛出一个out_of_range异常

返回一个string,包含s中从pos 开始的n个字符的拷贝。pos的默认值为0。n的默认值为s.size () - pos,即拷贝从pos开始的所有字符,开始位置超过大小,会抛出一个异常,结束位置超出大小,会默认拷贝string的末尾。

改变string的其他方法

除了接受迭代器的insert和 erase版本外,string还提供了接受下标的版本。下标指出了开始删除的位置,或是insert到给定值之前的位置:

1
2
s.insert(s.size(), 5'!');				//在s末尾插入5个感叹号
s.erase(s.size()- 55); //从s删除最后5个字符

标准库string类型还提供了接受C风格字符数组的insert和 assign版本。例如,我们可以将以空字符结尾的字符数组insert 到或assign给一个string:

1
2
3
const char *cp = "stately, plump Buck";
s.assign(cp,7); // s == "Stately"
s.insert (s.size(), cp + 7); // s == "Stately, plump Buck"

我们也可以指定将来自其他string或子字符串的字符插入到当前string中或赋予当前string:

1
2
3
4
string s = "some string", s2 = "some other string";
s.insert (0, s2); //在s中位置0之前插入s2的拷贝
//在s [0]之前插入s2中s2[0]开始的s2.size()个字符
s.insert(0,s2,0, s2.size ());

append和replace函数

append操作是在string末尾进行插入操作的一种简写形式:

1
2
3
string s ("C++ Primer"),s2 = s;				//将s 和s2初始化为"C++ Primer"
s.insert (s.size()," 4th Ed."); //s == "C++ Primer 4th Ed."
s2.append(" 4th Ed." ); //等价方法:将”4th Ed."追加到s2;s == s2

replace操作是调用erase和insert的一种简写形式:

1
2
3
4
5
//将"4th"替换为"5th"的等价方法
s.erase (113); // s == "C++ Primer Ed . "
s.insert (11"5th" ) ; // s == "C++ Primer 5th Ed . "
//从位置11开始,删除3个字符并插入"5th"
s2.replace(11,3,"5th"); //等价方法:s == s2

此例中调用replace时,插入的文本恰好与删除的文本一样长。这不是必须的,可以插入一个更长或更短的string:

1
s.replace(11,3"Fifth");					// s == "C++ Primer Fifth Ed."

image.png

image.png

改变string的多种重载函数

assign和 append函数无须指定要替换string中哪个部分: assign总是替换string中的所有内容,append总是将新字符追加到string末尾。
replace函数提供了两种指定删除元素范围的方式。可以通过一个位置和一个长度来指定范围,也可以通过一个迭代器范围来指定。insert函数允许我们用两种方式指定插入点:用一个下标或一个迭代器。在两种情况下,新元素都会插入到给定下标(或迭代器)之前的位置。
可以用好几种方式来指定要添加到string 中的字符。新字符可以来自于另一个string,来自于一个字符指针(指向的字符数组),来自于一个花括号包围的字符列表,或者是一个字符和一个计数值。当字符来自于一个string或一个字符指针时,我们可以传递一个额外的参数来控制是拷贝部分还是全部字符。

string搜索操作

搜索操作:成功返回一个无符号的string::size_type值,失败返回npos(cont string::size_type)的static成员(他们应该是同一种类型,但变量名字不同),因此不建议使用int储存。

1
2
3
4
string name ("AnnaBelle");
auto posl = name.find ( "Anna"); // pos1 ==o
string lowercase ( "annabelle" );
pos1 = lowercase.find ( "Anna"); // posl ==npos

find操作对大小写有区分A与a会区分开来

image.png

image.png

1
2
3
4
5
6
string numbers ( "0123456789"), name ( "r2d2") ;
//返回1,即,name中第一个数字的下标
auto pos = name . find_first_of (numbers) ;
string dept ( "03714p3");
//返回5——字符'p'的下标
auto pos = dept.find_ first_not_of (numbers) ;

指定可选位置

find函数第二个参数为从哪里开始,默认为0,使用它可以在字符串中循环搜索子字符串的所有位置:

1
2
3
4
5
6
string : : size_type pos = 0;
//每步循环查找name中下一个数
while ((pos = name.find_first_of (numbers, pos)) != string::npos) {
cout <<"found number at index : " << pos <<" element is " <<name [pos] << endl;
++pos; //移动到下一个字符
}

逆向搜索

1
2
3
string river ( "Mississippi" );
auto first_pos = river.find ( "is"); //返回1
auto last_pos - river.rfind ( "is"); //返回4

compare函数

string类型提供的与C标准库 的strcmp函数相似的函数,根据s等于、大小、小于返回0、整数、负数。

image.png

数值转换

一些函数以可实现string与数值的转换:

1
2
3
int i = 42;
string s = to_string(i); //将整数i转换为字符表示形式
double d = stod (s) ; //将字符串s转换为浮点数

要转换为数值的string中第一个非空白符必须是数值中可能出现的字符:string s2 = “pi = 3.14” ;

1
2
//转换s中以数字开始的第一个子串,结果d = 3.14
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));

这里将字符串种第一个出现数字的地方的子串传入函数,直到遇到不可能是数值的字符,然后将其转换。参数中第一个非空白符号必须是+、0、数字或是是小数点,且可以包含e、E、x、X等其他进制需要的字母。

如果string不能转换为一个数值,这些函数抛出一个invalid_argument异常(参见5.6节,第173页)。如果转换得到的数值无法用任何类型来表示,则抛出一个 out_of range异常。

image.png

容器适配器

本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如,stack适配器接受一个顺序容器(除array或forward_list外),并使其操作起来像一个stack一样。

image.png

定义一个适配器

首先我们可以用一个容器初始化另一个容器:

1
2
deque<int> deq = {1,2,3,4};
stack<int> stk (deq); //从deq考贝元素到stk

我们还可以尖括号内重载构造函数,就可以创建一个适配器:

1
2
3
4
//在vector上实现的空栈
stack<string, vector<string>> str_stk;
//str_stk2在vector上实现,初始化时保存svec的拷贝
stack<string, vector<string>> str_stk2 (svec) ;

所有适配器都要求有添加和删除以及访问尾元素能力,所以不能用array、forward_list构造。

栈适配器

image.png

栈默认基于deque实现,所以可以省略第二个参数:

1
2
3
4
5
6
7
8
9
stack<int> intstack; //空栈
//填满栈
for (size_t ix = 0; ix != 10; ++ix)
intStack.push (ix); // intStack保存0到9十个数
while ( !intstack.empty()) { // intStack中有值就继续循环
int value = intStack.top ( ) ;
//使用栈顶值的代码
intStack.pop() ; //弹出栈顶元素,继续循环
}

虽然是基于deque实现,但我们不能使用底层容器的操作,所以只可以用push,而不可以使用push_back。

队列适配器

image.png

image.png

priority_queue 允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。默认情况下,标准库在元素类型上使用<运算符来确定相对优先级。后面会学习如何重载这个默认设置。

因为网上很多叫教程都是缺一点东西,导致只能自己去看报错解决,花了一些时间,所以为了大家不继续踩坑,我将全部的过程都写了下来,如果还是不能成功欢迎在评论区留下你的问题。

下载环境

下载下面两个软件,安装时直接一直跳过就好

语句

简单语句

空语句

当某个地方语法上需要,逻辑上不需要,则会使用,且应该加注释标明。

1
2
3
//重复读入数据直至到达文件末尾或某次输入的值等于sought
while (cin >> s &&s != sought)
; //空语句
阅读全文 »

函数

函数基础

包括返回类型、函数名字、0到多个形参组成的列表及函数体。

调用运算符来执行函数。函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。

形参和实参

形参和实参数量类型顺序必须一一对应。

阅读全文 »

写在前面:由于我的失误操作,导致第七章被第六章的内容覆盖,怀着悲痛的心情准备写第二遍,不过也应该可以比第一遍写的更好,在此提醒大家,不要随意切换文件并点击系统弹出来的保存,并即使做好备份。这是一个十分悲痛的教训,望大家注意。

类的基本思想是数据抽象(data abstraction)封装(encapsulation)。数据抽象是一种依赖于接口(interface)和**实现(implementation)**分离的编程(以及设计〉技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

定义抽象数据类型

设计

sales_data的接口应该包含以下操作:

  • 一个isbn 成员函数,用于返回对象的ISBN编号
  • 一个combine成员函数,用于将一个sales_data对象加到另一个对象上
  • 一个名为add 的函数,执行两个sales data对象的加法
  • 一个read函数,将数据从istream读入到sales_data对象中。
  • 一个print函数,将sales data对象的值输出到ostream

使用改进的Sales_data类

在考虑如何实现我们的类之前,首先来看看应该如何使用上面这些接口函数。举个例子,我们使用这些函数编写1.6节(第21页)书店程序的另外一个版本,其中不再使用sales_item对象,而是使用sales_data对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sales_data total ;								//保存当前求和结果的变量
if(read (cin, total)){ //读入第一笔交易
sales_data trans; //保存下一条交易数据的变量
while(read (cin,trans)){ //读入剩余的交易
if (total .isbn ( ) == trans.isbn ( )) //检查 isbn
total.combine (trans); //更新变量total当前的值
else {
print (cout, total) << endl; //输出结果
total = trans; //处理下一本书
}
print (cout, total) << endl ; //输出最后一条交易
}
else { //没有输入任何信息
cerr <<"No data?! " << endl ; //通知用户
}

定义改进的Sales_data类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sales_data {
//新成员:关于sales_data对象的操作
std: :string isbn ( ) const { return bookNo;}
Sales_data& combine (const sales_data&);
double avg_price ( ) const;
//数据成员和2.6.1节(第64页)相比没有改变
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// sales_data的非成员接口函数
sales_data add (const Sales_data&,const Sales_data&) ;
std::ostream &print(std: :ostream&,const sales_data&) ;
std::istream &read(std: :istream&,Sales_data&) ;

定义成员函数

所有成员必须声明在类内部,但是成员函数体可以在类外定义。

在isbn函数中是如何bookNo所依赖的对象的呢?

引入this

调用为:total .isbn ()这样的调用实际上是隐式地指向调用该函数的对象成员,这里返回的就是total.bookNo。

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this,如

total .isbn ()则编译器负责把total的地址传递给isbn的隐式形参this,可以等价地认为编译器将该调用重写成了如下的形式://伪代码,用于说明调用成员函数的实际执行过程Sales_data : :isbn (&total)

引入const成员函数

const修饰的成员函数,实际修饰该成员函数隐藏的this指针,表明在该成员函数中不能对类的任何成员进行修改。

且const对象只能调用const函数。

1
2
3
4
5
6
7
8
9
10
11
class Data{
void Display();
void Display1()const{};
}
int main()
{
const Date d1;
d1.Display1();//d1为const成员,不可修改
Date d2;
d2.Display();//d1没有要求,可改可不改
}

类作用域和成员函数

类本身就是一个作用域,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

因为编译器是首先编译成员的声明,其次才是成员函数体。

在类的外部定义成员函数

我们可以在类的外部定义成员函数,这样做的作用可以保持类内代码看起来更加清晰简洁。

但在外部定义必须在类内提前声明,且与类外函数保持一致。不同之处在于需要加上类名:

1
2
3
4
5
6
double sales_data: :avg_price () const {
if (units_sold)
return revenue/units_sold;
else
return 0 ;
}

定义返回this对象的函数

我们可以把自己这个对象返回,如:

1
2
3
4
5
Sales_data& Sales_data : :combine (const Sales_data &rhs){
units_sold += rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue += rhs.revenue;
return *this;//返回调用该函数的对象
}

这个函数可以把自己一些数据和参数的数据相加,然后以引用的形式返回。

定义类相关的非成员函数

类的作者常常需要定义一些辅助函数,比如 add、read和 print等。尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身。

类中的输入与输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//输入的交易信息包括ISBN、售出总数和售出价格
istream &read(istream &is, Sales_ data &item)
{
double price = 0;
is >> item. bookNo >> item.units_ sold >> price;
item. revenue = price * item.units_ sold;
return is;
}
ostream &print (ostream &os,const Sales_ data &item)
{
os << item.isbn() << " " << item.units_ sold <<" "
<< item.revenue <<” ”<< item.avg_ price() ;
return OS;
}

read函数由于将流中数据读到给定对象,print函数将给定对象打印到流中。

由于与流数据有交互,所以需要将IO类的引用作为参数。

构造函数

每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来,控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

合成的默认构造函数

当没有定义任何构造函数时,创建对象则会执行合成的默认的构造函数:

●如果存在类内的初始值(参见2.6.1节,第64页),用它来初始化成员。
●否则,默认初始化(参见2.2.1节,第40页)该成员。

但合成的默认构造知识和简单的类,复杂的类容易出错,所以尽量自己去定义默认构造函数。

定义的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Sales_ data {
//新增的构造函数
Sales_ data() = default;
Sales_ data(const std: :string &s) : bookNo(s) {}
Sales_ data (const std: :string &s,unsigned n, double p) :
bookNo(s),units_ sold(n), revenue (p*n) { }
Sales_ data(std: :istream &) ;
//之前已有的其他成员
std: :string isbn() const { return bookNo; }
Sales_ data& combine (const Sales_ data&) ;
double avg_ price() const;
std: :string bookNo;
unsigned units_ sold = 0;
double revenue = 0.0;
};

默认构造函数

Sales_ data() = default;这是一个默认的构造函数,他的作用和合成的默认构造函数一样。

上面的默认构造函数之所以对Sales data有效,是因为我们为内置类型的数据成员提供了初始值。如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表(马上就会介绍)来初始化类的每个成员。

构造函数初始值

1
2
3
Sales_ data(const std: :string &s) : bookNo(s) { }
Sales_ data (const std: :string &s,unsigned n, double p) :
bookNo(s),units_ sold(n), revenue (p*n) { }

使用初始值列表为一个或几个数据成员赋值,且构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。如果你不能使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。

在类外定义构造函数

1
2
3
4
5
Sales_ data::Sales_ data(std::istream &is)
{
read(is,*this); // read 函数的作用是从is中读取一条交易信息然后
//存入this对象中
}

首先它没有返回类型,且必须指定类名,由于这里的初始值列表为空,所以初始化任务交给函数体,没没有被构造函数赋值的成员将执行默认初始化。如string为空string,int为0。

函数read的第二个形参为该对象的引用。

拷贝、赋值和析构

一般来说编译器会默认的合成拷贝、赋值和析构,例如赋值:

1
2
3
4
5
6
total = trans; //处理下一本书的信息.
// 它的行为与下面的代码相同
// Sales_ data的默认赋值操作等价于:
total .bookNo = trans . bookNo;
total.units_ sold = trans.units sold;
total. revenue = trans. revenue;

不可依赖合成版本

编译器默认生成的函数常常会出现一些问题,所以后面会了解到如何自定义这些函数。

访问控制与封装

可以使用访问说明符加强类的封装性:

  • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。

  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了( 即隐藏了)类的实现细节。

说明符数量不限,且作用域到下一个访问说明符为止。出于统一编程风格的考虑,当我们希望定义的类的所有成员是public的时,使用struct; 反之,如果希望成员是private的,使用class。

友元

若要想某些函数可以访问类内私有成员,我们可以将他声明为友元,只需要增加一条以friend关键字开始的函数声明语句即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Sales_data {
//为sales_data的非成员函数所做的友元声明
friend sales_data add (const sales_data&,const sales_data&) ;
friend std: :istream &read(std: :istream&,sales_data&) ;
friend std: :ostream &print(std: :ostream&,const sales_data&) ;
//其他成员及访问说明符与之前一致
public:
sales_data() = default;
sales_data (const std: :string &s, unsigned n,double p):
bookNo (s) , units_sold(n) , revenue (p*n){ }
sales_data (const std: :string &s): bookNo(s){ }
sales_data(std: :istream&) ;
std: :string isbn ( ) const { return bookNo; }
sales_data &combine (const sales_data&) ;
private:
std: :string bookNo ;
unsigned units_sold = 0;double revenue = 0.0;
} ;
// sales_data接口的非成员组成部分的声明
sales_data add (const sales_data&, const sales_data&);
std: :istream &read(std: :istream&, sales_data& );
std::ostream &print(std: :ostream&,const Sales_data& );

友元必须在类内声明,最好写在开头和劫为的尾置。

封装的好处

  • 确保用户代码不会无意间破坏封装对象的状态。
  • 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。

友元的声明

友元声明相当于给这个函数开通了权限,函数还是需要声明和定义。

类的其他特性

类成员再探

1
2
3
4
5
6
7
class screen {
public:
typedef std: :string : :size_type pos;
private:
pos cursor = 0;
pos height = 0, width = 0 ;std: : string contents ;
};

定义一个窗口类,其中使用typedef来重命名,其作用等同于:

1
2
3
4
5
6
class Screen {
public:
//使用类型别名等价地声明一个类型名字
using pos = std::string::size_type;
//其他成员与之前的版本一致
};

Screen类成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class screen {
public:
typedef std::string::size_type pos;
Screen() = default; //因为Screen有另一个构造函数,
//所以本函数是必需的
//cursor被其类内初始值初始化为0
screen(pos ht, pos wd,char c) : height (ht), width (wd),contents (ht * wd, c) {}
char get () const //读取光标处的字符
{ return contents [cursor] ; } //隐式内联
inline char get (pos ht, pos wd)const ; //显式内联
screen &move(pos r, pos c); //能在之后被设为内联
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};

这里第二个构造函数只接受了三个参数,所以另一个成员采用类内初始值的方式初始化。

令成员作为内联函数

类内的函数是固定为内联函数的,当类外函数需要作为类内成员时,可以加上inline声明成内联函数。

其可以在类内声明(不推荐,因为类内函数就是内联函数),也可以在类外声明,但最好只在类外声明。

重载成员函数

成员函数与非成员函数都可以被重载,使用时根据参数数量来决定用哪种函数。

1
2
3
screen myscreen;
char ch = myscreen. get ( ) ; //调用screen : :get ()
ch = myscreen.get (0 , 0); //调用screen: :get (pos, pos)

可变数据成员

如何希望一个变量无论什么情况都可以被改变。可以在变量声明时加入mutable关键字。即使他是const对象的成员,或通过const函数赋值,都可以被改变。

类数据成员初始值

1
2
3
4
5
6
class window_mgr {
private:
//这个window _mgr追踪的Screen
//默认情况下,一个window_mgr包含一个标准尺寸的空白Screen
std::vector<screen> screens{ screen (2480, ' ' )};
};

当初始化类类型成员,可以使用列表初始化的方式,类内初始值必须以=或者{}表示。

返回*this的成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Screen{
public:
Screen &set (char) ;
Screen &set (pos, pos, char) ;
//其他成员和之前的版本一致
};
inline Screen &Screen::set (char c)
{
contents [cursor] = C; //设置当前光标所在位置的新值
return *this; //将this对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch){
contents[r*width + col] = ch; //设置给定位置的新值
return *this; //将this对象作为左值返回
}

set函数返回值是调用set的对象的引用,可以作为左值:

1
2
3
4
5
//把光标移动到一个指定的位置,然后设置该位置的字符值
myscreen.move ( 4,0 ) .set ('#’);
//上述语句等价于
myScreen.move ( 4,0);
myscreen.set('#’);

若返回值不是引用,则:

1
2
3
//如果move返回Screen而非screen&
screen temp = myscreen . move ( 4 ,o) ; //对返回值进行烤贝
temp.set ('#’); //不会改变myscreen的 contents

const成员函数返回*this

若为前面的类定义一个display操作,因为打印不需要改变类中的成员,所以令display为一个const成员,所以*this是一个const对象。返回值是一个const对象的引用,所以:

1
2
3
screen myScreen;
//如果display返回常量引用,则调用set将引发错误
myScreen.display (cout).set ('*');

基于const的重载

一个函数可以重载为const和非const,分别用在常量对象,和非常量对象的调用:

1
2
3
4
5
6
7
8
9
10
class screen {
public:
//根据对象是否是const重载了display函数
screen &display(std::ostream &os){ do_display(os) ; return *this; }
const screen &display(std::ostream &os) const{ do_display (os) ; return *this; }
private:
//该函数负责显示Screen的内容
void do_display(std: :ostream &os)const {os <c contents; }
//其他成员与之前的版本一致
};

其中do_display是一个公共代码,他的好处为:

  • 一个基本的愿望是避免在多处使用同样的代码。
  • 我们预期随着类的规模发展,display函数有可能变得更加复杂。
  • 我们很可能在开发过程中给do_display函数添加某些调试信息,而这些信息将在代码的最终产品版本中去掉。显然,只在 do_display一处添加或删除这些信息要更容易一些。
  • 这个额外的函数调用不会增加任何开销。因为我们在类内部定义了do_display,所以它隐式地被声明成内联函数。

类类型

每一个类都是唯一的,即使他们的成员完全一样,所以他们也不可以互相赋值。

类的声明

类的声明可以只声明不定义,也被称为向前声明,在未定义前他是一个不完全类型,不完全类型只能在非常有限的情景下使用:

  • 可以定义指向这种类型的指针或引用,
  • 可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。

创建它的对象之前他必须被定义过!

友元再探

类之间的友元

如果想在A类的成员函数内可以控制另一个B类的成员,可以将A类在B类中声明称友元,如:

1
2
3
4
class B{
friend class A;
//其他内容...
}

则A类的所有成员函数都可以访问B类的私有成员。

注意:友元关系没有传递性,若A中声明了友元C类,C只可以访问A,而不能访问B。

成员函数作为友元

若不需要将整个类作为友元,则可以只为一个函数声明友元,且必须明确属于哪个类。且应注意一定的顺序:

  • 定义A类,并声明其中的需要改变B类成员的函数(简称C函数吧),但不要定义。
  • 定义B类,声明友元函数C。
  • 最后定义C函数。

函数重载和友元

若一个函数名存在多个重载,则友元函数需要声明多个,且他们是一一对应的。

友元声明和作用域

友元声明不是必须在类或者函数之后,但无论如何一定要在类外声明一次。

1
2
3
4
5
6
7
8
9
struct X{
friend void f() {/*友元函数可以定义在类的内部*/}
X(){ f(); }//错误:f还没有被声明
void g() ;
void h();};
}
void x: :g() { return f(); } //错误:f还没有被声明
void f() ; //声明那个定义在中的函数
void x : :h() { return f(); } //正确:现在f的声明在作用域中了

根本还是因为友元只是开通了某个人进入这个地方的权限,而这个人需要被承认是一个人。才能使用该权限。

类的作用域

类有自己的作用域,在类外必须由对象、引用或指针使用成员。

1
2
3
4
5
screen::pos ht = 24, wd = 80 ;			//使用screen定义的pos类型
screen scr (ht, wd, ' ') ;
Screen *p = &scr;
char c = scr.get () ; //访问scr对象的get成员
c = p->get () ; //访问p所指对象的get成员

类外的成员函数,因为在类外,所以并不知道类内的成员,所以必须加上类名,包括返回值如果是类内的成员也必须加上类名。

名字查找与类的作用域

名字查找的大致过程为:

  • 首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
  • 如果没找到,继续查找外层作用域。
  • 如果最终没有找到匹配的声明,则程序报错。

对于定义在类内部的成员函数:

  • 首先,编译成员的声明。
  • 直到类全部可见后才编译函数体。

类成员声明的名字查找

声明过程中使用的名字必须在使用前确保可见,如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在类外中继续查找。

1
2
3
4
5
6
7
8
9
10
typedef double Money;
string bal;
class Account {
public:
Money balance () { return bal; }//这里的返回值为类外定义的double,
//bal在函数体内,所以整个类可见后才处理,所以这里返回的是类内的bal。
private:
Money bal;
// ...
};

类型名要特殊处理

类内可以重新为一个类型定义名字,但如果已经使用过了,就不能在定义它了:

1
2
3
4
5
6
7
8
typedef double Money;
class Account {
public:
Money balance( ) { return bal; } //使用外层作用域的Money
private:
typedef double Money; //错误:不能重新定义Money
Money bal; // ...
};

成员函数使用名字解析

  • 首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。
  • 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。
  • 如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。

如果成员函数参数名字与类成员名字冲突,那么使用类内的成员最好写成类名::的形式,更加清楚。当然,更好的办法是另起一个名字。

构造函数再探

构造函数初始值列表

定义变量时最好立即对其进行初始化,如没有初始化,则会执行默认初始化。

初始值有时必不可少

有时遇到无法默认初始化的类型、常量或者引用,则必须添加初始值。

初始化顺序

初始化顺序不是按参数的顺序,而是按照在类内声明的顺序。

建议:构造函数初始化顺序与成员声明最好一致,且不用成员去初始化成员

默认实参和构造函数

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

如果你希望用户给出一个非0实参的同时给处其他的实参,则建议不要给他形参添加默认值。例如图书管理程序,用户提供一本书的名字时,你需要他同时提供书的价格、序列号等,就不应该给形参设置默认值,这样用户就必须输入图书全部信息。

委托构造函数

1
2
3
4
5
6
7
8
9
10
11
class Sales_data {
public:
//非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt,double price):
bookNo(s), units_sold(cnt), revenue (cnt*price){ }
//其余构造函数全都委托给另一个构造函数
sales_data() : sales_data ("",0,0){}
sales_data (std::string s) : sales_data(s, 0,0){}
sales_data (std::istream &is) : sales_data(){ read (is,*this) ; }
//其他成员与之前的版本一致
};

它也有成员初始值列表和一个函数体,参数列表须与委托的构造函数匹配。最后一个构造函数委托的是默认构造函数,默认构造执行后,执行read()函数。

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。在 sales data类中,受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。

默认构造函数的作用

默认初始化在以下情况发生

  • 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
  • 当类类型的成员没有在构造函数初始值列表中显式地初始化时。

值初始化在以下情况发生

  • 在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
  • 当我们不使用初始值定义一个局部静态变量时。
  • 当我们通过书写形如T( )的表达式显式地请求值初始化时,其中T是类型名(vector的一个构造函数只接受一个实参用于说明vector大小,它就是使用一个这种形式的实参来对它的元素初始化器进行值初始化)。类必须包含一个默认构造函数以便在上述情况下使用,其中的大多数情况非常容易判断。

一个常犯的错误

对于C++的新手程序员来说有一种常犯的错误,它们试图以如下的形式声明-一
个用默认构造函数初始化的对象:

1
2
Sales_ data obj() ;				//错误:声明了一个函数而非对象
Sales_ data obj2; //正确: obj2是一个对象而非函数

隐式的类类型转换

如果构造函数接受一个实参,那么实际上也定义了隐式转换的机制,例如:A类中有一个构造函数只接受一个string类型的参数,那么在需要A类的地方,我们可以由string去代替,编译器会自动的将string转换为A。

只允许一步类类型的转换

如果直接把一个常量字符串用在A类的地方,需要先转换成string,再转换为A,所以是错误的。可以先显示的转化为string,如:string("999"),再放到需要A的地方。

这种转换取决于用户对使用它的看法,并不总是有效。

抑制构造函数隐式转换

在构造函数前加上explicit用来阻止隐式转换的发生,它只对有一个参数的函数有效:

1
2
3
4
5
6
class Sales_data{
public:
explicit Sales_data (const std::string &s) : bookNo(s) { }
explicit Sales_data (std::istream&) ;
//其他成员与之前的版本一致
};

explicit构造只用于直接初始化

1
2
3
4
string null_book = "999";
Sales_ _data item1 (null_book) ; // 正确:直接初始化
//错误:不能将explicit构造函数用于拷贝形式的初始化过程
Sales_ data item2 = null_book;

使用该关键字后不可用于拷贝。

显示转换构造函数

explicit函数会阻止隐式的转换,但是我们依然可以用该函数显示的进行转换:

1
2
3
4
//正确:实参是一个显式构造的Sales_ data对象
item. combine (Sales_ data (null_ book)) ;
//正确: static_ cast可以使用explicit的构造函数
item. combine (static_ cast<Sales_ data>(cin)) ;

标准库显式的构造函数的类:

我们用过的-.些标准库中的类含有单参数的构造函数:

  • 接受一个单参数的const char*的string构造函数不是explicit的。
  • 接受一个容量参数的vector构造函数(参见3.3.1节,第87页)是explicit的。

聚合类

满足下列条件,可以说它是一个聚合类:

  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类,也没有virtual函数。

如:

1
2
3
4
struct Data{
int ival;
string S;
};

聚合类的显示初始化方法:

1
2
// val1. ival= 0; val1.s = string ( "Anna" )
Datavall= { 0, "Anna" };

显示初始化的缺点:

  • 要求类的所有成员都是public的。
  • 将正确初始化每个对象的每个成员的重任交给了类的用户(而非类的作者)。因为用户很容易忘掉某个初始值,或者提供–个不恰当的初始值,所以这样的初始化过程冗长乏味且容易出错。
  • 添加或删除-一个成员之后,所有的初始化语句都需要更新。

字面值常量类

数据成员都是字面值类型的聚合类就是字面值常量类。不是聚合但符合下列要求也是:

  1. 数据成员都必须是字面值类型。

  2. 类必须至少含有一个constexpr构造函数。

  3. 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。

  4. 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

对于条件的理解:

满足条件1,就可以在编译阶段求值,这一点和聚合类一样。

满足条件2,就可以创建这个类的constexpr类型的对象。

满足条件3,就可以保证即使有类内初始化,也可以在编译阶段解决。

满足条件4,就可以保证析构函数没有不能预期的操作。

constexpr构造函数

构造函数不能是const的,但字面值常量类的构造函数可以是constexpr的,且必须至少有一个constexpr构造函数。

constexpr构造函数函数体一般来说是空的:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Debug {
public:
constexpr Debug (bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug (bool h,bool i, bool o) : hw(h),io(i), other(o) { }
constexpr bool any() { return hw || io || other; }
void set_ io(bool b) { io = b; }
void set_ hw(bool b) { hw = b; }
void set_ other (bool b) { hw = b; }
private:
bool hw; //硬件错误,而非IO错误
bool io; // I0错误
bool other; //其他错误
};

这样声明以后,就可以在使用constexpr表达式或者constexpr函数的地方使用字面值常量类了。

IO库

以往用到的IO库设施:

  • istream(输入流)类型,提供输入操作。

  • ostre am(输出流)类型,提供输出操作。

  • cin, 一个istream对象,从标准输入读取数据。

  • cout,一个ostream对象,向标准输出写入数据。

  • cerr,一个ostream对象,通常用于输出程序错误消息,写入到标准错误。

  • >>运算符,用来从一个istream对象读取输入数据。

  • <<运算符,用来向一个ostream对象写入输出数据。

  • getline函数,从一个给定的istream读取一行数据,存入一个给定的string对象中。

IO类

头文件 类型
iostream istream, wistream从流读取数据
ostream, wost ream向流写入数据
iostream,wiostream读写流
fstream ifstream, wifstream 从文件读取数据
ofstream, wofstream 向文件写入数据
fstream,wfstream读写文件
sstream istringstream, wistringstream 从string读取数据.
ostringstream, wostringstream 向string写入数据
stringstream, wstringstream 读写string

为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t 类型的数据。宽字符版本的类型和函数的名字以一个w开始。例如,wcin、wcout和wcerr是分别对应cin、cout和cerr的宽字符版对象。宽字符版本的类型和对象与其对应的普通char版本的类型定义在同一个头文件中。例如,头文件fstream定义了ifstream 和wifstream类型。

IO类型间的关系

设备类型和字符大小不会影响我们执行的IO操作,例如使用>>读取数据,我们不需要管是从控制台还是磁盘文件、还是string读取,同样也不需要管字符存入的是char还是wchar_t。这一点实际上是通过类的继承机制实现的。类型ifstream和istringstream都继承自istream. 因此,我们可以像使用istream对象-样来使用ifstream和istringstream对象。

IO对象无拷贝或赋值

1
2
3
4
ofstream out1, out2;
out1 = out2; //错误:不能对流对象賦值
ofstream print (ofstream) ; //错误:不能初始化ofstream参数
out2 = print (out2) ; //错误:不能拷贝流对象

由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

条件状态

一个流发生错误,后续的IO操作都会失败,所以因通过代码检查它是否处于良好状态:

1
2
3
while(cin >> word){
//如果能够进来,代表流状态良好
}

查询流的状态

上面的操作只知道有没有错误,但不知道是什么错误,所以IO库定义了一个与机器无关的iostate类型:

代码 说明
badbit 系统级错误,如不可恢复的读写错误。
failbit 期望读取的数值却读出一个字符等错误,这种问题可以修正,且流还可以使用。
到达文件结束位置会被置位。
eofbit 到达文件结束位置,其会被置位。
goodbit 值为0表示流未发生错误,如果上面任何一个被置位,则检测流状态的条件会失败

标准库还定义了一组函数来查询这些标志位的状态。操作 good在所有错误位均未置位的情况下返回true,而 bad、fail和 eof则在对应错误位被置位时返回true。此外,在 badbit被置位时,fail也会返回true。这意味着,使用good或fail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。而eof和 bad操作只能表示特定的错误。

管理条件状态

流对象的rdstate成员返回一个iostate值,对应流的当前状态。setstate操作将给定条件位置位,表示发生了对应错误。clear成员是一个重载的成员(参见6.4节,第206页):它有一个不接受参数的版本,而另一个版本接受一个iostate类型的参数。
clear不接受参数的版本清除(复位)所有错误标志位。执行clear()后,调用good会返回true。我们可以这样使用这些成员:

1
2
3
4
5
//记住cin的当前状态
auto old_state = cin.rdstate(); //记住cin的当前状态
cin.clear(); //使cin有效
process_input (cin) ; //使用cin
cin.setstate(old_state) ; //将cin置为原有状态

带参数的clear版本接受一个iostate值,表示流的新状态。为了复位单一的条件状态位,我们首先用rdstate读出当前条件状态,然后用位操作将所需位复位来生成新的状态。例如,下面的代码将failbit和 badbit复位,但保持eofbit不变://复位failbit和badbit,保持其他标志位不变cin.clear(cin.rdstate () & ~cin.failbit & ~cin.badbit);

管理输出缓冲

每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面的代码

os << "please enter a value: ";

文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。

导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多;

  • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
  • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
  • 我们可以使用操纵符如 endl(参见1.2节,第6页)来显式刷新缓冲区。
  • 在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。
  • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin和 cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新。

刷新输出缓冲区

1
2
3
cout <<"hi! " <<endl;			//输出hi和一个换行,然后刷新缓冲区
cout << "hi ! " << flush; //输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi ! " <<ends; //输出hi和一个空字符,然后刷新缓冲区

unitbuf操纵符

如果想在每次输出操作后都刷新缓冲区,我们可以使用unitbuf操纵符。它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而nounitbuf操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:

1
2
3
cout << unitbuf;					//所有输出操作后都会立即刷新缓冲区	
//任何输出都立即刷新,无缓冲
cout <<nounitbuf ; //回到正常的缓冲方式

如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。
当调试一个已经崩溃的程序时,需要确认那些你认为已经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。

关联输入和输出流

当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将cout和 cin关联在一起,因此下面语句导致cout的缓冲区被刷新。
cin >> ival;
tie可以将自己和一个输出流绑定起来,并且返回与自己关联的对象:

1
2
3
4
5
6
cin.tie ( &cout) ;						//仅仅是用来展示:标准库将cin和cout关联在一起
// old_tie指向当前关联到cin的流(如果有的话)
ostream *old_tie = cin.tie(nullptr); //cin 不再与其他流关联
//将cin 与cerr关联;这不是一个好主意,因为cin应该关联到cout
cin.tie ( &cerr); //读取cin会刷新cerr而不是cout
cin.tie(old_tie) ; //重建cin和 cout间的正常关联

在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了tie。为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream。

文件输入输出

头文件 fstream定义了三个类型来支持文件IO:ifstream从一个给定文件读取数据,ofstream向一个给定文件写入数据,以及fstream可以读写给定文件。

fstream中定义的类型还增加了一些新的成员管理与流关联的文件

代码 说明
fstream fstrm; 创建一个未绑定的文件流。fstream是头文件fstream 中定义的一个类型
fstream fstrm (s); 创建一个fstream,并打开名为s的文件。s可以是string类型,或者是
一个指向C风格字符串的指针。这些构造函数都是explicit的。默认的
文件模式mode依赖于fstream的类型
fstream fstrm
(s,mode) ;
与前一个构造函数类似,但按指定mode打开文件
fstrm.open (s) 打开名为s 的文件,并将文件与 fstrm绑定。s可以是一个string或一个指向
C风格字符串的指针。默认的文件mode依赖于fstream的类型。返回void
fstrm.close () 关闭与fstrm绑定的文件。返回void
fstrm.is_open () 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭

使用文件流对象

向读写一个文件时,需要先定义一个文件流对象,如果提供了一个文件名,则会自动调用open:

1
2
ifstream in (ifile);						//构造一个ifstream并打开给定文件
ofstream out; //输出文件流未关联到任何文件

输入流in,初始化为从文件读取数据,输出流out,未关联。

用fstream代替iostream&

根据在要求使用基类对象的地方,我们可以使用继承类型的对象来替代。所以在调用一个具有iostream的参数时,可以用fstream来调用。

成员函数open和close

基础

基本概念

一元运算符作用于一个运算对象,二元运算符作用于两个运算对象,三元对运算对象没有限制。

C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为**重载运算符( overloadedoperator)**。IO库的>>和<<运算符以及string对象、vector对象和迭代器使用的运算

左值和右值

阅读全文 »