C++ Primer - 第二部分 C++标准库

C++ Primer - 第二部分 C++标准库

第8章 IO库

IO类

iostream定义了用于读写流的基本类型

fstream定义了读写命名文件的类型

sstream定义了读写内存 string对象的类型

用法都是完全相同的,得益于继承机制

流的状态:

auto old_state = cin.rdstate(); // 获取流的当前状态
cin.clear(); // 将所有条件状态复位,将流的状态置为有效
cin.setstate(old_state); // 根据给定的标志位对流进行复位

输出缓冲:每个输出流都管理一个缓冲区,保存程序读写的数据。

控制输出缓冲:

cout << "hi!" << endl; // 多输出一个换行符,然后刷新缓冲区
cout << "hi!" << flush; // 输出后直接刷新缓冲区
cout << "hi!" << ends; // 多输出一个空字符,然后刷新缓冲区
cout << unitbuf; // 所有输出操作后都立即刷新缓冲区
cout << nounitbuf; // 回到正常的缓冲方式

关联输入和输出流:如果某一个输入流和输出流关联,则从输入流读取的操作会对这个输出流进行刷新。

标准库将 coutcin关联在一起

cin.tie(&cerr); // 将cin和cerr关联在一起

文件输入输出

ifstream in("infile");
ofstream output("outfile");
string s;
while(getline(in,s)){
    output << s << endl;
}
return 0;

显式打开或者关闭文件流:

ofstream output; // 空文件流对象
output.open("outfile"); // 调用open进行关联
output.close(); // 关闭文件流

文件模式:

ofstream output("outfile");

这种方式其实隐含了以输出模式打开文件并进行截断,显式控制如下:

ofstream output("outfile",ofstream::out | ofstream::trunc);

为了保留之前的文件内容,需要显式指定 app模式

ofstream output("outfile",ofstream::out | ofstream::app);

string流

ifstream in("infile");
ofstream output("outfile",ofstream::out | ofstream::app);
string s;
while(getline(in,s)){
    string a,b,c;
    istringstream s1(s);
    ostringstream s2;
    s1 >> a;
    s2 << a;
    s1 >> b;
    s2 << b;
    s1 >> c;
    s2 << c;
    cout << s2.str() << endl;
}
return 0;

第9章 顺序容器

一个容器就是一些特定类型对象的集合,顺序容器为程序员提供了控制元素存储和访问顺序的能力。

顺序容器种类

vector是可变大小数组,支持快速随机访问。但是在尾部之外的位置插入或者删除元素可能很慢。

deque是双端队列,支持快速随机访问,在头尾部插入或者删除元素的速度很快。

list是双向链表,只支持双向顺序访问,在 list中任意位置进行插入/删除的速度都很快

forward_list是单向链表,只支持单向顺序访问,在链表中任意位置进行插入/删除的速度都很快

array是固定大小的数组,支持快速随机访问,不能添加或者删除元素

string是与 vector相似的容器,专门用于保存字符,随机访问快,在尾部插入/删除的速度很快

顺序容器几乎可以保存任意类型的元素

各种迭代器:

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

元素的拷贝初始化:

list<string> a = {"Milton","SHakespeare","Austen"};
list<string> a2(a);

array具有固定的大小,并且可以进行拷贝

array<int,10> ia1 = {0,1,2,3,4,5,6,7,8,9};
array<int,10> ia2 = ia1;

使用 assign对不同但相容的类型进行赋值

list<string> names;
vector<const char*> oldstyle;
names.assign(oldstyle.cbegin(),oldstyle.cend());

添加元素三种方法:push_front()insert()push_back()

新标准对应了三种直接构造元素的方法:emplace_front()emplace()emplace_back()

更安全的访问元素的方法:svec.at(0)

改变容器大小并使用某个元素填充更大的部分:ilist.resize(15,-1)

管理容量的成员函数:

c.capacity(); // 不重新分配内存空间的话最多能保存多少元素
c.reserve(n); // 分配至少能容纳n个元素的内存空间
c.shrink_to_fit() // 请求将capacity()减小为size()一样的大小

数值转换:

int i = 42;
string s = to_string(i);
double d = stod(s);
cout << s << " " << d << endl;
42 42

第10章 泛型算法

对于容器的其他操作,并没有通过定义成员函数的方式实现,而是定义一套泛型算法,实现了一些算法的公共接口。

在容器中对值进行查找使用 find,返回查找元素的指针的位置

auto result = find(vec.cbegin(),vec.cend(),val)

返回元素在容器中出现的次数:

vector<int> a;
int temp;
for(int i=0;i<10;i++){
    cin >> temp;
    a.push_back(temp);
}
cin >> temp;
auto result = count(a.cbegin(),a.cend(),temp);
cout << result << endl;

泛型算法本身不会执行容器的操作,只会运行于迭代器之上,执行迭代器的操作。

因此泛型算法永远不会改变底层容器的大小。

各种泛型算法

元素求和:

vector<int> a{1,2,3,4,5,6,7,8};
int sum = accumulate(a.cbegin(),a.cend(),0);
cout << sum << endl;
36

可以推广到字符串中用来连接字符串:

vector<int> a{1,2,3,4,5,6,7,8};
vector<string> b{"df","fsfds","rte"};
string sum = accumulate(b.cbegin(),b.cend(),string(""));
cout << sum << endl;
dffsfdsrte

确定两个序列中保存的值是否相同(假定第二个序列至少与第一个序列一样长)

vector<int> a{1,2,3,4,5,6,7,8};
vector<string> b{"df","fsfds","rte"};
vector<string> c{"df","fsfds","rte","fdsf"};
auto sum = equal(b.cbegin(),b.cend(),c.cbegin());
cout << sum << endl;
1

使用 fillfill_n填充元素:

vector<int> a{1,2,3,4,5,6,7,8};
fill(a.begin(),a.end(),0);
for(auto i : a){
    cout << i << " ";
}
cout << endl;
fill_n(a.begin(),a.size(),1);
for(auto i : a){
    cout << i << " ";
}
cout << endl;
0 0 0 0 0 0 0 0 
1 1 1 1 1 1 1 1

算法是不会检查写操作的,泛型算法也不能更改容器的大小。因此需要自行检查容器是否越界等问题。

安全的方式:插入迭代器

vector<int> a{1,2,3,4,5,6,7,8};
vector<string> b{"df","fsfds","rte"};
vector<string> c{"df","fsfds","rte","fdsf"};
fill_n(back_inserter(a),10,1);
for(auto i : a){
    cout << i << " ";
}
cout << endl;
1 2 3 4 5 6 7 8 1 1 1 1 1 1 1 1 1 1

a1的内容拷贝到 a2copy(begin(a1),end(a1),a2)

对元素进行排序去重:

vector<string> d{"the","quick","red","fox","jumps","over","the","slow","red","turtle"};
sort(d.begin(),d.end()); // 排序
auto end_unique = unique(d.begin(),d.end()); // 将重复的移到末尾,并同时返回最后一个不重复的元素的后一位置
d.erase(end_unique,d.end()); // 使用容器操作删除重复的元素(因为泛型算法无法改变容器大小)
for(auto i : d){
    cout << i << " ";
}
cout << endl;
fox jumps over quick red slow the turtle

lambda表达式:

一个 lambda表达式表示一个可调用的代码单元,可以理解为一个未命名的内联函数。

auto f = []{return 42;};
cout << f() << endl;
42
int a = 0, b = 1;
auto f6 = [&a, &b]{ return a + b; };
cout << f6() << endl;
1

lambda表达式还有其他的一些用法。

其他迭代器

插入迭代器:

back_inserter:创建一个使用 push_back的迭代器

front_inserter:创建一个使用 push_front的迭代器

inserter:创建一个使用 insert的迭代器

list<int> lst = {1,2,3,4};
list<int> lst2 = {5,6};
list<int> lst3 = {9,10,11};
list<int> lst4 = {12};
copy(lst.cbegin(),lst.cend(),front_inserter(lst2));
for(auto i : lst2){
    cout << i << " ";
}
cout << endl;
copy(lst.cbegin(),lst.cend(),inserter(lst3,lst3.begin()));
for(auto i : lst3){
    cout << i << " ";
}
cout << endl;
copy(lst.cbegin(),lst.cend(),back_inserter(lst4));
for(auto i : lst4){
    cout << i << " ";
}
cout << endl;
4 3 2 1 5 6 
1 2 3 4 9 10 11 
12 1 2 3 4

流迭代器:

istream_iterator<int> in(cin),eof;
cout << accumulate(in,eof,0) << endl;
> 2 1 4 5 6 7 8 9
42
ostream_iterator<int> out(cout," ");
for(int i=0;i<10;i++){
    out = i;
}
cout << endl;
0 1 2 3 4 5 6 7 8 9

反向迭代器:

vector<int> vi{1,2,3,4,5,6,7,8,9,10};
for(auto i = vi.crbegin();i != vi.crend();i++){
    cout << *i << " ";
}
cout << endl;
10 9 8 7 6 5 4 3 2 1

要注意反向迭代器真的是反的。。。比如下面的例子:

string line = "first,middle,end";
auto rcomma = find(line.crbegin(),line.crend(),',');
cout << string(line.crbegin(),rcomma) << endl;
cout << string(rcomma.base(),line.cend()) << endl;
dne
end

第11章 关联容器

关联容器中的元素是按关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。

map是关键字-值对的结合

map<string,size_t> word_count;
string word;
while(cin >> word){
    ++word_count[word];
}
for(const auto &w : word_count){
    cout << w.first << " " << w.second << endl;
}
> a b c d e d b c
a 1
b 2
c 2
d 2
e 1

关联容器的元素都是根据关键字存储的,因此不支持位置相关的操作。

multimapmultiset允许相同关键字:

vector<int> vi{1,2,3,4,5,5,4,3,2,1};
set<int> iset(vi.cbegin(),vi.cend());
multiset<int> miset(vi.cbegin(),vi.cend());
cout << iset.size() << " " << miset.size() << endl;

关联容器的迭代器:

vector<int> vi{1,2,3,4,5,5,4,3,2,1};
set<int> iset(vi.cbegin(),vi.cend());
for(auto set_it = iset.cbegin();set_it != iset.cend();set_it++){
    cout << *set_it << " ";
}
cout << endl;
1 2 3 4 5

插入元素:

iset.insert(8);

查找元素的下标操作:

c[k]; // 如果没有会添加,并对值进行初始化
c.at(k); // 如果没有会抛出异常

访问元素:findcount

multimap中查找元素:

multimap<string,int> mi{make_pair("as",1),make_pair("as",2),make_pair("ab",2),make_pair("ac",2),make_pair("ac",5)};
for(auto pos = mi.equal_range("as");pos.first != pos.second;++pos.first){
    cout << pos.first->second << " ";
}
cout << endl;

根据转换规则对文件内容进行转换:

转换规则:

brb be right back
k okay?
y why
r are
u you
pic picture
thk thanks!
l8r later

文件内容:

where r u
y dont u send me a pic 
k thk l8r

转换代码:

// 实际的转换工作,生成转换文本
const string & transform(const string &s, const map<string,string> &m){
    auto map_it = m.find(s);
    if (map_it != m.cend()){
        return map_it->second;
    }
    else{
        return s;
    }
}


// 读入给定文件,建立转换映射
map<string,string> buildMap(ifstream &map_file){
    map<string,string> trans_map;
    string key,value;
    // 读取第一个单词存入key,剩余内容存入value
    while(map_file >> key && getline(map_file,value)){
        if(value.size() > 1){
            trans_map[key] = value.substr(1);
        }
        else{
            throw runtime_error("No rule for " + key);
        }
    }
    return trans_map;
}

int main(void){
    ifstream map_file("rules");
    ifstream input("text");
    auto trans_map = buildMap(map_file); // 保存转换规则
    string text; // 保存输入中的每一行
    while(getline(input,text)){
        istringstream stream(text); // 读取每个单词
        string word;
        bool firstword = true; // 控制是否打印空格
        while(stream >> word){
            if(firstword){
                firstword = false;
            }
            else{
                cout << " ";
            }
            cout << transform(word,trans_map);
        }
        cout << endl;
    }
    return 0;
}

输出:

where are you
why dont you send me a picture
okay? thanks! later

无序容器:不适用比较运算符来组织元素,而是使用哈希函数组织元素。

一般情况下的性能要比有序容器更好,但是不能按照顺序输出。

第12章 动态内存

动态内存与智能指针

前面都是静态对象,由程序自动分配内存并销毁。而动态对象需要被显式进行释放。

动态内存需要显式进行分配和释放,因此很容易忘记释放导致一些问题。因此定义了两种智能指针来管理这些动态对象,自动进行释放。

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

默认初始化的智能指针中保存着一个空指针。

最安全的分配和使用动态内存的方式是调用 make_shared的标准库函数。

shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<string> p4 = make_shared<string>(10,'9');
shared_ptr<int> p5 = make_shared<int>();

shared_ptr会自动记录有多少个其他 shared_ptr指向相同的对象,如果没有了,会自动销毁所管理的对象并自动释放相关联的内存。

离开作用域也会被销毁。如果返回这个指针,也不会被销毁(就是挺智能的)

直接管理内存:使用 newdelete

int *pi = new int;
string *ps = new string(10,'9');
const string *pcs = new const string;
delete pi;
delete ps;
delete pcs;

delete不会抛出任何异常,尽管可能已经释放过了,甚至有可能都不是指针也会释放,会造成一些问题。

delete还可能会造成空悬指针,因此这个 delete只提供了有限的保护。

不要混用智能指针和普通指针,不要使用 get初始化另一个智能指针或者赋值。

unique_ptr“拥有”它所指向的对象,某个时刻只能由一个 unique_ptr指向一个给定对象。销毁指针就一起销毁了。

weak_ptr指向一个 shared_ptr管理的对象,不会改变 shared_ptr的计数,计数为 0后会自动释放。

动态数组

使用 new分配一个 int数组:

int *p = new int[42];

实际上并没有得到一个数组类型的对象,而是得到一个数据元素类型的指针。

动态分配并初始化数组:

int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};

动态分配的数组的大小可以为0,会返回一个类似于尾后迭代器的指针。

释放动态数组:delete [] p

智能指针管理动态数组:

unique_ptr<int[]> up(new int[10]);
for(size_t i = 0;i != 10;++i){
    up[i] = i;
}
up.release();

allocator将内存分配和对象构造分离开来,提供一种类型感知的内存分配方法。

int n = 5;
allocator<string> alloc;
auto const p = alloc.allocate(n);

这个 allocator为5个 string分配了内存

在内存中构造对象:

auto q = p;
alloc.construct(q++,"hi");
cout << *p << endl;
hi

案例:文本查询程序

在一个给定文件中查询单词,最终返回单词在文件中出现的次数及其所在行的列表。


C++ Primer - 第二部分 C++标准库
https://zhangzhao219.github.io/2022/08/21/c-plus-stl/
作者
Zhang Zhao
发布于
2022年8月21日
许可协议