第11条:理解自定义分配子的合理用法
如果你觉得自己在内存管理方面比STL实现得更好,自己管理STL的内存分配,这样可以通过自己实现分配子来实现,在声明STL对象时指定自己定义的分配子。主要是要实现allocate和deallocate两个函数。(mallocShared/ freeShared为自定义分配方法)
template< typename T>
class ShareMemoryAllocator {
public:
…
pointer allocate< size_type numObjects, const void* localityHint = 0 )
{
return static_cast<pointer>( mallocShared(numObjects * sizeof(T)));
};
void deallocate( pointer ptrToMemory, size_type numObject )
{
freeShared( ptrToMemory );
}
};
使用时使用typedef简化一下:
typedef vector<double, ShareMemoryAllocator<double> > SharedDoubleVec;
但问题是,这样SharedDoubleVec v;的话,对像v容纳的元素将会分配到自定义的内存块中是实现了,但v自身是放在其它地方,总之不会是自定义的内存中!如果想要也放到自定义内存块,可以实现以下操作:
void * pVectorMemory = mallocShared( sizeof( SharedDoubleVec )); //使用自定义函数分配
SharedDoubleVec * pv = new ( pVectorMemory ) SharedDoubleVec;//指定new的位置
释放时要手工调用:
pv->~SharedDoubleVec();
freeShared( pVectorMemory );
这于这种“分配/构造/析构/释放”四部曲还是建议避免为上。总的来说,尽量不要自己实现了吧。
Effective STL学习总结二(12-18)
kejieleung
第12条:切勿对STL容器的线程安全性有不切实际的依赖
对一个STL实现最多可以期望:
(1) 多个线程读是安全的(读的个程中不能对容器有任何写入操作)
(2) 多个线程对不同容器做写入操作是安全的
对于线程的同步问题还是要手工实现。可通过构建一个Lock类,在它的构造时获得一个互斥对象,在析构时释放,同时能尽量减少影响。利用C++的作用域可以很好的实现。
template< typename Container >
class Lock
{
public:
Lock( const Container& container ): c( container ) {
getMutesFor( c );
}
~Lock() {
releaseMutexFor( c );
}
private:
const Container& c;
};
可以这样使用,Lock作为局部对象:
vector<int> v;
...
{ //规定在这个作用域时加上互斥实现同步
Lock<vector<int>> lock(v);
//对vector作需要同步的操作
...
} //退出作用域时释放互斥,使影响范围尽可以少
另外,基于Lock的方案还是异常安全的!C++保证如果有异常抛出,局部对象会被析构!(当然前提是上层有捕获这个异常)
(从13 – 18主要介绍vector和string)
第13条:vector和string优先于动态分配的数组
如果你选择用new来动态分配内存时,这意味着你将承担以下责任:
(1) 必须要有人用delete删除所分配的内存,否则会造成资源泄漏
(2) 必须正确的使用delete,分配的是对象,用delete;如果分配的是数组,用delete[] 的方式,否则也会造成资源泄漏和破坏内存
(3) 必须确保只delete一次,如果一次分配被多次delete,结果不确定。
所以当你想动态分配内存时,如new T[…]就考虑一下用vector和string代替。它们会消除上面负担,因为它们会自动管理内存(当元素加入到内存时会自动增长,而当它们析构时会自动析构包含的这些元素)。
虽然使用动态分配的数组可以消除不必要的内存分配和不必要的字符拷贝,从而提高很多应用程序的效率。不过在string里已有通过引用计数来实现优化,所以性能方面不会有太大差别了,不过当然最好还是在具体应用时实验验证。
但在string里使用引用计数在多线程环境下会有较大的性能问题。
第14条:使用reserve来避免不必要的重新分配
STL的容器会自动增长以便容器容纳下你放入其中的数据,每一次需要更多的空间的时候,就会调用与realloc类似的操作:
(1) 每次容量以2的倍数增长
(2) 把容器的所有元素从旧的内存拷贝到新的内存中
(3) 析构掉旧内存中的对象
(4) 释放旧内存
而这些步骤是非常耗时的,所以应尽量减少这样不必要的操作。同时,每次扩容后,原来的指针、迭代器会得无效。通过reserve可以把重新分配的次数减少到最低限度。
先解释一下四个表示大小的函数:
(1) size():容器里有多少个元素,但不会得到容器分配了多少内存
(2) capacity():容器已分配的内存可以容纳多少个元素,如果想知道容器里有多少内存没有被使用,可以使用(capacity() - size() ) * sizeof( Elem )
(3) resize( Container::size_type n ):强迫容器改变到包含n个元素的状态。如果n比原size少,容器尾部的元素将会被析构。如果n比原size大,则会通过默认构造函数创建新的元素添加到容器的尾部。如果n比原capacity大,则先重新分配内存
(4) reserve( Container::size_type n ):强迫容器把容量变为n,前提是n不少于当前大小。
可以测试一下使用reserve前后的对比:
start = GetTickCount();
for( i=0; i<1000000; ++i )
vecint1.push_back( i );
cout<<GetTickCount() - start <<endl;
328
(导致2-10次的分配)
start = GetTickCount();
vecint2.reserve( 1000000 );
for( i=0; i<1000000; ++i )
vecint2.push_back( i );
cout<<GetTickCount() - start <<endl;
219
(开始时分配后,后面就不会导到处重新分配)
从结果知,容量越大,差别就越明显。但也从别一个角度说明,STL本身已经做了很好的优化,再看一个动态分配的时间,既使加上动态分配的时间,也只是用了:16!!
start = GetTickCount();
int * data = new int[ 1000000 ];
for( i=0; i<1000000; ++i )
data[i] = i;
cout<<GetTickCount() - start <<endl;
delete[] data;
呵呵,原始方式果然是高效呀(10倍以上了)!
第15条:注意string实现的多样性
Sizeof(string)长度是多少?平时真的没去注意过。由于STL内实现的不同,会出现不同的长度,在vc6下测试是 16,在linux下测试是 4。具体要看实现形式。
第16条:了解如何把vector和string数据传给旧的API
Vector的数据是存储在连续的内存中,取得vector的元素起始指针可以用:&v[0];,但下面方式可能会有问题:
Vector
DoSomething( char *, int);
DoSomething(&v[0], v.size() ); // size 为 0
所以应该多加个判断:
If( !v.empty() ) { // 判断空要直接使用empty
DoSomething(&v[0], v.size() );
}
还有不应该用 v.begin() ,它是返回迭代器iterator, 如果非要用可以 &* v.begin()。
注意string不能这样传递起址:(1)string数据不一定是连续存储在内存中的(2)内部不一定以空字符结尾。所以需要调用 .c_str(),返回C形式的字符串,加上了空字符在结尾,但是产生的指针不一定指向内部表示,指向一个不可修改的拷贝。
如果想通过C API字符串初始化一些容器的话,可能通过先入到vector
Size_t fillArray( double * pArray, size_t arraySize );
Vector
Vd.resize( fillArray( &vd[0], vd.size() );
Deque
List
Set
这样可以减少使用动态数组。
第17条:使用swap去除多余容量
Shrink-to-fit技术:
Vector
先创建一个临时对象,它是contestants的拷贝,只会分配需要的元素空间。之后再将临时对象上的元素与原对象交换,这样contestants就具有了去除之后的容量了!!临时变量析构同时释放原容器的对象。
如string可以使用 string().swap(s);达出使用最少内存。
第18条:避免使用vector
可以用bitset代替
第19条:理解相等性(equality)和等价(equivalence)的区别
find对于“相同”的定义是相等,以operator ==为基础,而set::insert对于“相同”的定义是等价,是以operator < 比较。
使用函数对象
struct CIStringCompare: public binary_function
{
bool operator() ( const string& lhs,
const string& rhs) const
{
return ciStringCompare( lhs, rhs ); //自定义的比较函数
}
}
set< string, CIStringCompare> ciss; //这样就定义了一个按自己比较排序的set
这时,如果使用 set的成员函数查找find可以成功,使用全局的find就可能不成功了(不会调用到自定义的比较函数)
第20条:为包含指针的关联容器指定比较的类型
比如有:
set
ssp.insert( new string("abc") );
ssp.insert( new string("efg") );
...
for( set
i != ssp.end();
++i )
cout<< *i << endl; //这样不会得到你想要的结果!!因为使用 **i才正确
其实set
但同时也有问题,元素按指针值排序,而不是按插入的元素,所以,需要为这样的指针型容器指定比较类型
为了少写代码可以用 copy,也容易帮你发现错误
copy( ssp.begin(), ssp.end(), ostream_iterator
可以写自己的比较函数
struct CIStringCompare: public binary_function
{
bool operator() ( const string * ps1,
const string * ps2 ) const
{
return *ps1 < * ps2; //自定义的比较函数
}
}
写成通用的模板类型
struct DereferenceLess {
template< typename PtrType >
bool operator() ( PtrType pT1,
PtrType pT2 ) const
{
return *pT1 < * pT2;
}
}
第21条:总是让比较函数在等值情况下返回false
第22条:切勿直接修改set或multiset中的键值
同样对map/multimap也是适应,不过map/multimap是不可以修改键部分,可以修改值部分。而set/multiset可以修改键!因为set
EmpIDSet::iterator i = se.find( selectedID );
if ( i != se.end () ) {
Employee e( *i );
e.setTitle("xxx"); //改变了key值
se.erase( i++ ); //使迭代器保持有效性
se.insert( i, e ); //插入新元素,提示位置与原来相同
}
第23条:考虑用排序的vector替代关联容器
第24条:当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择
有map
m[1] = 1.50; 等同于:
typedef map
pair
m.insert( IntWidgetMap::value_type( 1, Widget() )); //key 1和默认构造函数
result.first->second = 1.50; //为新对角赋值
所以发为不必要的临时对象,换成直接的insert直接调用:
m.insert( IntWidgetMap::value_type( 1, 1.50 ) );
这样可以节省:
1. 默认构造的临时对象
2.析构临时对象
3.Widget的赋值符
这些函数调用的开销越大,节省就越大
如果是更新呢?
m[ k ] = value;
可以改写成:m.insert( IntWidgetMap::value_type( k, value) ).first->second = value; //先调用insert找出key的pair对象
当然这样写的话就比较麻烦,不过我们这里考虑的是效率
第25条:熟悉非标准的哈希容器
hash_container;
通常使用的是hash_map
在VS2005下使用stdext名字空间可以使用到hash_map, 如果在gcc环境下,需要为string特例化比较函数对象:
#endif
hash_map的优点在于查询性能上,那就先与map对比插入,查询:
|
插入10w string |
查询1000 string |
windows(release) hasp_map |
250 |
15 |
linux hasp_map |
18 |
1 |
windows map |
265 |
32 |
linux map |
27 |
3 |
不知为何相差如此大,看来在windows上使用hash_map性能没提高多少,特别是插入时,难道是与gcc下自定义的函数对象有关?!
另外有一个部题困挠了很久,就是在读文件时,在windwos好好的,但到了linux下取不正常,后来对上传到linux服务器下的文件做了一次 dos2unix 后问题才消除
第26条:iterator优先于const_iterator、reverse_iterator以及const_reverse_iterator
第27条:使用distance和advance将容器的const_iterator转换成iterator
IntDeque d;
ConstIter ci;
...
Iter i( d.begin() );
advance( i, distance( i, ci )); //移动i,使它指向ci所指的位置
advance( i, distance
第28条:正确理解由reverse_iterator的base()成员函数所产生的iterator的用法
注意rbegin() 不同于end(), rbegin() 是最后一个元素,end()是最后一个元素的后一个位置
调用reverse_iterator的base()可以得到与之相对应的iterator, 插入的时候 ri与ri.base()是等价的,但是要注意删除:
要指定删除ri指向的元素,即是要删除ri.base()的前面位置的元素
可以通过这样完成:v.erase( (++ri).base() );