C++STL使用常见注意事项

1、使容器里对象的拷贝操作轻量而正确
容器容纳对象,获取容器的对象,是对象的一份拷贝;向容器加入对象,也是对原有对象的一份拷贝。

如果用拷贝昂贵的大对象填充容器,大对象进入容器将成为性能瓶颈。

分割问题:由于继承的存在,拷贝会导致分割。

规则1:对于大对象,用指针容器代替对象容器。

【注】

1)容器销毁前需要自行销毁指针所指向的对象;否则就造成了内存泄漏;

2)使用排序等算法时,需要构造基于对象的比较函数,如果使用默认的比较函数,其结果是基于指针大小的比较,而不是对象的比较;

 

规则2:像数组一样使用连续内存容器。

向连续内存容器中,只在容器的末尾添加对象。

2、用empty来代替检查size()是否为0
任意容器c:

用if(c.empty())代替if(c.size() == 0)。

对于所有的标准容器的时间复杂度o(n),empty是常数时间,但对于一些list实现,size是线性时间。

3、删除容器中的元素
1、连续内存容器:(vector、deque、string)使用erase - remove或者erase - remove_if惯用法;

eg:
 Container<int> c;

    c.erase(remove(c.begin(), c.end(), 1963), c.end());

    c.erase(remove_if(c.begin(), c.end(), badValue), c.end());
2、标准关联容器(set、multiset、map、multimap、unordered_map):调用erase。

eg:
 AssocContainer<int> c;

c.erase(1963);
删除满足条件的元素:

错误做法:
    AssocContainer<int> c;

    ...

    for (AssocContainer<int>::iterator i = c.begin(); i!= c.end(); ++i) 
    {

        if (badValue(*i)) c.erase(i);

    }
因为调用erase后,迭代器i会失效。

正确做法:
    AssocContainer<int> c;
    ...
    for (AssocContainer<int>::iterator i = c.begin(); i != c.end(); ) 
    {
        if (badValue(*i))
            c.erase(i++);
        else
            ++i;
    }

4、尽量使用vector和string来代替动态分配的数组
动态分配(new)数组,你应承担的负担:

1. 你必须确保有的人以后会delete这个分配。如果后面没有delete,你的new就会产生一个资源泄漏。

2. 你必须确保使用了delete的正确形式。对于分配一个单独的对象,必须使用“delete”。对于分配一个数组,必须使用“delete []”。如果使用了delete的错误形式,结果会未定义。在一些平台上,程序在运行期会荡掉。另一方面,它会默默地走向错误,有时候会造成资源泄漏,一些内存也随之而去。

3. 你必须确保只delete一次。如果一个分配被删除了不止一次,结果也会未定义。

vector和string消除了上面的负担。

5、vector和string使用reserve来避免不必要的重新分配
vector和string增加新的元素,且空间不够时,会进行内存的重新分配。重分配的步骤如下:

1. 分配新的内存块,它有容器目前容量的几倍。在大部分实现中,vector和string的容量每次以2为因数增长;

2. 把当前元素拷贝到新内存中;

3. 销毁旧内存中的对象;

4. 回收旧内存;

5. 将新值放入新空间指定位置。

上述步骤包括所有元素的分配,回收,拷贝和析构,这些步骤都很昂贵,应最小频率的执行它们。reserve(Container::size_type n)强制容器把它的容量改为至少n,提供的n不小于当前大小。因此,reserve成员函数允许你最小化必须进行的重新分配的次数。

避免重新分配方式:使用reserve尽快把容器的容量设置为足够大,最好在容器被构造之后立刻进行。

低效做法:
    vector<int> v;
    for (int i = 1; i <= 1000; ++i) v.push_back(i);
高效做法:
    vector<int> v;
    v.reserve(1000);
    for (int i = 1; i <= 1000; ++i) v.push_back(i);

6、在std::vector尾部添加对象时应尽量使用emplace_back,而不要使用push_back
使用push_back是一个复制行为,会比emplace_back(采用右值引用)多调了转移构造函数和析构函数,因而性能偏低。

低效做法:

std::vector<string> strs;
strs.push_back(“Hello World”);

高效做法:

strs.emplace_back(“Are You OK?);

7、需要将对象全部转移到另外一std::vector时,应使用std::vector.swap()、std::swap()、std::move(std::vector)
std::vector ver;

ver.emplace_back(“Hello”);

ver.emplace_back(“World”);

ver.emplace_back(“Are You OK?”);

例子:将上述对象中的内容转移到新的对象中。

低效做法:

std::vector<string> other;
for (int i = 0; i < ver.size(); ++i)
{
    other.push_back(ver[i]);
}

高效做法:

other.swap(ver);

1、代码简单,表达意图清晰;

2、转移的过程只是更改了指针的指向,没有发生任何复制或拷贝,效率高。

8、如果std::vector中在存放指针对象,即std::vector<T*>,则应使用智能指针
std::vector<std::unique_ptr>

std::vector<std::shared_ptr>

如果std::vector中存放指针,指针指向的对象并不受std::vector管理,所以需要智能指针帮助管理这些对象的生命期。

9、利用“交换技巧”使vector和string的内存“收缩到合适”
由于vector和string的内存分配机制,可能导致容器占有不被使用的内存容量,导致内存的浪费。要避免vector或string持有它不再需要的内存,需要有一种方法来把它从曾经最大的容量减少到它现在需要的容量。

vector的做法:

vector contestants;

… // 使contestants变大,然后删除所有

vector(contestants).swap(contestants);

表达式vector(contestants)建立一个临时vector,它是contestants的一份拷贝:vector的拷贝构造函数做了这个工作。但是,vector的拷贝构造函数只分配拷贝的元素需要的内存,所以这个临时vector没有多余的容量。然后我们让临时vector和contestants交换数据,这时我们完成了,contestants只有临时变量的修整过的容量,而这个临时变量则持有了曾经在contestants中的发胀的容量。在这里(这个语句结尾),临时vector被销毁,因此释放了以前contestants使用的内存。

string的做法:

string s;

… // 使s变大,然后删除所有

string(s).swap(s);

10、当关乎效率时,更新元素用map::operator[],增加元素用map-insert
map::operator[]被设计为简化“添加或更新”功能。

map<K, V> table;

table[k] = v;

上述语句的动作:检查键k是否已经在map里。如果不,就添加上,以v作为它的对应值。如果k已经在map里,它的关联值被更新成v。

对应的operator[]的工作原理:operator[]返回一个与k关联的值对象的引用。然后v赋值给所引用(从operator[]返回的)的对象。当要更新一个已存在的键的关联值时很直接,因为已经有operator[]可以用来返回引用的值对象。但是如果k还不在map里,operator[]就没有可以引用的值对象。那样的话,它使用值类型的默认构造函数从头开始建立一个,然后operator[]返回这个新建立对象的引用。

eg:

1、map增加元素的实现:

低效做法:

map<int, double> m;

m[1] = 1.5;

功能等价于:

pair<map<int, double>::iterator, bool> result = m.insert(map<int, double>::value_type(1, double()));

result.first->second = 1.5;

高效做法:

m.insert(map<int, double>::value_type(1, 1.5));

2、map更新元素的实现:

在上述基础上,将<1, 1.5>改为<1, 2.3>

低效做法:

m.insert(pair<int, double>{1, 2.3});

高效做法:

m[1] = 2.3;

11、尽量用区间成员函数代替单元素操作
使用区间成员函数有以下好处:1)更少的函数调用; 2)更少的元素移动; 3)更少的内存分配。

给定vector类型的容器对象v1和v2,将v2后半部分赋值给v1:

1、低效做法

for (auto iter = v2.begin() + v2.size() / 2; iter != v2.end(); ++iter )

v1.push_back(*iter );

2、高效做法,使用区间成员函数assign

v1.assign(v2.begin() + v2.size() / 2, v2.end());

12、尽量用算法替代手写的循环
1)效率相比手写更高;

STL的代码都是C++专家写出来的,专家写出来的代码在效率上很难超越;除非我们放弃了某些特性来满足特定的需求,可能能快过STL;比如,基于特定场合下的编程,放弃通用性,可移植性;

2)不容易出错;

3)使用高层次思维编程。

相比汇编而言,C是高级语言;一条C语言语句,用汇编写需要好几条;同样的,在STL的世界中,我们也有高层次的术语:

高层次的术语:insert/find/for_each(STL算法)

低层次的词汇:for/while(C++语法)

用高层次术语来思考编程,会更简单;

例子:在容器vector v中查找字符串,如“hello”。

低效做法:

for (auto iter = v.begin(); iter != v.end(); ++iter)
{
    if (*iter == “hello”)
    //Do Something;
}

高效做法1:

if (v.find(“hello”) != v.end())
//Do Something;

高效做法2:

for_earch(v.begin(), v.end(), FUNC());

13、尽量用成员函数代替同名的算法
1)基于效率考虑,成员函数知道自己这个容器和其他容器有哪些特有属性,能够利用到这些特性;而通用算法不可以;

2)对于关联容器,成员函数find基于等价搜索;而通用算法find基于相等来搜索;可能导致结果不一样;

【知识点】

相等的概念是基于operator==的。如果表达式“x == y”返回true,x和y有相等的值,否则它们没有。

等价是基于在一个有序区间中对象值的相对位置;通常基于operator<。

14、使用函数对象代替裸函数作为算法的输入参数
因为内联,在函数对象的方式中,内联有效,而作为函数指针时,一般编译器都不会内联函数指针指向的函数;即使指定了inline;

例子:对排序算法sort。

低效做法:

inline bool doubleGreater(double d1, double d2)
{
    return dl > d2;
}
vector<double> v;
...
sort(v.begin(), v.end(), doubleGreater);

这个调用不是真的把doubleGreater传给sort,它传了一个doubleGreater的指针。

高效做法:

template<typename T>

struct greater
{
	bool operator()(T d1, T d2)
	{
		return dl > d2;
	}
}
sort(v.begin(), v.end(), greater<double>())

【注】《Effective STL》的实验结论表明,使用函数对象一般是裸函数的1.5倍,最多能快2倍多。

15、选择合适的容器
为什么vector不提供push_front()成员方法?因为效率太差,如果有太多从前面插入的需求,就不应该使用vector,而用list。

关心查找速度,首先应该考虑散列容器(非标准STL容器,如:unordered_map,unordered_set);其次是排序的vector,然后是标准的关联容器。

关心增删改的效率,优先选择vector、list等,尽量不使用关联容器(unordered_map、map),他们采用平衡二叉树,其插入机制及内存分配不适合做频繁的内存操作。

16、合理选择vector、list、deque
(1)vector是连续存储结构,相比数组,多了动态内存管理。

优点:支持高效的随机访问,支持尾端高效插入/删除。占用内存小。

缺点:首端和中间位置的插入/删除效率低下。

(2)duque是连续存储结构,相比vector,deque维护了容器的首端地址,故支持高效的首端插入/删除操作。

优点:支持高效的随机访问,支持首尾端高效插入/删除。

缺点:占用的内存大。

(3)list是非连续存储结构,是双向链表结构,每个节点维护其前后节点的地址,故支持前/后向遍历。

优点:不使用连续内存完成动态操作,支持任意位置高效的随机插入/删除。

缺点:不能进行内部随机访问(不支持[]和.at()),占用的内存大。

(4)三者使用原则:

① 如果需要高效的随机存取,而不在乎插入/删除的效率,使用vector

② 如果需要大量高效的插入/删除,而不在乎随机存取时间,使用list

③ 如果需要高效的随机存取,还要大量的首尾端插入/删除,使用deque

17、正确使用查找函数(算法)
一些函数的功能如下:

count是简单地查找所有满足条件的元素。

find找到满足条件的元素,并返回它所在的位置。

binary_search是返回元素在不在。

lower_bound是返回满足条件的第一个元素的位置。

upper_bound是返回满足条件的元素后的第一个元素的位置。

equal_range返回lower_bound和upper_bound。

猜你喜欢

转载自blog.csdn.net/pynash123/article/details/88723557
今日推荐