Effective STL 读书笔记
本篇文字用于总结在阅读《Effective STL》时的笔记心得,只记录书上描写的,但自己尚未熟练掌握的知识点,不记录通用、常识类的知识点。
STL按照容器类型,可以分为标准序列容器(deque,vector,list,string),标准关联容器(map,multimap,set,multiset),另外,还有非标准容器,序列类型有slist和rope,关联类型有hash_**类相关容器。标准关联容器总是保持排列顺序。
在关联容器中,确保关联容器对于所使用的比较函数,对于相等的值,总是返回False。
在stl代码中,入参常见缩写,lhs=left hand side,rhs=right hand side。
STL对于数据的处理原则是,进入是拷贝,出来也是拷贝(有些出来是引用,需要根据实际调用函数来决定)
命名空间不要使用全部的空间,比如单个文件中用到vector,可以这样包含
#include <vector>
using std::vector
尽量减少对外部的依赖。
模板形参统一使用typename,而不使用class,因为typename可以用来重定义内部参数类型,而class不行。
C++标准规定,在抛出异常的地方,在退出之前,会调用局部对象的析构函数,确保局部对象析构完成后再退出。
派生类的构造函数、拷贝构造函数、=重载,==重载等函数在实现时,需要显示调用基类对应的函数,调用方法通过类型::函数名的方式来实现,确保派生类相关函数在语义上面的一致性。
vector可以看出是动态增值的数组,它的size为实际存储数据个数,capacity为为存储数据而分配的内存空间。每次扩容,会伴随着内存申请、容器内容拷贝和旧数据的销毁,在VS平台上的vector扩容策略为每次增加当前容量的一般。
vector的优化策略:
-
对于预存数据数目可预先确定的情况,可以通过预先调用reserve来分配空间,再执行push_back,这种方法比直接push_back效率高
-
为了减少容器类数据拷贝延迟,可以在容器中存指针
-
对于判空操作,使用容器自带的empty函数优于使用size函数判空
-
多余内存空间释放技巧 vector
(dest).swap(dest)
vector(dest)创建一个临时变量,它的内容是dest的拷贝,使用vector的拷贝构造函数(它只拷贝真实存放的变量),然后将这个临时变量里面的数据和dest作交换后,dest里面的容量恢复正常状态,临时变量的容量则为dest的容量(通常情况下很大),执行完这句话,临时变量自动析构,释放之前占据的内存,dest则shrink to fit。
map<K,v>里面的元素类型为 pair<const K, V>,这也就说明不要试图去修改map里面的K值。
map[k] = v 的含义是 添加和更新,添加操作=临时变量的默认构造函数+赋值+析构该临时对象,在更新时,效率比insert高
map.insert(v):的含义:仅仅是添加,在添加时,效率比[]高。
remove不是真正意义上的删除,它做不到,它移动区间里面的元素,其中"不用被删除"的元素移动到区间的前部(保持原来相对顺序),它返回一个迭代器,指向"之后不用被删除"的元素之后的元素,一般有erase-remove是惯用法。这其中有例外,list::remove是真正删除元素.
迭代器
输入输出迭代器,这里面的输入输出的视角是以迭代器的视角,左侧站着的是容器,中间是迭代器,右侧的是算法,输入迭代器的意思是:以迭代器为主要视角,往迭代器输入,那么对于左侧的容器来说,就是输出,也就是读取迭代器里面的内容了。
基础迭代器分为输入迭代器(同一位置只读一次)和输出迭代器(同一位置只写一次),在两者功能的基础上,衍生出前向迭代器(同一位置可重复读写,并且支持向前单步移动),在前向迭代器功能基础上,衍生出双向迭代器(支持向后单步移动,也就意味着 支持双向单步移动),在双向迭代器的基础上,衍生出随机迭代器(支持跳跃移动)
算法
使用stl的算法和循环在大多数情况下,比自己手写循环在效率、正确性、可维护性上面都更胜一筹。
使用for_each(a.begin(), a.end(), func);
这里的fun有几种各种不同的情况:
- func为同名本地函数,传入的是函数指针
- func为同名类,传入的是类的"()"操作符的重载函数
- func为类的成员函数,传入的是类的成员函数地址
C和C++的标准库函数遵循传递函数指针是按照传值方式来,形如方式1,在传入函数指针数值,在循环内部根据此指针去寻找确切的函数
函数对象,简单的说,就是对象,这种对象,重载了()运算符,比如 class A,那么可以像调用函数一样使用它,比如A(5),这样使用。函数对象相比于全局函数,它可以保存状态信息,
struct WidgetNameCompare:
public std::binary_function<Widget, Widget, bool>{
bool operator()(const Widget& lhs, const Widget& rhs)const;
}
一般情况下,传递给unary_function(单个入参,返回值类型)或binary_function(两个入参,返回值类型)的非指针类型需要去掉const、引用和入参变量名称。
如果是带指针的入参,则需要如以下来定义:
struct PtrWidgetNameCompare:
public std::binary_functon<const Widget, const Widget, bool>{
bool operator()(const Widget* lhs, const Widge* rhs)const;
}
针对第三种情况,传入判别式为类的成员函数时,根据当前容器类型,如果当前容器里面存储的是类实例时,使用mem_fun_ref包装类成员函数,使其适配for_each的调用格式。当当前存储的是类实例指针时,使用mem_fun来包装.
使用函数对象而不是函数作为STL算法参数,是因为函数指针作为参数会抑制内联优化,而函数对象不会.
算法的名称含义
transform算法:某个函数将被应用到一个区间中的每一个对象,并把调用结果写到某一个地方。
replace_if算法:区间中所有满足判别式条件的对象都将被修改。
partition算法:一个区间中的对象将会被移动,所有满足某个判别式条件的对象会被组织到一起。
在有序序列中,可以使用lower_bound(查找第一次出现4的地方) , upper_bound(查找最后一个出现4的地方的后一个位置),equal_range相当于 lower_bound+upper_bound。
lower_bound(first,last,4)
1-->2-->3 -->4-- >4-->4-->4 -->5- ->6-->7--->8
upper_bound(first,last,4)
stl提供了在有序序列中的二分查找算法 binary_search
在算法调用和手写循环中,关于代码清晰度取决于你要在循环中做什么事情,如果要做的事情和一个算法实现的功能很相近,那么用算法调用会更好。
在软件工程领域中,一份代码被阅读的次数要远远大于它被编写的次数,可读性、可维护性需要特别注意。