2 C++基础
2.4 const限定符
2.4.1 const的引用
1. 顶层const
top-level const 表示指针本身是个常量
2. 底层const
low-level const表示指针所指的对象是一个常量。
const int * p;
2.4.4 constexpr和常量表达式
C++新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。
constexpr int mf = 20;
只有当size()返回一个常量时,才是一个正确的申明语句
constexpr int sz = size()
2.5 处理类型
2.5.1 类型别名
两种方法用于定义类型别名:
1. 使用关键词typedef
typedef double wages; //wages是double的同义词
typedef wages *p; // p是double*的同义词
2. 别名声明
using SI = Sales_item; // SI是Sales_item的同义词
SI item; // 等价于 Sales_item item;
2.5.3 decltype类型指示符
decltype类型指示符:选择并返回操作符的数据类型。只得到类型,不实际计算表达式的值。
decltype(f()) sum = x ; // sum的类型就是函数f()的返回类型
2.6 自定义数据结构
2.6.3 头文件保护
1. ifndef A_H
会为了防止头文件被包含多次,都会在每个头文件中写与如下类似的代码:
// a.h
#ifndef A_H
#define A_H
//内容
#endif
2 pragma once/
微软提供了一个指令使你可以更方便的保护:
#pragma once
优点:方便
缺点:无可移植性
pragma once是告诉编译器在编译阶段只被编译器包含一次,是否有效与具体的编译有关,在跨平台时不应该使用。而头文件保护符是与语言相关的,推荐跨平台时使用。
编译器每次读到#if !define 时,如果已经定义过了则跳过,但还是要搜索整个文件,找到#endif 时退出,此时无疑增加了编译时间。而加上#pragma once一句,则可以让编译器立即退出,减少了编译的时间。
综上,一般用法为:
#if !defined ...
#define ...
#pragma once
...
#endif // !defined (...)
3 字符串、向量和数组
3.1 命名空间的using声明
头文件不应包含using声明
每个使用了该头文件的文件都会有这个声明,容易导致意外的名字冲突
3.2 string
返回string:size_type
string line;
auto len = line.size() // len的类型就是string:size_type
3.2.3 处理string对象中的字符
C++程序的头文件应该使用cname,而不应该使用name.h的形式 :::
3.3 vector
3.3.1 初始化vector
1. 列表初始化vector对象
vector<T> v5{a,b,c...};
vecrot<T> v5={a,b,c...}; // 等价于v5{a,b,c...}
如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,不能放在圆括号里
vector<string> v1{"a", "an", "the"}; // 列表初始化 OK
vector<string> v2("a", "an", "the"); // 错误
2. 值初始化
通常情况下,可以只提供vector对象容纳的元素数量,而略去初始化。此时库会创建一个值初始化(value-initialized)的值元素初值,并把它赋给容器内的所有元素,这个初值由vector对象中元素的类型决定
如果vector对象的元素是内置类型,比如int,则元素初始值自动设为0,如果元素是某种类的类型,比如string,则元素由类默认初始化
vector<int> iv(10); // 10个元素,每个都初始化为0
vector<string> sv(10); // 10个元素,每个都初始化为空string对象
3. 列表初始化还是元素数量
vector<string> v1{"hi"}; // 列表初始化,有1个元素
vector<string> v2("hi"); // 错误;不能使用字符串字面值来构建vector对象
vector<string> v3{10} // 10个默认初始化的元素
vector<string> v4{10,"hi"} // 10个值为hi的元素
v3和v4中,提供的值不能作为元素的初始值,确认无法执行列表初始化后,编译器默认会尝试用默认值初始化vector对象
3.4 迭代器
cbegin() and cend()
auto it = v.cbegin();
返回vector:: const_iterator
3.5 数组
标准库函数begin and end
begin string和vector的成员,返回指向第一个元素的迭代器。也是一个标准库函数,输入一个数组,返回指向该数组首元素的指针。
end string和vector的成员,返回一个尾后迭代器。也是一个标准库函数,输入一个数组,返回指向该数组尾元素的下一个位置的指针。
int ia[] = {1,2,3};
int *beg = begin(ia);
int *last = end(ia);
4 表达式
4.1 基础
重载运算符:为已经存在的运算符赋予了另外一层含义。
左值、右值:
当一个对象用作右值得时候,用的是对象的值(内容)。
当对象被用作左值得时候,用的是对象的身份(在内存中的位置)。
4.6 成员访问运算符
点运算符和箭头运算符
n = (*p).size();
n = p->size();
4.9 sizeof运算符
sizeof运算符返回一条表达式或一个类型名字所占的字节数,其所得值是一个size_t类型,是一个常量表达式。
sizeof (type)
sizeof expr
5 语句
5.4 范围for语句 (range for 语句)
遍历给定序列中的每个值执行某种操作
for (declaration : expression)
statement
expression部分是一个对象,用于表示一个序列(比如string、vector);declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。
每次迭代,declaration部分的那个变量,将被初始化为expression部分的下一个元素值。
Exmaple:
#include <cctype>
string str("Hello World");
for (auto &c: str)
{
cout << isalpha(c) << endl;
c = toupper(c);
}
cout << str << endl;
等价于
#include <cctype>
string str("Hello World");
for (auto beg = str.begin(),end =v.end(); beg != end; ++beg)
{
auto &c = *beg;
cout << isalpha(c) << endl;
c = toupper(c);
}
cout << str << endl;
5.6 try语句块和异常处理
C++中异常处理包括:throw表达式、try语句块。
try和catch,将一段可能抛出异常的语句序列括在花括号里构成try语句块。catch子句负责处理代码抛出的异常。
throw表达式语句,终止函数的执行。抛出一个异常,并把控制权转移到能处理该异常的最近的catch字句。
terminate函数
如果最终没有能找到任何匹配的catch子句,程序转到名为terminate的标准库函数。 该函数行为与系统有关,一般情况下,执行该函数将导致程序非正常退出
6 函数
6.2.6 含有可变形参的数组
如果函数的实参数量未知,但是全部实参的类型相同,可以使用initializer_list类型的形参。
initializer_list是一种标准库类型,用户表述某种特性类型的值的数组
void err_msg(initializer_list<string> li)
{
for(auto beg = li,begin()l beg != li.end(); +=beg)
cout << *beg<< " ";
cout << endl;
}
6.2.5 main
prog -d -o ofile data0
int main(int argc, char **argv[]) { ... }
argc = 5
argv[0] = “prog”
argv[1] = “-d”
argv[2] = “-o”
argv[3] = “ofile”
argv[4] = “data0”
argv[5] = 0
6.5.2 内联函数和constexpr
1. 内联函数
使用关键词inline来声明内联函数。
内联用于优化规模较小,流程直接,频繁调用的函数。
2. constexpr函数函数
constexpr函数是指能用于常量表达式的函数。
constexpr int new_sz() { return 43 ;} // 只能有一条return语句
constexpr int foo = new_sz()
内联函数和constexpr函数,可以多次定义,通常定义在文件头
6.7 函数指针
函数指针指向的是函数而非对象。
bool lengthCompare (const string &, const string &);
bool (*pf) (const string &, const string &);
注意*pf两端的括号不可少
使用函数指针
pf = lengthCompare
pf = &lengthCompare //等价的赋值语句: 取地址符可选
bool b1= pf("Hello", "World"); // 调用lengthCompare
bool b1= (*pf)("Hello", "World"); // 一个等价的调用
bool b1= lengthCompare("Hello", "World"); // 一个等价的调用
重载函数指针
void ff(int*)
void ff(unsigned int);
void(*pf1)(unsigned int) = ff; // pf1指向 ff(unsigned)
函数指针形参
形参可以是指向函数的指针
void useBigger (const string &s1, const string &s2, bool pf(const string &, const string &));
等价于
void useBigger (const string &s1, const string &s2, bool (*pf)(const string &, const string &));
返回指向函数的指针
using F= int(int*, int); // F是函数类型,不是指针
using PF = int(*)(int*, int);//PF 是指针类型
PF f1(int); // 正确:f1返回指向函数的指针
F f1(int); // 错误,f1不能返回一个函数
F *f1(int); // 正确:显示的指定返回类型是指向函数的指针
也可以采用如下方式直接声明f1
int(*f1(int)) (int*, int);
尾置返回类型
auto f1(int) -> int(*)(int*, int)
7.类
7.1 定义抽象数据类型
构造函数
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。
编译器创建的构造函数被称为合成的默认构造函数。
struct Sales_data {
Sales_data () = default;
}
= default表示要求编译器生成构造函数,等价于使用合成默认构造函数
7.5 构造函数再探
7.5.3 委托构造函数
使用它所述类的其他构造函数执行它自己的初始化过程。
Class Sales_data
{
public:
Sales_data(string s) : m_book(s) {};
Sales_data() : Sales_data("") {};
};
7.5.4 隐式的类类型转换
只运允许一步类类型转换
item.combine("99999"); // 错误:先把“99999”转换为string,在把临时的string转换为Sales_data
item.combine(string("99999")); // 正确
抑制构造函数定义的隐式转换explicit
在要求隐式转换的程序上下文中,可以通过将构造函数申明为explicit加以阻止.
关键字explict只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所有无需指定为explict。
只能在类内申明构造函数时使用explict关键字,类外定义是不应重复。
Class Sales_data
{
public:
Sales_data() = default;
Sales_data(string s) : m_book(s) {};
explicit Sales_data(std::istream&);
};
item.combine(cin)// 错误: istream的构造函数时explicit的
item.combine(Sales_data(cin)); // 正确:显示的使用explicit构造函数
explicit构造函数智能用于直接初始化
Sales_data item1(cin); // 正确
Sales_data item2 = (cin); // 错误:不可用于拷贝形式的初始化过程
7.5.5 聚合类(aggregateclass)
聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法
当类满足一下所有条件时,才是聚合类:
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
struct Data {
int ival;
string s;
};
class Cperson
{
public:
int id;
string name;
}
聚合类的初始化
直接使用花括号进行初始化
注意:初始化顺序必须与类内定义的顺序一致
struct Data {
int ival;
string s;
};
Data val = { 0,"Hello" }; //正确
Data val2 = {"Hello",0 }; //错误
class Cperson
{
public:
int id;
string name;
};
Cperson person={ 66,"xiaoming" };
7.5.6 字面值常量类
字面值的常量类有两种定义:
- 数据成员都是字面值类型(算术类型,引用和指针,以及字面值常量类)的聚合类是字面值常量类。
- 或者满足如下的定义:
- 数据成员都必须是字面值类型(算术类型,引用和指针,以及字面值常量类)。
- 类必须至少含有一个constexpr构造函数。
- 如果一个数据成员含有类内初始值,则内置类型的初始值必须是一条常量表达式。或者如果成员属性某种类类型,则初始值必须使用成员自己的constexpr构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
8 IO库
C++语言不直接处理输入输出,而是通过一组定义在标准库中的类型来处理IO。
- iostream处理控制台IO
- fstream处理命名文件IO
- stringstream完成内存string的IO
ifstream和istringstream继承自istream
ofstream和ostringstream继承自ostream
8.1 IO类
1. 刷新输出缓冲区endl、 flush、ends
flush刷新缓冲区,但不输出任何额外的字符;
ends向缓冲区插入一个空字符,然后刷新缓冲区。
cout << "hi" << endl;
cout << "hi" << flush;
cout << "hi" << ends;
2. unitbuf操作符
每次输出操作后,都刷新缓冲区
cout << unitbuf // 每次输出操作后,都刷新缓冲区
cout << nounitbuf // 回到正常的缓冲方式
关联输入输出流tie
cin.tie(&cout) // 关联cin和cout
ostream *old_tie = cin.tie(nullptr) // cin不再与其它流关联
9 顺序容器
前言
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。标准库还提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型匹配。
9.1 顺序容器概述
表 9.1:顺序容器类型 | |
---|---|
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素很慢 |
deque | 双端队列(double-ended queue)。支持快速随机访问。在头尾位置插入或删除元素很快 |
list | 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素 |
string | 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快 |
- 除了固定大小的array外,其他容器都提供高效、灵活的内存管理,可以添加和删除元素,扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率有着固定的,有时是重大的影响。
- string和vector将元素保存在连续的内存空间中,元素的下标来计算其地址是非常快速的,但是在两种容器的中间位置添加或删除元素非常耗时。
- list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问。与vector、deque和array相比,这两个容器的额外内存开销也很大。
- deque是一个更为复杂的数据结构,元素可以从两端弹出。与string和vector类似,deque支持快速随机访问。与string和vector一样,在两种容器的中间位置添加或删除元素的代价(可能)很高。但是在deque的两端添加或删除元素很快,与list或forward_list添加/删除元素的速度相当。
forward_list和array是新C++标准增加的类型。与内置数组相比,array更加安全、更容易使用。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能
。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。
注意:现代C++程序应该使用标准库容器,而不是更原始的数据结构。
Ⅱ)选择容器的基本原则
1)除非有很好的理由选择其他容器,否则使用vector是最好的选择。
2)如果程序有很多小元素且空间的额外开销很重要,不要使用list或forward_list。>
3)要求随机访问元素,应该使用vector或deque。>
4)要求中间插入或删除元素,应该使用list或forward_list。>
5)要求在头尾插入或删除元素,且中间不进行插入或删除,应该使用deque。>
6)如果只在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素。首先可以考虑在读取输入时使用vector,再调用sort函数重排容器中的元素,从而避免在中间位置添加元素。如果必须在中间位置插入元素,考虑在输入阶段使用list,输入完成将list拷贝到vector中。
注意:如果实在不确定使用哪种容器,可以在程序中只使用vector和list的公共操作迭代器而非下标,避免随机访问。这样可以在必要时选择使用vector或list。
9.2 容器库概览
表 9.2:容器操作
类型别名 | |
---|---|
iterator | 此容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
different_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型,与value_type&含义相同 |
const_reference | 元素的const左值类型,即constvalue_type& |
构造函数 | |
---|---|
C c; | 默认构造函数,构造空容器 |
C c1(c2); | 构造 c2 的拷贝 c1 |
C c(b, e) | 构造 c,将迭代器 b 和 e 指定的范围内的元素拷贝到 c(array不支持) |
赋值与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 |
9.2.4 容器定义和初始化
C seq(n,t)
只有顺序容器的构造函数才接受大小参数,关联容器不支持
标准库arrary具有固定大小
// 对array可以进行拷贝或对象赋值操作
大小是arrary类型的一部分
array<int, 10> digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> copy = digits;
array<int> copy2 = digits; // 错误
9.2.5 赋值和swap
统一使用非成员版本的swap是一个好习惯
swap(a,b)
9.3 顺序容器操作
9.3.1 向顺序容器添加元素
push
insert
使用emplace操作
当我们调一个emplace成员函数时,是将参数传递给元素类型的构造函数,emplace成员使用这些参数在容器管理的内存空间中直接构造元素
对应关系:
emplace -> insert
emplace_back -> push_back emplace_front ->
push_front
c.emplace_back("999", 25,15.99);
c.emplace_back();// 使用Sales_data的默认构造函数
c.emplace(iter,"999");
c.push_back(Sales_data("999", 25,15.99));// 先创建临时对象
9.4 容器大小操作管理
shrink_to_fit | 只适用于vector、string和deque;capacity和reserve只适用于vector和string |
c.shrink_to_fit() | 将capacity()减少为与size()相同大小 |
c.capacity() | 不重新分配内存空间的话,c可以保存多少元素 |
c.reserve(n) | 分配至少能容纳n个元素的内存空间。不改变容器中元素的数量,仅影响vector预先分配多大的内存空间 |
9.6 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。
priority_queue:
定义:priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆
新加入的元素会排在所有优先级比他低的已有元素之前
一般是:
//升序队列
priority_queue <int,vector<int>,greater<int> > q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
例子:
#include<iostream>
#include <queue>
using namespace std;
int main()
{
//对于基础类型 默认是大顶堆
priority_queue<int> a;
//等同于 priority_queue<int, vector<int>, less<int> > a;
priority_queue<int, vector<int>, greater<int> > c; //这样就是小顶堆
priority_queue<string> b;
for (int i = 0; i < 5; i++)
{
a.push(i);
c.push(i);
}
while (!a.empty())
{
cout << a.top() << ' ';
a.pop();
}
cout << endl;
while (!c.empty())
{
cout << c.top() << ' ';
c.pop();
}
cout << endl;
b.push("abc");
b.push("abcd");
b.push("cbd");
while (!b.empty())
{
cout << b.top() << ' ';
b.pop();
}
cout << endl;
return 0;
}
输出
4 3 2 1 0
0 1 2 3 4
cbd abcd abc
10 泛型算法
10.2 初视泛型算法
10.2.1只读算法
只会读取其输入范围内的元素,而从不改变元素。
1. find
2. accumulate
求和算法
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
// 对vec中的元素求值,和的初值是0。第三个参数的类型决定了函数中使用哪个加法运算以及返回值的类型。
string sum = accumulate(v.cbegin(), v.cend(), string("")); // 将vector中所有string元素连接
string sum = accumulate(v.cbegin(), v.cend(), ""); // 错误,传递的是字符串字面值,用于保存和的对象的类型将是const char*,没有定义+运算符。
3. equal
*** 用于确定两个序列是否保存相同的值,相同返回true,否则返回false。 ***
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
// 将第一个序列中的每个元素与第二个序列中的对应元素比较,元素类型不是必须一样,如可以是vector<string>和list<const char*>
// 接受3个迭代器,前两个接受第一个序列中的元素范围,第三个表示第二个序列的首元素。
注意:对于只读取不改变元素的算法,通常最好使用cbegin()和cend()。但是,如果使用算法返回的迭代器改变元素的值,就需要使用begin()和end()。
10.2.2 写容器元素的算法
1. fill
fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
fill(vec.begin(), vec.begin() + vec.size/2, 10); // 将容器的一个子序列设置为10
// 迭代器表示范围,第三个参数用于接受一个值赋予输入序列中的每个元素
注意:
① 序列原大小至少不小于要求算法写入的元素数目。
② 操作两个序列的算法之间的区别在于我们如何传递第二个序列。
③ 用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。
2. fill_n(dest, n ,val)
// 用fill_n将一个新值赋予vector中的元素
vector<int> ivec; // 空vector
fill_n(ivec.begin(), ivec.size(), 0); // 所有元素重置为0,调用形式:fill_n(dest, n, val);
// 注意:假定dest指向一个元素,dest开始的序列至少包含n个元素
vector<int> ivec; // 空vector
fill_n(ivec.begin(), 10, 0); // 错误!想要写入10个元素,但ivec中实际没有元素,语句未定义
3. back_inserter
插入迭代器 back_inserter:接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,我们可以通过此迭代器赋值,将一个具有给定值的元素添加到容器中。
back_inserter
定义在头文件iterator
中
#include <iterator> //
vector<int> ivec; // 空向量
auto it = back_inserter(ivec); // 将元素添加到ivec中
*it = 42; // ivec中现在有一个元素:42
我们常常使用back_inserter创建一个插入迭代器,作为算法的目的位置使用
vector<int> vec; // 空向量
fill_n(back_inserter(vec), 10, 0); // ,向vec的末尾添加了10个元素0。
4. copy
拷贝算法:将输入范围内的元素拷贝到目的序列中。
// 使用copy实现内置数组的拷贝
int a1[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int a2[sizeof(a1)/sizeof(*a1)]; // 确保a2和a1大小一样
auto ret = copy(begin(a1), end(a1), a2); // ret指向拷贝到a2的尾元素之后的位置
5. replace
// replace读入一个序列,并将其中所有等于给定值的元素都改为另一个值
replace(ilst.begin(), ilst.end(), 0, 42) // 将所有值为0的元素改为42
6. replace_copy
// ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中的值为0的元素在ivec中都变成42
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);
10.2.3 重排容器元素的算法
sort + unique + erase
void elimDups(vector<string> &words) {
sort(words.begin(), words.end());
// 按字典序排序
auto end_unique = unique(words.begin(), words.end());
// unique重排输入范围,使得每个单词只出现一次
// 将出现一次的单词排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
words.erase(end_unique, words.end());
// 删除重复元素,end_unique到容器尾部保存重复的单词。注意:即使没有重复元素也可以。
}
注意:标准库算法对迭代器而不是容器进行操作。因此,算法不能添加或删除元素。
10.5 泛型算法结构
泛型算法:与类型无关的算法
10.5.2 算法参数模型
大多数算法具有如下4种形式之一:
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表示算法所操作的输入范围
10.5.3 算法命名规范
1. 谓词
一些算法使用重载形式传递一个谓词:
unique(beg, end); // 使用==运算符比较元素
unique(beg, end, comp); // 使用comp比较元素
2. _if版本的算法:
find(beg, end, val); // 查找输入范围中val第一次出现的位置
find_if(beg, end, pred); // 查找第一个令pred为真的元素
3. _copy拷贝元素和不拷贝元素:
reverse(beg, end); // 反转输入范围中元素的顺序
reverse_copy(beg, end, dest); // 将元素逆序拷贝到dest
remove_if(v1.begin(), v1.end(), [](int){ return i % 2; }); // 从v1中删除奇数元素
remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int){ return i % 2; }); // 将偶数元素从v1拷贝到v2,v1不变
10.6 特定容器算法
注意:
对于list和forword_list,应该优先使用成员函数版本的算法,而不是通用算法,因为代价太大
。
表10.6:list和forward_list成员函数版本的算法
lst.merge(lst2) | 将来自lst2的元素合并入lst,lst和lst2必须都是有序的 |
lst.merge(lst2, comp) | 元素将从lst2中删除。在合并之后,lst2变为空。第一个版本使用<运算符;第二个版本使用给定的比较操作 |
lst.remove(val) lst.remove_if(pred) | 调用erase删除掉与给定值相等或令一元谓词为真的每个元素 |
lst.reverse( ) | 反转lst中元素的顺序 |
lst.ort( ) lst.sort(comp) | 使用<或给定比较操作排序元素 |
lst.unique( ) lst.unique(pred) | 调用erase删除同一个值的连续拷贝。第一个版本使用==;第二个版本使用给定的二元谓词。 |
链表还定义了splice算法,是链表数据结构所特有的,因此不需要通用版本。
splice可以很方便地对链表进行一些连接断开的操作。因此我们下面来详细总结并实测一下splice的几种用法。
list1.splice(iterator position, list2)
list1.splice(iterator position, list2, iterator i)
list1.splice(iterator position, list2, iterator first, iterator last)
splice有以上三种函数签名,其中
- list1表示被拼接的列表,操作以后其结构会发生改变(如果有改变的话)
- iterator position表示操作开始时list1的迭代器位置
- list2为将其元素拼接到list1的列表。
- iterator i是list2中要拼接的迭代器元素。
- iterator first, iterator last则是list2中要拼接元素的第一个与最后一个。
注意:链表特有的操作会改变容器。
11 关联容器
关联容器支持高效的关键字查找和访问。
类型 | 备注 | 头文件 |
---|---|---|
map | 关联数组,保存关键字-值对 | map |
set | 值保存关键字的容器 | set |
multimap | 关键字可重复出现的map | map |
multiset | 关键字可重复出现的set | set |
unordered_map | 用哈希函数组织的map | unordered_map |
unordered_set | 用哈希函数组织的set | unordered_set |
unordered_multimap | 哈希组织的map;关键字可以重复出现 | unordered_map |
unordered_multiset | 哈希组织的set;关键字可以重复出现 | unordered_set |
11.3.5 访问元素
c.find(k) | 返回一个迭代器,指向第一个关键字k的元素,如k不在容器中,则返回尾后迭代器 |
c.count(k) | 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1 |
c.lower_bound(k) | 返回一个迭代器,指向第一个关键字不小于k的元素;不适用于无序容器 |
c.upper_bound(k) | 返回一个迭代器,指向第一个关键字大于k的元素;不适用于无序容器 |
c.equal_bound(k) | 返回一个迭代器pair,表示关键字等于k的元素的范围。如k不存在,pair的两个成员均等于c.end() |
11.4 无序容器
如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器
无序容器使用关键字类型的==运算符和一个hash<key_type>类型的对象来组织元素。
无序容器在存储上组织为一组桶,适用一个哈希函数将元素映射到桶。
自定义hash模板
还可以自定义自己的hash模板
需要提供函数来代替 ==运算符和哈希值函数计算函数
using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;
SD_multiset bookStore(42, haser, eqOp);
19.3 枚举类型
C++包含两种枚举:限定作用域和不限定作用域的
。C++11新标准引入了限定作用域的枚举类型(在枚举类型的作用域外是不可访问的)
// 限定作用域 enum class
enum class open_modes{input, output, append};
// 不限定作用域
enum color {red, yellow, green};
// 未命名,不限定作用域的枚举类型
enum { floatPrec=6, doublePrec=10};
使用:
open_modes om = 2; // 错误:2不属于类型open_modes
om = open_modes::input // 正确
指定enum的大小
enum intValues: unsigned longlong { charType = 255, shortType = 65535};
19.6 union
Union其实是不想介绍的一个关键字,因为union的功能即将被C++17标准中的variant所代替,而且variant更加安全。但是还是要在这里介绍一下。
union(联合体)和struct相似,也可以包含多个数据成员,但是不同的是同时只允许一个成员有效,因此经常作为作为节约空间的类使用。
C++11中union除了继承c语言的数据共享内存之外,行为上越来越像一个类,比如成员默认是public类型。
在C++11以后,很多基础语法都进行了修正。其中 union 的行为向类对象进行了发展,在兼容原有语法定义的基础上进行了扩充:
- 联合体可拥有成员函数(包含构造函数和析构函数),但不能有虚函数。
- 联合体不能有基类且不能用作基类。
- 联合体不能拥有引用类型的非静态数据成员。
联合体不能含有带非平凡特殊成员函数(复制构造函数、复制赋值运算符或析构函数)的非静态数据成员。(C++11 前)
若联合体含有带非平凡特殊成员函数(复制/移动构造函数,复制/移动赋值,或析构函数)的非静态数据成员,则联合体中的该函数默认被弃置,且需要程序员显式定义它。若联合体含有带非平凡默认构造函数的非静态数据成员,则该联合体的默认构造函数默认被弃置,除非联合体的变体成员拥有一个默认成员初始化器。至多一个变体成员可以拥有默认成员初始化器。(C++11 起)
这些说明都很难懂,举两个小例子:
struct Point{
Point() {}
Point(int x, int y): x_(x), y_(y) {}
int x_, y_;
};
union U{
int z;
double w;
Point p; // 在C++03中是不合法(point有一non-trivial建構式),但是在C++11是合法的
U() {} // 由于 Point 成员的存在,必须要定义一个构造函数
U(const Point& pt) : p(pt) {} // 通过初始化列表构造 Point 对象
U& operator=(const Point& pt) { new (&p) Point(pt); return *this; } // 通过原地new方式赋值构造Point对象
};
#include <iostream>
#include <string>
#include <vector>
union S{
std::string str;
std::vector<int> vec;
~S() {} // 需要知道哪个成员活跃,仅在联合体式的类中可行
}; // 整个联合体占有 max(sizeof(string), sizeof(vector<int>)) 的内存
int main(){
S s = {"Hello, world"};
// 在此点,从 s.vec 读取是未定义行为
std::cout << "s.str = " << s.str << '\n';
s.str.~basic_string();
new (&s.vec) std::vector<int>;
// 现在,s.vec 是联合体的活跃成员
s.vec.push_back(10);
std::cout << s.vec.size() << '\n';
s.vec.~vector();
}
输出:
s.str = Hello, world
1
需要注意的是
联合体的默认成员访问是 public。
解释
联合体的大小仅足以保有其最大的数据成员。其他数据成员分配于该最大成员的一部分相同的字节。分配的细节是实现定义的,且从并非最近写入的联合体成员进行读取是未定义行为。许多编译器作为非标准语言扩展,实现读取联合体的不活跃成员的能力。
#include <iostream>
#include <cstdint>
union S{
std::int32_t n; // 占用 4 字节
std::uint16_t s[2]; // 占用 4 字节
std::uint8_t c; // 占用 1 字节
}; // 整个联合体占用 4 字节
int main(){
S s = {0x12345678}; // 初始化首个成员,s.n 现在是活跃成员
// 于此点,从 s.s 或 s.c 读取是未定义行为
std::cout << std::hex << "s.n = " << s.n << '\n';
s.s[0] = 0x0011; // s.s 现在是活跃成员
// 在此点,从 n 或 c 读取是 UB 但大多数编译器都对其有定义
std::cout << "s.c is now " << +s.c << '\n' // 11 或 00,取决于平台
<< "s.n is now " << s.n << '\n'; // 12340011 或 00115678
}
可能的输出:
s.n = 12345678
s.c is now 0
s.n is now 115678
各个成员都如同它是类的仅有成员一样进行分配。
可以看出,使用union存在着一些隐患,这是由于union虽然能节约空间。但是它没有类型信息。
参考
https://www.zhihu.com/tardis/bd/art/343271809?source_id=1001
https://blog.csdn.net/tan_yuan/category_11536707.html