#include <iostream> int main(void) { int a = 1; cout << a << endl; test1(a); cout << a << endl; test2(&a); cout << a << endl; return (0); } |
void test1(int & a) { void test2(int * a) { int main(void) { test1(a); ...... test2(&a); } |
static int g = 0; class Y { ~Y() { Y pass_by_value(Y a) { return a; } int main(void) |
我们注意到三个不同的函数传递参数的方法是不同的:
Y pass_by_value(Y a) { return a; }
Y pass_by_reference(Y& a) { return a; }
Y * pass_by_pointer(Y * a) { return a; }
这段代码能够运行的,但是仔细看输出的结果,却非常令人吃惊:
create, this pointer is 0x0012FEC4, s is 0x371360, m_i is 1 destroy, this pointer is 0x0012FDB4, s is 0x371360, m_i is 1 destroy, this pointer is 0x0012FEA4, s is 0x371360, m_i is 1 destroy, this pointer is 0x0012FEB4, s is 0x371360, m_i is 1 destroy, this pointer is 0x0012FEC4, s is 0x371360, m_i is 1 |
因为构造函数只被调用了一次,而析构函数被调用了四次。经过反汇编后(细节不列出来了),能够找到四次这行代码:
call Y::~Y
其中在pass_by_value中,退出该函数之前调用一次,另外三次在退出main函数的时候,按照c, b, a的顺序分别调用了三次,由于d是Y*,不会调用析构函数。
从反汇编的代码中,我们可以看到,在调用pass_by_value之前,系统先把a的内容复制了一份到新的地址,而不是调用constructor产生一个新的Y的实例,而在退出该函数的时候,按照这个新的地址进行deconstructe。
在pass_by_reference调用之前,把a的地址&a压栈然后调用函数,但是在退出这个函数的时候,反汇编的代码中显示,先把a的内容复制一份到c的地址,而没有call constructor产生新的实例.
我们稍微想象一下,在Y的析构函数Y()中给char * s分配内存,也就是增加
s = new char [100];
而在~Y()中释放内存:
delete s;
这样,我们就制造了很大的麻烦!
因为在第一次从pass_by_value返回的时候,我们就已经调用了~Y(),并把s释放了,而所有的s的地址都是相同的,再释放它或者访问它其中的内容都是非法的。
不执行pass_by_value这个函数会不会解决这个问题呢?不会,pass_bye_reference也存在问题,因为在退出这个函数的时候,同样复制了一份a的内容给c,而在退出main的时候需要析构c和a,在这时c.s和a.s的指针依然是相同的。
解决问题的方法之一:使用指针传递参数。例如我们在pass_by_pointer中new一个新的Y, 而在main中delete, 如下代码:
Y * pass_by_pointer(Y * a) int main(void) |
这显然是解决问题的办法。
解决办法二:采用copy-constructor
Copy-Constructor就是形如Y(Y& y)的构造函数,看起来这有点不容易理解,但实际上这个形式是为了解决上面的问题而提出来的。当遇到形如:
f1(Y a)
f2(Y& a)
的函数,编译的时候会生成的调用默认调用Y(Y& y)这个构造函数。代码如下:
Y(const Y& y) { |
函数体里的代码与Y()函数的代码是完全相同的。输出的结果如下:
create , this pointer is 0x0012FEC4, s is 0x371360, m_i is 1 |
构造函数和析构函数分别调用了四次,使用反汇编跟踪,或者在代码中打印更多的信息(这要写一些代码在程序里),我们都可以看到以下的顺序:
1) 第一次调用Y(),显然是在Y a;
2) 然后在调用pass_by_value之前调用了Y(const Y&),生成新实例
3) 退出pass_by_value之前,调用Y(const Y&)给main函数中的b
4) 再调用~Y()析构在第2步生成的实例, 然后退出pass_by_value
5) 调用pass_by_refrence,简单地传递了&a
6) 退出pass_by_refrence之前,调用Y(const Y&)给main函数中的c
7) 调用pass_by_pointer,这个比较简单
8)顺序调用~Y(),析构c, b, a
其中的效率问题和代码安全问题就不用我再多说了,一琢磨就清楚了。
3. 与此有关系的有趣的话题
1)如果写成这样的函数会如何呢?
Y& pass_by_reference(Y& a) { return a; }
在反汇编之后,清楚地看到,前面的写法是在pass_by_reference函数内部调用Y(const Y&),而现在的这种写法如果仍然采用
Y c = pass_by_reference(a)
的方式,则编译器在退出pass_by_reference之后调用Y(const Y&)构造函数,对于本例来说更好一点的方法是——而且仅对本例
Y& c = pass_by_reference(a),
这条语句不会再调用任何的构造函数,只是引用变量,与
Y * d = pass_by_poiinter(a)
有一样的效果,本质上都采用了指针的方式。
但是:
Y& pass_by_reference(Y& a) { Y y ; return y; }
至少编译器会发出警告,“warning C4172: 返回局部变量或临时变量的地址”。所以,不用认为使用&就一定是安全的,&比*也好不到哪去,使用Y&和Y*这样的返回值要非常地仔细,一定要注意的是变量的生命周期,在函数内部的局部变量不能够在外部访问。还有不要忘记delete也不要乱用delete,这些是通用的编程原则。
2) Y(const Y& y)的写法:
const不是必须的,但最好写成带const限制的,这样会明确地告诉编译器你不想对y的地址本身做任何事情。
Y(Y y)是不可以的,编译器规定这是不合法的声明。我们可以想象编译器在调用Y(Y y)之前,先要调用一个Copy-Constructor,那么是不是要引起无穷的递归了?所以编译器干脆就不接受这样的构造函数声明。
Y(Y * y)是合法的写法,但是编译器不认为这是一个Copy-Constructor,所以根本不会被默认调用,跟没有一样。
另外我们注意到,Y(const Y& y)函数体中的代码与Y()中的代码是完全相同的,那么干脆就这样写:
Y(const Y& y) { Y(); }
行不行啊?这个问题就留给聪明的你吧,相信这是一个很有趣的问题。是不是很YY啊?
以下是完整的代码:
#include <iostream> static int g = 0; class Y { Y(Y y) { ~Y() { Y pass_by_value(Y a) { return a; } int main(void) } |