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

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

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

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 xx
	return xx;
}
//C++ 伪码:
void bar(X &__result){
	// default constructor invocation
	// Pseudo C++ Code
	__result.X::X();  //可以对比下上面的例子,这个地方竟然是直接用_result置换了 xx.
	// ... process in __result directly
	return;
}
    对照着上面的例子看起来,明显的区别,原先是要经过拷贝构造函数,现在直接用_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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值