0%

effective_STL

容器

调用empty而不是检查size是否为0

对任一容器C,下面代码

1
2
if(c.size()==0)
...

本质上与

1
2
if(c.empty())
...

等价的。但是为什么使用emtpy,这是因为emtpy对所有的标准容器都是常数操作时间,而对于一些list的实现,size耗费线性时间。

image-20221013165354696

这段代码只是把容器从头到尾数一遍才能知道有多少个元素,所以size()是线性的。

区间成员函数优化于之对应单元素成员函数

给定V1和V2两个矢量,使得V1的内容与V2的后半部分相同的最简单的操作是什么?

若不使用区间成员函数来解决此问题,就得要写一个显式的for循环,我们有代码

1
2
3
4
5
vector<Widget>v1,v2;
...
v1.clear();
for(vector<Widget>::const_iterator ci=v2.begin()+v2.size()/2;ci!=v2.end();++ci )
v1.push_back(*ci);

我们可以通过assign函数来解决此问题。若使用copy函数,通过实例化之后,与显式循环的速度相同。

若使用insert函数,则有

1
2
3
4
int data[numValues];
vector<int>v;
...
v.insert(v.begin(),data,data+numValues);//把整数插入到v的前端

而若通过显式地循环调用insert,则可能如下

1
2
3
4
5
vector<int>::iteator insertLoc(v.begin());
for(int i=0;i<numValues;i++){
insertLoc=v.insert(insertLoc,data[i]);
++insertLoc;
}

我们必须要把insert的返回值进行记录,否则会遇到两个问题,第一、第一次迭代后的所有循环迭代都将导致不可预料的行为,因为每次调用insert都会使insertloc无效,其次,即使insertLoc仍然有效,插入总是发生在vector的最前面,结果这个数组被以相反的顺序拷贝到v当中。

若是把循环替换对copy的调用,则有以下代码

1
copy(data,data+numValues,insert(v,v.begin()))

当copy模板被实例化之后,基于copy的代码和显式循环使用的代码基本相同。

然而采用单元素成员函数,会存在以下疸

  • 第一次影响不必要的函数调用把numValues个元素逐个插入到v中导致了对insert的numValues次调用。而使用区间形式的insert,则只做了一次函数调用,节省了numValues-1次。当然,使用内联(inlining)可能会避免这样的影响,但是,实际中不见得会使用内联。只有一点是肯定的:使用区间形式的insert,肯定不会有这样的影响。
  • 第二种影响是即把v中已有的元素频繁地移动到插入后它们所处的位置。每次调用insert把新元素插入到v中时,插入点后的每个元素都要向后移动一个位置,以便为新元素腾出空间。所以,位置p的元素必须被移动到位置/7+7在通常情况下,把numValues个元素逐个插入到含有n个元素的vector的前端将会有/inumValues次函数调用的代价:(n~l)numValues次调用Widget的赋值操作符和numValues次调用Widget的拷贝构造函数。即使这些调用是内联的,仍需要移动n次。

image-20221014184459774

别一方面参数inputIterator表示任何类型的输入迭代器都是可以接受的。

当心C++编译器的分析机制

我们有代码

1
2
3
ifstream dataFile("ints.bat");
list<int>data(istream_iterator<int>(dataFile),
istream_iterator<int>());

然而上面这串代码可能 不会如你所愿。这段代码可以通过编译,但是在运行时,它什么也不会做。

如有以下代码,这行代码声明了一个带double参数并返回int的函数

1
int f(double d);

下面这行做了同样的事情,参数d两边的括号是多余的

1
int f(double d);

这行声明了同样的函数,只是它省略了参数名称

1
int f(double);

我们再来看三个函数声明,第一个声明了一个函数g,它的参数是一个指向不带任何参数的函数指针,该函数返回double值。

1
int g(double(*pf)()); //g以指向函数的指针为参数

另一种方式也可表明同样的意思

1
int g(double pf());

参数名称亦可省略

1
int g(double());

我们再来研究代码

1
2
3
ifstream dataFile("ints.bat");
list<int>data(istream_iterator<int>(dataFile),
istream_iterator<int>());
  • 第一个参数名称是dataFile,它的类型是istream_iterator,dataFile两边的括号是多余的会被忽略。
  • 第二个参数没有名称,它的类型是不带参数的函数指针,该函数指针返回istream_iterator

我们有以下常见的错误

1
2
class Widget(...); //假定Widget有默认构造函数
Widget w();

它没有声明名为w的Widget,而是声明了一个名为w的函数,该函数不带任何参数,并返回一个Widget。

慎重选择删除元素的方法

若有一个连续内存的容器(vector,deque或string)那么最好的办法是earse-remove用法

1
2
c.erase(remove(c.begin(),c.end(),1936),c.end())
//当c是vector,deque,string时,erase remove是删除特定元素的最好方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <class ForwardIterator, class T>
ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val)
{
ForwardIterator result = first;
while (first!=last)
{
if (!(*first == val))
{
*result = move(*first);
++result;
}
++first;
}
return result;
}

remove函数是通过迭代器不断向前移动来达到删除元素的目的。

对于list,则是list的成员函数更加有效.

1
c.remove(1963)

而对于关联窗口而言,则应该调用erase函数

1
c.erase(193)

若我们不再从c中删除所有等于特定值的元素,而是删除使下面的判别式返回true的每一个对象

1
bool badValue(int);//返回x是否是坏值

image-20221018152219620

而对于标准关联容器而言,我们有两种解决办法,一种易于编码,一种效率更高

image-20221018155435817

image-20221018155519318

image-20221018155722721

image-20221019120022709