构造函数语意学 笔记(三)

来源:互联网 发布:淘宝杜蕾斯官方旗舰店 编辑:程序博客网 时间:2024/05/29 13:11

今天是构造函数语意学这个章节的第三次笔记,说实话,学到了很多,也困惑很多。不过闲时还是感叹真乃神书也。

若存在错误 请指正 万分感谢

1.程序转化语意学(Program Transformation Semantics)

  引例:

#include <iostream>using namespace  std;//加载头文件#include "X.h"X foo(){X x_1;//对对象x_1进行处理的相关操作。return x_1;}

  两种正常假设:

   1.每调用一次foo()函数,会返回一个对象x_1的值。

   2.应该会调用类中的拷贝构造函数。

    两个假设的正确性需要参看类X中的定义。

2. 显式的初始化操作(Explicit Initialization)

如下定义: X x0;//定义一个对象x0;

  示例:

void foo_bar(){X x1(x0);X x2 = x0;X x3 = X(x0);}//上面三种初始化操作显式的用x0初始化三个对象。//但是在实际的编译器中可能会发生如下的转换。//1.重写定义,其中的初始化操作会被剥离 。//2.调用相关的拷贝构造函数。//C++伪码:↓void foo_bar(){X x1;  X x2;X x3;x1.X::X(x0); //调用拷贝构造函数。x2.X::X(x0);x3.X::X(x0);//可能在类X中会有类似的声明://X::X(const X&);}
    可以看到,其实编译器背着我做了转化操作。可能我们理解的简单操作,在编译器内部实现却是另一番光景。

3.参数的初始化:

    C++ Standard 中说过,当按值传递或者按值返回的时候,其中的操作类似下面的行为

如 X xx= arg; //其中arg 是实际参数, xx是我们在函数中看见的形参。
    想一下下面的函数调用操作会发生什么?

X xx;void foo(X x0);foo(xx);
   根据我们的理解,简单的传值操作嘛,调用拷贝构造函数嘛。那么看仔细了,下面的说法可能让你大吃一惊。

编译器内部的实现策略是这样的:导入临时对象,调用拷贝构造函数来初始化临时对象,然后讲将此临时对象交给函数。

伪码:↓

X temp;temp.X::X(xx);//函数的调用操作可能要被改写:foo(temp);
  但是这样的方案貌似是不合适的,因为你怎么能直接操纵临时对象呢?你可并不是引用型参数哦。

  传值的调用过程会有两个问题:

  1.产生临时对象并且调用拷贝构造函数进行初始化

  2.采用bitwise 的方式,把临时对象的内容拷贝到形参中。(我们在前面讲过了,bitwise 拷贝是不需要拷贝构造函数就可以实现的,后面我们还会遇到)

  这个地方我可是有很大疑惑的。传统的传值调用,我们可是这个样子理解的:产生临时对象,然后把临时对象交给了函数处理,但是你在这个地方看到的,它竟然还有一个bitwise 方式的拷贝。这点一度让我困惑。

  后来我测试了下,似乎有点理解。正如我上面所说的,bitwise 方式的操作是不调用拷贝构造函数的,所以你根本无法通过显式的设置一个语句来检测是否发生了bitwise 方式的拷贝。如果想看,那么只能看汇编。

  但是如果我们修改一下函数的原型是不是会好很多呢?

void foo(X& x0);
   按照我们已经知道的理论来解释,这个地方是不应该产生临时对象的。但是按照上面的理论进行解释的话,你可以看到仍然会产生临时对象,但是由于参数是引用型,所以省去bitwise 的那一步,直接操作的是临时对象,那么是否对临时对象的操作会影响到我们的xx对象呢?按照我们已经有的知识去看,对引用型参数的操作是会直接改变自身的,因为我们把引用当成别名理解的。但是当我们对临时对象的操作也会改变自身嘛?其次的一个疑问是,临时对象是无名的,那么如何建立引用?这个地方有争议,我一直也没找到答案,最好的猜测是编译器动了手脚。建议大家还是按你已知的来理解。

4.返回值初始化:

    看一下如下函数:

X bar(){X xx;return xx;}
    正如我们知道的,返回的不是对象xx本身,而是xx的副本拷贝。那么你知道内部是怎么进行转化的嘛。聪明的我们一定知道是调用了拷贝构造函数,因为我们自己可以很简单的测试出来。

    我们来看一下在Cfront 编译器中的实现方式:

    进行一个双阶段的转换操作:

       1.先加上一个额外的引用型参数;

       2.在return 语句之前安插拷贝函数的调用操作。

伪码:↓

void bar(X& _result){X xx;//声明一个局部对象的时候,会调用相应的默认构造函数进行初始操作xx.X::X();//默认构造函数的调用操作。_result.X::X(xx);//安插拷贝函数的调用操作return;  //这个地方就直接返回了。}
    真正的返回值是什么呢?可以看到是不返回任何东西的,直接一个return 语句结束了。

    下面像下面的这样函数调用操作会发生什么事情?

如: X xx=bar();//会发生什么呢?我猜应该是这样的。X xx;void bar(xx);//此处省略一万行。 
    可以清楚的看见转化了吧。

    当有函数指针的时候也会发生转化:

如:X (*pf)();     pf=bar;
   可能的转化操作:

void (*pf)(X&);pf=bar;  //转化的时候多了一个引用型参数。
   看完上面的伪码,应该可以知道为什么要调用拷贝构造函数,何时调用

5.在使用者层面做优化:

    这个是一个程序员的优化操作,的确很新奇,我在学习的时候遇到过,当时只会用,却从来不知道缘由。

    这个是某位大神提出的。定义一个计算用的 Cstror.

   示例:

X bar(const X& p1, const X& p2){X xx; //声明一个局部对象作为容器接收产生的新对象。//..利用参数p1,p2生成一个xx;return xx; //返回xx.这个应该是我们常见的。}//编译器内部的伪码:void bar(X& _result, const X&p1, const X& p2){X xx;//..利用参数p1,p2生成一个xx;//下面调用拷贝构造函数_result.X::X(xx);  //调用了拷贝构造函数。return;  //什么也不返回。}

    示例:

X bar(const X& p1, const X& p2){return X(p1 + p2);//原书是p1,p2,我感觉这+号样子也是可以接受的,表达了用p1,p2生成对象。}//编译器内部的伪码:void bar(X& _result, const X&p1, const X& p2){//下面调用拷贝构造函数_result.X::X(p1+p2);  //这个地方调用的是什么函数呢?是拷贝构造函数嘛?return;  //什么也不返回。}
   可以看到少了一个局部对象的生成,拷贝构造函数也省略了,是不是感觉对象是被计算出来的。

   举个具体的例子,上面的例子只是来分析。

   示例:

#include <iostream>using namespace  std;class Base{private:int x, y;public:Base():x(0),y(0){cout << "Using the default Constructor " << endl;}Base(int x_x, int y_y) :x(x_x), y(y_y){cout << "Using the defined Constructor " << endl;}~Base(){cout <<"Using the Destructor " << endl;}Base(const Base& p){//memcpy(this, &p, sizeof(Base)); //一种写法。cout << "Using the Copy Constructor " << endl;x = p.x; y = p.y;}//故意声明为友元函数,让+函数你们的函数充实起来。friend Base operator+(const Base& p1, const Base& p2){//留意这个写法。return Base(p1.x + p2.x, p1.y + p2.y);}friend Base operator-(const Base& p1, const Base& p2){Base tmp;tmp.x = p1.x - p2.x;tmp.y = p1.y - p2.y;return tmp;  //对比上面的写法。}//能看懂下面的函数原型嘛?friend ostream& operator<<(ostream& os, const Base& p){os << "p.x = " << p.x << "  " << "p.y = " << p.y << endl;return os;}Base operator=(const Base p){cout << "Using the assignment operator " << endl;memcpy(this, &p, sizeof(Base)); //一种写法。return *this;}};int main(){Base b1(2, 3);Base b2(3, 4);Base b3=b1+b2;//b3 = b1+b2;  //能看出初始化和赋值的区别嘛?cout << "b3 " << b3 << endl;system("pause");Base b4 = b1 - b2;cout << "b4 " << b4 << endl;system("pause");return 0;}
   自己测试下应该能看出区别的。

6.编译器层面的优化:

  其实这个方面我是不想记录下来的,因为有点乱。

  示例:

X bar(){X xx;// ... process xxreturn xx;}//C++ 伪码:void bar(X &__result){// default constructor invocation// Pseudo C++ Code__result.X::X();  //可以对比下上面的例子,这个地方竟然是直接用_result置换了 xx.// ... process in __result directlyreturn;}
    对照着上面的例子看起来,明显的区别,原先是要经过拷贝构造函数,现在直接用_result替换了xx.

_result.X::X(xx);  //调用了拷贝构造函数。
__result.X::X();  //可以对比下上面的例子,这个地方竟然是直接用_result置换了 xx.
   下面这种行为就是编译器的NRV优化。
     NRV优化的本质是优化掉拷贝构造函数,去掉它不是生成它。

     当然了,因为为了优化掉它,前提就是它存在,也就是欲先去之,必先有之,这个也就是NRV优化需要有拷贝构造函数存在的原因。

     NRV优化会带来副作用,目前也不是正式标准,倒是那个对象模型上举的应用例子看看比较好。

     极端情况下,不用它的确造成很大的性能损失,知道这个情况就可以了。
     为什么必须定义了拷贝构造函数才能进行NRV优化?

     首先它是lippman在inside c++ object mode里说的。那个预先取之,必先有之的说法只是我的思考。

     查阅资料,实际上这个可能仅仅只是cfont开启NRV优化的一个开关。

     上面关于NRV,我摘录了别人一段解释,我翻了很多资料,感觉这个解释是相对比较合理的。

7.是否需要拷贝构造函数?

    示例:

#include <iostream>using namespace  std;class Point3D{private:int x, y, z;public:Point3D(int x_x = 0, int y_y = 0, int z_z = 0) :x(x_x),y(y_y), z(z_z){cout << "Using the Cstor " << endl;}friend ostream& operator<<(ostream& os, const Point3D& p){os << "(" << p.x << "," << p.y << "," << p.z << ")" << endl;return os;}};int main(){Point3D p1(2, 2, 3);Point3D p2 = p1;cout << p2 << endl;return 0;}
    你可以看到,我没用显式的定义拷贝构造函数,但是却在底下用类的对象初始化另一个对象。而且这个也不符合我们前面讲的合成构造函数的情形,具体的可以翻看前面。这个时候就是采用bitwise copy 的方式实现,根本用不到拷贝构造函数

    但是若能遇见到有大量的memberswise 操作,那么最好是显式定义一个拷贝构造函数,大前提是你的编译器能开启所谓的NRV优化。不然不如采用bitwise操作。

    你显式定义的拷贝构造函数可能是如下的:

Point3D(const Point3D& p){//介绍两种写法。//memcpy(this, &p, sizeof(Point3D));//第二种写法x = p.x;this->y = p.y;z = p.z;}
   但是当你想用memcpy/memset 之类的函数,请务必记住以下的内容。

当你的类中存在虚机制的情况下,比如虚函数,虚基类。更全面的说法是不内含任何有编译器产生的内部members,最常见的就是vptr了。

   那么你务必不要那样使用,因为你可能动了vptr的奶酪。

   示例:

#include <iostream>using namespace std;class Base{public:Base(){memset(this, 0, sizeof(Base));}virtual~Base(){}};//关于memset函数的作用,感兴趣可以自己查一下.//看一下Base()函数的伪码:Base::Base(){//括号里面应该是有this指针的,默认省略了。//编译器安插代码设置下vptr指针。这个操作必须在用户自定义代码之前。_vptr_Base = _vtbl_Base;//让vptr指向虚表。//接下来的操作有趣了。memset(this, 0, sizeof(Base);//vptr=0?你看出事情了。}
注意正确使用内存相关的函数操作,尤其是在类中操作更要注意,因为编译器动了很多手脚。

8.总结:

    我一开始看见这个章节的标题是很奇怪的,程序转化语意学,我想哪里转化了?但是随着书中的一次次伪码分析,引人入胜。

    我强烈推荐大家一读。看了上面的文章,你应该很容易看见程序哪里发生转化了,哪里调用了拷贝构造函数,哪里调用默认构造函数。大家也可以自己试着写伪码。

9.参考文献:

  1. 网易博文:参考博文地址

  2.<<深度探索C++对象模型>>

End

     这个章节应该还有最后一篇笔记....








0 0
原创粉丝点击