C++标准库进阶1

本文分享了关于STL容器的高效使用技巧,包括选择合适容器、封装迭代器、使用智能指针避免内存泄漏等内容,旨在帮助开发者提高程序性能。

一、容器的选择

一般情况下,最容易想到也最常用的就是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并使用智能指针

 

九、使用reverse避免不必要的重新分配内存

对于vector和string来说,如果知道预先要添加多少元素,要使用reverse扩大容器的容量,避免重新分配内存,提高程序的效率

 

参考

《Effective STL》

 

欢迎大家评论交流,作者水平有限,如有错误,欢迎指出

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值