一、容器的选择
一般情况下,最容易想到也最常用的就是vector,但是如果要是频繁在容器对象的中间删除和插入元素,就要考虑使用list,因为list是基于节点的容器,当频繁在容器中间删除和插入元素时,不需要将很多元素整体向前或者向后移动,只需要操作节点元素指针指向的位置,效率较高且不会导致迭代器失效。如果频繁在容器两端删除和插入元素,不仅可以使用vector,也可以使用deque.
如果对容器中的元素的顺序有要求的话,可以考虑有序关联内容器,如果对容器对象的查找速度有要求的话,可以使用无序关联容器
二、封装容器的迭代器
封装容器的迭代器的好处是当代码中的容器从一种换到另一种时,不会修改大量的代码
示例
void iter()
{
vector<int> v={1,2,3,4,5,6};
vector<int>::iterator it1 =find(v.begin(),v.end(), 3);
vector<int>::iterator it2 = v.erase(++v.begin());
}
上面的代码没啥问题,但是当需要把vector换成list时,需要改动3.4.5行三处代码
如果封装容器的迭代器,则不用那么麻烦
using viter1=vector<int>::iterator;//C++11新的类型别名用法
typedef vector<int>::iterator viter2;
void ecapiter()
{
vector<int> v={1,2,3,4,5,6};
viter1 it1 =find(v.begin(),v.end(), 3);
viter1 it2 = v.erase(++v.begin());
}
此时,如果需要将容器修改为list,则只需要修改第一行和第六行代码,只需要修改两处,可能感觉修改的地方也没少多少,但是,如果当需要修改容器时,代码量一多,需要修改的地方就会越来越多,所以,把容器的迭代器封装起来吧
三、当把自定义类的对象拷贝到容器中时,请拷贝自定义类的对象的指针,
当把元素放入容器中时,会将元素的拷贝放入容器中,拷贝是STL的工作方式
示例
class test
{
public:
test(){cout<<__func__<<endl;}
~test(){cout<<__func__<<endl;}
test(const test &t) {cout<<__func__<<__LINE__<<endl;}
int a;
};
class test2:public test
{
public:
test2(){cout<<__func__<<endl;}
~test2(){cout<<__func__<<endl;}
test2(const test2 &t) {cout<<__func__<<__LINE__<<endl;}
int b;
int c;
};
int main(int argc, char const *argv[])
{
test a;
vector<test> va;
test2 a2;
va.insert(va.begin(), a2);
cout<<va.capacity()<<endl;
va.push_back(a2);
cout<<va.size()<<endl;
return 0;
}

上面的代码的时序如下:分别创建了test和test2两个对象,然后将a2放入容器中,此时子类对象失去其中的特有数据并将基类部分拷贝到容器中,所以调用了一次拷贝构造函数,而此时的capacity为1,当再次添加元素时,vector对象重新分配内存,将两个元素重新拷贝到新的内存中,所以调用了两次拷贝构造函数,然后将原来内存释放,所以调用了一次析构函数,主函数执行结束后,分别释放一个子类对象和三个基类对象
上述代码中的问题:1.如果子类和基类的数据较多,拷贝较慢而且会频繁的分配和释放内存,会导致程序性能差。2.将子类对象拷贝到基类的容器中,会使子类失去其中的特有数据b。
所以,当我们将自定义类型的元素放入容器中时,首先要看容器的元素类型和要放入的元素类型是否匹配,如果类型不匹配,拷贝时会有类型转化,如果有且失去的数据很重要,那么就要使容器中的元素类型和存入的数据类型匹配。其次,因为自定义类型在拷贝和构造时,可能会有大量的数据,当把自定义对象放入容器中时,会导致程序性能差,所以最好将容器的存储元素换成自定义类型的指针,这样在拷贝时,只需要拷贝四字节的指针,而不需要拷贝大量的数据,但是,此时要注意防止内存泄露
四、检查容器是否为空时,请调用empty(),而不是size()==0
成员函数empty()和表达式size()==0是等价的,对于连续内存的容器来说,二者效率相差不大,但是对于list俩说,empty()的效率是常数时间,而size()是线性时间,所以,检查容器是否为空时,优先调用empty()
五、向容器中添加大量元素时,优先考虑容器的范围添加函数
每一种顺序容器容器的添加,删除,创建和赋值操作都有区间操作,区间操作接受迭代器参数组成的范围进行删除,插入,创建和赋值。关联容器除了没有区间赋值assign操作外,其余的都有
当一次性进行大量的元素操作时,使用区间操作会提高程序的效率
示例
void inserttest()
{
vector<int> v1(10e5, 1);
vector<int> v2(1000, 3);
vector<int>::iterator it2(v2.begin());
cout<<clock()<<endl;
for (vector<int>::iterator it=v1.begin();it<v1.end();++it) {
it2=v2.insert(it2, *it);
++it2;
}
cout<<clock()<<endl;
cout<<"---------------------"<<endl;
vector<int> v3(10e5, 1);
vector<int> v4(1000,3);
cout<<clock()<<endl;
v4.insert(v4.begin(), v3.begin(), v3.end());
cout<<clock()<<endl;
cout<<(v4==v2)<<endl;
}

上面两段代码的效果完全一样,但是使用范围插入函数的效率远远高于使用单元素的insert函数。这是因为:1、对于单元素的insert来说,每次插入一个元素,都要使v2中的元素向后移动,这样就会产生100000*1000次移动元素的操作,如果容器中存储的元素是自定义类型的元素,又需要调用拷贝构造函数或者赋值运算符,耗费的时间大大增加,但是对于范围插入元素来说,一次性将100000个元素插入容器中,只会产生1000次移动元素的操作,而且调用拷贝构造函数或者赋值运算符的次数也会大大减少,所以范围插入insert比单元素插入insert的效率要高。2、由于vector在插入元素的过程中要不断申请新的内存,释放旧的内存,该过程的次数越多,代码越耗时,在上面的代码中,单元素的insert操作使vector重新分配内存的次数比一次性范围insert多。
所以,综合上面两点,如果在程序中要一次性插入元素时,优先考虑范围插入函数,因为那样效率更高
使用范围插入操作不仅效率高,而且会少写代码,单元素插入操作使用了循环,而范围插入操作仅仅用了一行代码
范围赋值,范围初始化和范围删除操作也是同理
六、正确使用流迭代器
拷贝将一个文件中的内容拷贝到一个list中
void ifstreamlist()
{
ifstream ifs1("./test2.cpp");
list<char> data1(istream_iterator<char>(ifs1), istream_iterator<char>());
cout<<data1.size()<<endl;
}
上面这段代码看起来没啥问题,但是编译时编译器提示data1是一个函数,而不是一个对象

data1是一个函数的声明,返回值是个list<char>,两个形参都是输入流迭代器
所以,如果想正确实现该功能,需要将两个流迭代器定义成变量,然后去初始化list
void ifstreamlist()
{
ifstream ifs1("./test2.cpp");
//list<char> data1(istream_iterator<char>(ifs1), istream_iterator<char>());
istream_iterator<char> begin(ifs1);
istream_iterator<char> end;
list<char> data1(begin, end);
cout<<data1.size()<<endl;
}

七、如果容器中的指针指向了堆内存,需要使用智能指针将堆内存释放
考虑要在指针中创建100个int,根据第三点,定一个了一个vector<int *>,然后每个元素指向一个int数据
void rightdelete()
{
vector<int *> v;
for (int i=0;i<100;++i) {
v.push_back(new int(10));
}
}
上面的代码会产生内存泄露,一个不是那么好的解决办法就是在函数的退出的时候,对vector中的每个指针都进行delete,但是如果函数出现异常,很有可能直接退出而没有delete,所以,这时候要使用智能指针管理内存
void rightdelete()
{
vector<shared_ptr<int>> v;
for (int i=0;i<100;++i) {
v.push_back(shared_ptr<int>(new int(10)));
}
}
上面的代码就不会产生内存泄露,首先创建一个临时的智能指针变量,然后将临时变量的拷贝push_back到vector中,然后临时变量被释放,此时shared_ptr的共享者数量为1,当函数退出时,v中的智能指针被销毁,共享者数量为0,指向的内存自动被释放
八、 删除算法remove和成员方法erase
remove并不能真正的删除元素
示例
void removeanderase()
{
vector<int> v(10, 0);
v[1]=v[3]=v[5]=10;
remove(v.begin(), v.end(), 10);
cout<<v.size()<<endl;
}

可见,remove并不能真正的将元素从容器中删除
所以,如果想真的把元素从容器中删除,需要在remove后再调用erase
void removeanderase()
{
vector<int> v(10, 0);
v[1]=v[3]=v[5]=10;
v.erase(remove(v.begin(), v.end(), 10), v.end());
cout<<v.size()<<endl;
for (auto i:v) {
cout<<i<<endl;
}
}

将remove的返回值传给erase是一种习惯用法
list将remove单独实现,和remove算法不同的是,list的remove成员函数可以成功删除元素
void removeanderase()
{
list<int> l(10, 0);
l.remove(0);
cout<<l.size()<<endl;
for (auto i:l) {
cout<<i<<endl;
}
}

同样,list单独实现的unique和算法unique也也remove一样,算法unique并不会真正的对元素去重,而仅仅是对重复元素进行覆盖,具体见博客https://blog.youkuaiyun.com/Master_Cui/article/details/108491058
而list的unique会真正的删除元素,见博客https://blog.youkuaiyun.com/Master_Cui/article/details/107891729
因为算法remove并不会真正的删除元素,所以对于存储指向堆内存指针的容器来说,单独调用remove不仅没有删除元素,而且还会产生内存泄露,解决办法就是在remove后,调用erase并使用智能指针
对于vector和string来说,如果知道预先要添加多少元素,要使用reverse扩大容器的容量,避免重新分配内存,提高程序的效率
参考
《Effective STL》
欢迎大家评论交流,作者水平有限,如有错误,欢迎指出
本文分享了关于STL容器的高效使用技巧,包括选择合适容器、封装迭代器、使用智能指针避免内存泄漏等内容,旨在帮助开发者提高程序性能。
4万+

被折叠的 条评论
为什么被折叠?



