- 信息学竞赛 CPP 进阶篇
- define XXX
- define PI 3.14
- define ADD(x,y) x+y
- 宏(2)
- 内联函数
- 函数模板
- 函数模板与数组引用
- 类模板
- 栈的数组实现
- 类模板特化
- 队列模板
- traits 技术
- 迭代器
- 示例
- STL的迭代器
- 函数对象
- STL中的函数对象
- STL通用容器
- vector
- vector(2)
- vector应用
- deque
- array
- 韩信分油问题
- list
- stack与queue
- 迷宫问题
- 迷宫问题(2)
- 集合
- 更多的集合
- 映射
- map的应用
- multimap
- STL算法概览
- STL算法概览(2)
- 函数适配器
- 匿名函数
- 移动语义
- 堆算法
- 查找算法
- 变序算法
- 删除与替换
- 数值算法
- 生成与变异
- 关系算法
- 其它算法
- 智能指针
- 散列表
- boost库
- boost串处理
- boost格式化
- boost大整数
- 正则表达式
- 正则表达式(2)
信息学竞赛 CPP 进阶篇
宏
软件的发展历史,是不断追求重用的历史。
c++ 如何实现重用?
- 函数:通过参数变化,暴露接口,隐藏实现
- 宏: 模板代码,编译时替换展开
- 类:更复杂的抽象,暴露接口,隐藏内部结构、状态、实现细节
- 模板泛型:把算法和类型分离开
宏的常见用法
三种格式
define XXX
只是定义了一个符号,常用于条件编译
可用#undef XXX 取消该定义
define PI 3.14
定义一个常量,避免魔法数字,新标准建议用 const
define ADD(x,y) x+y
有参的宏定义
表面看像个函数,实际上是代码的硬替换
有建议,用内联函数代替有参宏定义
宏在编译前进行宏替换处理,也叫:宏展开
这种替换是硬性的替换,与类型、语法无关,使用不当,容易引发诡异的错误。
例如:#define PI 3.14;
其后的源码:a = PI r r;
在编译前,被替换为:a = 3.14; r r;
原因仅仅是:#define 语句后边多了个分号。
宏的常见应用场景
避免魔法数字,增加程序可读性,可维护性。
#define M 100
....
char x[M];
x[M-1]='\0';
注意陷阱:
#define M 100+5
...
int x[2*M]; // 想开两倍空间,实际展开:int x[2*100+5];
预定义宏
__FILE__ // 两个下划线,当前文件
__LINE__ // 当前行号
__DATE__ // 当前日期
__TIME__ // 当前时间
防止头文件多次包含
传统方案是用宏配合条件编译:
#ifndef _MY_H_
#define _MY_H_
// 文件内容
#endif
另一个方案,文件开始处:#pragma once
以此表明:此文件只编译一次
但,有些编译器可能不支持
管理调试信息
开发中,经常会在程序的中间步骤上输出调试信息,开发结束后则不需要这些输出
//#define LOG(x) cout << (x) << endl;
#define LOG(x)
int main()
{
cout << "normal" << endl;
LOG("only for debug...")
return 0;
}
不调函数,提高效率
比如,经常交换两个变量,不想用函数开销,则可以用有参宏替换:
#define SWAP(a,b) {int t=a; a=b; b=t; }
int main()
{
int a=5,b=8;
SWAP(a,b)
cout << a << "," << b << endl;
return 0;
}
这有个小缺点:当类型不是整数,岂不悲哀?
我们心里希望,那个临时变量类型与 a, b 是一样的。不一定是整数!
也有办法:
#define SWAP(a,b) {typeof(a) t=a; a=b; b=t;}
typeof 与 sizeof 类似,它在编译阶段来确定参数的类型,所以并非真正的“动态”。
另一个有参宏的例子:
求最大值
#define MAX(a,b) \
({ typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; })
int main()
{
cout << MAX(2+3,4*5) << endl;
return 0;
}
实时效果反馈
1. 关于c++宏, 说法正确的是:__
A 宏的展开在程序运行时进行
B 在宏定义的末尾不能出现分号
C 宏定义只能写在同一行
D 为了避免魔法数字,也可以使用const定义常量
2. 下列x平方的宏,哪个更恰当?__
A
#define PF(x) x*x
B
#define PF(x) (x*x)
C
#define PF(x) ((x)*(x))
D
#define PF (x) ((x)*(x))
答案
1=>D 2=>C
宏(2)
宏在编译前执行,可以很简单,也可以超级复杂。
因为宏可以嵌套展开。
#define V1 "27.3.6"
#define V2 "1.4"
#define VER(pre,v) pre "." #v
#define PR(x) cout << (x) << endl;
int main()
{
PR(VER(V1,25))
PR(VER(V2,25))
return 0;
}
运行结果:
更多的宏特性
宏的展开规则:类似于函数调用,有参宏展开时,先对其参数宏展开
有例外:如果宏定义中有#或##则其参数不展开
我们可以利用这个特性,观察宏展开的结果:
#define TO_STR(x) TO_S(x)
#define TO_S(x) #x
....
PR(TO_STR(VER(V1,25)))
如果x是参数,则#x 是把x加双引号包围
##
则是拼接的意思,可以造出一个标识符来
#define ODD(x,y) int y##x ()
ODD(in,ma)
{
cout << "haha" << endl;
return 0;
}
这个功能太强大了,如果精心设计,我们可以改变c++的语法!
但,程序的可读性会严重下降。
宏的优势
宏的优势在于,可以在编译前对源码进行变换。
这种变换不占用运行时,使用得好,可以减少模板代码,可以提高效率。
c++还有很多关于宏的很细致的规定和约束,
并非一开始就设计成那样,这是长期实践,积累,修补的结果。
一般认为,适量使用宏可以获得效率、代码可读性、兼容性、可维护性等方面的益处。
但,过于复杂的宏很可能是隐患。
宏注意
- 有参宏的括号重要性
#define AREA(r) 3.14*r*r
有参宏,定义了圆面积的计算方法。
如果使用:
double a = AREA(1+1) ;
cout << a << endl;
不会得到期望的结果,以内,硬性替换后,相当于:
double a = 3.14*1+1*1+1;
较好的方法是:
#define AREA(r) (3.14*(r)*(r))
宏替换的第一个位置必须为合法的标识符
#define 0x ABC
宏替换的后边内容也不能随意写,引号必须配对
#define P1 at"
双引号内的内容不会被替换
#define S(a) "a"
一个标识符整体被替换,它的一部分不会替换
宏:只负责文字上的替换,不管语法的事情,也不会计算表达式。
常量,可以用 const 代替
有参宏,一般可以用 inline 函数代替
很多情况也可以使用 函数模板、类模板来解决。
总结
就像 goto 语句一样,宏在有些场合用,很容易,很恰当;但,滥用,会导致灾难。
理论上,宏足以强大到用 c 构建出另一种高级语言来。
实践上,复杂的宏,常常让我们涌起砸碎显示屏的冲动。
实时效果反馈
1. 关于宏的弱点, 说法==不正确==的是:__
A 逻辑稍复杂后,可读性差
B 难于调试
C 没有名字空间,污染全局域
D 拖慢运行速度
2. c中的有参宏,c++中建议用什么代替?__
A const
B inline function
C function template
D function pointer
答案
1=>D 2=>B
内联函数
为什么要定义如下宏?
#define MIN(x,y) ((x)<(y)?(x):(y))
更好一点的版本:
#define MIN(a,b) \
({ typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a < _b ? _a : _b; })
既然,这么简单的功能,如此繁冗的代码,使用函数不好吗?
函数调用的弱点
因为,如果用一个函数,调用函数会耗费运行时。
函数调用过程:
- 保存当前的执行断点(执行上下文)
- 分配形参变量空间
- 用实参来初始化形参
- 跳转到被调函数代码执行
- 释放形参变量(可能有必要的清理工作,比如析构函数)
- 恢复原来的执行断点,继续主调函数的工作
如果频繁地切换小函数,只执行很短的代码,则切换环境会相对浪费时间。
如果,直接写 ?: 表达式,又太麻烦,可读性不好。
宏呢?虽然简单如此,也要一堆括号,防止参数中含有表达式。
使用宏的弱点
比起函数来,宏的可读性差,宏不容易调试,不容易维护
宏没有类型,编译器没办法做更多的介入和检查
宏不能访问类的私有成员,但使用函数,可以声明友元函数
inline function
有没有什么办法,既有函数的形式,又不真的调用函数呢?
内联函数是个解决方案
inline int min(int a, int b)
{
return a < b ? a : b;
}
int main()
{
cout << min(5, 6) << endl;
cout << min(6+8, 5+4) << endl;
return 0;
}
这段代码,
编译时,会把对 min 的调用直接展开为代码,
这样,虽然最终生成的代码变长了,也包含了重复,但执行效率提高了。
注意事项
- inline 关键字并非函数签名的一部分,它并非是接口,它是控制实现方式的。
inline 只有与函数的定义(而非声明)放在一起才有效。
- 定义在 class 内的成员函数自动变为内联函数
class MyA{
public:
int add() { return i+j;}
inline int dec() { return i-j;}
int getNum();
private:
int i,j;
}
inline int MyA::getNum(){
return i;
}
- 小的函数用内联(建议小于10行),大函数得不偿失
带来的问题
宏是不管类型的,仅仅是文字上的替换。
函数是有类型的,编译器可以介入检查,虽然安全,但带来了类型限制。
比如,min 函数怎么指定其参数是:字符串,或其它更复杂的类型呢?
这方面,宏的类型无关反而成了优点。
但,c++的函数模板,类模板可以解决这个问题。
实时效果反馈
1. 关于inline, 说法==不正确==的是:__
A inline 写在.h文件中,则该函数编译为内联函数
B inline 与实现写在一起才有效,以便于编译时展开。
C 直接定义在 class 中的函数,自动编译为内联函数
D 一般建议小函数用 inline,函数太庞大,用内联得不偿失
2. 内联函数与宏的关系是:__
A 内联函数可以完全代替宏的功能
B 使用内联比使用宏执行效率更高
C 一般情况下,与内联函数相比,使用宏的代码可读性更好一些
D 与宏相比,内联函数的可读性,可维护性更好些。
答案
1=>A 2=>D
函数模板
c++中有泛型编程的概念。
即,编程时,类型是不确定的,在调用处,替换为真正的具体类型。
c++的指针泛化,在一定程度上解决了泛型编程的问题,但指针泛化是在运行时发生的,对泛型指针的调用,比普通的成员函数调用,更耗费处理器时间。
而这里所说的泛型编程,力求在编译阶段解决问题,不能降低执行效率。
c++使用函数模板,类模板来解决这个问题。
函数模板定义
以 template
告诉编译器, T是个待定的类型,在具体调用时编译为实际的类型。
template <typename T>
void swap1(T& a, T& b)
{
T t = a;
a = b;
b = t;
}
int main()
{
int a=5,b=8;
swap1<int>(a,b);
//swap1(a,b);
cout << a << "," << b << endl;
return 0;
}
标准库中已经有了 std::swap,所以改了名字
调用的时候,
这里,不指定也可以,因为,编译器能推断出 T 的类型是 int
可以把 T 换成其它类型,甚至是用户自己定义的类型来试试。
使用注意
- 编译器会对函数模板进行两次编译,第一次编译模板函数本身,第二次替换类型后,当作普通函数编译
- 函数模板不能隐式转换,类型需要严格匹配
- 函数模板可以有多个模板类型参数
- 无法自动推断返回值类型,需要自己指定
template<typename T1, typename T2>
T1 f(T2 x, T2, y) { ... }
....
auto t1 = f<int>(a, b); // 指定了返回值类型
类型名需要从左到右依次指定(可以部分指定),所以返回值应该定义为 T1,
否则,比如在最后一个typename, 为了指定返回值,就需要把所有名字都指定了。
与函数重载关系
可以共存。
函数重载,在有限的类型种类中,分别定制如何处理,而模板类型可以没有限定。
当发生二义时,首先选普通函数,其次才考虑模板
如果模板可以更精确匹配,选择模板
我们可以使用空尖括号,限定只能使用模板
与 inline 联合使用
这样用可以代替一些有参宏,并且是类型安全的。
比如,经典的求最大值
template <class T>
inline T max1(const T& a, const T& b) // 标准库中已有max
{
return a > b? a : b;
}
这样,兼具了宏的类型无关,和内联函数编译监督两个好处。
实时效果反馈
1. 关于函数模板, 说法正确的是:__
A 与宏一样,函数模板在编译前展开
B 函数模板比普通函数执行更有效率
C 函数模板在编译时,把泛定类型确定为具体类型
D 函数模板,在运行时把泛定类型替换为普通类型
2. 如果有 template<class T> T f(T a, Tb){....}
,以下错误的是:__
A f
(1,2); B f(1,2);
C f(1.2, 3.4);
D f(1.2, 3);
答案
1=>C 2=>D
函数模板特化
语法固然重要,但更重要的是最佳实践。
宏可以带来效率,可以类型无关;函数可以有可预料的行为和安全的类型。
内联的函数模板可以兼具两者优势。
常见的用法举例:
// 删除堆指针的时候,一般的行为
template <typename T>
inline void SafeDelete(T*& p)
{
if(p){delete p; p=NULL;}
}
// 返回数组变量的元素个数
template <typename T>
inline int CountOf(const T& array)
{
return sizeof(array) / sizeof(array[0]);
}
// 判断一个类型是否为 unsigned 类型
template <typename T>
inline bool IsUnsigned(T)
{
return (T)-1 > 0;
}
对特殊情形的单独处理
- 可以定义一个重载的普通函数
- 可以定义一个特化的函数模板
特化的例子
template <class T>
bool eq(const T& a, const T& b)
{
return a==b;
}
template<>
bool eq<char*>(char* const & p1, char* const & p2)
{
cout << "eq...<>" << endl;
return strcmp(p1,p2)==0;
}
调用处:
int a = 10;
cout << eq(a,5+5) << endl;
char* p1 = "abc";
char* p2 = "abc";
char p3[] = "abc";
cout << eq(p1, p2) << endl;
//cout << eq(p1, p3) << endl;
注意:p1,p3 比较会报编译错误,因为:类型不一致。模板函数类型严格。
p1 类型:char*
p3 类型:char[4]
其实,我们也可以直接用重载,它会进行一些必要的类型转换,不会要求那么严格匹配。
bool eq(char* p1, char* p2)
{
cout << "eq...normal" << endl;
return strcmp(p1,p2)==0;
}
int main()
{
int a = 10;
cout << eq(a,5+5) << endl;
char* p1 = "abc";
char* p2 = "abc";
char p3[] = "abc";
cout << eq<>(p1, p2) << endl; // 强制使用模板
cout << eq(p1, p3) << endl;
return 0;
}
实时效果反馈
1. 关于函数模板与重载函数的关系, 说法正确的是:__
A 重载函数不可以和函数模板同名
B 模板与重载函数都匹配时,优先使用具体函数版本
C 函数模板匹配参数很宽松,可以自动进行必要的类型转换
D 函数模板不可以实现为内联函数
2. 为解决函数模板对有些类型不通用的问题,哪个解决方案最==不推荐==?:__
A 定义函数模板的特化版本
B 定义针对具体类型的普通函数重载版本
C 在函数模板实现中用 if 语句断定类型,分别处理
D 设计复杂的宏来取代模板
答案
1=>B 2=>D
函数模板与数组引用
当数组作为参数传递时,会退化为指针。
也就是说,数组的元素个数信息会丢失。我们可以通过typeid来检查验证。
void g(int x[3])
{
cout << typeid(x).name() << endl;
// [3] 是徒劳,因为退化问题,与 int x[], 或者 int* x 是一样的
}
注意,别忘了:#include
调用处:
int* p;
int q[] = {1,2,3};
int a = 16;
g(q);
cout << typeid(p).name() << endl;
cout << typeid(q).name() << endl;
但是,当数组作为引用传递时,就不一样:
void g(int (&x)[3])
{
cout << typeid(x).name() << endl;
}
注意,括号的使用,
若不然,表示x是数组,元素是 int&
如果希望元素个数==泛定==怎么办呢?
我们可以这样考虑,在编译的时候,肯定是知道元素个数的,可不可以先写N,在编译时,再替换为具体的数字呢?这不恰恰是模板的特征吗?
确实可以!
template <int N>
void f(int (&x)[N])
{
cout << typeid(x).name() << endl;
}
在试试传入数组吧。
最佳实践
经常,我们希望 cout 在输出数组类型时,能输出有所得元素,并不想打印指针
能不能写一个通用得函数模板,对所有的数组都打印它的所有元素呢?
template <class T, int N>
void print_array(T (&x)[N])
{
for(int i=0; i<N; i++) cout << x[i] << " ";
cout << endl;
}
int main()
{
int a[] = {1,2,3,4,5};
char* b[] = {"abc", "hah..", "123"};
print_array(a);
print_array(b);
return 0;
}
实时效果反馈
1. 关于数组和指向它第一个元素的指针, 说法正确的是:__
A 该数组与指针是同一个类型
B 这是两个完全不同的类型,不能自动转换
C 指针类型可以自动转为数组类型
D 数组类型可以自动转为指针类型
2. template <类型列表>,语法==错误==的是:__
A
template <>
B
template <typename T>
C
template <typename T, int 3>
D
template <typename T1, int N>
答案
1=>D 2=>C
类模板
我们在前边定义过 Point 类型,包含 x,y 坐标
struct Point{
int x;
int y;
};
但,x, y 类型有几种可能: int, double, long long 甚至 char 都是选择。
我们不希望对类似的代码定义许多次,仅仅是 x y 类型上有差异,其它都一样或类似。
类模板就是用来解决这个问题的。
语法上,与函数模板类似,以 template 来开启模板的定义:
template <typename T>
struct Point{
Point(T& a, T& b):x(a),y(b){ }
friend ostream& operator<< (ostream& out, Point<T>& obj){
out << "(" << obj.x << "," << obj.y << ") ";
return out;
}
T x;
T y;
};
调用方式:
Point<int> a(1,2);
Point<double> b(2.3, 4);
cout << a << endl;
cout << b << endl;
注意,这里的 operator<< 并非是成员函数,它是模板函数,
而且,它一般是定义在类外的。
但此处,如果挪到类外:
template <typename T>
ostream& operator<< (ostream& out, Point<T>& obj){
out << "(" << obj.x << "," << obj.y << ") ";
return out;
}
会导致编译不通过。
这是由于模板的二次编译造成的。
解决的方案,是明确告知编译器, operator<< 是个函数模板,并在类内具化。
即,增加前置声明
template <typename T> class Point;
template <typename T>
ostream& operator<< (ostream& out, Point<T>& obj);
template <typename T>
struct Point{
Point(T a, T b):x(a),y(b){}
friend ostream& operator<< <T> (ostream& out, Point<T>& obj);
T x;
T y;
};
template <typename T>
ostream& operator<< (ostream& out, Point<T>& obj){
out << "(" << obj.x << "," << obj.y << ") ";
return out;
}
一般工程规范情况下,要划分 .h 文件和 .cpp 文件。
划分职责如下:
头文件主体内容:
///////// .h
template <typename T> class Point;
template <typename T>
ostream& operator<< (ostream& out, Point<T>& obj);
template <typename T>
struct Point{
Point(T a, T b):x(a),y(b){}
friend ostream& operator<< <T> (ostream& out, Point<T>& obj);
double distance(Point<T>& that);
T x;
T y;
};
.cpp 主体内容:
////////// .cpp
template <typename T>
ostream& operator<< (ostream& out, Point<T>& obj){
out << "(" << obj.x << "," << obj.y << ") ";
return out;
}
template <typename T>
inline SQ(T x) { return x * x; }
template <typename T>
double Point<T>::distance(Point<T>& t)
{
return sqrt(SQ(t.x-x) + SQ(t.x-x));
}
使用场景
int main()
{
Point<int> a(1,2);
Point<int> b(3,4);
Point<double> c(2.3, 4);
cout << a << b << c<< endl;
cout << a.distance(b) << endl;
return 0;
}
实时效果反馈
1. 关于类模板, 说法正确的是:__
A 类模板在实例化时,可以根据参数自动推断泛定类型
B 类模板实例化时,需要用<>中指定具化类型
C 与宏类似,类模板在编译之前进行替换展开
D 类模板只能有1个模板参数
2. 当在类模板内声明友元函数,类外去定义,说法错误的是:__
A 类模板和友元函数模板都要前置声明
B 类内声明要对友元函数模板具化
C 该友元函数一定实现为函数模板形式
D 该友元函数不能重载
答案
1=>B 2=>D
栈的数组实现
前边的课程中,我们实现过简单栈,但元素的类型是定死的。
现在有了模板,我们可以类型灵活起来,顺带也让数组大小灵活起来
还记得我们之前实现的支持任意类型的栈吗?
那个任意是有代价的。
通过 IObj* 访问 User 对象的成员是多态访问,不是直接编译为确定函数的调用
MyStack 与 User 数据间是聚合关系,不是组合。
栈的数组中只保存对象指针,而不是对象本身,内存多次 new,效率不高
如果用户的数据,有些是局部的,有些在堆中,管理将十分麻烦。
有了类模板,我们可以在不损失性能的前提下,实现类型无关。
template <typename T>
struct IStack{
virtual ~IStack() {}
virtual IStack& push(const T&) = 0;
virtual T pop() = 0;
virtual bool empty() = 0;
};
c++中,接口并不特殊,当然可以是模板类型
其它类可以继承这个模板,可以普通类,也可以是类模板。
但,无论如何,在继承时,都需要对 IStack进行具化处理。
template <typename T>
class MyStack:public IStack<T>{
public:
MyStack() { n = 0; }
virtual MyStack& push(const T& x) override{
if(n>=100) throw "stack overflow";
data[n++] = x;
return *this;
}
virtual T pop() override{
if(n==0) throw "empty pop!";
return data[--n];
}
virtual bool empty() override{
return n == 0;
}
private:
T data[100];
int n;
};
这个类模板还有点小缺陷,数组的大小顶死了,其实可以当模板参数的。
在用户创建实例的时候指定这个参数:
template <typename T, int N>
class MyStack:public IStack<T>{
public:
MyStack() { n = 0; }
virtual MyStack& push(const T& x) override{
if(n>=N) throw "stack overflow";
data[n++] = x;
return *this;
}
virtual T pop() override{
if(n==0) throw "empty pop!";
return data[--n];
}
virtual bool empty() override{
return n == 0;
}
private:
T data[N];
int n;
};
当然,调用处要多指定一个具化参数:
MyStack<Point,200> a;
实时效果反馈
1. 关于类模板, 说法正确的是:__
A 含有虚函数的类不能做成类模板
B 接口不能是类模板
C 类模板无法被普通的类继承
D 类模板可以指定多个泛型参数
2. 关于类模板的继承,==错误==的说法是:__
A 继承的类不一定是类模板,也可以是普通类
B 使用类模板继承时,子类的模板参数个数必须与父类模板相同
C 继承时,需要对父类模板具化处理。
D 类模板的成员函数,如果定义在类外,一定是函数模板
答案
1=>D 2=>C
类模板特化
与函数模板的情况类似,类模板有时也不能完全兼容所有泛定类型的处理。
这时,可以固定全部或部分的类型,来实现一个特殊的版本。
template <typename T1, typename T2>
class MyA{
public:
MyA(const T1& a, const T2& b){
cout << "MyA(T1, T2)" << endl;
}
void f1() { cout << "f1" << endl; }
void f2() { cout << "f2" << endl; }
};
// MyA 模板的特化版,如需要,必须重新定义所有函数,或添加新函数
template<>
class MyA<int, int>{
public:
MyA(const int& a, const int& b){
cout << "MyA(int, int)" << endl;
}
...
};
调用:
MyA<int, double> a(1, 1.0);
MyA<int, int> b(1, 1);
b.f1(); // 错误,特化版中没有这个函数
如果我们的特化版本仅仅是某个局部有不同处理,大部分都相同,那不如用继承:
// 可以重用 MyA 模板的内容
struct MyB: public MyA<double,double>{
MyB(int x, int y):MyA(x,y){}
};
调用:
MyB c(2.0,2.0);
c.f1();
特化的花样
特化能做出很多花样来,前边的例子是全特化
全特化
template <>
class MyA<int, int> { // 这里所有参数都固定下来
....
}
部分特化
template <typename T> // 列出剩余没有固定的类型
class MyA<T, int> { // 只固定部分类型
...
}
偏特化
对类型施加某些限制
template <typename T1, typename T2>
class MyA<T1*, T2*>{ // 限定了泛型必须为指针类型
...
}
再比如:
template <typename T>
class MyA<T, T>{ // 限定了两个泛型必须是相同的类型
...
}
如果某个调用同时匹配多个特化,就会产生二义性。
可以针对特殊情况定义相应的全特化版。
与函数模板特化的比较
函数模板只能全特化
函数可以重载,类无法重载,同名的类会冲突
namespace my{
template <typename T1, typename T2>
bool eq(const T1& a, const T2& b){
return a == b;
}
template <typename T>
bool eq(double a, const T& b){
return fabs(a-b) < 0.0001;
}
template <typename T>
bool eq(const T& a, double b){
return fabs(a-b) < 0.0001;
}
}
int main()
{
cout << my::eq(1.000001, 1) << endl;
cout << my::eq(1, 0.999999) << endl;
//cout << my::eq(1.000001, 1.0001) << endl;
return 0;
}
工程实践中,建议尽量优先使用函数模板特化、类模板特化,
无法满足要求时,才使用函数重载、定义新的类
实时效果反馈
1. 关于模板特化, 说法正确的是:__
A 函数模板支持全特化和部分特化
B 类模板支持全特化和部分特化
C 函数模板只能部分特化
D 类模板只能部分特化
2. 当类模板对某个泛型的参数需要轻微修正函数行为,一般怎么处理?:__
A 考虑继承类模板,重载其中不满足要求的函数
B 写一个普通类,重载类模板
C 写一个全新的类
D 使用条件编译,分别处理
答案
1=>B 2=>A
队列模板
前边的学习中,实现过循环队列,是用数组实现的。
并且,也可以通过指针泛化来支持任意的用户定义类型。
但泛型版本更有效率上的优势。
本节利用模板知识,实现一个单链表的队列,其中的元素类型是泛定的。
队列容量没有限制。
原理
Que
节点 Node
Node 包装了用户类型 T data
实现
template <typename T>
class Que{
public:
Que() { front = back = NULL; }
~Que() {
while(!empty()){ cout << "debug: " << remove() << endl; }
}
Que& add(const T& x);
T remove();
bool empty() { return front==NULL; }
private:
class Node{
public:
Node(const T& x){ data = x; next=NULL; }
T data;
Node* next;
};
private:
Node* front;
Node* back;
};
节点类是队列内部使用的,不需要暴露给用户,所以是private
需要注意的是, class Node 并没有定义为模板类,是因为Que在具化的时候,就会一并具化 class Node,因而就消除了不确定的类型。
类外的实现:
template <typename T>
Que<T>& Que<T>::add(const T& x)
{
Node* p = new Node(x);
if(empty())
front = p;
else
back->next = p;
back = p;
return *this;
}
template <typename T>
T Que<T>::remove()
{
if(empty()) throw -1;
Node* p = front;
front = front->next;
if(front==NULL) back = NULL;
T rt = p->data;
delete p;
return rt;
}
这些都需要实现为函数模板,并且其中引用类的时候,要具化。
注意:析构的重要性,否则会产生内存泄漏
调用处:
Que<int> a;
a.add(1).add(2).add(3);
while(!a.empty()) cout << a.remove() << endl;
a.add(4).add(5);
模板对宏的比较优势
模板泛型体系是c++较晚才引入的特性。
此前,通过有参宏替换来实现。
其问题是:
- 宏展开的时候,无法语法追踪,写法太灵活。
- 宏替换在编译时,已经确定为具体的类型,无法让两个泛型实例并存。
实时效果反馈
1. 关于队列, 说法正确的是:__
A 队列是后进先出的线性结构
B 队列的实现可以是链表,数组,动态数组,块链或者更复杂的结构
C 泛型队列模板与指针泛化的方案比起来,主要优势是适用更多类型
D 两个不同用户类型的 Que 无法并存
2. 对于本节Que的实现,当物理内存耗尽,表现是:__
A 程序行为不确定,与内存随机数据有关
B 栈溢出错误
C 会抛出异常,程序终止
D add新数据没有效果
答案
1=>B 2=>C
traits 技术
我们在适用函数模板,类模板编程的时候,很快就会发现,一个模板代码很难尽善尽美,
经常会出现特殊情况。即,对某个类型,或某种类型要特殊处理。
使用类模板的特化如何?
只修改一点点,大部分都要抄下来,违背了 don’t repeat 原则
if ( T 是某某类型) 则: 。。。。
这个如果是动态判断,消耗运行时处理器时间,有时,效率很敏感。
其实,我们可能也希望写 if( T 是某种类型 ) 。。。。 但,我们希望这个 if 能在编译时确定好,不用运行时现场断定类型,也就是所说的:静态反射机制
traits, 翻译为:特性,或特质,按c++之父的说法:
Think of a trait as a ==small object== whose main purpose is to ==carry information== used by another object or algorithm to determine “policy” or “implementation details”.
可以看到,关键字就是:携带类型信息,服务于其它类或函数。
而,这个类型信息是编译时的,不是运行时的。
示例
template <typename T, bool isPointer>
class Test{
public:
void f() {
if(isPointer) cout<< "POINTER "; // 类型不同特殊处理
cout << "do somthing..." << endl;
}
};
int main()
{
Test<int,false>().f();
Test<int*, true>().f();
return 0;
}
当类型 T 为指针时,处理逻辑上轻微调整。
如果,用 if 实现分支处理,则需要传入一个 bool信息
客户会奇怪,因为这本应该与他无关。
实际上,这个bool信息显然可以在编译过程中,从T提取,方法:
template <typename T>
struct TestHelper{
static const bool isPointer = false;
};
template <typename T>
struct TestHelper<T*>{
static const bool isPointer = true;
};
template <typename T>
class Test{
public:
void f() {
if(TestHelper<T>::isPointer) cout<< "POINTER ";
cout << "do somthing..." << endl;
}
};
int main()
{
Test<int>().f();
Test<int*>().f();
return 0;
}
经过这样处理,isPointer 这个逻辑就被封装在Test中
示例2
比如,我们开始有这样的类模板,
template <typename T>
class Test{
public:
T f(T x);
private:
T data;
};
后来,需求变更了,要求:
- 当 T 为指针时,传入为 int, 传出为 int*
- 当 T 为 int 时,传入为 int, 传出为 double
这时,就可以用trait技术了,
我们可以用trait辅助类封装原来的输入值和输出值的类型:
template <typename T>
struct mytrait{
typedef T ret_t;
typedef T par_t;
};
template <typename T>
struct mytrait<T*>{
typedef int* ret_t;
typedef int par_t;
};
template <>
struct mytrait<int>{
typedef double ret_t;
typedef int par_t;
};
template <typename T>
class Test{
public:
typename mytrait<T>::ret_t f(typename mytrait<T>::par_t x);
private:
T data;
};
注意: mytrait
为了避免冗长,可以把这个长长的类型再次typedef
template <typename T>
class Test{
public:
typedef typename mytrait<T>::ret_t ret_t;
typedef typename mytrait<T>::par_t par_t;
ret_t f(par_t x);
private:
T data;
};
这种技巧很实用,因为 ret_t 很可能在程序中多处被引用。
实时效果反馈
1. 关于traits, 说法正确的是:__
A traits 是一个复杂的函数模板,负责分发请求
B traits 一般是一个很小的类模板,封装与泛定类型相关的类型信息
C traits 是一个个独立的接口,普通类可以继承多个trait
D traits 是一些重载函数
2. 使用traits封装后,间接地使用类型,其好处是:__
A 隔离不同类型间的差异,使得调用有统一的形式
B 对冗长的类型重定义,使得表达更短小
C 对特殊的类型更友好,使得其操作快速
D 对指针类型重定义,区别对待
答案
1=>B 2=>A
迭代器
迭代器是重要的抽象,它提供遍历容器中元素的方法,却要求与容器的物理实现无关。
首先考虑一个简单的数组 int [N],我们如何遍历?
显然,最容易想到的方法,一个 int* 指针,指向第一个元素,
一直往后走,直到数组尾部。
int a[N] = ....;
int* p = a;
int* end = a + N;
while(p != end) cout << *p << endl;
那么,如果容器并非数组,而是链表呢?块链呢?
能统一地、无差别地对待吗?
这时,我们需要一个智能指针类型,表面上,感觉就是一个指针,支持:
p++, *p, p != end 这些操作。
我们只需要重载这些运算符就好。
示例
使用动态数组时:
class MyArray{
public:
MyArray(){ size=10; data = new int[size]; n=0; }
~MyArray() { delete [] data; }
void add(int x){
if(n==size){
int* old = data;
data = new int[size * 2];
memcpy(data, old, size * sizeof(int));
delete [] old;
size *= 2;
}
data[n++] = x;
}
private:
int size; // 初始大小
int n; // 已有元素个数
int* data;
};
使用块链结构时:
class MyBlock{
public:
MyBlock() { tail = head = new Node(); }
~MyBlock() {
while(head){
Node* p = head;
delete [] p;
head = head->next;
}
}
void add(int x){
if(tail->n == BLOCK_SIZE) {
Node* p = new Node();
tail->next = p;
tail = p;
}
tail->data[tail->n++] = x;
}
private:
static const int BLOCK_SIZE = 10;
struct Node{
Node() { n=0; next=NULL; }
int data[BLOCK_SIZE];
int n;
Node* next;
};
private:
Node* head;
Node* tail;
};
统一的显示操作:
分别为这两个类增加一个内部 Iter 类型,可以理解为智能指针。
class MyArray{
public:
MyArray(){ size=10; data = new int[size]; n=0; }
~MyArray() { delete [] data; }
void add(int x){ .... }
public:
typedef int* Iter; // 迭代器就是 int* 本身
Iter begin() { return data; }
Iter end() { return data + n; }
private:
int size; // 初始大小
int n; // 已有元素个数
int* data;
};
MyBlock 的Iter就要复杂了:
class MyBlock{
public:
MyBlock() { tail = head = new Node(); }
~MyBlock() { .... }
void add(int x){ .... }
public:
struct Node;
class Iter{
public:
Iter(Node* it, int n){
this->it = it;
this->p = it->data + n;
}
int operator* () { return *p; }
void operator++(int) {
p++;
if(p==it->data + it->n && it->next){
it = it->next;
p = it->data;
}
}
bool operator!= (const Iter& that){
if(this->it != that.it) return true;
if(this->p != that.p) return true;
return false;
}
private:
Node* it; //当前块
int* p; // 当前位置
};
Iter begin() { return Iter(head, 0); }
Iter end() { return Iter(tail, tail->n); }
public:
static const int BLOCK_SIZE = 10;
struct Node{
Node() { n=0; next=NULL; }
int data[BLOCK_SIZE];
int n;
Node* next;
};
private:
Node* head;
Node* tail;
};
使用:
MyArray a;
MyBlock b;
for(int i=0; i<50; i++){
a.add(i);
b.add(i);
}
for(MyArray::Iter i=a.begin(); i!=a.end(); i++)
cout << "a: " << *i << endl;
for(MyBlock::Iter i=b.begin(); i!=b.end(); i++)
cout << "b: " << *i << endl;
使用函数模板统一:
template <typename T>
void display(T& x){
for(auto i=x.begin(); i!=x.end(); i++)
cout << *i << " ";
cout << endl;
}
int main()
{
MyArray a;
MyBlock b;
for(int i=0; i<50; i++){
a.add(i);
b.add(i);
}
display(a);
display(b);
return 0;
}
实时效果反馈
1. 关于迭代器, 说法正确的是:__
A 迭代器就是指向容器中元素的指针
B 迭代器的目的是能更方便地访问容器中的元素
C 迭代器的目的是能更高效地访问容器中的元素
D 迭代器的目的是能更统一地访问容器中的元素,与容器类型无关
2. 当迭代器p不是原生指针,要重载运算符,一般哪个==不==需要重载?__
A p++
B *p
C p != q
D p[5]
答案
1=>D 2=>D
STL的迭代器
STL中提供了丰富的容器类型。
为了用统一的方式,访问这些容器中的元素,使得算法与类型隔离,与容器的物理结构隔离,广泛使用了迭代器的概念。
最常见的表现形式:
for(auto i=容器.begin(); i!=容器.end(); ++i){
// 对 *i 进行操作
}
常见的迭代器有以下几种类型:
这个图画的不是继承关系,只是表达越往后边的迭代器功能越多。
比如, p 是forward迭代器,则支持: ++p, 但不支持 —p,而双向迭代器就可以。
迭代器功能
p,p1 是迭代器对象,则:
所有迭代器:
| 运算 | 含义 | | —— | —————————— | | p++ | 自增迭代,返回增前值 | | ++p | 自增迭代,返回之后值 |
输入迭代器
| 运算 | 含义 | | ———- | ——————————— | | xx = *p | 读取迭代器位置的元素值 | | p = p1 | 迭代器赋值 | | p == p1 | 迭代器比较 | | p != p1 | 迭代器比较 |
输出迭代器
| 运算 | 含义 | | ———— | ———————— | | *p = xxx | 向迭代器位置写入 | | p = p1 | 迭代器赋值 |
正向迭代器 = 提供所有输入、输出迭代器的功能
双向迭代器(提供正向所有功能)
运算 | 含义 |
---|---|
—p | 前置递减,返回减前的值 |
p— | 后置递减,返回减后的值 |
随机迭代器
| 运算符 | 含义 | | ———- | ————————————— | | p += i | 连续前进 i 步 | | p -= i | 连续后退 i 步 | | p + i | 相对 p 位置前进 i 位置 | | p - i | 相对 p 位置后退 i 位置 | | p[i] | 相对 p 偏移 i 位置的元素值 | | p < p1 | p 在 p1 之前 | | p <= p1 | p 在 p1 之前,或相等 | | p > p1 | p 在 p1 之后 | | p >= p1 | p 在 p1 之后,或相等 |
可以看出,迭代器是对指针的概念的升华。
与指针的区别
迭代器是“假”指针,表面上装作指针的样子,指针的行为,但毕竟是个对象。
比如:容器.end() 返回的是最后一个元素后边那个位置,称为:尾后迭代器,它并非一个真正的“位置”,而 *p 也是引用一个并不存在的元素。
还有,p++ 是广义的 + ,移动方向未必是指针增量方向。比如反向遍历时,用的也是++
我们对 string 使用迭代器概念:
string s = "1234567";
for(auto i = s.rbegin(); i != s.rend(); ++i){
cout << *i << endl;
}
看到,反向的输出效果,但 ++i, 不是 —i
iterator 的编译类型
从编译器的角度看,迭代器有 4 种:
容器::iterator
正向迭代器容器::const_iterator
常量正向迭代器容器::reverse_iterator
反向迭代器容器::const_reverse_iterator
常量反向迭代器
我们应该使用尽量具体的类型,以提供更多的编译保护。
比如,上边的反向遍历串种元素,如果只是显示,不需要更改,则:
string s = "1234567";
for(string::const_reverse_iterator i = s.crbegin();
i != s.crend(); ++i){
cout << *i << endl;
}
STL各种容器类型种的迭代器
这里列举主要的STL容器特点:
容器 | 容器结构 | 支持迭代器了类型 |
---|---|---|
vector | 动态数组,提供可以扩容的连续存储空间 | random access |
deque | 数组类型,可以快速在两端进行插入删除,可按需扩容 | random access |
list | 链表,不支持随机访问,插入、删除快速 | bidirection |
set | 快速判定元素是否在容器种关键字=元素值 | bidirection |
multiset | 与 set 类似,但关键字可以包含重复元素 | bidirection |
map | 键值对儿的容器,根据键可以高效地查找值 | bidirection |
multimap | 类似map,但不同的是,每个关键字可以关联多个值 | bidirection |
实时效果反馈
1. 如果我们定义单链表,则它的迭代器应该是:__
A 前向迭代器
B 双向迭代器
C 随机迭代器
D 输入迭代器
2. set类型容器为什么不提供随机迭代器?:__
A set 是哈希表实现的,随机没有线性的概念
B 为了快速判断元素是否存在,元素不是按某种顺序规律存储,随机访问会很慢
C set 是链表,不方便提供随机访问
D 为了安全性不提供
答案
1=>A 2=>B
函数对象
一个对象可以伪装为函数。
- 对象(参数,…) 表面上看好像函数调用
- 一个需要函数指针的地方,可以传入与之类型匹配的函数对象
- 可以通过重载 operator() 运算符来实现
struct MyF{
int operator() (int a, int b) { return a * 10 + b; }
};
调用方法:
MyF a;
cout << a(2,3) << endl;
明明可以用普通的函数,现在搬出个对象来有什么用?
答曰:对象可以持有状态
比如,我们要多次调用一个 add(int x),最后获得累加值,该如何处理呢?
如果用普通函数,就必须借助 static 或全局变量:
int g_a = 0;
void add(int x)
{
g_a += x;
}
调用:
g_a = 0;
for(int i=0; i<10; i++) add(i);
cout << g_a << endl;
这个方案的弱点是:
- 引入全局变量,终究是个隐患
- 多个线程中执行add过程,将引发灾难
如果改用函数对象,对象可以持有自己的状态,就能避免全局变量与线程竞争问题。
函数对象实现add
class MyF{
public:
MyF():sum(0){}
void operator() (int x) { sum += x; }
int get_sum() { return sum; }
private:
int sum;
};
调用:
MyF a;
for(int i=0; i<10; i++) a(i);
cout << a.get_sum() << endl;
让一个函数持有状态,计算过程与状态有关,这是现代高级语言中的函数闭包
c++的函数对象与函数闭包比较,有更强大的能力和可塑性。
与STL协作
STL的核心思想是算法与容器类型分离
有些算法,需要传入用户自己定义的处理过程,即函数指针,此时如果需要持有状态,就可以用函数对象来代替之。
比如:for_each,它需要传入2个迭代器,以及一个函数
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
MyF a = for_each(v.begin(), v.end(), MyF());
cout << a.get_sum() << endl;
注意,需要 include
#include <algorithm>
#include <vector>
实时效果反馈
1. 怎样的类,才能生成函数对象:__
A 类中重载了 operator() 运算符
B 类中定义了函数指针
C 类中定义了函数模板
D 类中定义了特殊函数
2. 函数对象,与普通函数比,有何优势?__
A 能够支持任意多的参数
B 能够指定默认值
C 能够更好地重载
D 能使用局部变量来持有状态
答案
1=>A 2=>D
STL中的函数对象
根据函数输入参数数目的不同,STL中常用到的函数对象:
发生器
没有任何参数,返回值为某个类型。比如:随机数产生器
一元函数
一个输入参数,返回值类型可能与输入参数不同
二元函数
两个输入参数,返回值可能与输入参数不同
一元判定函数(一元谓词)
返回值是 bool 的一元函数
二元判定函数(二元谓词)
返回值是 bool 的二元函数
STL算法的通用性
STL算法希望能够通用,它采用:
- 模板定义,与具体的类型隔离开,同时又不损失运行期效率。
- 函数对象,与具体的行为细节隔离开,可以接受函数对象或者函数指针
int add(int a, int b){ return a + b; }
struct MyF{
int operator() (int a, int b){ return a*b; }
};
int main()
{
int a[] = {1,2,3,4,5,6,7,8,9};
const int N = sizeof(a)/sizeof(int);
cout << accumulate(a, a+N, 0, add) << endl;
cout << accumulate(a, a+N, 1, MyF()) << endl;
return 0;
}
注意,需要 #include
accumulate 算法是通用的,它工作在迭代器上,与容器类型无关。
它把元素逐个==累积==起来,至于如何累积,需要用户自己传入一个函数来决定。
这有些类似于,函数式编程语言中的:reduce
再举个一元谓词的例子:
inline bool f(char x) { return x>='0' && x<='9'; }
int main()
{
string s = "1234abcd5678xyz";
cout << count_if(s.cbegin(), s.cend(), f) << endl;
return 0;
}
STL内建函数对象
STL提供了大量的内建函数对象,都是模板。
主要包含算数运算,关系运算,逻辑运算三大类型。
常用的列举如下:
名称 | 类型 | 行为描述 | ||
---|---|---|---|---|
plus |
二元 | 加法: arg1 + arg2 | ||
minus |
二元 | 减法:arg1 - arg2 | ||
multiples |
二元 | 乘法:arg1 * arg2 | ||
divides |
二元 | 除法: arg1 / arg2 | ||
modules |
二元 | 模取: arg1 % arg2 | ||
negate |
一元 | 取负值:-arg1 | ||
equal_to |
二元 | 等于:arg1 == arg2 | ||
not_equal_to |
二元 | 不等于:arg1 != arg2 | ||
greater |
二元 | 大于:arg1 > arg2 | ||
greater_equal |
二元 | 大于等于:arg1 >= arg2 | ||
less |
二元 | 小于: arg1 < arg2 | ||
less_equal |
二元 | 小于等于:arg1 <= arg2 | ||
logical_and |
二元 | 逻辑与:arg1 && arg2 | ||
logical_or |
二元 | 逻辑或:arg1 \ | \ | arg2 |
logical_not |
一元 | 逻辑非:!arg1 |
还是前边例子,如果用内置函数对象,就是:
int a[] = {1,2,3,4,5,6,7,8,9};
const int N = sizeof(a)/sizeof(int);
cout << accumulate(a, a+N, 1, multiplies<int>()) << endl;
再举个排序的例子:
#include <iostream>
#include <algorithm>
#include <iterator> // ostream_iterator
using namespace std;
template <typename T, int N>
void mysort(T(&a)[N])
{
sort(a, a+N, greater<T>());
}
template <typename T, int N>
void disp(T (&a)[N])
{
copy(a, a+N, ostream_iterator<T>(cout, " "));
cout << endl;
}
int main()
{
int a[] = { 15, 22, 13, 28, 3, 16, 4, 27, 11 };
mysort(a);
disp(a);
return 0;
}
这里,再次看到c++泛型编程的强大威力。
copy 把源迭代器的元素值,写入到目标迭代器的位置。它并不关心目标的类型,源的类型,可以适用于各种支持迭代器的的对象。
sort 也不清楚容器的物理结构,它只负责排序算法(比如快速排序),也不管怎样去比较元素的大小,元素的比较问题就是由外部传入的函数对象管理的。
而,这个函数对象可能又转而把这个责任交给 T 类型的元素自身。
实时效果反馈
1. 关于STL与函数对象, 说法正确的是:__
A STL的算法参数中,默认传递函数对象的引用
B STL的算法参数中,默认传递函数对象的指针
C STL的算法参数中,默认传递函数对象的拷贝
D STL的算法参数中,默认传递函数对象的成员函数
2. 一元谓词,指的是哪种函数?:__
A 有一个参数,返回 bool 类型
B 有一个参数,反追指针类型
C 有二个参数,返回 bool 类型
D 有二个参数,返回相同类型
答案
1=>C 2=>A
STL通用容器
STL主要提供了通用容器、通用算法及其辅助设施。
容器是容纳一组某种特定类型元素的复合类型。
STL容器可分为:
顺序容器
按照线性排列来存储元素
vector deque list
关联容器
与顺序容器比,关联容器更注重高效地检索数据的能力。把元素值或它的部分作为键,来检索元素的存储位置。
set multiset map multimap
适配器
对已有的容器进行某种封装,不是真正的新容器。
stack queue
所有容器的共性
- 所有的容器都是值语义。赋值会引起元素的复制。
- 默认的构造会建造一个空容器
- 支持关系运算符:==,!=, <, <=, >, >=
- begin(), end() 获得容器首、尾后迭代器
- clear() 将容器清空
- empty() 判断容器是否为空
- size() 容器中元素的个数
- s1.swap(s2) 互换两个容器的内容
- S::iterator, S::const_iterator 迭代器类型
不同的结构,不同的性能
以顺序容器为例:
vector 内部是数组,与普通数组不同,是动态增长的
可以快速随机访问
尾巴插入、删除最容易
中间插入、删除与元素个数和位置有关
deque 内部也是数组,但采用分块数组的策略,头尾端都可以增加新块。
可以较快速随机访问(较vector慢)
头部、尾部 增删都容易
中间插入删除更困难些
list 双向链表
可以双向遍历,不能随机访问
任意位置插入、删除都很容易
“坑”也通用
这里介绍一个迭代器失效问题
string s = "abcdefg";
for(auto i=s.begin(); i!=s.end(); ++i){
cout << *i << endl;
if(*i=='g') s.erase(i);
}
cout << s << endl;
本意是删除串中所有的 ‘g’
但,执行完 erase 后,迭代器就失效了,再调用 ++i,行为不确定
我们可以试试其它容器, vector, deque, list ….
每个容器都有很不同的反应,有些时候十分诡异!
实时效果反馈
1. 关于STL容器, 说法正确的是:__
A list 是关联容器
B vector 不可以在头部插入数据
C list 在任何位置插入删除都是常数时间,与元素个数无关
D deque 不支持随机访问元素
2. 对于STL容器,容器.end()
,含义是:__
A 取得反向迭代器
B 取得正向迭代的尾后迭代器
C 取得最后一个元素的指针
D 取得指向最尾元素的迭代器
答案
1=>C 2=>B
vector
c 的原生数组创建后大小就不可改变。堆中开数组,容易忘记释放。
vector 可以理解为动态数组。它在需要的时候,可以自动扩容。
比较优势:尾部的添加、删除;随机访问。
注意,以下代码都需要头文件:#include
初始化
int x[] = {10,20,30};
vector<int> a; // size为 0
vector<int> b(5); // 指定初始大小,元素随机值
vector<int> c(5,9); // 5个元素,每个都是9
vector<int> d(c); // 拷贝构造
vector<int> e(d.begin(), d.end()); //重要,通过另一个容器构造自己
vector<int> f(x,x+3); //真假指针通吃
读取元素的方法
v[n] 下标访问,可读可写
v[2] = v[3] + 10;
front, back
v[0] v[v.size()-1] 同样
迭代器访问,读取时尽量用 const_iterator
vector<int> a(5,9);
for(auto i=a.cbegin(); i!=a.cend(); ++i){
cout << *i << endl;
}
常用操作
assign
类似于初始化,不过是对象已经存在后,重新赋值
vector<int> a;
a.assign(5,9);
cout << a[4] << endl;
push_back
, pop_back
在尾部可以高效地操作
注意,由于效率问题,没有对应的 push_front, pop_front
vector<int> a;
a.push_back(10);
a.push_back(20);
a.push_back(30);
a.pop_back();
cout << a.back() << endl;
empty
, clear
判断是否为空,清空vector中元素(不释放空间)
erase
删除一个元素,或一个区间内的元素。参数是迭代器。
vector<int> a;
for(int i=0; i<10; i++) a.push_back(i*10);
a.erase(a.begin());
a.erase(a.begin()+3,a.begin()+5);
for(int i=0; i<a.size(); i++) cout << a[i] << " ";
cout << endl;
insert
任意位置插入一个元素,或者插入一个迭代区间
vector<int> a(5,9);
int b[] = {1,2,3,4,5};
a.insert(a.begin(), 100);
a.insert(a.begin()+1, b+2,b+5);
for(int i=0; i<a.size(); i++) cout << a[i] << " ";
cout << endl;
整体比较 ==,!=,>=,>,<=,<
与算法的配合
需要头文件:#include
sort 排序
string s = "sothat";
vector<char> a(s.begin(), s.end());
sort(a.begin(), a.end());
copy(a.begin(), a.end(), ostream_iterator<char>(cout," "));
cout << endl;
copy 从一个迭代区间拷贝到另一个迭代器
find 在迭代区间查找
reverse 迭代区间元素反转
实时效果反馈
1. 关于vector, 说法正确的是:__
A vector 内部用数组实现,可以根据需要扩大
B vector 内部用多个数据块拼接
C vector 在头部和尾部添加比较高效
D vector 随机访问效率随着元素增加而降低
2. 如果我们想显示vector中所有元素,又不想写循环,怎样最简便?__
A 使用 printf
B 使用 copy 算法
C 重载 operator<<
D 使用递归函数
答案
1=>A 2=>B
vector(2)
vector 内部是用数组实现的。
当空间不够时,会申请更大的空间。怎样去观察这个空间的大小?
size 与 capacity 区别
vector<int> a(10);
a.pop_back();
cout << a.size() << endl; // 9
cout << a.capacity() << endl; // 可能是10
cout << a.max_size() << endl; // 很大的数
如果希望预留大空间,不希望多次“搬家”
可以用 v.reserve(999999); 预留一个大空间
vector的内存策略,只增大,删除或clear都不会自动释放空间,析构时自动释放
如何释放多余的空间?可以新建vector,拷贝老数据出去。
在 c++11 标准
中新增加了 shrink_to_fit
方法来缩小多余的空间。
但此调用仅仅是对编译器发出请求,并没有强制性。
自定义类型
当心,使用自定义类型,有些编译器需要全局定义。
如果,希望容器比较,自己的类型要可以比较 ,重载 operator<
struct Point{
int x;
int y;
bool operator< (const Point& t) const { return x < t.x; }
friend ostream& operator<< (ostream& os, const Point& t)
{
os << "(" << t.x << "," << t.y << ")";
return os;
}
};
调用处:
vector<Point> a;
a.push_back(Point{2,5});
a.push_back(Point{3,1});
a.push_back(Point{1,8});
sort(a.begin(), a.end()); // 需要用户类 operator<
copy(a.begin(), a.end(), ostream_iterator<Point>(cout, " "));
cout << endl;
at 与 [] 的区别
引用第 i 个元素,可以用:
v[i], 也可以用 v.at(i) 可以读,也可以写:
v.at(5) = xxx; // 写入 v[5] = xxx;
xxx = v.at(3);
at() 返回的是元素引用,所以可以赋值。
使用 at 的好处是,会检查数组是否越界,越界会抛出异常
删除大量元素的策略
在使用STL迭代器时,要牢记:删除操作后,迭代器可能失效
那么,如何处理遍历过程中的删除呢?
vector<int> a;
for(int i=1; i<=10; i++) a.push_back(i);
for(auto i=a.begin(); i!=a.end();){
if(*i % 2 == 0)
a.erase(i);
else
++i;
}
可以看到,对vector而言,删除后,下一个元素换到当前的删除位置,因此,迭代器不需要进行 ++i 的动作了。
实时效果反馈
1. 关于vector预留空间的释放, 说法正确的是:__
A vector 会在clear() 后释放当前空间
B vector会在 size 远小于 capacity的时候,释放当前空间
C vector会在 size 为 0 时释放当前空间
D vector只会在析构时,释放当前空间
2. vector.at(i),说法正确的是:__
A at(i) 返回第 i 个元素的值
B at(i) 返回第 i 个元素的引用
C at(i) 当访问越界时,会返回 0
D at(i) 不会进行越界检查
答案
1=>D 2=>B
vector应用
只是明白语法,了解原理,意义不大。
在实际问题中反复演练,在工程实践中不断打磨,才能真正掌握。
题目
给定一个正整数 n,拆分为多个正整数的和,有多少拆分方法? n = 6 5 + 1, 4 + 2, 3 + 3 4 + 1 + 1, 3 + 2 + 1 1 + 1 + 1 + 1 + 1 + 1 等都是合适的拆分。 因为加法的交换律,不同次序的拆分算同一种。 比如:3 + 2 + 1 与 2 + 1 + 3 是同一种 题目的要求是,根据 n,列出所有的拆分形式。
思路
对一个大问题,可以划分为多个子问题。 如果,每个子问题解决了,把结果收集整理,即是大问题的解。
这是我们人类解决复杂问题的最基本手段。
把所有的拆分按如下划分为等价类 1 + … 2 + … 3 + … 等 也就是说: 4 + 2, 4 + 1 + 1 就应该算做同一子类
接下来,怎么解决次序无关呢? 我们不妨约定: 后边的数字不可以大于前边的数字。 也就是对 n 进行分解时,同时还增加个限高来约束。
解决方案
typedef vector<int> VI;
typedef vector<VI> VVI;
// n: 待分解, h: 限高
VVI f(int n, int h)
{
VVI rt;
if(n==0){
rt.push_back(VI());
return rt;
}
for(int i=1; i<=n; i++){
if(i>h) break;
VVI t = f(n-i, i);
for(auto k=t.cbegin(); k!=t.cend(); ++k){
VI t2;
t2.push_back(i);
t2.insert(t2.end(),(*k).begin(), (*k).end());
rt.push_back(t2);
}
}
return rt;
}
实时效果反馈
1. v1, v2 是vector, 如何实现 v1,v2 的拼接?__
A copy(v2.begin(), v2.end(), v1.end())
B v1.insert(v1.begin(), v2.begin(), v2.end())
C v1.insert(v1.end(), v2.begin(), v2.end())
D v1 + v2
2. 如果一个vector 的元素也是vector,如何定义?:__
A
vector<vector<int>> x;
B
vector<<vector>int> x;
C
typedef vector T; vector<T> x;
D
vector<>vector<int> x;
答案
1=>C 2=>A
deque
deque在逻辑上是一片连续的存储区,与array, vector一样
但,存储实现上,是许多独立的“块”,deque主要任务是管理这些块的指针,使得外部看起来,是一整片空间。
因为随机访问效率低于 vector,
当数据较多的时候做sort,可以考虑先放入 vector, sort 后再copy 回 deque
特色
操作上,大部分与 vector 相同
push_front,pop_front 有很高的效率。
具有双端队列性质的应用,用deque很合适
deque<int> a = {1,2,3};
a.push_front(0);
a.push_front(-1);
a.push_back(4);
copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));
约瑟夫环问题
n个小朋友排队站一个圆圈,从1号开始报数,报到3出列,下一个人重新从1报数
求最后剩下谁?
思路:
让小朋友站一队,报1,报2的小朋友跑到队尾
报3的小朋友直接出队
最后剩下一个…
deque<int> a;
int N = 10;
for(int i=1; i<=N; i++) a.push_back(i);
while(a.size()>1){
a.push_back(a.front());
a.pop_front();
a.push_back(a.front());
a.pop_front();
a.pop_front();
}
cout << a[0] << endl;
实时效果反馈
1. 关于STL的deque存储结构, 说法正确的是:__
A 双向块链存储
B 分块数组存储
C 双向链表
D 二叉树
2. vector 比 deque 的优势在?:__
A 只扩容,不收缩
B 在头部、尾部都可以快速增删元素
C 扩容时,不需要大规模拷贝元素
D 随机访问速度快
答案
1=>B 2=>D
array
有的时候我们需要的仅仅是一个定长数组,并没有动态需要,用vector有点大材小用。
array基本等同于原生数组,是c++11
才引入的容器。
优点:
- 使用 at() 访问元素可以防止数组越界的尴尬
- 当作参数传递时,同时传递了数组的大小,不用像c语言那样,传2个参数
- 符合容器的接口,是值语义,两个数组见复制很容易。
- 创建多维数组更方便,直观
注意:
array完全是对原生数组的封装,只包含数据元素本身,甚至没有保存大小(由编译器记录)
array把原生数组加了浅表封装,使之可以兼容普通容器的功能。
swap 的行为是交换所有元素,而不是互换指针控制权。
示例
array<int,3> a = {10,20,30};
array<array<int,3>,2> b = {1,2,3,4,5,6};
b[0] = a; // 数组作为值语义整体赋值
cout << b[0][1] << endl;
cout << b[1][2] << endl;
copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout," "));
示例2
螺旋填充一个矩阵,形如:
可以看到,规则就是:先向右行进,遇到已经填充的数字或是边界,则右转弯。
直到填满整个数组。
所谓:行进方向,可以用行的增量与列的增量来联合描述。
所谓改变方向也不要想复杂,因为只有4种方向,是有规律循环的。
我们把问题焦点放在:“遇到数字或出界” 这个条件上,能不能很简练地表达呢?
此处,用 array 就可以简练,
因为,所谓的“出界”就是:当抛出异常时。。。
可以让“遇到数字”也抛出异常,这样就统一了。
template <int M, int N>
void fill(array<array<int,N>,M>& da)
{
struct{ // 定义了匿名类的对象
void next() { p = (p+1) % 4; } // 下一个偏移模式
int i(){ return da[p][0]; } // 当前行偏移
int j(){ return da[p][1]; } // 当前列偏移
int da[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
int p = 0; // 当前偏移模式
} dr;
int i=0,j=0; // 当前位置。i=行,j=列
for(int k=1; k<=M*N; k++){
da[i][j] = k;
try{
if(da.at(i+dr.i()).at(j+dr.j())>0) throw 1;
}catch(...){
dr.next();
}
i += dr.i();
j += dr.j();
}
}
调用处:
const int M = 4;
const int N = 5;
array<array<int, N>,M> x;
// 清零
for(int i=0; i<x.size(); i++) x[i].fill(0);
// 螺旋填充
fill<M,N>(x);
// 结果显示
for(int i=0; i<x.size(); i++){
for(int j=0; j<x[i].size(); j++)
cout << setw(3) << x[i][j];
cout << endl;
}
实时效果反馈
1. 关于array比原生数组的优势, 说法==不正确==的是:__
A 提供了与其它STL容器一致的接口
B at() 访问时,会进行越界检查
C 无论传引用还是指针,都会携带size信息,不会产生“指针退化”现象
D 可以用字面常量为数组赋初值
2. array虽然是浅封装,仍然提供大量成员函数,下列哪个==没有==提供?:__
A size()
B begin()
C swap()
D capacity()
答案
1=>D 2=>D
韩信分油问题
韩信走马分油问题,国外也称为:泊松分酒问题。
很古老的益智游戏。
话说韩信骑马街上遛弯儿,遇到两个人在路边为分油而发愁。
大葫芦中装满油,10斤。
另有 3 斤,7斤葫芦。
问,如何导出 5 斤油来。
韩信还没下马,就想出了答案。。。。
这个问题一般化:
有 N 个容器,容量 V1, V2, V3 …,当前装的油量:v1, v2, v3, …
求如何腾挪,出现 x 在任意容器中。
分析
我们可以把所有瓶子中的油量看作是一种状态,
从当前状态 s0 出发,所有可以的倒油法,会产生新的状态 s11, s12, s13…
由这些新状态出发,又能产生新状态 s21, s22, s23, s24, …
直到我们撞上目标,或者,无法产生出不重复的新状态。
其实,这就是: BFS 广度优先遍历。
只不过,这些状态并非是先就存在,而是计算中动态产生的而已。
实现
template <int N>
struct Bottles{
array<int,N> V; //容量
array<int,N> v; //现量
bool operator==(const Bottles& t) const {
for(int i=0; i<N; i++) if(v[i] != t.v[i])
return false;
return true;
}
// 从第 i 个瓶子倒入 第 j 个瓶子
bool flow(int i, int j){
if(i==j) return false;
if(v.at(i)==0) return false; // 源瓶空
if(v.at(j)==V.at(j)) return false; // 目标瓶满
if(v[i] + v[j] < V[j]){
v[j] += v[i];
v[i] = 0;
}
else{
v[i] -= V[j] - v[j];
v[j] = V[j];
}
return true;
}
friend ostream& operator<<(ostream& os, const Bottles<N> t){
cout << "(";
for(int i=0; i<N; i++) cout << t.v[i] << " ";
cout << ")";
return os;
}
};
template <int N>
bool ok(const Bottles<N>& bt, int goal)
{
auto it = find(bt.v.cbegin(), bt.v.cend(), goal);
return it != bt.v.cend();
}
template <int N>
bool fen(Bottles<N> bt, int goal)
{
deque<Bottles<N>> da; // 工作队列,执行BFS
da.push_back(bt);
vector<Bottles<N>> his; // 记录所有已知的状态
his.push_back(bt);
while(true){
int n = da.size();
if(n==0) return false;
for(int i=0; i<n; i++){
auto t = da.front();
if(ok(t, goal)) return true;
da.pop_front();
for(int i=0; i<N; i++)
for(int j=0; j<N; j++){
auto t2 = t;
if(!t2.flow(i,j)) continue;
if(find(his.cbegin(), his.cend(), t2) != his.cend()) continue;
cout << "?" << t2 << endl;
his.push_back(t2);
da.push_back(t2);
}
}
cout << "?------------" << endl;
}
}
实时效果反馈
1. 下列哪种情况,更适合用array 而不是vector:__
A 数组大小在开始是知道的,并且不会在运行期间变化
B 数组只在尾巴添加,不会在其它位置插入数据
C 数组中的元素是用户定义的类型
D 数组中的元素是指针类型
2. 下列哪种情况,更适合用 deque,而不是 vector:__
A 数组在中间位置频繁添加和删除
B 数组的首尾两端都有频繁增删的情况
C 数组规模特别巨大
D 多维数组,各个维度的大小都可能动态变化
答案
1=>A 2=>B
list
以双向循环链表实现。
优势在于:任何位置插入删除都是常数时间,与规模无关。
缺点:不能随机访问。
大量的通用容器方法很容易理解:
front, back, push_back,push_front, pop_back, pop_front 等
begin, cbegin, rbegin …
clear, erase, size, empty …
特有的函数
unique() 删除相邻的重复元素
splice() 从第2个链表剪切,拼接到第1个链表
merge() 归并法,把链表2,并入链表1
sort() 对链表排序,默认升序
remove() remove_if() 删除特有的元素
参考示例
排序
list<int> a = {5,7,10,22,15,3,6,5,1,1,5};
a.sort();
a.unique();
copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));
归并
list<int> a = {1,3,5,7,9}; // 已经有序
list<int> b = {2,2,6,10}; // 已经有序
a.merge(b);
copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));
剪切与拼接
list<int> a = {1,2,3,4,5};
list<int> b = {10,20,30,40};
auto i = a.begin();
advance(i,3);
a.splice(i, b); // 也可以只剪切 b 的一部分
copy(a.cbegin(), a.cend(), ostream_iterator<int>(cout, " "));
cout << endl;
copy(b.cbegin(), b.cend(), ostream_iterator<int>(cout, " "));
cout << endl;
注意与 insert 的区别,这里剪切-粘贴,insert是:拷贝-粘贴
很有意思的是,list 是双向循环链表,可以很高效地实现“循环移动”效果:
list<int> a = {1,2,3,4,5};
auto i = a.begin();
advance(i,2);
a.splice(a.begin(), a, i, a.end()); // 3,4,5,1,2
链表做过滤是很高效的:
bool is_digit(const char& x)
{
return x >= '0' && x <= '9';
}
/// 调用处:
string s = "a1b2c33de4f";
list<char> a(s.cbegin(), s.cend());
a.remove_if(is_digit);
copy(a.begin(), a.end(), ostream_iterator<char>(cout, " ");
实时效果反馈
1. 关于list, 说法正确的是:__
A list内部实现用单链表
B list 可以快速随机访问元素
C list 可以快速在任何位置插入、删除
D list 比vector更节约存储空间
2. 下列哪个动作,list 比 vector 更高效?__
A 容器 a 的值复制到容器 b
B 删除容器中符合某种特征的元素
C 大量在尾部添加新元素
D 把容器的中间位置一分为二
答案
1=>C 2=>B
stack与queue
stack与queue属于容器适配器。
它们默认在底层封装了一个 deque容器。
stack 和 queue 都是弱化了 deque 的功能,把 deque的使用限定在特定的逻辑层上。
强限定可以在使用时更安全,在大型项目开发上,也便于追踪、排查错误。
工程上的实践是:要求程序员一定不要xxx,往往没有多少约束力;让编译器限定他能做哪些动作,不能做哪些,要有效得多。
stack
stack 实现后进先出(LIFO)的逻辑。
如果把 deque 比作透明的软管,两端可以方便操作,也可以看见任意元素,并且可以在中间吃力地操作元素;
那么,stack 就是一个不透明的带底的硬桶。只能在一端操作,
此外,没有办法看见或者操作中间的元素
注意,stack 不提供迭代器,没有办法遍历所有元素!
逆波兰表达式计算
(1 + 2)* (3 + 4) 是我们常见的表达式的形式,运算符在中间,很直观。
缺点是:有时,需要用括号控制计算次序。
一般地:
a + b
为中缀形式;
+ a b
为前缀形式,也成为波兰表达式;
a b +
则为后缀形式,也称为逆波兰表达式
后缀形式有 2 个优点: 1. 不需要括号, 2. 方便机器求值
比如,(1 + 2) * (3 + 4)
的后缀形式为:1 2 + 3 4 + *
求值的过程是:
- 遇到操作数,则压入栈中
- 遇到操作符,则从栈中弹出两个操作数,计算后再压入栈中
stack<int> w;
stringstream ss("10 20 + 1 2 + *");
string s;
while(ss >> s){
if(s=="+" || s=="*"){
int b = w.top(); w.pop();
int a = w.top(); w.pop();
if(s=="+")
w.push(a + b);
else
w.push(a * b);
}else{
w.push(stoi(s));
}
}
cout << w.top() << endl;
注意:与多数高级语言不同
stack.pop() 只是删除,并不返回栈顶的元素,只能通过stack.top() 返回引用
这样设计是为了高效率。
queue
与stack情况类似,只是,queue 实现先进先出的逻辑。
可以把queue比作一个不透明的硬管子,并且标明了流向。
它的一端只能插入,另一端只能取出。
我们同样没有办法遍历其中的元素。
实时效果反馈
1. 关于STL的stack, 说法正确的是:__
A stack底层是数组
B stack底层是链表
C stack是对deque的再封装,底层是分块数组
D stack可以遍历所有元素
2. STL提供适配器类型容器的目的是?:__
A 提高编译速度
B 提高运行效率
C 增加安全性,方便调试和维护
D 兼容 c 库
答案
1=>C 2=>C
迷宫问题
看如下的迷宫问题:
一种朴素的想法是:
把入口标为 0,
所有和它相连的通路标为 1
所有和1相连的通路标为 2 (已经标记过的略过)
….
重复这个动作,直到撞上出口,所标数字即是解
如果标记进行不下去,仍没有碰到出口,则为无解
从文件读入数据
由于测试数据可能很大,我们可以从文件读入。
文件格式规定为:
行数 列数
第一行。。。。
第二行。。。。
。。。。
因为STL对流的统一抽象,从文件读入和从控制台读入几乎没什么差别。
读入的数据存入二维数组。
但这个数组是动态大小的,可以用 vector 来模拟。
具体地说,我们打算做一个函数 read, 其格式如下:
vector<vector<char>> read(const char* filename)
注意对迭代器和通用算法的应用:
vector<vector<char>> read(const char* fname)
{
ifstream in(fname);
int M, N;
in >> M >> N;
vector<vector<char>> t(M, vector<char>(N,'1'));
in.ignore();
string s;
for(int i=0; i<=M; i++){
getline(in, s);
copy(s.cbegin(), s.cend(), t[i].begin());
}
return t;
}
这里还有个小陷阱:
当读入了两个整数后,回车被剩在缓冲区中,导致下一个getline读到一个空行。
为解决这个十分普遍的问题,输入流提供了 ignore() 可以读取并丢弃一些内容。
默认就是读到第一个换行符的出现。
增加围墙
为了将来处理方便,我们把整个数据外围增加一圈墙,以解决访问越界问题
vector<vector<char>> read(const char* fname)
{
ifstream in(fname);
int M, N;
in >> M >> N;
vector<vector<char>> t(M+2, vector<char>(N+2,'1'));
in.ignore();
string s;
for(int i=1; i<=M; i++){
getline(in, s);
copy(s.cbegin(), s.cend(), t[i].begin()+1);
}
return t;
}
数据准备好后,就可以继续后边的工作了(待续)
实时效果反馈
1. 对于大小不能事先确定的整型二维数组, 用以下哪个结构比较方便?__
A array
> B vector
> C vector
> D 原生数组
2. 二维数组a, a[2][3]
与a.at(2).at(3)
的区别是:__
A 用 at 访问进行越界检查,可能抛出异常
B at访问比 [ ] 访问更高效
C at 只能读取元素,不能改写
D at 是c++11 才有的标准
答案
1=>B 2=>A
迷宫问题(2)
当前所处的位置可以做成对象:
struct Point{
int row; int col; // 当前的行号,列号
array<Point, 4> go(){
array<Point, 4> t;
t[0] = Point{row-1, col};
t[1] = Point{row+1, col};
t[2] = Point{row, col-1};
t[3] = Point{row, col+1};
return t;
}
};
我们提供一个go函数,返回从当前位置能走到的4个位置。
这个返回值是固定大小的,用 array 合适,c语言无法直接返回原生数组。
解法:
int solve(vector<vector<char>>& da)
{
struct Point{
int row; int col;
array<Point, 4> go(){
array<Point, 4> t;
t[0] = Point{row-1, col};
t[1] = Point{row+1, col};
t[2] = Point{row, col-1};
t[3] = Point{row, col+1};
return t;
}
};
queue<Point> w;
w.push(Point{1,1});
int k = 0;
while(w.size()>0){
int n = w.size();
for(int i=0; i<n; i++){
auto t = w.front();
if(t.row == da.size()-2 && t.col == da[0].size()-2)
return k;
auto tt = t.go();
for(auto j=tt.cbegin(); j!=tt.cend(); ++j){
if(da[(*j).row][(*j).col]=='.'){
da[(*j).row][(*j).col] = '*';
w.push(*j);
}
}
w.pop();
}
k++;
}
return -1;
}
可以看到,本题目的核心算法仍然是 BFS,
BFS一般并不需要访问中间的元素,只是从一端读取数,向另一端压入数,
此种情形,用queque就足够了。
实时效果反馈
1. 求最短路径问题,一般可以用:__
A 深度优先搜索算法
B 广度优先搜索算法
C 递归算法
D 二分搜索算法
2. 在queue上,==不可以==执行的操作是?:__
A front() 读取元素
B pop() 删除元素
C size() 求包含元素个数
D begin() 取得正向迭代器
答案
1=>B 2=>D
集合
数学上的集合包含了若干元素,其中每个元素都是唯一的。
类似于数学上的集合概念。set可以高效率地判断一个元素是否在本集合中。
可以从集合中高效率地删除元素、向集合中插入元素。
但,不支持随机访问。
STL 的 set 是关联型容器。内部存储结构是非线性的。
其内部以二叉树形式实现。并且,为了增删的高效,使用了红黑树。
当频繁增删时,较AVL树有更高的效率。
红黑树是一种排序二叉树,元素间必须可比较大小。
如果元素是自己定义的类型,必须实现比较操作 operator<
简单示例
set<int> a = {1,2,3,4};
a.insert(4);
a.insert(5);
a.insert(6);
for(auto i=a.begin(); i!=a.end(); ++i) cout << *i << " ";
cout << endl;
判断元素是否存在
set<int> a = {1,2,3,4};
if(a.find(3) != a.end()) cout << "exist" << endl;
cout << a.count(3) << endl;
有两种方法。
注意:find 的操作与其它容器是一致的,找到则返回其迭代器,找不到返回 a.end()
自定义的元素类型
struct STU{
string name;
int age;
bool operator<(const STU& t) const {
if(age==t.age) return name < t.name;
return age < t.age;
}
};
注意:
自定义类型一定要重载 operator< 来决定元素间的大小次序。
同时,也决定了元素间是否相等。
相等的定义是:
a > b, b > a 都是 false
测试:
set<STU> a;
a.insert(STU{"zhang", 15});
a.insert(STU{"li", 16});
a.insert(STU{"tao", 10});
a.insert(STU{"mao", 10});
for(auto i=a.begin(); i!=a.end(); ++i)
cout << (*i).name << ": " << (*i).age << endl;
实时效果反馈
1. STL的set容器, 底层采用的数据结构是:__
A AVL平衡二叉树
B 红黑树
C B+ 树
D 线索树
2. STL 的 set,对用户定义的类型,有什么要求?:__
A 重载 operator ==
B 重载 operator <
C 重载 operator <<
D 重载 operator !=
答案
1=>B 2=>B
集合的运算
数学上,集合有:交、差、并、补等运算。
这些功能并不在 set 中。
STL把此功能归为算法,适用于更多的数据类型,不仅仅是集合。
set_intersection : 交集
set_difference: 差集
set_union: 并集
set_symmetric_difference: 对称差
基本示例
以set_intersection为例,其调用格式:
需要第一个容器的迭代区间,第二个容器的迭代区间,
然后是一个输出迭代器,表示结果写到哪里去。
这是源代码:
template <class InputIterator1, class InputIterator2,
class OutputIterator>
OutputIterator set_intersection (
InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2,
OutputIterator result)
{
while (first1!=last1 && first2!=last2)
{
if (*first1<*first2) ++first1;
else if (*first2<*first1) ++first2;
else {
*result = *first1;
++result; ++first1; ++first2;
}
}
return result;
}
显而易见,这里并不要求特定的容器类型,
但要求元素已经排好了次序,容器支持遍历就可以了。
这里需要注意,输出迭代的容器要提供足够的存储空间,以 vector为例:
set<int> a = {1,2,3,4};
set<int> b = {3,4,5,6};
vector<int> t(10); // 需要留出足够空间
set_intersection(a.cbegin(),a.cend(),
b.cbegin(),b.cend(),
t.begin());
copy(t.cbegin(),t.cend(), ostream_iterator<int>(cout," "));
需要注意几点:
set_intersection 是通用的算法,不仅仅支持set,还可以支持其它容器
但要求:元素间需要能比较大小 operator<
a, b 都必须是已经有序的容器(事先排过序)
输出位置必须有足够的空间供给写出操作
实际上,set_intersection返回了输出迭代器的结束位置,我们可以保存它。
.....
auto end = set_intersection(a.cbegin(),a.cend(),
b.cbegin(),b.cend(), t.begin());
copy(t.begin(), end, ostream_iterator<int>(cout," "));
注意,如果希望输出的容器是个集合,该如何处理?
vector -> set ?
直接放个集合不可以:1. 没法预留空位, 2. 集合的迭代器是无法修改值的
实际上,我们的需求是,在输出迭代器写入的时候,自动变成插入操作。
set<int> a = {1,2,3,4};
set<int> b = {3,4,5,6};
set<int> c;
set_intersection(a.begin(),a.end(),b.begin(),b.end(),
insert_iterator<set<int>>(c,c.begin()));
copy(c.begin(), c.end(), ostream_iterator<int>(cout," "));
注意:嵌入头文件:
很多高级语言,提供了集合的运算符重载:
a + b 求并集,不改变 a, b 返回新的集合
a += b 求并集,直接改变 a
a - b 求差集, 不改变 a, b 返回新的集合
a -= b 求差集,直接修改 a
STL 没有提供这些功能,如果需要直接修改集合,其实可以直接调用set成员函数
a.insert 实现并集
差集就要自己用erase一个一个删除了,效率稍差些
a += b 效果
set<int> a = {1,2,3,4};
set<int> b = {3,4,5,6};
a.insert(b.begin(), b.end());
copy(a.begin(), a.end(), ostream_iterator<int>(cout," "));
a -= b 效果:
set<int> a = {1,2,3,4};
set<int> b = {3,4,5,6};
for(auto i=b.begin(); i!=b.end(); ++i) a.erase(*i);
copy(a.begin(), a.end(), ostream_iterator<int>(cout," "));
实时效果反馈
1. 如果集合a={1,2,3,4}, b={3,4,5,6} , a, b 的对称差为:__
A {1,2}
B {5,6}
C {1,2,5,6}
D {3,4}
2. setdifference 算法对参数的要求,哪个是不必要的?:_
A 输入容器必须是已排序
B 输入容器的元素必须支持是可比较的
C 输出迭代器所在容器已经预留好足够的空间
D 容器的元素类型重载了 operator==
答案
1=>C 2=>D
更多的集合
multiset
与 set 基本相同,只是它支持元素的重复。
当把重复的元素加入集合中,这些重复的元素同时存在。
可以用count(x) 返回重复元素的个数
multiset<int> a = {1,2,3,4};
a.insert(3);
a.insert(4);
a.insert(5);
a.erase(4);
for(auto i=a.begin(); i!=a.end(); ++i) cout << *i << " ";
cout << endl;
运行结果:
bitset
类似于一个元素值为0或1的 array
在逻辑上,等价于set
可以有效节约存储空间,并提高运行速度。
可以与string类型间相互转换
bitset<8> a("11110000");
cout << a[0] << "," << a[7] << endl;
a[7] = 0;
cout << a.to_string() << endl;
它的模板参数是含有的位数,可以非常大。
bitset 提供了丰富的与位运算有关的高效率的函数:
函数 | 功能 |
---|---|
b.any() | 整个set中是否存在1 |
b.none() | 整个set都是0,与any()反义 |
b.count() | 1 的个数 |
b.test(pos) | 与 b[pos] 读取同义 |
b.set() | 所有位都置为 1 |
b.reset() | 所有位置都清 0 |
b.flip() | 所有位取反 |
os << b | b中的位输出到流 |
应用示例
求 n 以内的所有素数的个数
当 n 较大时,最有效的方法仍然是古老的筛法。
template <int N>
int su()
{
bitset<N> da;
da.set();
da[0] = 0;
da[1] = 0;
for(int k=2; k<N; k++){
if(da[k]==0) continue;
int p = k + k;
while(p<N){
da[p] = 0;
p += k;
}
}
return da.count();
}
调用处:
cout << su<1000 * 1000>() << endl;
参数 N 必须是个编译期的常数,所以不能做成传入参数的形式。
实时效果反馈
1. 关于bitset, 说法正确的是:__
A bitset 可以支持用户自定义类型的元素
B bitset 可以支持元素为 string 类型
C 逻辑上,bitset元素固定为其size() 范围内的整数
D bitset 比用普通 set 省空间,但速度慢些
2. 求大范围内的所有素数,怎么计算较快?:__
A 逐一测试所有数字是否有因数
B 用高级数学公式,快速求出
C 用古老的筛法,配合按位运算
D 使用费尔马定理
答案
1=>C 2=>C
映射
map是关联容器,提供对<键-值>结构的高效检索。
它的内部是通过红黑树实现,所以要求元素的键要支持比较大小的操作。
注意,仅仅对 key 有要求, value 则没有任何限制。
如果使用迭代器来操作,能修改value,但不能修改key。
map是类模板
定义对象时,需要两个参数: key 的类型和 value 的类型
map<string,int> a;
a["zhang"] = 70;
a["li"] = 58;
a["wang"] = 99;
a["zhang"] += 10;
a["li"] = 85;
cout << a["zhang"] << endl;
[ ] 操作有很多功能,可以
- 读取某个key的值,
- 也可以添加新的 key-value 键值对
- 也可以修改某个 key 的 value 为新值
注意:一个常见的陷阱,
当访问的 key 不存在的时候,会添加一个 key-value 到map中, value取缺省值
map<string,int> a;
a["zhang"] = 70;
a["li"] = 85;
a["wang"] = 99;
a["zhang"] += 10;
cout << a.size() << endl;
cout << a["zhang1"] << endl; // 会偷偷添加一个键值对
cout << a.size() << endl;
结果:
判断某个 key 是不是存在,可以用 m.find(key) != m.end() 来实现
也可以用 m.count(key)
这方面与 set 相同。
可以用 m.erase() 删除某个键值对,有3种格式
- m.erase(key) 删除某个值,返回的是删除的个数(0 或 1)
- m.erase(position) 给定迭代器,删除该位置键值对,返回下一个位置迭代器
- m.erase(first,last) 给定迭代区间,删除该区间元素,返回下一个位置迭代器
map 的遍历
虽然 map 底层是树形结构,仍然是可以遍历的。其遍历特性与 set 类似。
但,i 的类型是*键值对,在程序上表达为 pair
pair 是个模板类,仅仅就是把两个类型捆绑在一起而已。
访问的时候,用 p.first, p.second 即可。
map<string,int> a;
a["zhang"] = 70;
a["li"] = 85;
a["wang"] = 99;
a["zhang"] += 10;
for(auto i=a.begin(); i!=a.end(); ++i)
cout << i->first << ":" << i->second << endl;
结果:
map的用途
map 的优势在于能快速地定位元素,并携带关联信息。
如果广义地看,数组在逻辑上也可以看成是以 int 为 key 的 map
有些高级语言中只提供一种数据结构,就是 map,可见其用途之大。
比如,循环链表以 map 来表示。
约瑟夫环问题,可以用 1->2, 2->3, …. n->1 来表示小朋友间的关系。
这个思路的解决方案:
int N = 10; // 参加约瑟夫环的人数
map<int,int> da;
for(int i=1; i<=N; i++) da[i] = i + 1;
da[N] = 1;
int k = 1; // 当前持有令牌
while(da[k]!=k){
k = da[k];
da[k] = da[da[k]];
k = da[k];
}
cout << k << endl;
实时效果反馈
1. 关于STL中的map, 说法正确的是:__
A map 是序列型容器
B map 的底层实现是自平衡的排序二叉树结构
C x = map[key] 会修改 x 的值,不会修改map
D map 不支持元素的遍历
2. m是map类型的对象,对m.erase() 调用格式错误的是:__
A m.erase(某个键)
B m.erase(某个迭代器)
C m.erase(某个迭代区间)
D m.erase(某个键值对)
答案
1=>B 2=>D
map的应用
map 在保存数据间的关系方面特别有用,几乎是万能的数据结构。
前边课程中的【韩信分油】问题,如果求具体的分油方案,就可以用 map
思路
从初态 ${s_0}$到达某个终点态${s_k}$,我们希望通过 ${s_k}$来追溯它的历史:
${sk}$ => ${s{k-1}}$ => ${s_{k-2}}$ => … => ${s_0}$
解法
#include <iostream>
#include <vector>
#include <array>
#include <deque>
#include <map>
#include <algorithm>
using namespace std;
template <int N>
struct Bottles{
array<int,N> V;
array<int,N> v;
bool operator==(const Bottles& t) const {
return v == t.v;
}
bool operator<(const Bottles& t) const {
return v < t.v;
}
// 从第 i 个瓶子倒入 第 j 个瓶子
bool flow(int i, int j){
if(i==j) return false;
if(v.at(i)==0) return false; // 源瓶空
if(v.at(j)==V.at(j)) return false; // 目标瓶满
if(v[i] + v[j] < V[j]){
v[j] += v[i];
v[i] = 0;
}
else{
v[i] -= V[j] - v[j];
v[j] = V[j];
}
return true;
}
friend ostream& operator<<(ostream& os,
const Bottles<N> t){
cout << "(";
for(int i=0; i<N; i++) cout << t.v[i] << " ";
cout << ")";
return os;
}
};
template <int N>
bool ok(const Bottles<N>& bt, int goal)
{
auto it = find(bt.v.cbegin(), bt.v.cend(), goal);
return it != bt.v.cend();
}
template <int N>
vector<Bottles<N>> fen(Bottles<N> bt, int goal)
{
deque<Bottles<N>> da; // 工作队列,执行BFS
da.push_back(bt);
map<Bottles<N>, Bottles<N>> his; // 记录所有已知的状态
his[bt] = bt;
vector<Bottles<N>> rt;
while(true){
int n = da.size();
if(n==0) return rt;
for(int i=0; i<n; i++){
auto t = da.front();
if(ok(t, goal)) {
while(!(his[t] == t)){
rt.push_back(t);
t = his[t];
}
return rt;
}
da.pop_front();
for(int i=0; i<N; i++)
for(int j=0; j<N; j++){
auto t2 = t;
if(!t2.flow(i,j)) continue;
if(his.find(t2) != his.end()) continue;
his[t2] = t;
da.push_back(t2);
}
}
}
}
int main()
{
Bottles<3> bt;
bt.V = {10,7,3};
bt.v = {10,0,0};
vector<Bottles<3>> x = fen(bt,5);
for(int i=0; i<x.size(); i++)
cout << x[i] << endl;
return 0;
}
实时效果反馈
1. 关于通用find算法, 对容器中元素要求是:__
A 容器中元素要重载 operator<
B 容器中元素要重载 operator==
C 容器中元素要重载 operator()
D 容器中元素要重载 operator[ ]
2. 如果判断某个 key 是否在 map 中?合适的方案是:__
A 调用 map[key]
B 调用 STL 通用的 find 算法
C 调用 map 自身的 find
D 调用 map.test
答案
1=>B 2=>C
multimap
与 multiset 的情况类似,multimap与map基本相同,
除了它可以支持多个重复key的并存
可以通过count 检索重复元素的个数
multimap 因为同样key可以存在多个,所以插入的位置就有很多变化。
其 insert 较普通的 map 要复杂一些。
insert 示例
假设我们有一个应用,需要:名次-->姓名
这样的键值关系。
同一名次可能有多个人,给定一个名次,要立即返回所有这些人,用什么结构描述好呢?
multimap 就是不错的选择。
multimap<int, string> a;
a.insert(pair<int,string>(1, "zhang"));
auto it = a.insert(pair<int,string>(2,"wang"));
// 返回迭代器,指向新元素
a.insert(pair<int,string>(2,"li"));
a.insert(it, pair<int,string>(2,"zhao")); // 尽量在 it 之前插入
a.insert({{4,"zhou"},{4,"wu"},{4,"zheng"}}); // c++11 标准
for(auto i=a.begin(); i!=a.end(); ++i)
cout << i->first << ":" << i->second << endl;
多值查找
给定一个键,如何返回多个值?
通用的 find 可以找到第一个,再自己向后迭代。
equal_range
也可以更方便实现这个功能。
multimap<int, string> a;
a.insert(pair<int,string>(1, "zhang"));
auto it = a.insert(pair<int,string>(2,"wang"));
// 返回迭代器,指向新元素
a.insert(pair<int,string>(2,"li"));
a.insert(it, pair<int,string>(2,"zhao")); // 尽量在 it 之前插入
a.insert({{4,"zhou"},{4,"wu"},{4,"zheng"}}); // c++11 标准
auto pa = a.equal_range(2); // 返回一对迭代器 (起始,结束)
for(auto i=pa.first; i!=pa.second; ++i)
cout << i->first << ":" << i->second << endl;
应用示例
一个十分常见的应用场景:从 key —> value 的映射,获得它的逆映射:
即:value —> key
用原来的 value 作为key,原来的 key 为 value, 同时,又不能损失信息。
map<string, string> a;
a["xx1001"] = "zhang";
a["xx1002"] = "wang";
a["xx1003"] = "li";
a["xx1004"] = "zhao";
a["xx1005"] = "wang";
multimap<string, string> b;
for(auto i=a.begin(); i!=a.end(); ++i)
b.insert(make_pair(i->second, i->first));
cout << b.count("zhang") << endl;
cout << b.count("wang") << endl;
auto it = b.find("wang");
while(it != b.end() && it->first == "wang"){
cout << it->second << endl;
++it;
}
从multiset 中查询,可以用 equal_range,也可以用原来的 find
find 返回了迭代器,所有相同key的记录,必然存在相邻的位置,
所以让迭代器向后遍历即可。
实时效果反馈
1. 关于multimap, 说法正确的是:__
A 其中元素按
进行联合排序 B key相同的元素按插入的顺序排序
C key相同的元素默认按插入顺序排序,但可以通过程序控制插入位置
D 可以通过迭代器修改 key 和 value
2. multimap 与 vector 相比,哪个操作较慢?:__
A 查找某个元素
B 删除某个元素
C 求交集
D 取出第 n 个元素
答案
1=>C 2=>D
STL算法概览
STL作为c++标准的一部分,提供了十分实用、高效、健壮的基础设施。
学好STL,是提高生产力的有效途径。
同时,STL以源码的形式提供,可作为我们分析、学习程序设计的范本。
STL算法以函数模板的形式呈现。
这是c++特有的类型泛化方式,能同时坚持了三样东西:
类型泛化
这是算法抽象的要求,高级程序语言的必由之路。
静态类型
编译期类型、语法检查,把大多数错误消灭在编译阶段。
高执行效率
c++的模板采用展开与二次编译方式,把抽象局限在编译阶段,能够以极小的代价获得类型的灵活性与算法的通用性。
STL 的算法尽量保持了通用性,与具体的容器特性相隔离
STL 算法是以函数模板的形式存在的。
也就是说,提供的是源代码,需要经过二次编译。
即,当我们确定了模板参数后,编译器替我们展开为特定的具体函数,再进一步编译。
容器内是具体的操作目标,可以把容器看成:名词
仿函数,也叫函数对象,提供比函数更好的封装特性,是对可定制动作的抽象,看作:动词
函数适配器,把函数的接口规格进行适配、整合,看作:副词
可分类
非变易算法(nomodifying)
不修改所操作的内容(只是读取)
可变易算法(modifying)
修改操作对象
排序算法(sorting)
包括排序,合并,搜索,有序序列上的集合操作
数值算法(numeric)
数值计算,科学计算
其它算法
堆、关系运算、生成算法等
一般规律
迭代区间的表示
[first, last) 形成前闭后开的半开区间。
包含 first,不包含 last
迭代器的类型有 5 种,算法参数总是定义成它的最低要求
高类型的迭代器可以胜任低类型迭代器的工作,反之不可。
注意,迭代器的类型间并非继承关系,没有强制的类型约束
编译通过了,仍然可能有运行问题
加_if 后缀的,与无后缀的比较,需要额外的一个谓词函数(返回值为bool)
变易算法通常有两个版本,一个是在原来的容器上修改,称为 in-place (就地执行)
另一个是 copy 到另外的地点:一般为了区分,加 _copy 后缀
数值算法需要包含
, 其他算法需要包含:
实时效果反馈
1. STL算法引入iterator的目的是:__
A 使得算法与容器的具体类型隔离开,算法更具有一般性
B 使得所有操作通过指针完成,提高效率
C 使得算法可以兼容传统的 c 指针和 c++ 的对象
D 是的算法可以用模板完成,增强泛型能力
2. c++泛型策略比较于其它高级语言,特点是:__
A 类型更具有一般性
B 能完全兼容非面向对象(C语言)的编程模式
C 具有更高的一致性和内在的自洽性
D 提供抽象的同时,尽量做到极小的运行成本
答案
1=>A 2=>D
STL算法概览(2)
STL 算法强调效率,甚至无所不用其极。
来看用途十分广的 copy 算法
copy 不会分配空间,我们自己要确保有足够的空间
vector<int> a = {1,2,3,4,4,5,6,7};
array<int,4> b;
copy(a.end()-4, a.end(), b.begin());
copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));
也可以自己拷贝给自己,如果重叠,自己要当心:
vector<int> a = {1,2,3,4,5,6,7,8};
copy(a.begin(), a.begin()+4, a.begin()+2);
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
这里,根据容器的的类型,做了特化处理,实际调用 memmove 对overlap无惧
如果换成: list
list<int> a = {1,2,3,4,5,6,7,8};
auto i2 = a.begin();
i2++; i2++;
auto i4 = i2;
i4++; i4++;
copy(a.begin(), i4, i2);
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
注意,list 的迭代器并非可以随机访问,只好自己慢慢移动了。
当然,如果我们自己调整了拷贝的顺序,还是可以:
list<int> a = {1,2,3,4,5,6,7,8};
auto i4 = a.begin();
i4++; i4++; i4++; i4++;
auto i6 = i4;
i6++; i6++;
copy_backward(a.begin(), i4, i6);
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
如果,我们不希望自己开空间,可以用各种迭代器适配器。 ostream_iterator 便是。
注意包含
vector<int> a = {1,2,3,4,5,6,7,8};
vector<int> b;
copy(a.begin(), a.begin()+4,
back_insert_iterator<vector<int>>(b));
copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));
此外,还有 c++11 引入的:
这些被实践证明的很方便有用的模板风格值得学习,也胜于自己写循环。
set<int> a0 = {3,5,7};
struct F{
F(set<int> &t):s(t){};
set<int> s;
bool operator()(int x){
return s.find(x) == s.end();
}
};
vector<int> a = {1,2,3,4,5,6,7,8};
vector<int> b;
copy_if(a.begin(), a.end(),
back_insert_iterator<vector<int>>(b), F(a0));
copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));
实时效果反馈
1. STL的copy算法,会不会自动解决 overlap 问题 ?:__
A 不会,看重叠的位置的运气
B 会自动解决,根据重叠位置决定拷贝的顺序
C 对有些简单类型的情况进行了优化,多数类型不可
D 编译器不允许重叠拷贝
答案
1=>C
函数适配器
有的时候,在逻辑上,我们已经有了判别函数(即所谓的谓词函数),
但参数太多,或其它方面的格式不对,没法传入到算法模板里。
这个时候,函数适配器就派上用场了。
大约的分类:
类型 | 实现的功能 | 函数 |
---|---|---|
绑定适配器 | 对 n 元函数,绑定一个参数,使之成为 n-1 元函数 | bind1st, bind2nd |
组合适配器 | 谓词的结果取反 | not1, not2 |
指针适配器 | 把函数(或者指针),变为functor,以便被其它适配器使用 | ptr_fun |
成员函数适配器 | 把成员函数变为对象,可以被其它适配器使用 | mem_fun, mem_fun_ref |
示例
bool f(set<int>& t, int x)
{
return t.find(x) == t.end();
}
int main()
{
set<int> a0 = {3,5,7};
vector<int> a = {1,2,3,4,5,6,7,8};
vector<int> b;
copy_if(a.begin(), a.end(),
back_insert_iterator<vector<int>>(b),
bind1st(ptr_fun(f), a0));
copy(b.begin(), b.end(),
ostream_iterator<int>(cout, " "));
return 0;
}
注意,需要 include
例2
找到第一个大于10的元素
可以用STL的内置的函数对象模板 greater
vector<int> a = {1,2,31,45,5,16,7,28};
auto i = find_if(a.begin(), a.end(),
bind2nd(greater<int>(),10));
cout << *i << endl;
适配也可以反复组合,达到所需要的效果。
比如,f(x,y,z) 三个参数,我们希望把 x z 定死,只要y,就可以:
bind2nd(bind1st(ptr_fun(f), x0), z0)
比如,统计不大于10的元素的个数
vector<int> a = {1,2,31,45,5,16,7,28,10};
int x = count_if(a.begin(), a.end(),
not1(bind2nd(greater<int>(),10)));
cout << x << endl;
实时效果反馈
1. 关于函数适配器, 说法正确的是:__
A 函数适配器是为了解决函数参数类型不符合算法要求的情况
B 函数适配器主要目的是解决函数规格不符合算法要求,主要是参数目数不对
C 函数适配器不能嵌套使用
D 函数适配器只能用于函数对象,不能用于普通函数
2. std::greater是:__
A 函数适配器
B 普通函数
C 单目的函数模板
D 双目的函数模板
答案
1=>B 2=>D
匿名函数
c++11 提供了对匿名函数的支持。
匿名函数,也称为:lambda函数,或者:lambda表达式。
其格式:
[捕获变量列表](参数列表) -> 返回值类型 { 函数体 }
返回值类型能推断的时候,可以不写。
如果没有 return 语句,返回的是 void
如果没有参数,圆括号也可以省略。
匿名函数的用途
有的时候,在需要传入一个函数的时候,
定义全局的函数或者写一个类来生成函数对象都有点繁琐,有小题大做之意味。
函数是对用户定义动作的封装,最方便的写法是直接写在调用处,
类似立即数,字面常量的说法,
可以类比称为:立即码,字面常码。。。
很多的高级语言都提供了匿名函数,python, ruby, scala, go 。。。。
c++ 需要紧跟时代,c++11 引入了这个特性
千呼万唤始出来,仍然是为了效率和安全问题吵架。
闭包特性
可以捕获所在作用域的变量
可以按引用捕获,或按值捕获
[] //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[x, &y] //x 按值捕获, y 按引用捕获.
[&] //用到的任何外部变量都隐式按引用捕获
[=] //用到的任何外部变量都隐式按值捕获
[&, x] //x显式地按值捕获. 其它变量按引用捕获
[=, &z] //z按引用捕获. 其它变量按值捕获
示例
map<int,string> a;
a[5] = "zhang";
a[8] = "wang";
a[99] = "newton";
string s = "-->";
for_each(a.begin(), a.end(),[s](pair<int,string> p){
cout << p.first << s << p.second << endl;
});
再看 count_if 的新版本写法:
vector<int> a = {3,6,7,12,14,27,16,9,10};
int x = count_if(a.begin(), a.end(), [](int t){
return t <= 10;
});
cout << x << endl;
实时效果反馈
1. 关于匿名函数, 说法正确的是:__
A 匿名函数就是没有名字的函数指针
B 匿名函数就是没有名字的函数对象
C 匿名函数就是有闭包特性的临时函数
D 匿名函数是STL99的特性
2. 使用匿名函数时,如果没有写返回类型,则:__
A 视为写了 void
B 可以通过 return 类型来推断出来,没有return则为void
C 产生编译错误
D 视为返回 int
答案
1=>C 2=>B
移动语义
c++11 引入的特性。为了解决遗留问题,提高效率。
包括std::move(移动语义) 和 std::forward(完美转发)
左值(lvalue)和右值(rvalue)
左值就是可以取地址的值,也可以理解为有名字的值
右值则恰是其反面,比如:字面常量,临时对象,匿名对象等
可以简单地认为:所有不是左值的就右值
问题在?
先看一段代码:
string a = "abc";
string b = "xyz123";
string t = a;
a = b;
b = t;
cout << a << "," << b << endl;
这个字符串交换有什么问题吗?
有个效率的问题,比如,a,b都是几百兆的大串。
a = b; 时到底发生了什么?
执行了 operator= 对象赋值操作,如何执行?
仔细想想,不难发现,我们没必要这样大折腾,
仅仅让 a, b 交换它们的堆空间指针以及size不久好了吗?
也就是说,可不可以不要拷贝数据,只移交控制权?
解决方案
c++11 支持了这个想法。
std::move(x) 可以把 x 从左值变量变为它的右值引用,这样,在赋值时,
y = std::move(x) 的时候,调用 y 的 operator=(string&& t)
而不是 operator=(const string& t)
string a = "abc";
string b = std::move(a);
cout << a << "," << b << endl;
输出:
string 类的 operator=(string&& t) 中,直接把自己的指针指向 t 的内部空间,
并且把 t 的内部指针置空。
这不会严重影响程序以前的行为,
类 A 如果不定义 operator=(A&& ),就调用它的 operator=(const A&)
如果这个也没定义,则执行默认的内存照相拷贝动作。
类似地,还有:拷贝构造与移动构造。
其定义类似:A(const A& t) , A(AA& t)
自定义类支持移动语义
struct A{
int* p;
int n;
A() { n=0; p=nullptr; }
A(int n) {
this->n = n;
p = new int[n];
}
A(const A& t){
n = t.n;
p = new int [n];
memcpy(p, t.p, n * sizeof(int));
}
A(A&& t) {
n = t.n;
p = t.p;
t.n = 0;
t.p = nullptr;
}
~A() {
if(p) delete [] p;
}
void operator=(const A& t) {
if(p) delete [] p;
n = t.n;
p = new int [n];
memcpy(p, t.p, n * sizeof(int));
}
void operator=(A&& t) {
if(p) delete [] p;
n = t.n;
p = t.p;
t.n = 0;
t.p = nullptr;
}
};
对 A 的使用:
A a(100);
A b = std::move(a);
cout << a.n << endl;
这个功能用在 vector中就很有用了。
vector<A> a;
for(int i=5; i<10; i++){
A t(i);
a.push_back(move(t));
cout << t.n << endl;
}
move 只是把左值伪右值,引导程序的move行为
如果已经是右值,则无需 move
对返回值,不同编译器有不同的优化处理,需要自己多实验,在必要时加move
实时效果反馈
1. c++11 引入 move 语义的目的是:__
A 服务于STL基础设施,取代拷贝构造
B 把左值变量右值化,指示编译器调用右值重载版本
C 指针赋值前,自动删除原来分配的内存
D 提供引用计数,必要时释放资源
2. 下列哪个不是右值变量?__
A 函数的返回值
B 临时变量
C 代码中的立即数
D 全局变量
答案
1=>B 2=>D
堆算法
堆的概念再来温习一下。
堆是二叉树,父节点的数值不小于子节点,子节点间没有约束。
堆是完全二叉树,每层都是满的,除了最后的叶子层。
这样的规定,是为了便于用数组顺序存储节点。
因为父节点和左右子节点间的索引号有明确的规律。
对于已有的堆结构,增加新的节点,仍然保持堆性质,代价很小。
STL堆算法与其它算法一样,不会强制底层的数据结构。
但显然,如果经常在堆顶、堆底右边(堆尾)操作,用 deque 结构比较合适。
堆结构有什么价值?
效率。
比如,从一百万个元素中输出前 10 个元素。
如果用排序算法,需要统一排序,然后输出前 10 个。
而用堆,100万个元素建立堆比排序要快很多。
然后输出堆顶,用堆尾元素补空位,再调整一下,再输出下一个堆顶。。。
主要函数
push_heap
向堆尾添加一个元素,并调整为新堆。
pop_head
从堆顶取走元素,用堆尾元素替换,并调整为新堆。
而取走的元素不删除,而是放入原来堆尾元素的位置。
注意,逻辑上,此时的新堆元素个数少了一个。
make_heap
把给定区间的元素建成堆。
sort_heap
给定区间,执行堆排序算法。
示例
不要排序,取出序列中最大的前三个元素。
算法默认是建立大顶堆,如果希望小顶堆,需要指定另一个参数—比较器
deque<int> a = {3,8,2,9,11,4,6,13,7,1,10,12};
make_heap(a.begin(), a.end());
pop_heap(a.begin(), a.end());
pop_heap(a.begin(), a.end()-1);
pop_heap(a.begin(), a.end()-2);
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
输出:
比较器设为:greater
deque<int> a = {3,8,2,9,11,4,6,13,7,1,10,12};
make_heap(a.begin(), a.end(), greater<int>());
pop_heap(a.begin(), a.end(), greater<int>());
pop_heap(a.begin(), a.end()-1, greater<int>());
pop_heap(a.begin(), a.end()-2, greater<int>());
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
优先队列
priority_queue 是个适配器,不是底层容器,它封装了堆逻辑。
有的时候,用起来更方便。
同样的问题,用它来解决,是这样子的:
priority_queue<int> a;
int t[] = {3,8,2,9,11,4,6,13,7,1,10,12};
for(int i=0; i<sizeof(t)/sizeof(int); i++)
a.push(t[i]);
cout << a.top() << endl; a.pop();
cout << a.top() << endl; a.pop();
cout << a.top() << endl;
注意,这个适配器需要 include
如果要定制为小顶堆:
priority_queue<int, deque<int>, greater<int>> a;
实时效果反馈
1. 关于大顶堆的说法, 正确的是:__
A 建好的堆,堆顶元素一定是最大
B 最小元素一定是在最底层最右边
C 堆中元素个数必须为 2 的整数次幂 - 1
D 堆是二叉树的结构,必须用指针结构来存储
2. priorityqueue 适配器的底层容器是:_
A vector
B array
C deque
D map
答案
1=>A 2=>C
查找算法
STL提供了大量的查找算法,较常见的:
算法 | 含义 | 对区间要求 |
---|---|---|
find, find_if | 查找值第一次出现的位置 | |
count, count_if | 查找匹配个数 | |
adjacent_find | 查找相邻的等值元素 | |
binary_search | 二分查找,存在否 | 有序区间 |
lower_bound, upper_bound | 二分查找符合的边界 | 有序区间 |
equal_range | 二分查找等值区间 | 有序区间 |
search,search_n | 查找子序列 | |
find_end | 查找子序列最后出现位置 | |
find_first_of | 查找给定集合中任意元素 |
大体上可分为:
单值匹配
搜索符合条件的元素
普通查找
要求元素要支持相等性比较(operator==)
二分查找
要求区间必须已经有序
要求元素必须支持大小比较(operator<)
最好能支持随机访问,advance, distance 来处理指针大跳
序列匹配
并非查找单个元素,而是要求匹配一个序列,类似于在串中查找子串
算法举例
二分搜索十分重要,自己写很容易出纰漏。
int a[] = {2,5,12,8,6,3,13,25,119};
const int n = sizeof(a)/sizeof(int);
sort(a, a+n);
copy(a, a+n, ostream_iterator<int>(cout," "));
cout << binary_search(a, a+n, 25) << endl;
搜索序列:
vector<int> a = {1,2,3,4,1,2,3,4,5,6,7,8,4,5,6,7};
vector<int> b = {4,5,6};
auto i = search(a.begin(), a.end(), b.begin(), b.end());
cout << (i!=a.end()) << endl;
序列搜索也适用于子串的查找
char* a = "abcd123abc123xyz";
string b = "123";
char* p = find_end(a, a+strlen(a), b.begin(), b.end());
cout << (p-a) << endl;
算法对char* 这样的原生类型操作时,一点也不牺牲效率。
除了匹配序列,还有匹配集合中任意元素的操作:
char* a = "hernameis..";
string b = "aeiou";
char* p = find_first_of(a, a+strlen(a), b.begin(), b.end());
cout << *p << endl;
实时效果反馈
1. 关于二分搜索, 说法正确的是:__
A 搜索序列中不能包含相等的元素
B 搜索序列必须支持随机访问
C 搜素序列必须已经有序
D 序列中的元素必须支持operator==
2. equalrange 对序列中的元素要求是:_
A 提供相等性比较 operator==
B 提供大小比较 operator<
C 提供拷贝构造函数
D 提供移动构造函数
答案
1=>C 2=>B
变序算法
改变元素的顺序,并不删除或增加元素。
主要有:
算法 | 含义 | 备注 |
---|---|---|
inplace_merge | 对两个有序区间merge,结果就放在这两个区间中 | 需要输入有序 |
merge | 合并两个序列,放到另外的地方 | 需要输入有序 |
nth_element | 以第 n 个元素为标尺,使得小于它的在前,其它在后 | |
partial_sort | 部分排序,排序元素放在 [first, middle) | 输入区间为:[first, middle, last) |
partition | 把元素分两组,使用输入谓词判定,false在前,true在后 | |
random_shuffle | 洗牌 | |
reverse,reverse_copy | 反序 | |
rotate, rotate_copy | 元素轮换位置,middle为开始元素 | 输入:[first, middle,last) |
sort | 升序,原地排序,可指定比较规则 | |
stable_sort | 与sort类似,保持相等元素的顺序关系 |
示例
inplace_merge 不需要额外空间,在原地实现归并算法
vector<int> a = {2,6,8,12,3,7,2,5,6,11,9,};
sort(a.begin(), a.begin()+5);
sort(a.begin()+5, a.end());
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
inplace_merge(a.begin(), a.begin()+5, a.end());
cout << "\n";
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
partial_sort 可以实现只保证获得 前 n 个有序元素(但待排序区间是整个区间)
vector<int> a = {2,6,8,12,3,7,2,5,6,11,9,};
partial_sort(a.begin(), a.begin()+5, a.end());
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
运行结果:
而,partition 则是根据谓词函数,把整个序列分成两个部分
vector<int> a = {2,6,8,12,3,7,2,5,6,11,9,};
auto it = partition(a.begin(), a.end(), [](int x){
return x % 2 == 0;
});
cout << (it-a.begin()) << endl;
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
运行结果:
此谓词函数位置,可以用函数,函数对象,或者匿名函数
stable_sort与sort 的区别是对等值元素保持原来的顺序
这对用户自定义类型很有价值。
struct STU{
string name;
int age;
bool operator> (const STU& t) const {
return name > t.name;
}
};
int main()
{
vector<STU> a = {
STU{"zhang", 20},
STU{"wang", 30},
STU{"li", 25},
STU{"wang", 20},
STU{"zhao", 22},
STU{"wang", 50},
STU{"wang", 12},
STU{"li", 15}
};
sort(a.begin(), a.end(), greater<STU>());
for(int i=0; i<a.size(); i++){
cout << a[i].name << ", " << a[i].age << endl;
}
return 0;
}
比较器的控制,默认是 less,此处换成了 greater
注意,这样做,同时要修改STU中的比较函数: operator< 改为: operator>
rotate 则实现了轮换的效果,就是把头上的元素删除,并添加到尾部。
用这个思路做一版约瑟夫环问题:
vector<int> a = {1,2,3,4,5,6,7,8,9,10};
while(a.size()>1){
rotate(a.begin(), a.begin()+1, a.end()); // 模拟报数 1
rotate(a.begin(), a.begin()+1, a.end()); // 报数 2
rotate(a.begin(), a.begin()+1, a.end()); // 报数 3
a.pop_back(); // 报 3 的人删去
}
cout << a[0] << endl;
实时效果反馈
1. 关于变序算法, 说法==错误==的是:__
A 不会增加新的元素,不会删除已经存在的元素
B 会改变已经存在的元素的存储位置
C 相等的元素也可能被移动位置
D 同样的数据和算法,两次运行的结果一定相同
2. sort排序算法,说法==错误==的是:__
A 对相等的元素,不能保证它们原来的位置关系
B 可以在数组结构上排序,也可以在list上,但效率不高
C 可以给定比较器,来决定元素顺序的定义
D 当元素数目 n 很大的时候,sort 的复杂度大约 ${O(n^2)}$
答案
1=>D 2=>D
删除与替换
删除和替换的共性是:销毁了一些已经存在的信息,需要小心使用,
如果将来还需要这些信息,可以先暂存在其它地方。
算法 | 含义 | 备注 |
---|---|---|
copy, copy_backward | 复制序列,反向复制 | 不保证能正确处理重叠区(overlap) |
iter_swap, swap | 交换元素 | 支持移动语义 |
remove, remove_if | 删除 | 不改变容器size,仅把后边元素向前覆盖 |
remove_copy,remove_copy_if | 把处理结果拷贝到新位置 | 目标位置必须自己预留空间 |
replace, replace_if | 旧值用新的值代替 | |
replace_copy, replace_copy_if | 替换后的结果放别处 | 自己留好空间 |
swap_ranges | 指定范围的元素,与另一个序列交换 | |
unique | 清楚序列中重复元素 | 连续重复的才删除,否则需要先有序 |
unique_copy | 除去重复,不过结果放它处 |
示例
删除算法并不会真的去删除元素,从而导致容器size的变化。
remove 只是把后边的元素向前拷贝,覆盖掉删除的元素。
下例,删除了所有负数:
vector<int> a = {2,5,-3,0,19,-8,-6,2,-1,12,-7};
auto i = remove_if(a.begin(), a.end(),
bind2nd(less<int>(),0));
cout << (i-a.begin()) << endl;
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
结果:
这里的谓词函数用了函数式编程技巧。
下例,把所有的负数替换为0,并且写到新的位置。
vector<int> a = {2,5,-3,0,19,-8,-6,2,-1,12,-7};
vector<int> b;
replace_copy_if(a.begin(), a.end(),
back_insert_iterator<vector<int>>(b),
[](int x){ return x < 0; },
0);
copy(b.begin(), b.end(), ostream_iterator<int>(cout, " "));
这里使用了后插入迭代器,而不是先开避足够的空间。
谓词函数的位置使用了匿名函数。
下一个例子,演示了如何去掉重复
vector<int> a = {1,2,5,2,3,6,2,1,3,4,4};
sort(a.begin(), a.end());
auto i = unique(a.begin(), a.end());
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
a.resize(i-a.begin());
cout << "\n";
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " "));
结果:
实时效果反馈
1. 使用STL算法,而不是自己写循环, 好处是:__
A 达到最高的效率
B 代码更加简洁,对初学者更友好
C 健壮性与通用性,将来更易于维护
D 更酷炫,显档次
2. 关于STL删除(remove系列)算法,说法==错误==的是:__
A 不会真的删除,只是后边的元素向前移动
B 可能会使得容器size变小
C 不能对 map, set 操作
D 可以自定义谓词函数,决定删除哪些元素
答案
1=>C 2=>B
数值算法
这里的数值运算是广义的。
它支持我们自己定义的任何运算。
注意:大部分需要包含 numeric 头文件
算法 | 含义 | 备注 |
---|---|---|
min,max,minmax | 求最大最小值 | 不能对容器操作 |
min_element, max_element | 求区间元素的极值 | 对容器 |
accumulate | 对区间元素求累加和 | “和”是广义的 |
partial_sum | 累积过程 | |
inner_product | 两个序列的内积 | 广义 |
adjacent_difference | 相邻元素的差 | 相当于离散的微分 |
示例
求极值十分常用:
int a=5, b=8, c=4;
cout << max(a,b) << endl;
cout << min({a,b,c}) << endl; //可以对多个变量求极值
string x = "thisiacupofcoffeeplease";
auto p = max_element(x.begin(), x.end());
cout << *p << endl;
串也可当作容器来操作,它在很多功能上提供了两人API
下例,序列求和:
int a[] = {1,2,3,4,5};
cout << accumulate(a,a+5,0) << endl;
int x = accumulate(a, a+5, 0,
[](int a, int b){ return a*10+b;});
cout << x << endl;
结果:
有的时候,我们需要累加的中间过程,可以如下:
int a[] = {1,2,3,4,5};
int b[5];
partial_sum(a, a+5, b);
copy(b,b+5,ostream_iterator<int>(cout," "));
adjacent_difference 是求相邻项的差,
比如,已知月产量,求环比最大项:
vector<double> a = {1.5, 2.2, 3.5, 4.1, 4.6, 10.8, 11.9};
vector<double> b(a.size());
adjacent_difference(a.begin(), a.end(), b.begin(),
[](double a, double b){
return (a-b)/b*100; } );
copy(b.begin(), b.end(), ostream_iterator<double>(cout, " "));
cout << endl << *max_element(b.begin(), b.end());
实时效果反馈
1. 当不能使用 minelement 的时候,如何求vector序列的最值较好:_
A 自己写循环,遍历vector
B 用 for_each 自己累积
C 用 accumulate 自定义“和”的行为: min(a,b)
D 用 inner_product
答案
1=>C
生成与变异
通过运算,产生新的数据。
有的修改了原序列,有的不修改原来的序列。
算法 | 含义 | 备注 |
---|---|---|
fill, fill_n | 用给定的元素填充 | 改变容器中值 |
for_each | 用指定的函数依次访问元素 | 不改变容器 |
generate, generate_n | 用发生器函数来填充 | |
transform | 对输入序列中元素进行运算,产生新序列 | 可以改变或不改变容器 |
示例
for_each 对其Iterator范围内的元素,调用用户提供的函数fn
可以进行显示或其它复杂的,比如累积性的操作。
vector<int> a = {1,2,3,4,5};
int sum = 0;
for_each(a.begin(), a.end(),
[&sum](int x){ sum += x; });
cout << sum << endl;
结果:
相比于 accumulate, for_each 更加灵活,可以支持不同于元素类型的累积结果。
vector<int> a = {1,2,3,4,5};
string sum;
for_each(a.begin(), a.end(),
[&sum](int x){ sum = sum + to_string(x) + ","; });
cout << sum << endl;
结果:
这个匿名函数使用了引用的方式绑定了外部的变量。
generate 提供了比 fill 更丰富的填充方式,利用一个函数不断产生新的数据。
int a[10];
int i = 0;
generate(a, a+10,
[&i](){ ++i; return i*i; });
copy(a,a+10,ostream_iterator<int>(cout," "));
结果:
transform 的功能很强,大体相当 于函数式语言中的 map
可以由用户指定的函数对序列中的元素进行变换。
int a[] = {1,2,3,4,5};
transform(a, a+5, a, [](int x){ return x*5; });
copy(a,a+5,ostream_iterator<int>(cout," "));
结果:
我们也可以对两个序列进行运算,生成新的结果:
int a[] = {1,2,3,4,5};
int b[] = {5,4,3,2,1};
int c[5];
transform(a, a+5, b, c,
[](int x, int y){ return x*y; });
copy(c,c+5,ostream_iterator<int>(cout," "));
实时效果反馈
1. 当STL算法中需要用户自定义行为时, 哪项==不可以==作为实参:__
A 自定义的函数
B 自定义类生成的函数对象
C 匿名函数
D 函数模板
2. 很多高级语言中有zip操作,可以把[1,2,3],[a,b,c] 变成:[(1,a),(2,b),(3,c)],
在c++STL中应该用哪个算法实现这个功能?__
A for_each
B transform
C accumulate
D 没有对应的实现
答案
1=>D 2=>B
关系算法
并不改变参加运算的序列,只是对它们的关系进行某种判定。
主要算法:
算法 | 含义 | 备注 |
---|---|---|
equal | 两个序列是否等值 | |
mismatch | 两个序列首个不匹配的位置 | |
includes | 一个序列的所有元素是否包含在另一序列中 | 序列需要有序 |
lexicographical_compare | 字典比较时,是否第一个序列小 | 可自定比较规则 |
all_of | 是否所有元素都有某个性质 | |
any_of | 最否存在任一元素满足要求 | |
none_of | 是否没有任何元素满足要求 |
基本示例
equal 比较两个序列区间,“相等”的含义可以自己定义。
int a[] = {1,2,3,4,5,11,12,13,14,15};
bool x = equal(a,a+5,a+5,
[](int a, int b){ return a%10==b%10;});
cout << x << endl;
结果:
any_of 等是 c++11 引入的特性。
vector<int> a = {2,5,1,6,-3,0,12,15,-4};
if(any_of(a.begin(), a.end(), bind2nd(less<int>(),0))){
cout << "find negative element" << endl;
}
结果:
这里的函数部分传入的是:STL组合函数
虽然STL的算法很多,但最常使用的却有限。
5位数的黑圈问题
任意一个5位数,重新排列其各个十进制位,可得最大的数 a, 最小的 b(可以前导零)
求 a-b,如果不足5位,前补零。
不断重复这个过程,必然落入一个循环圈。
求这样循环圈一共有几个?
例如:1002 => (2100 - 0012=2088) => (8820 - 0288=8532) => ….
思路:x -> x’ -> x’’ 不断加入 vector, 若发现重复,则发现了一个解
收集所有的解,只要不重复,就可以了。
#include <iostream>
#include <algorithm>
#include <vector>
#include <iterator>
using namespace std;
int next(int x)
{
string s = to_string(x);
while(s.length()<5) s.push_back('0');
sort(s.begin(), s.end());
int a = stoi(s);
reverse(s.begin(), s.end());
int b = stoi(s);
return b - a;
}
vector<int> f(int x)
{
vector<int> a;
while(true){
auto i = find(a.begin(), a.end(), x);
if(i != a.end()){
a.erase(a.begin(),i);
return a;
}
a.push_back(x);
x = next(x);
}
}
int main()
{
vector<int> da;
for(int i=10000; i<=99999; i++) {
vector<int> t = f(i);
if(find(da.begin(), da.end(), t[0]) == da.end()){
copy(t.begin(), t.end(),
back_insert_iterator<vector<int>>(da));
da.push_back(-1);
}
}
cout << count(da.begin(), da.end(), -1) << endl;
copy(da.begin(), da.end(),
ostream_iterator<int>(cout, " "));
return 0;
}
结果:
实时效果反馈
1. 使用STL函数模板,判断[2,1,5], 是否包含在[1,2,3,4,5]中,方法是:__
A 用 includes
B 用 sort 和 includes
C 用 find
D 用 sort 和 find
2. 怎样从vector中删除元素?__
A 用 vector 的 erase
B 用 vector 的 remove
C 用 STL 的 erase
D 用 STL 的 remove
答案
1=>B 2=>A
其它算法
生成排列组合
next_permutation 当前排列的下一个排列
prev_permutation 当前排列的前一个排列
集合算法
set_union, set_intersection, set_difference, set_symmetric_difference
前边课程中讲过
堆算法
make_heap, pop_heap, push_heap, sort_heap 等,前边课程中讲过
示例
生成全排列:
string s = "ABCD";
do{
cout << s << " ";
}while(next_permutation(s.begin(), s.end()));
结果:
八皇后问题
前边的课程中讲过。
基本的条件是:每行放一个皇后,每列放一个皇后。
所以结果可以用一个数组表示,包含8个元素,第个数字表示该行的皇后放在哪一列。
显然,数组中的数字不能重复,否则就同列了,因而必然包含所有 0 ~ 7 的数字。
这个问题,就变成,求 01234567 的全排列,然后
#include <iostream>
#include <algorithm>
#include <array>
using namespace std;
bool ok(array<int,8> a)
{
for(auto i=a.begin(); i!=a.end(); ++i){
for(auto j=i+1; j!=a.end(); ++j){
if(j-i==abs(*i-*j)) return false;
}
}
return true;
}
void show(array<int,8> a)
{
for_each(a.begin(), a.end(),
[](int x){
string s(8, '.');
s[x] = '*';
cout << s << endl;
});
}
int main()
{
array<int,8> a = {0,1,2,3,4,5,6,7};
do{
if(ok(a)) {
show(a);
break;
}
}while(next_permutation(a.begin(),a.end()));
return 0;
}
实时效果反馈
1. 使用nextpermutation, 对[1,2,2,3]求全排,有多少项?_
A 24
B 36
C 12
D 10
2. 当使用array为函数参数时,说法正确是:__
A 按值传递,形参会拷贝实参的每个元素
B 按指针传递,与实参共享同一组元素
C 按值传递,但元素个数与实参不必相同
D 按指针传递,可以指定不同的元素个数
答案
1=>C 2=>A
智能指针
在c++ 的开发中,我们很熟悉内存泄漏问题。
时刻要注意: new 与 delete 的成对儿出现。但这很难做到。
MyA* p = new MyA();
dosomething(p); // 如果这个函数中抛出了异常就。。。。
delete p;
智能指针就能解决这个问题,防止我们忘记 delete
#include <memory>
...
auto_ptr<MyA> p(new MyA());
dosomething(p.get());
...
p表面是个指针,可以像指针一样操作,但实际是对象,在生存期结束时会自动 delete
实践中, auto_ptr 有一大堆的坑,要处处注意。
比如:不能指向数组,不能共享所有权,不能做为容器的元素,不能作为参数传递。。。
指针赋值,reset(), release() 使用不当等。。
auto_ptr 重载了operator=,两个auto_ptr间赋值,会导致右值被置为空。
传参也有类似的效果。
c++11 中把 auto_ptr 标记为 [deprecated],其功能可以用unique_ptr 代替。
c++11中提供的智能指针有:
智能指针 | 含义 | 说明 |
---|---|---|
unique_ptr | 独占的(排它的) | 只能一个指针有控制权,可以移交 |
shared_ptr | 共享控制权(引用计数) | 引用计数为0时,释放 |
weak_ptr | 弱引用 | 常与shared_ptr配合,不参与计数 |
unique_ptr
unique_ptr 与 auto_ptr 类似,但更安全,更容易使用。
struct MyA{
~MyA(){ cout << "MyA destroy..." << endl; }
void f() { cout << "MyA.f()..." << endl; }
};
int main()
{
// MyA* p = new MyA();
unique_ptr<MyA> p(new MyA);
p->f(); // 对象冒充指针
return 0;
}
unique_ptr 是独占的,不能两个指针指向同一对象。但可以转移控制权。
unique_ptr<MyA> p(new MyA);
p->f();
unique_ptr<MyA> q;
q = p; // 编译出错
改为:q=move(p); 则可以。
因为 p 变成了空指针,q接管了控制权。
传入到其它函数中,也是同样道理。
传引用可以,直接传值不可以,但可以用 move 移动语义。
#include <iostream>
#include <memory>
using namespace std;
struct MyA{
~MyA(){ cout << "MyA destroy..." << endl; }
void f() { cout << "MyA.f()..." << endl; }
};
void g(unique_ptr<MyA> r){ }
int main()
{
unique_ptr<MyA> p(new MyA);
p->f();
g(move(p));
return 0;
}
注意,include
shared_ptr
shared_ptr 允许多个指针指向同一个对象,那么谁负责释放资源呢?
有一个引用计数被关联到对象,记录着指向对象的指针数目,为0,则产生释放。
struct MyA{
~MyA(){ cout << "MyA destroy..." << endl; }
void f() { cout << "MyA.f()..." << endl; }
};
void g(shared_ptr<MyA> r){ r->f(); }
int main()
{
shared_ptr<MyA> p = make_shared<MyA>();
shared_ptr<MyA> q = p;
g(p);
cout << p.use_count() << endl; // 有引用计数的概念
return 0;
}
shared_ptr有个著名的 循环引用问题:
struct Node{
int data;
shared_ptr<Node> next;
};
int main()
{
shared_ptr<Node> p(new Node());
p->next = p;
cout << p.use_count() << endl;
return 0;
}
表面看起来,shared_ptr很完美,似乎可以解决动态内存问题,实际上远远不够。
比如:如果出现了环状结构就惨了。
有些场景可以用weak_ptr 代替 shared_ptr,避开循环引用,
但不是万能,仅仅靠引用计数实现自动内存管理,尚无完美理论解法。
weak_ptr 不会引起引用计数变化,其它与shared_ptr相同,可以与shared_ptr 协同工作。
实时效果反馈
1. 关于autoptr, 说法正确的是:_
A c++11引入的智能指针
B 创建时必须初始化
C 一个auto_ptr赋值给另一个 auto_ptr 会编译错
D c++11 中已标为废弃
2. 对weakptr 说法==错误==的是:_
A 可以创建weak_ptr,不指向任何对象
B 可以从另一个 weak_ptr拷贝构造 weak_ptr
C 可以从另一个 shared_ptr拷贝构造 weak_ptr
D 一般与 unique_ptr 搭配使用
答案
1=>D 2=>D
散列表
hash_map 在很多编译器中都在用,所谓既成事实。
但hash_map并非 c++ 标准。直到 c++11 才引入了标准的散列系列。
unordered_map, unordered_multimap
unordered_set, unordered_multiset
分别是
这里讲 unordered_map,余可类推。
散列原理
unordered_map在逻辑上,也是存储《键-值》对儿,但与map机制不同。
它从key 用一个散列公式计算,得出存储位置,计算量与规模无关。
精心设计 hash 公式,可以使得:这个位置在大概率上不会与其它key的计算结果相同。
如果相同,也非灾难,即所谓:碰撞。
有很多解决方案。比如:就存在一起好了。保存可能多个pair的这个东西叫:bucket
可以翻译为:桶
所以,unordered_map 对元素的要求与 map 不同。
- key 不需要排序,因而不需要重载:operator<
- key 需要相等性比较,因而需要重载: operator==
使用unordered_map 而不是map的理由:
当数据规模很大的时候,比如100万。
map 的查找复杂度大约 20 次,而unordered_map 是常数,与规模无关。
另外,元素不要求排序能力。
在小规模数据上,使用哪个差别甚微。
unordered_map 的成员方法
unordered_map 属于关联型容器,也属于无序型容器。
其含有的特殊方法:
方法 | 含义 | 备注 |
---|---|---|
bucket_count() | 当前的桶的数量 | 即可放键值对的位置有多少个 |
bucket(key) | key为键的桶的编号 | |
bucket_size(n) | n 号桶中键值对的个数 | 一般都是 0或1, 除非发生hash碰撞 |
load_factor() | 负载因子 | 元素个数 / 桶的个数 |
rehash(n) | 重设桶的数量 |
示例
typedef unordered_map<string, int> my_map;
int main()
{
my_map a = {{"us",10},{"uk",20},{"fr",30},{"de",40}};
for(int i=0; i<a.bucket_count(); ++i){
cout << "bucket " << i << ": " << "has ";
cout << a.bucket_size(i) << endl;
}
cout << a["de"] << endl;
cout << a.at("cc") << endl;
return 0;
}
实时效果反馈
1. 关于用散列表的map,比起用红黑树的map, 优点是:__
A 数据量很大时,查找效率更高
B 比红黑树更省空间
C 可以用string作为 key
D 可以修改 key
2. 设计散列表时,如何处理碰撞问题?下列哪项==最不可取==:__
A 顺序探测,存在下一个最近的空位。
B 使用再散列公式,重新得到一个位置
C 使用溢出链表存储碰撞的数据
D 在一块单独的区域顺序存储
答案
1=>A 2=>D
boost库
boost 是c++的一个准标准库,可以看作STL的延续和扩充。
它秉承的理念和思想也是模板泛型,与STL一脉相承。
STL集中在算法部分,而boost更加实用,提供了大量的工具类,可以应用到相当具体、多样的场景中去,因而,boost提供的类库十分丰富、庞大。
Boost社区发起人Dawes本人就是c++标准化委员会的成员之一,其内容经常成为下一代c++标准库的内容,对c++社区影响巨大,“准标准”的名头不折不扣。
Boost以源码的方式提供,商业与非商业的使用都是允许并且鼓励的。
Boost强调可移植性,强调最佳实践,强调c++标准。虽然有一些大胆、前卫、实验性质的东西,但大体上可以有工业级的质量,其稳定性、健壮性很不错。
综上,如果c++标准库无法满足要求,boost库是首选。
大致分类
字符串与文本处理
conversion(c++ 类型转换增强), format(实现类似printf的格式化对象)
regex, spirit, string algo(字符串相关算法),tokenizer(把字符串拆成一组记号),
wave(预处理器),xpressive(免编译正则库)
容器库
array, bitmap, circular buffer, disjoint sets, dynamic bitset, GIL(通用图像库),
ICL(区间容器),intrusive(侵入式容器和算法), multi-array(多维数组), multi-index,
pointer container(指针容器),property map, property tree, unordered(散列容器),variant(复杂类型联合体)
迭代器类
GIL,graph, iterators, operators(用少量操作符生成操作符重载), tokenizer
算法库
foreach, GIL, graph, min-max, range, string algo, untility
函数对象与高阶函数
bind,function, functional, functional/factory(工厂模式),functional/forward
functional/hash, lambda(匿名函数),ref, signals(观察者模式), phoenix(函数式编程)
泛型编程库
call traits, concept check, enable if, function types, inplace factory, operators, property map, static assert(编译期诊断), type traits, TTI
模板元编程
fusion(编译时tuple容器与算法),MPL, proto(嵌入式),static assert type traits
并发编程
asio(异步IO),interprocess(进程间通信), MPI, thread(增强的线程处理能力),
context(协程),atomic, corontine(协程支持),lockfree(无锁数据结构)
数字与数学
accumulators, integer, interval(区间概念),math, math common factr,
math octonion(八元数),math quaternion(四元数),numeric conversion,
random, rational(有理数),uBLAS(线性代数),geometry(几何库),ratio(编译期分数),
multiprecision(高精度)
数据结构
any, bitmap, compressed pair(优化的pair), fusion, ICL, multi-index, pointer container, property container, tuple, uuid, variant, heap, type erasure(运行时多态)
输入输出
内存管理
pool, smart(智能指针), utility
杂类
CRC, Date Time, flyweight 享元模式, …
安装boost
boost库以源码形式提供,大部分库就是个可包含的头文件。
少数库需要编译后的动态库的支持。boost提供了unix和windows上的编译配置。
如果没有特殊需要,只要解压安装包,稍加配置即可,不用安装编译过程。
从官网下载某个版本的安装包
解压在某个位置,内部目录大致:
其中的 boost 是主要目录,内有很多头文件,直接包含了模板的定义
在 qt 的工程配置文件中增加
目录位置要写 boost 主目录的上一级目录的位置。
使用示例
c++11 才提供 lambda表达式的写法,而且语法比较保守。
如果不舒服,用 boost 提供的 lambda 表达式试试,有很前卫的感觉。
#include <boost/lambda/lambda.hpp>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
using namespace boost::lambda;
int a[] = {1,2,3,4,5};
transform(a, a+5, a, (_1 * 2));
for(auto i: a){
cout << i << endl;
}
}
这里同时也使用了 c++11 提供的 for range 循环的语法。
实时效果反馈
1. 关于boost, 说法==错误==的是:__
A boost的很多库已经成为了c++新标准的特性
B boost十分庞大,是一系列库的集合
C boost项目的初衷是为c++标准化工作提供可参考的实现
D boost主体在不同平台上有不同的二进制库
答案
1=>D
boost串处理
同STL的其它类相比,string类已经算是比较臃肿的了,但还是有许多常见功能没有提供。
比如:java中的 trim, endswith, join, split 等等,甚至是大小写转换。
只要是“人有我无”的东西,就到 boost 海洋里翻吧,十有八九能找到。
首先,要包含头文件。
很多功能分散在各个文件中,为方便,有个大全型的 string.hpp
#include <boost/algorithm/string.hpp>
一般为方便,还要引入名字空间:
using namespace boost::algorithm;
大小写转换
using namespace boost::algorithm;
string s = "abcdAB";
cout << to_upper_copy(s) << endl;
to_lower(s);
cout << s << endl;
这里的STL惯例:
有 _copy 的,表示不修改输入参数,而是生成新的串对象
删除匹配的字符串
string s = "abc,123;xyz,1111,123,ttt";
cout << erase_first_copy(s,"123") << endl;
cout << erase_nth_copy(s,",",2) << endl;
cout << erase_last_copy(s,",") << endl;
cout << erase_all_copy(s,",") << endl;
cout << erase_head_copy(s,3) << endl;
cout << erase_tail_copy(s,3) << endl;
类似的还有:
replace_first_copy, replace_all_copy 等
串的连接
string a[] = {"zhang", "wang", "li", "zhao"};
cout << join(a, ",") << endl;
去空白
string s = " abc ";
cout << "|" << trim_left_copy(s) << "|" << endl;
cout << "|" << trim_right_copy(s) << "|" << endl;
cout << "|" << trim_copy(s) << "|" << endl;
还可以自定义什么叫空白
string s = "abc ,,; .";
cout << "|" <<
trim_copy_if(s, is_any_of(",; .")) << "|" << endl;
子串匹配
string s = "abc1234xyz";
cout << starts_with(s, "abc") << endl;
cout << ends_with(s, "xyz") << endl;
cout << contains(s, "123") << endl;
串的拆分
string s = "123,abc,xyz,,qqq";
vector<string> v;
split(v, s, [](char x){return x==',';});
for(auto i: v) cout << i << " ";
忽略大小写
如果希望不要区分大小写字母,可以用以 i 开头的版本。
比如: erase_all_copy 对应: ierase_all_copy
实时效果反馈
1. 关于boost中的串算法, 说法正确的是:__
A 所有算法都在 algorithm/string.hpp 这个源文件中。
B 带有 _copy 字样的名字,表示需要输出迭代器
C 带有_if 字样的名字,表示需要使用方提供一个谓词函数
D 如果希望不区分大小写,需要调用增加一个参数的重载版本
2. 如果想把”abc, ,,”尾巴上的逗号或空格全去掉,返回一个新串,应该用:__
A trim_all_copy
B trim_right_copy
C trim_right_copy_if
D trim_right_if
答案
1=>C 2=>C
boost格式化
ostream 提供了与 printf 对应的精确输出控制,但总有人觉得冗长,不方便。
使用 cout 比直接使用 printf 的格式化串的好处是:类型安全。
boost 的format库提供了类似printf 格式控制符的方案,同时能够保证类型安全。
并且,format库引入了很多新特性,使得format工作更方便、更精细、更完美。
头文件
#include <boost/format.hpp>
boost::format 类实例化时,传入格式化串。
之后,可以喂入被格式化的项,然后输出,或转为string。
其中的格式化串的写法,支持 3 大类风格:
位置标记(最简单)
boost::format fmt("%2% - %3% - %1%");
fmt % 2008 % 8 % 31;
cout << fmt << endl;
// string s = fmt.str(); // 取出结果为string
// 同一个位置符,可以出现多次。比如:"%1%-%2%-%1%"
这种位置占位符的风格在现代高级语言中很流行,比如:python, go 等
继承并强化了printf的格式化串
%[N$] [flag] [width] [.precision] type-char
N$ 是可选的,要么都省略,要么都有
boost::format fmt("|%1$10.2f|%1$-10.3f|%1$=10.2f");
fmt % 3.1415936;
cout << fmt << endl;
cout << boost::format("%b,%d,%04x")%true%254%254 << endl;
结果:
设置成打印风格,更直观点
%|spec|
其实就是 printf 风格的控制符
boost新的格式说明符
%10t 保证前边有10个位置,为了下一行对齐
%10Tx 同上,用 x 作为被位的填充符
cout << boost::format("a%20tbcd") << endl;
cout << boost::format("aa%20tbbbb") << endl;
cout << boost::format("aaaa%20T*bbbbb") << endl;
结果:
format 对象是可以反复使用的,这在大量输出同样格式数据时,很有用。
int a = 5, b = 6, c = 128;
boost::format fmt("%s=%d, %010p");
cout << fmt % "a" % a % &a << endl;
cout << fmt % "b" % b % &b << endl;
cout << fmt % "c" % c % &c << endl;
实时效果反馈
1. boost的format库,提供了类似printf的格式化, 下列格式化串中=的含义是:__
“%=12.3f”
A 精确输出
B 居中对齐
C 小数位不足补0
D 无论正数负数,都输出前导符号
2. boost的format库,在格式化串中,可以 %10t,其含义是?:__
A 提示下一项需要对齐输出,从位置11开始
B 强迫下一项从位置10开始输出
C 插入10个制表符
D 宽度为10,用户自定义类型的输出
答案
1=>B 2=>A
boost大整数
python中直接支持大整数,java中有BigInteger,c++中要使用大整数,经常是自己动手。
这样做是重复造轮子,不安全,而且低效。
boost的数学库中提供了对高精度数值的支持,就包括任意精度的大整数。
已经重载了常见运算符,使用起来与普通的整数没什么大区别。
示例-斐波那契数列
#include <boost/multiprecision/cpp_int.hpp>
#include <iostream>
using namespace std;
int main()
{
using namespace boost::multiprecision;
cpp_int a = 0;
cpp_int b = 1;
int n = 100;
for(int i=0; i<n; ++i){
cpp_int c = a + b;
a = b;
b = c;
}
cout << a << endl;
}
结果:
与string间的转换也简单
cpp_int a("123456789");
a = a * cpp_int("987654321");
string s = a.str();
cout << s << endl;
实时效果反馈
1. boost的cppintbackend是高精度整数实质类型, 为了使用方便,在止基础上又定义了许多包装类型,其中==不==包括:__
A cpp_int
B cpp_rational
C int256_t
D big_int
2. checkedint128t与int128t的区别是:_
A 除 0 是否会抛异常
B 溢出是否抛异常
C 默认初始化是否为0
D 从string转换时是否抛异常
答案
1=>D 2=>B
正则表达式
正则表达式对文本的处理很有用,但一直没有纳入到c++标准,很多人用boost库。
终于,c++11把正则标准加进来,其实现并非是boost, 性能、bug等都待时间检验。
如果能用c++11,当然最好不再依赖boost,毕竟标准是难以抗拒的。
先要嵌入头文件:
regex re("\\d+");
cout << regex_match("12345", re) << endl;
cout << regex_match("12ab345", re) << endl;
这段代码判断一个串与 正则 模式是否匹配。
先要 regex 类实例化一个对象,其参数是一个串,即:正则表达式。
正则有自已的文法,与c++语法不相干,其复杂度堪比一门独立的语言。
历史上,存在不同版本的正则文法,其大部分内容是相同或相近的。
c++内置了对多种文法的支持,创建时可以给参数。
文法 | 说明 | 备注 |
---|---|---|
ECMAScript | javascript中即是 | 默认选项 |
basic | 基本的 POSIX 正则语法 | |
extended | 扩展的POSIX 正则语法 | |
awk | awk 工具 | |
grep | grep 工具 | |
egrep | grep 工具 |
正则表达式中经常用转义符,写在串常量中,又要再次转义,很不方便。
这时候,要以使用 raw string 字面量
R”(正则表达式本身)”
“\d+” 可以写为: R”(\d+)”
有很多符号是正则表达式的特殊符号,有特殊含义,如果使用这些字符本身,需要转义。
字符 | 含义 |
---|---|
. | 匹配任意字符 |
[ | 字符类开始 |
] | 字符类结束 |
{ | 重复量词开始 |
} | 重复量词结束 |
( | 子组开始 |
) | 子组结束 |
\ | 转义符 |
\\ |
转义符自身 |
* | 量词,0个或多个 |
+ | 量词,1个或多个 |
? | 量词,0个或1个 |
^ | 行开始,或者是:否定 |
$ | 行结束 |
\n | 换行 |
\t | 制表符 |
\xhh | 用两位十六进制表示的 unicode字符 |
\xhhhh | 用4位十六进制表示的 unicode 字符 |
- 字符的分类
字符分类 | 同义表示法 | 说明 |
---|---|---|
[[:alnum:]] | [a-zA-Z0-9] | 字母和数字 |
[[:alnum:]_] | \w | 字母数字,以及下划线 |
[^_[:alnum:]] | \W | 非字母数字以及下划线 |
[[:digit:]] | \d [0-9] | 数字 |
[^[:digit:]] | \D | 非数字 |
[[:space:]] | \s | 空白字符 |
[^[:space:]] | \S | 非空白字符 |
[[:lower:]] | [a-z] | 小写字母 |
[[:upper]] | [A-Z] | 大写字母 |
[[:alpha:]] | [a-zA-Z] | 字母 |
[[:blank:]] | 非换行符的空白字符 | |
[[:cntrl:]] | 控制字符 | |
[[:print:]] | 可打印字符 | |
[[:punct:]] | 标点字符 | |
[[:xdigt:]] | 十六进制数字 |
常用的匹配举例
手机号
regex re(R"(1\d{10})");
cout << regex_match("13552335699", re) << endl;
cout << regex_match("21152335699", re) << endl;
cout << regex_match("135-3356999", re) << endl;
身份证
regex re(R"(\d{6}(19|20)\d{2}[01]\d[0-3]\d\d{3}[0-9xX])");
cout << regex_match("25122219600230332x", re) << endl;
cout << regex_match("25122219602130332x", re) << endl;
cout << regex_match("251222196002453320", re) << endl;
邮箱
regex re(R"(([[:alnum:]_\.\-])+\@([[:alnum:]_\.\-])+\.[a-zA-Z]{2,4})");
cout << regex_match("guo-ti@163ab.com", re) << endl;
cout << regex_match("gyh@163abc", re) << endl;
cout << regex_match("xx.yy.zhang16@111.com", re) << endl;
可以看出:
或者的表示法 |
量词的表示法 {min, max},或者 {N}
特别地,
{0,1} 相当 于 ?
{1, } 相当于 +
{0, } 相当于 *
注意,正则用到的 + - * 括号等符号有特殊的含义,如用其本身,则要转义
\-
在 [ ] 要放在最尾,否则会误判为范围表示符
迭代器
对于局部的多次匹配,用迭代器更方便,且高效率。
根据字符类型不同,有4种迭代器。
常用:
cregex_iterator 对应 const char*
sregex_iterator 对应 const string
string s = "abc123xyz456 kkk 1399 dfs gag 026";
regex re("\\d+");
auto i = sregex_iterator(s.begin(), s.end(), re);
auto end = sregex_iterator();
while(i != end){
cout << i->str() << endl;
++i;
}
- 使用regex_match 获得详情
实时效果反馈
1. 下列正则文法中, 想表达1个或多个数字,==错误==的是:__
A [[:digit:]]+
B \d+
C [0-9]{1,}
D [\d]{1,}
2. c++11正则:[[:alpha:]_]\.{0,3}[^0-9]
,与下列哪个串匹配?:__
A “bx=z8”
B “_abcde”
C “+abc51”
D “z3.1+”
答案
1=>D 2=>D
正则表达式(2)
正则表达式匹配后,还可以获得更丰富的信息。
如果希望得到这些信息,需要一个匹配结果对象 match_results 模板类的一个对象。
其声明方式:
match_results<string::const_iterator> result;
// 等价于:
smatch result;
或者:
match_results<const char*> result;
// 等价于:
cmatch result;
使用 regex_search 查找匹配串
regex re("\\d+");
smatch result;
string s = "abc123xyz7890-ttt";
auto it = s.cbegin();
auto end = s.cend();
while(regex_search(it, end, result, re)){
cout << result[0] << endl;
it = result[0].second;
}
如果使用的是 char* , 则如下:
regex re("\\d+");
cmatch result;
const char* s = "abc123xyz7890-ttt";
auto it = s;
auto end = s + strlen(s);
while(regex_search(it, end, result, re)){
cout << result[0] << endl;
it = result[0].second;
}
匹配子组
模式串中,可以加括号,把匹配分为若干子组,子组的编号以左括号开始顺序计算。
在匹配结果中,用数组方式引用子组。
regex re("(\\d{4})-(\\d{2})-(\\d{2})");
string s = "xyz 2030-05-14 thaisanewday";
smatch result;
if(regex_search(s,result,re)){
cout << result[0] << endl;
cout << result[1] << endl;
cout << result[2] << endl;
cout << result[3] << endl;
}
替换
regex_replace 可实现对匹配项的替换。
regex re("(\\d{4})-(\\d{2})-(\\d{2})");
string s = "xyz 2030-05-14 thaisanewday";
cout << regex_replace(s,re,"<***>") << endl;
regex函数经常有多个重载版本,比如,有的版本可以实现把结果写到指定的位置。
在替换文本中也可以使用子组的引用
比如,想把 year - month - day 的格式,改为 month - day - year 格式:
regex re("(\\d{4})-(\\d{2})-(\\d{2})");
string s = "xyz 2030-05-14 thaisanewday";
cout << regex_replace(s, re, "$2-$3-$1") << endl;
实时效果反馈
1. 用正则模式”((.)(.).)”, 去匹配串“abc” , result中子组2是:__
A “a”
B “b”
C “c”
D “abc”
2. 关于regexreplace,说法正确的是:_
A 替换直接在原串上进行
B 多个版本都是返回替换完成的结果串
C 多个版本都是返回发生了多少次替换
D 替换串中可以使用$n 表示匹配的子组
答案
1=>A 2=>D