第八章
IO类
头文件 | 类型 |
---|---|
iostream | istream, wistream |
ostream, wostream | |
iostream,wiostream | |
fstream | ifstream,w… 从文件读取数据 |
ofstream,w… | |
fstream | |
sstream | istringstream,w… 从string读取 |
ostringstream,w… | |
stringstream,w… |
w开头的是为了支持宽字符语言。
IO对象无拷贝无赋值
进行IO操作的函数通常以引用的方式传递和返回流,并且传递和返回值不能是const的。
条件状态
第九章
顺序容器概述
deque | 双端队列 |
---|---|
list | 双向链表 |
forward_list | 单向链表 |
array | 固定大小数组 |
- 如果容器元素为一个类且该类是一个没有默认构造函数的类型,那么我们需要提供元素的初始化器。
顺序容器中的差别
- array不能执行插入\删除操作
- forward_list没有size成员函数
当不需要写访问时,应该使用cbegin和cend。
容器定义和初始化
标准库array具有固定大小
array不支持普通容器的构造函数,它的构造函数需要确定容器的大小,要么隐式或显式。
array<int,10>;
同时,使用array的类型,也需要指定大小
array<int,10>::size_type i;
array的初始化和普通数组一样,如果初始化元素个数不足则后续补默认初始化,如果超出容器大小则出错。
如果元素类型是一个类,那么其必须要有一个默认构造函数,以便值初始化能够进行。
值得注意的是:不能对内置数组类型进行拷贝或对象赋值操作,但是array可以,但元素类型和容器大小必须一致。
array<int,10>a1{1,2,3};
array<int,10>a2=a1;
同时,array不支持花括号的赋值操作(注意,不是初始化,而是赋值操作)
a2 = {1,2,3};//错误,array不支持花括号赋值操作。
赋值和swap
assign函数,是一个替换元素的操作。其不适用于关联容器和array。
seq.assign(b,e) | 将容器seq的元素替换成迭代器b和e之间的元素 |
---|---|
seq.assign(il) | 将容器seq的元素替换成 初始化列表 il 中的元素 |
seq.assign(n,t) | 将seq中的元素替换为n个t的元素 |
assign允许我们从一个不同但相容的类型赋值
list<string> names;
vector<char *>old;
names=old;//错误,类型不匹配
names.assign(old.begin(),old.end());//正确
swap
与其它容器不同,对一个string调用swap会导致迭代器,引用,指针失效。
顺序容器操作
类似emplace_back的emplace 前缀的操作是 构造一个新元素插入的操作,与push_back不同,后者是拷贝再插入的操作,所以前者可能还需要提供构造函数的参数。
- vector和string不支持在头部push_front,pop_front的操作
- forward_list有自己的操作,不要和vector等的混用(后面会提)
insert操作
c.insert(p,t) | 在p指向元素之前创建一个值为t的元素 |
---|---|
c.insert(p,n,t) | 不需要多解释吧 |
c.insert(p,b,e) | 在p指向元素之前插入迭代器b和e之间的元素 |
c.insert(p,il) | 在p指向元素之前插入花括号列表中il中的元素 |
注意:insert的插入都是插入到迭代器指定的元素的前一个位置。返回值为指向第一个新加入元素的迭代器。
如果传递给insert一对迭代器,那么迭代器不能指向与目标位置相同的容器
使用emplace
emplace_front,emplace,emplace_back分别对应push_front,insert,push_back操作。
只是emplace是将参数传递给构造函数,然后在容器管理的空间中直接构造元素。而push等是将元素类型的对象传递給它们。
c.emplace_back("2474989489",25,15.99);//调用了构造函数创建了一个对象
c.push_back("2474989489",25,15.99);//错误,没有接受三个参数的push_back版本
访问元素
包括array在内每个顺序容器都要一个front成员函数,除了forward_list没有back其它都有。
访问成员函数返回的是引用
front,back,下标和at返回的都是引用,
如果我们希望用auto来保存并改变元素的值,必须将变量定义为引用类型。
安全的下标操作
如果我们希望确保下标是合法的,可以使用at函数,如果下标越界,其会抛出异常。
删除元素
forward_list有特殊版本的erase,forward_list不支持pop_back
此处只说明erase
c.erase§ | 删除p指定的元素,返回一个被删元素之后的元素的迭代器 |
---|---|
c.erase(b,e) | 删除迭代器之间的元素,返回指向最后一个被删元素的后一个元素的迭代器 |
特殊的forward_list操作
forward_list的插入 insert的操作和删除的操作,都是对当前迭代器的后一个元素进行的。
lst.before_begin() | 返回指向链表首元素之前不存在的元素的迭代器 |
---|---|
lst.isnert_after(p,t) | 在迭代器p之后进行的操作,返回的都是最后插入元素的下一个位置 |
lst.isnert_after(p,n,t) | |
lst.isnert_after(p,b,e) | |
lst.isnert_after(p,il) | |
emplace_after§ | |
lst.erase_after§ | |
lst.erase_after(b,e) | 删除b(不包括b)之后,到e之前的元素,返回最后一个删除元素的后一个位置 |
不要保存end返回的迭代器
当我们添加/删除元素vector或string的元素后,原来end返回的迭代器会失效,需要时重新获取是一个好的操作。
第十章
lambda表达式
一个lambda表达式表示一个可以调用的代码单元,我们可以将其理解成一个未命名的内联函数。
但与函数不同,一个lambda可以定义在函数内部。
lambda的一般形式:
[ 捕获列表](参数列表)->返回类型 {函数体};
其中:
- 捕获列表是一个lambda所在函数中定义的局部变量的列表
与普通函数不同:lambda必须使用尾置返回来指定返回类型
auto f = [](int i) -> int {return 42;};
lambda的调用方式和普通函数一样,都是用调用运算符。
f();
在lambda中忽略括号和参数列表等价于指定一个空参数列表。
如果lambda函数体包含了其它任何一条 return语句以外的内容,且 未指定 返回类型,则返回void。这种情况下,理所当然的,如果return了值,那么就会出错。
lambda 不能有默认参数。
空捕获列表表示此lambda不使用它所在函数中的任何局部变量。
使用捕获列表
在[] 中写入局部变量名字,lambda将会捕获它。一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,其才能在函数体中使用该变量。
注意:捕获列表只用于局部非 static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字。
lambda的值捕获和引用捕获
lambda对变量的捕获分为值捕获和引用捕获。
值捕获
相当于只是拷贝了变量的值,但是,与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
int i = 42;
auto f = [i]{return i;};
i=0;
cout<<f()<<endl;//将会输出42
可变lambda
默认情况下,对于一个值拷贝的变量,lambda不会改变其值,如果我们希望改变被捕获变量的值,就必须在参数列表前面加上 mutable
int i =42;
auto f = [i] matable(){return i++;};
cout<<f()<<endl;//输出43,注意这里是指改变lambda拷贝的变量,这并不是值捕获,故
cout<<i<<endl;//输出42;
引用捕获
在捕获的变量前面加上&表示引用捕获,引用捕获就相当于函数中的引用,在lambda中保存的是引用。
int i = 42;
auto f = [&i]{return i;};
i=0;
cout<<f()<<endl;//将会输出0;
引用捕获和返回引用有着相同的问题和限制,我们必须保证被引用的对象在lambda执行时是存在的。
隐式捕获
除了显式列出我们希望使用的来自所在函数的变量外,还可以让编译器自己推断。为了指示编译器推断,我们需要在捕获列表中写一个 =或&。=告诉编译器采用值捕获,&告诉编译器使用引用捕获。
wc=find_if(w.begin(),w.end(), [=](const string &s){return s.size()>sz;});
我们还可以混用隐式捕获和显式捕获,但需注意,捕获列表中的第一个元素必须是一个 = 或 &。
for_each(w.begin(),w.end(),[=,&os](const string &s){os<<s<<c;});
显式捕获的类型必须和隐式捕获不同。
参数绑定
现在假设在一个函数中,有一个局部变量sz。然后,我们需要用到一个算法 find_if,接受的是一个一元谓词,find_if需要的谓词中的判断,需要用到一个参数s,和sz。我们自然可以用lambda表达式的捕获,来获取sz,故只需要传入一个参数s。但是,如果把lambda表达式替换成函数,那么就避免不了需要传入两个参数,这显然是个二元谓词,与find_if需要的一元谓词不一致。
还可以这样理解:算法find_if给lambda传了s,sz是lambda自己捕获的。如果换成函数,find_if给函数传了s(find_if本身只有资源s),函数自己没有能力去获取sz。
我们可以使用bind来解决这个问题。
bind函数
调用bind函数的一般形式为:
auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表。对应给定的callable的参数。即我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可能包含形如 _n的名字,其中n是一个整数,这些参数是占位符,它们占据了传递给newCallable的参数的位置,并按照在arg_list中的位置传递给callable。
_1为newCallable的第一个参数,_2为第二个。
解决前面提到的问题
bool check_size(const string &s,string::size_type size);//定义暂且忽略
...
int sz=6;
...
auto wc = alg(w.begin(),w.end(),bind(check_size,_1,sz));
这样就可以解决问题了。
使用placeholders名字
名字_n都定义在一个名为 placeholders的命名空间中,而这个命名空间本身定义在std命名空间中他,为了分别使用
_n,我们可以使用using
using std::placeholders::_1;
或者各部分方便的用法
using namespace std;
using namespace placeholders;
或
using namespace std::placeholders;
bind的参数
我们可以用bind修正参数的值
auto g = bind(f,a,b,_2,_1);
g(s1,s2);//实际上调用了下面这个
f(f,a,b,s2,s1);
用bind重排顺序
auto g = bind(f,_3,_2,_1);
绑定引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用的参数中。但是,与lambda类似,有时候我们希望以引用的方式传递,或者要绑定的参数的类型无法拷贝(比如ostream)
这个时候,我们不能直接使用bind来代替,ostream的捕获。
如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数。
ostream& print(ostream &os,const string &s,char c)
{
return os<<s<<c;
}
for_each(word.begin(),word.end(),bind(print,ref(os),_1,' '));
标准库中还有一个cref函数,生成一个保存 const 引用的类。
第十一章
第十二章 动态内存
头文件
动态内存与智能指针
shared_ptr和unique_ptr都支持的操作
shared_ptrsp | 空智能指针,指向类型T的对象 |
---|---|
unique_ptrsp | |
p.get() | 返回p中保存的指针 |
swap(p,q) | 交换指针 |
p.swap(q) | 交换指针 |
shared_ptr类
shared_ptr特有的操作
make_shared(args) | 返回一个shared_ptr |
---|---|
shared_ptrp(q) | q是shared_ptr,p将拷贝其,并且递增q中的计数 |
p=q | p和q都是shared_ptr |
p.unique | 若p.use_count()为1,返回真,否则返回假 |
p.use_count | 返回智能指针p所指向对象的智能指针数量;可能很慢,主要用于调试 |
make_shared函数
最安全的分配和使用动态内存的方法。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
make_shared<T>()
shared_ptr<int> sp = make_shared<int>(42);
类似顺序容器的emplace成员,make_shared函数用其参数来构造给定类型的对象。如果我们不传递任何参数,对象就会进行值初始化。
shared_ptr<string> sp = make_shared<string>(10,'c');
我们通常用auto定义一个对象来保存make_shared的结果:
auto sp = make_shared<string>(10,'c');
对于一块内存,shared_ptr保证只要有任何shared_ptr对象引用它,它就不会被释放掉。
如果你将shared_ptr存放于一个容器中,而后不需要全部容器,而只需要其中一部分,要记得erase删除不需要的那些元素。
直接管理内存
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值是未定义的。
int *pi = new int;
string *ps = new string;
也可以对动态内存进行值初始化,只需要在类型名之后跟一对空括号即可
int *pi = new int();
string *ps = new string();
如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断类型。但是,以外编译器要用初始化器类型来推断分配的类型,只有当括号中仅有单一初始化器时才可以使用auto
auto it = new auto(obj);//正确
auto it = new auto{a,b,c};//错误,括号中只能有单个初始化器
动态分配的cosnet对象
const int * p = new const int();
内存耗尽
释放动态内存
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。
释放一块非new分配的内存,或者将相同指针值多次释放,行为都是未定义的。
shared_ptr和动态内存结合使用
接受指针参数的智能指针构造函数是explicit的,(阻止隐式转换),因此我们不能将一个内置指针隐式转换为智能指针,必须适用直接初始化。
直接初始化:不用 = 的初始化都是直接初始化。
shared_ptr<int>p1 = new int(42);//错误,这不是直接初始化
shared_ptr<int>p1(new int (42));//正确,直接初始化
出于同样的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针。
我们必须将shared_ptr显式绑定到一个想要返回的指针上:
shared_ptr<int> clone(int p)
{
return shared_ptr<int>(new int(p));
}
定义和改变shared_ptr的方法
shared_ptrp (q) | p管理内置指针q或智能指针q所指对象 |
---|---|
shared_ptrp(q,d) | p接管了内置指针q指向对象的所有权。并且使用d来代替p的delete |
shared_ptrp(p2,d) | p是shared_ptr p2的拷贝,并且用d来代替delete |
p.reset() | 实质就是改变p的指向。如果p是唯一一个指向对象的shared_ptr,那么 |
p.reset(q) | 自然就释放对象了。d和前面一样,使用d来替代delete |
p.reset(q,d) |
不要混合使用普通指针和智能指针
看下面这种情况:
void func(shared_ptr<int>p);
int *x(new int());
func(shared_ptr<int>(x));
*x=10;//错误,在上一步中,函数退出后智能指针的计数将变成0,内存释放。
也不要用get初始化另一个智能指针或为智能指针赋值
智能指针定义了一个get的函数,用来返一个内置指针。
看这种情况
shared_ptr<int>p(new int(42));
int *q=p.get();
{
shared_ptr<int>(q);//注意,虽然此处的智能指针和p指向的是同一块内存,但是各种智能指针的计数是1
}//所以当这里退出后,随着该代码块中的智能指针的销毁,内存将会释放。
int foo=*p;//错误
get用来将指针的访问权限传递给代码,只有确定代码不会delete指针的情况下,才能使用get。
智能指针陷阱
- 不使用相同的内置指针初始化(或reset)多个智能指针;
- 不delete get()返回的指针
- 不使用get()返回的指针初始化或reset另一个智能指针
- 如果使用了get返回的指针,当最后一个对应的智能指针销毁后,你的指针就无效了。
- 如果你使用的智能指针管理的资源不是new分配的资源,记住传递给它一个删除器
区分下面例子的区别
//p和q的计数器都是2
shared_ptr<int>p(new int(42));
shared_ptr<int>p(q);
//p和q的计数器都是1
int *x(new int(42));
shared_ptr<int>p(x);
shared_ptr<int>q(x);
unique_ptr
某一时刻只能有一个unique_ptr指向一个对象。当其销毁时,其指向的对象也销毁。
与shared_ptr类似,unique_ptr只能使用直接初始化形式
unique_ptr<T> p1;//可以指向一个int的unique_ptr
unique_ptr<int>p2(new int(42));
由于只允许一个unique_ptr指向它的对象,所以unique_ptr不支持普通的拷贝或赋值操作
unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1);//错误,不支持拷贝操作
unique_ptr<string> p3;
p3=p1;//错误,不支持赋值操作。
虽然我们不能拷贝或赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique_ptr。
unique_ptr<T,D>u | 空unique_ptr,u将会使用D的可调用对象来释放它的指针 |
---|---|
unique_ptr<T,D>u | |
unique_ptr<T,D>u(d) | 空unique_ptr,用类型为D的对象 d 来代替delete |
u=nullptr | 释放u指向的对象,将u置为空 |
u.release() | u放弃对指针的控制权,返回指针,并将u置空。 |
u.reset() | |
u.reset(q) | 先释放u指向的对象,如果提供了内置指针q,令u指向这个对象;否则把u置空 |
u.reset(nullptr) |
关于release的理解:其返回一个指针,把u置空,注意,把u置空和释放u所指对象,是两回事。这里把u置空,u所指对象还未被释放。
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr
unique_ptr<int>clone(int p)
{
return unique_ptr<int>(new int(p));
}
//还可以返回一个局部对象的拷贝
unique_ptr<int>clone(int p)
{
unique_ptr<int>clone(new int(p));
return clone;
}
向unique_ptr传递删除器
例子写法
unique_ptr<connextion.deletype(end_connextion)*>p(&c,end_connection);
weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr上不会影响改变shared_ptr的引用计数。一旦最后一个shared_ptr被销毁,其指向的对象就会销毁。
weak_ptrw | 空weak_ptr,可以指向类型为T的对象 |
---|---|
weak_ptr(sp) | 与shared_ptr指向相同的对象 |
w=p | p可以是一个shared_ptr或weak_ptr |
w.reset() | 将w置为空 |
w.use_count() | 与w共享对象的shared_ptr的个数 |
w.expired() | 若w.use_count为空,则返回true,否则返回false |
w.lock() | 若expired为true,即没有shared_ptr,则返回一个空shared_ptr,否者返回一个指向w的对象的shared_ptr |
使用的例子
auto p=make_shared<int>(42);
weak_ptr<int>w(p);
if(shared_ptr<int>np = w.lock())
{
}
动态数组
new和数组
为了让new分配一个对象数组,在类型名后面加上方括号,并且指明分配的对象的数目。
int *p = new int[10];
typedef int arrT[10];
int *p = new arr;
分配一个数组会得到一个元素类型的指针
如上述代码一样,用指针来接收。
虽然我们称为动态数组,但分配的内存不是一个数组类型,不能使用begin,end和for来进行处理。
动态数组的初始化
和普通数组的初始化差不多。
int *p = new int[10]();
string *sp= new string[10]();
虽然我们用空括号对数组中元素进行值初始化,但是不能在括号中提供初始化器,这也意味着不能使用auto分配数组。
动态分配一个空数组是合法的
释放动态数组
在指针前加上一个空括号对:
delete []p;
typedef int arrT[10];
int *p= new arr;
delete []p;//方括号是必须的。
智能指针和动态数组(重点
标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用unique_ptr管理动态数组,我们必须在对象类型后面跟一个方括号。
unique_ptr<int[]>u(new int[10])
当unique_ptr销毁它管理的指针时,会自动使用delete []。注意:shared_ptr是没有这个特征的。
当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟其指向的是数组不是对象。另一方面,可以使用下标运算符来访问数组中的元素。
与unique_ptr不同,shared_ptr不支持直接管理动态数组,如果希望用shared_ptr管理,必须提供自己的删除器:
shared_ptr<int>sp(new int[10],[](int *p){delete []p;});
sp.reset();//使用我们提供的lambda释放的数组,它使用delete[];
如果未定义删除器,这段代码将是未定义的。同时,shared_ptr未定义下标运算符。
allocator类
allocatora | 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存 |
---|---|
a.allocate(n) | 分配一段原始的,未构造的内存,保存n个类型为T的对象,返回指向首地址的指针 |
a.deallocate(p,n) | 释放从T*指针p中地址开始的内存,这段内存保存了n个类型为T的对象,p必须 是一个先前由allocate返回的指针,且n必须是p创建时要求的大小。 在调用deallocate之前,必须对每个在这块内存中创建的对象调用destroy |
a.construct(p,args) | p必须是一个类型为T*的指针,指向一块原始内存。args被传递给类型T的构造函数 用来在o指向的内存中构造一个对象 |
a.destroy§ | p为T*类型的指针,对p指向的对象执行析构函数。 |
allocator分配为构造的内存
allocator分配的内存是未构造的。还未构造对象的情况下就使用原始内存是错误的。
我们传递给deallocate的指针不能为空,必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须与调用allocate分配内存时提供的大小参数具有一样的值。
拷贝和填充未初始化内存的算法
uninitialized_copy(b,e,b2) | 从迭代器b和e之间的元素拷贝到迭代器b2未指定的原始内存中 |
---|---|
uninitialized_copy_n(b,n,b2) | 从b开始的n个元素拷贝到b2开始的内存中 |
uninitialized_fill(b,e,t) | 在迭代器b和e之间的原始内存中创建对象,对象均为t的拷贝 |
uninitialized_fill_n(b,n,t) | 在迭代器b开始的原始内存中构建n个对象 |