由压缩一个臃肿的STL Vector操作引发的思考

 压缩很多空余空间的vec

  std::vector<int> vec(5);  //capacity=5

  vec.reserve(100);         //capacity=100

  vec.reserve(10);          //capacity=100
	std::vector<int> temp(vec);  //temp的capacity=5  (拷贝构造函数的功能)  

	temp.swap(vec);              //temp的capacity=100,vec的capacity回复为5 (swap的功能)  

	temp.clear();   //temp功成身退

一条语句搞定:

      std::vector<int>  (vec).swap(vec)//临时变量在哪里

 

 

临时变量

  它们是被神所遗弃的孩子,没有人见过它们,更没有人知道它们的名字.它们命中注定徘徊于命运边缘高耸的悬崖和幽深的深渊之间,

  用自己短暂的生命抚平了生与死之间的缝隙.譬如朝露,却与阳光无缘.是该为它们立一座丰碑的时候了,墓铭志上写着:我来了,我走了,我快乐过.

  许多人对临时变量的理解仅仅限于:

  string temp;

  其实,从C++的观点来看,这根本就不是临时变量,而是局部变量.

  C++的临时变量是编译器在需要的时候自动生成的临时性变量,它们并不在代码中出现.但是它们在编译器生成的二进制编码中是存在的,也创建和销毁.在C++语言中,临时变量的问题格外的重要,因为每个用户自定义类型的临时变量都要触发用户自定义的构造函数和析构函数(如果用户提供了)

  又是该死的编译器!又该有人抱怨编译器总在自己背后干着偷偷摸摸的事情了.但是如果离开了编译器的这些工作,我们可能寸步难行.

  如果X是一个用户自定义的类型,有默认构造函数,拷贝构造函数,赋值运算函数,析构函数(这也是类的4个基本函数),那么请考虑以下代码:

X get(X arg){ return arg;  }

X a;

X b = get(a);

  即使是这么简单的代码也是很难实现的

  让我们分析一下代码执行过程中发生了什么?

  首先我要告诉你一个秘密:对于一个函数来说,无论是传入一个对象还是传出一个对象其实都是不可能的.

  让一个函数传入或传出一个内置的数据类型,例如int,是很容易的,但是对于用户自定义类型的对象却非常的困难,因为编译器总得找地方为这些对象写上构造函数和析构函数基本数据类型,在新函数栈里分配空间,拷贝BYTE就行。但对于非基本数据类型,不能直接拷贝一堆字节——《C++必知必会》),不是函数内,就是在函数外,除非你用指针或引用跳过这些困难(直接传地址)。

  那么怎么办?在这里,编译器必须玩一些必要的小花招,嗯,其中的关键恰恰就是临时变量

  对于以对象为形参的函数:

      void foo(X x0)
      {
      }


      X xx;
      foo(xx);


  编译器一般按照以下两种转换方式中的一种进行转换

  1.在函数外提供临时变量

      void foo(X& x0)    //修改foo的声明为引用
      {
      }

 


      X xx;         //声明xx
      X::X(xx);       //调用xx的默认构造函数
      X __temp0;      //声明临时变量__temp0
      X::X(__temp0, xx);  //调用__temp0的拷贝构造函数
      foo(__temp0);     //不是地址传递,而是值传递。那当然要有一个副本,将其传入调用函数的栈空间

      X::~X(__temp0);    //函数返回后,调用__temp0的析构函数
      X::~X(xx);      //调用xx的析构函数

 

 

  2.在函数内提供临时变量

      void foo(X& x0)    //修改foo的声明为引用
      {
         X __temp0;      //声明临时变量__temp0
         X::X(__temp0, x0); //值传递的副本在调用函数的栈空间内,二进制代码已留有空间。现在用拷贝构造函数赋值
         X::~X(__temp0);    //调用__temp0的析构函数 
      }
      X xx;         //声明xx
      X::X(xx);       //调用xx的默认构造函数
      foo(xx);       //调用foo
      X::~X(xx);      //调用xx的析构函数

  无论是在函数的内部声明临时变量还是在函数的外部声明临时变量,其实都是差不多的,这里的含义是说既然参数要以传值的语意传入函数,也就是实参xx其实并不能修改,那么我们就用一个一摸一样临时变量来移花接木,完成这个传值的语意

  但是这样做也不是没有代价,编译器要修改函数的声明,把对象改为对象的引用,同时修改所有函数调用的地方,代价确实巨大啊,但是这只是编译器不高兴而已,程序员和程序执行效率却没有影响

  对于以对象为返回值的函数:

      X foo()
      {
         X xx;
         return xx;
      }
      X yy = foo();

  编译器一般按照以下方式进行转换

      void foo(X& __temp0) //没有错,返回值处为void类型,参数列表里多了个返回值参数__temp0
      {
         X xx;          //声明xx
         X::X(xx);        //调用xx的默认构造函数
         __temp0::X::X(xx);    //调用__temp0的拷贝构造函数
         X::~X(xx);        //调用xx的析构函数
      }


      X yy;           //声明yy
      X __temp0;        //声明临时变量__temp0,__temp0为预留返回值
      foo(__temp0);      //传入__temp0
      X::X(yy, __temp0);   //__temp0的值拷贝给yy
      X::~X(__temp0);     //调用__temp0的析构函数
      X::~X(yy);        //调用yy的析构函数

  既然我们已经声明了yy,为什么还要紧接着声明__temp0,其实这里完全可以把yy和临时变量合一

  优化后,上面的代码看起来象这个样子:

      void foo(X& __temp0) //修改foo的声明为引用
      {
         X xx;         //声明xx
         X::X(xx);       //调用xx的默认构造函数
         __temp0::X::X(xx); //调用__temp0的拷贝构造函数
         X::~X(xx);      //调用xx的析构函数
      }
      X yy;          //声明yy
      foo(yy);        //调用foo
      X::~X(yy);       //调用yy的析构函数

  嗯,怎么说呢,这算是一种优化算法吧,其实这各个技巧已经非常普遍了,并拥有一个专门的名称Named Return Value(NRV)优化,NRV优化如今被视为标准C++编译器的一个义不容辞的优化操作(虽然其需求其实超出了正式标准之外)

 

  除了以(1)类为参数以外,如果参数的类型是(2)const T&类型,这也可能导致临时变量

      void fun(const string& str);        const char* name = "wgs";
                                   fun(name);

  嗯,还记得在const文档中的论述吗?对于这种特殊的参数类型,编译器是很乐意为你做自动转换的工作的,代价嘛,就是一个临时变量,不过如果是你自己去做,大概就只能声明一个局部变量了

  为什么函数和临时变量这么有缘,其实根本的原因在于对象传值的语意,这一个也是为什么C++中鼓励传对象地址的原因.和函数的情况类似的,还有一大类情况是临时变量的乐土,那就是(3)表达式

      string s,t;
      printf("%s", s + t);

  这里s+t的结果该放在什么地方呢?只能是临时变量中.

  这个printf语句带来了新的问题,那就是"临时变量的生命期"是如何的?

  对于函数的情况,我们已经看到了,临时变量在完成交换内容的使命后都是尽量早的被析构了,那么对于表达式呢?

      如果在s+t计算后析构,那么print函数打印的就是一个非法内容了,因此C++给出的规则是:

  临时变量应该在导致临时变量创建的"完整表达式"求值过程的最后一个步骤被析构

  什么又是"完整表达式"?简单的说,就是不是表达式的子表达式

  这条规则听起来很简单,但具体实现起来就非常的麻烦了,例如:

      X foo(int n)
      if (foo(1) || foo(2) || foo(3) )  (  ) 生命周期

      {  }

  其中X中有operator int()转换,所以可以用在if语句中

  这里的foo(1)将产生一个临时变量1,如果这部分为false,foo(2)将继续产生一个临时变量,如果这部分也为false,foo(3)...

  一个临时变量的参数居然是和运行时相关的,更要命的是你要记住你到底产生了几个临时变量并在这个表达式结束的时候进行析构以小心的维护对象构造和析构的一致

  我猜想,这里会展开成一段复杂的代码,并加入更多的if判断才能搞定,呵呵,好在我不是做编译器的

  上面的规则其实还有两条例外:

      string s,t;
      string v = 1 ? s + t : s - t;

  这里完整表达式是?语句,但是在完整表达式结束以后临时变量还不能立即销毁,而必须在变量v赋值完成后才能销毁,这就是例外规则1:

  凡含有表达式执行结果的临时变量,应该存留到对象的初始化操作完成后销毁

      string s,t;
      string& v = s + t;

  这里s+t产生的临时变量即使在变量v的赋值完成后也不能销毁,否则这个引用就没用了,这就是例外规则2:

  如果一个临时变量被绑定到一个引用,这个临时变量应该留到这个临时变量和这个引用那个先超出变量的作用域后才销毁

  这篇文章可能有些深奥了,毕竟大多数内容来自于<<Inside The C++ Object Model>>.这些都是编译器考虑的东西了,感谢上帝!

 

  那么就留下一条忠告:

  在stl中,以下的代码是错误的:把函数返回值临时变量的内部值传给一个外部变量。(编译器没有优化到这一步

      string getName();
      char* pTemp = getName().c_str();

  getName返回的就是一个临时变量,在把它内部的char指针赋值给pTemp后析构了,这时pTemp就是一个非法地址

  确实如C++发明者Bjarne Stroustrup所说,这种情况一般发生在不同类型的相互转换上

  在Qt中,类似的代码是这样的

      QString getName();
      char* pTemp = getName().toAscii().data();

  这时pTemp是非法地址

  希望大家不要犯类似的错误

posted on 2011-01-30 10:08  cvbnm  阅读(495)  评论(0)    收藏  举报

导航