《C++Primer》读书笔记(四)

153 阅读19分钟

第9章 顺序容器

vector // 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢
deque // 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list //双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快
foward_list // 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快。没有size操作
array //固定大小数组。支持快速随机访问。不能添加或删除元素
string //与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快
iterator //此容器类型的迭代器类型
const_iterator //可以读取元素,但不能修改元素的迭代器类型
size_type //无符号整数类型,足够保存此种容器类型最大可能容器的大小

difference_type //带符号整数类型,足够保存两个迭代器之间的距离
value_type //元素类型
reference //元素的左值类型,与value_type&含义相同
const_reference //元素的const左值类型

C c; //默认构造函数
C c1(c2); //构造c2的拷贝c1
C c(b, e); //构造c,将迭代器b和e指定的范围内的元素拷贝到c
C c(a, b, c...); 列表初始化c

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

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

//assign操作不适用于关联容器和array
seq.assign(b, e) //将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素
seq.assign(il) //将seq中的元素替换为初始化列表i1中的元素
seq.assign(n, t) //将seq中的元素替换为n个值为t的元素

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

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

向顺序容器添加元素

/*
这些操作会改变容器的大小;array不支持这些操作。
forward_list 有自己专有版本的isert和emplace;
forward_list 不支持push_back和emplace_back
vector和string不支持push_front和emplace_front
*/
c.push_back(t) //在c的尾部创建一个值为t或由args创建的元素。返回void
c.emplace_back(args) //同上
c.push_front(t) //在c的头部创建一个值为t或由args创建的元素。返回void
c.emplace_front(atfs) //同上
c.insert(p, t) //在迭代器p指向的元素之前创建一个值为t或由args创建的元素。返回指向新添加的元素的迭代器
c.emplace(p, args)//同上
c.insert(p, n, t) //在迭代器p指向的元素之前插入n个值为t的元素。返回指向新添加的第一个元素的迭代器;若n为0,则返回p
c.insert(p, b, e) //将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素。返回指向新添加的第一个元素的迭代器;若范围为空,则返回p
c.insert(p, i1) //il是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,返回p

访问元素

//at和下标操作只适用于string、vector、deque和array
//back不适用于forward_list
c.back() //返回c中尾元素的引用。若c为空,函数行未定义
c.front() //返回c中首元素的引用。若c为空,函数行为未定义
c[n] //返回c中下标未n的元素的引用。n时一个无符号整数。若n>=c.size(),则函数行为未定义
c.cat(n) //返回下标为n的元素的引用。如果下标越界,则抛出一out_of_range异常

删除元素

/*
这些操作会改变容器的大小,所以不适用于array
forward_list有特殊版本的erase
forward_list不支持pop_back;vector和string不支持pop_front
*/
c.pop_back() //删除c中尾元素。若c为空,则函数行为未定义。函数返回void
c.pop_front() //伤处c中首元素。若c为空,则函数行为未定义。函数返回void
c.erase(p) //删除迭代器p所指定的元素,返回一个指向被删除元素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器
c.clear() //删除c中的所有元素。返回void

特殊的forward_list操作

lst.before_begin() //返回指向链表首元素之前不存在的元素的迭代器,此迭代器不能解引用。cbefore_begin()返回一个const_iterator
lst.cbefore_begin()

lst.insert_after(p, t) //在迭代器p之后的位置插入元素。t是一个对象,n是数量,b和e是表示范围的一对迭代器(b和e不能指向lst内),il时一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p为尾后迭代器,则函数行为未定义
lst.insert_after(p, n, t);
lst.insert_after(p, b, e);
lst.insert_after(o, il);

emplace_after(p, args); //使用args在p指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义

lst.erase_after(p); //删除p指向的位置之后的元素,若删除从b之后直到(但不包含)e之间的元素。返回一个指向被删除元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p指向lst的尾元素或者时一个尾后迭代器,则函数行为未定义。
lst.erase_after(b, e);

改变容器大小:可以用resize来增大或缩小容器,array不支持resize。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,回将新元素添加到容器后部。

c.resize(n) //调整c的大小为n个元素。若n < c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化
c.resize(n, t) //调整c的大小为n个元素。任何新添加的元素都初始化为值t

容器操作可能使迭代器失效
在向容器添加元素后:

  • 如果容器时vector或string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍然有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。
  • 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
  • 对于list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。

当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效。

  • 对于list和forward_list,指向容器其他位置的迭代器、引用和指针仍有效
  • 对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除deque的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
  • 对于vector和string,指向被删除元素之前元素的迭代器、引用和指针扔有效。注意:当我们删除元素时,尾后迭代器总会失效。

vector对象是如何增长的

当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间。

capacity操作告诉我们容器在不扩张内存空间的情况下可以容纳多少元素。reverve操作允许我们通知容器它也应该准备保存多少个元素。

shrink_to_fit //只适用于vector、string和deque
capacity和reverve //只适用于vector和string
c.shrink_to_fit() //请将capacity()减少为与size()相同大小
c.capacity() //不重新分配内存空间的话,c可以保存多少元素
c.reserve(n) //分配至少能容纳n个元素的内存空间。只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量。

string操作

string s(cp, n) //s是cp指向的数组中前n个字符的拷贝。此数组至少应该包含n个字符
string s(s2, pos2) //s时string s2从下表pos2开始的字符的拷贝。若pos2>s2.size(),构造函数的行为未定义。
string s(s2, pos2, len2) //s是string s2下标pos2开始len2个字符的拷贝。

s.substr(pos, n) //返回一个string,包含s中从pos开始的n个字符的拷贝

s.insert(pos, args) //在pos之前插入args指定的字符。pos可以是一个下标或一个迭代器。
s.erase(pos, len) //删除从位置pos开始的len个字符。
s.assign(args) //将s中的字符替换为args指定的字符。返回一个指向s的引用
s.append(args) //将args追加到s。返回一个指向s的引用
s.replace(range, args) //删除s中范围range内的字符,替换为args指定的字符。range或者是一个下标和一个长度,或者是一对指向s的迭代器。

//搜索操作返回指定字符出现的下标,如果未找到则返回npos
s.find(args) //查找s中args第一次出现的位置
s.rfind(args) //查找s中args最后一次出现的位置
s.find_first_of(args) //在s中查找args中任何一个字符第一次出现的位置。
s.find_last_of(args) //在s中查找args中任何一个字符最后一次出现的位置
s.find_first_not_of(args) //在s中查找第一个不在args中的字符
s.find_last_not_of(args) //在s中查找最后一个不在args中的字符
//args是以下形式之一
c, pos //从s中位置pos开始查找字符c,pos默认为0
s2,pos //从s中位置pos开始查找字符串s2.pos默认为0

第10章 泛型算法

头文件include <alogrithm>

通过accumulatet来将vector中所有string元素连接起来

string sum = accumulate(v.cbegin(), v.cend(), string(""));
string sum = accumulate(v.cbegin(), v.cend(), 0);

算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。

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

back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:

vector<int>vec;
auto it = back_inserter(vec); //通过它赋值会将元素添加到vec中
*it = 42;

常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用

vector<int>vec;
fill_n(back_inserter(vec), 10, 0);

拷贝算法

int a1[] = {0,1,2,3,4,5,6,7,8,9};
int a2[sizeof(a1)/sizeof(*a)];
auto ret = copy(begin(a1),end(a1),a2); //把a1的内容拷贝给a2

replace(ilst.begin(), ilst.end(),0, 42); //将所有值设为42

消除重复

unique(words.begin(), words.end());

lambda表达式

一个lambda表达式表示一个可调用的代码但愿。我们将其理解一个未命名的内联函数。

lambda具有一个返回诶行、一个参数列表和一个函数体。lambda可能定义在函数内部

[capture list](parameter list) -> return type { function body }

其中capture list是一个lambda所在函数中定义的局部变量列表。

auto f = [] { return 42; }
[](const string &a, const string &b) { return a.size() < b.size(); }

值捕获:与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝

void fcn1() {
    size_t v1 = 42;
    auto f = [v1] { return v1; };
    v1 = 0;
    auto j = f();
}

引用捕获。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。

void fcn2() {
    size_t v1 = 42;
    auto f2 = [&v1] { return v1; };
    v1 = 0;
    auto j = f2();
}

隐式捕获。为了指示编译器推断捕获列表,应在捕获列表中写一个&或=。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。

wc = find_if(words,begin(), words.end(),[=](const string &s) { return s.size() >= sz});

可变lambda
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable.因此,可变lambda能省略参数列表:

void fcn3() {
    size_t v1 = 42;
    auto f = [v1] () mutable { return ++v1; }
    v1 = 0;
    auto j = f2();// j 为1
}
``

# 关联容器
```cpp
map //关联数组:保存关键值-值对
set //关键字即值,即只保存关键字的容器
mutimap //关键字可重复出现的map
multiset //关键字可重复出现的set

unordered_map 用哈希函数组织的map
unorderd_set 用哈希函数组织的set
unordered_multimap 哈希组织的map:关键字可以重复出现
unordered_multiset 哈希组织的set:关键字可以重复出现

pair类型 定义在头文件utility中

pair<string, string> anon; //保存两个string
pair<string, size_t> word_count; 
pair<string, vector<int>> line;

关联容器操作

key_type 此容器类型的关键字类型
mapper_type 每个关键字关联的类型;只适用于map
value_type 对于set,与key_type相同

插入元素

c.insert(v) //v是value_type类型的对象;args用来构造一个元素
c.emplace(args) //对于map和set,只有当元素的关键字不在c中时才插入元素。
c.insert(b, e) //b和e是迭代器
c.insert(il) //il是值的花括号列表

删除元素

c.erase(k) //从c中删除每个关键字为k的元素.返回一个size_type值,指出删除的元素的数量
c.erase(p) //从c中删除迭代器p指定的元素。返回一个指向p之后元素的迭代器
c.erase(b,e) //删除迭代器对b和e所表示的范围中的元素,返回e

访问元素

/*
lower_bound和upper_bound 不适用于无序容器
下标和at操作只适用于非const的map和unordered_map
*/
c.find(k) //返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器
c.count(k) //返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是01.
c.lower_bound(k) 返回一个迭代器,指向第一个关键字不小于k的元素
c.upper_bound(k) 返回一个迭代器,指向第一个关键字大于k的元素
c.equal_range(k) 返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair的两个成员均等于c.end()

第12章 动态内存

我们的程序目前为止只使用过静态内存或栈内存。静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。

栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或。程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。

在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针。

动态内存很容易出现问题,有时会忘记释放内存。

为了更容易使用动态内存,标准库提供了两种智能指针类型来管理动态对象。

  • share_ptr允许多个指针指向同一个对象;
  • unique_ptr 则“独占”所指向的对象
  • weak_ptr 一种若引用,指向shared_ptr所管理的对象。

share_ptr类

shared_ptr<string>p1; //shared_ptr, 可以指向string
shared_ptr<list<int>> p2; //shared_ptr, 可以指向int的list

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

shared_ptrunique_ptr都支持的操作

shared_ptr<T> sp //空智能指针,可以指向类型为T的对象
unique_ptr<T> up //空智能指针,可以指向类型为T的对象
p //将p用作一个条件判断,若p指向一个对象,则为true
*p //解引用p,获得它指向的对象
p->mem //等价于(*p).mem
p.get() //返回p中保存的指针。要小心使用,若指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q) //交换p和q中的指针
p.swap(q)
make_shaered<T>(args) //返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr<T>p(q) //p是shared_ptr的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
shared_ptr<int>p3 = make_shared<int>(42);

p = q //p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。
p.unique() //若p.use_count()为1,返回true;否则返回false
p.use_count() //返回与p共享对象的智能指针数量;可能很慢,主要用于调试

直接管理内存

c++语言定义了两个运算符来分配和释放内存。运算符new分配内存,delete释放new分配的内存。


int *pi = new int;
int *pi = new int();
string *ps = new string(10, '9');
auto p1 = new auto(obj);

const int *pci = new const int(1024);

为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。通过delete表达式来将动态内存归还给系统。delete接受一个指针,指向我们想要释放的对象:

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

shared_ptr和new结合使用

如果不初始化一个智能指针,它就会被初始化为一个空指针。

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

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

shared_ptr<int>p1 = new int(1024); //错误:必须使用直接初始化形式
shared_ptr<int>p2(new int(1024)); //正确:使用了直接初始化形式

shared_ptr还定义了其他一些操作,我么你可以用reset来将一个新的指针赋予了一个shared_ptr:

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

与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

if (!p.unique()){
    p.reset(new string(*p));
}
*p += newVal;

unique_ptr

一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。

unique_ptr<T> u1; //空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> u2;

unique_ptr<T, D> u(d) //空unique_ptr,指向类型为T的对象,用类型为D的对象代替delete
u = nullptr //释放u指向的对象,将u置为空
u.release() //u放弃对指针的控制权,返回指针,并将u置为空
u.reset() //释放u指向的对象
u.reset(q) //如果提供了内置指针q,令u指向这个对象;否则u置为空
u.reset(nullptr)

虽然我们不能拷贝或赋值unique_ptr,但是可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

unique_ptr<string> p2(p1.release()); //release将p1置为空
p2.reset(p3.release()); //reset释放了p2原来指向的内存

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr:

unique_ptr<int>clone)(int p) {
    return unique_ptr<int>(neq int(p));
}

向unique_ptr传递删除器

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

weak_ptr

weak_ptr是一种不控制所指对象生存期的智能指针,它指向由一个shared_ptr管理的对象。

将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就被释放。


weak_ptr<T>W
weak_ptr<T>w(sp)

w = p //p可以是一个shared_ptr或一个weak_ptr。赋值后w与p共享对象
w.reset() //将w置空
w.use_count() //与w共享对象的shared_ptr的书香
w.expired() //若w.use_count为0.返回true,否则返回false
w.loc //如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

当创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p = make_shared<int>(42);
weak_ptr<int>wp(p);

new和数组

int *pia = new int[10];
int *pia2 = new int[10]();
int *pia3 = new int[10]{0,1,2,3,4,5,6};

释放动态数组

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

标准库提供了一个可以管理new分配的数组的unique_ptr

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

当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。可以用下标访问数组中元素

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

allocator类

内存分配和构造对象分离

allocator提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

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

a.deallocate(p, n) //释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须时一个先前由allocate返回的指针,且n必须是p创建时所要求的大小。
while(q != p) {
    alloc.destory(--q);
}

拷贝填充

auto p = alloc.allocate(vi.size() * 2); //分配比vi中元素所占用空间大一倍的动态内存
auto q = uninitialized_copy(vi.begin(), vi.end(), p); //通过拷贝vi中的元素来构造从p开始的元素

uninitialized_fill_n(q, vi.size(), 42);//将剩余的元素初始化为42;