有时候还需要多些自信(面试小记2)

今天遇到了个面试,其中有的问题我当时还真不能确定,遂发出来,大家分享。
先大致讲一下流程,一面还挺顺利,游刃有余;二面就有些紧张了,是个额头头发不多但是显得很精干的男士(下文简称为A)。
只摘录其中的部分我很“为难”的地方:
A:string是值类型是引用类型?
ME:(我心想string是class,肯定是)引用类型
A:那我有个方法,参数为string,我在里面改变他的值,原来的会变吗?
ME:(这个我当时很犹豫,虽说string平时用,但是还真考虑过这个。我要是说会不会变吧,岂不是自打嘴巴?String是引用类型,怎么还值专递呢?)
       当时我就记得园子里有句话:String是引用类型,但是用起来像值类型。我就说的是不变。
下面上一段代码分析一下:
static void Foo(string  s)
        {
            s 
= "bbb";
        }

string s = "aaa";
            Foo(s);
Console.WriteLine(s);
这个确实是不会变的,调用完之后还是“aaa”,这是为什么呢?
 1 string s = "aaa";
 2 00000051 8B 05 88 20 C0 02 mov         eax,dword ptr ds:[02C02088h] 
 3 00000057 89 45 B8         mov         dword ptr [ebp-48h],eax 
 4     92:             Foo(s);
 5 0000005a 8B 4D B8         mov         ecx,dword ptr [ebp-48h] 
 6 0000005d E8 A6 AF D4 FF   call        FFD4B008 
 7 00000062 90               nop              
 8     93:             Console.WriteLine(s);
 9 00000063 8B 4D B8         mov         ecx,dword ptr [ebp-48h] 
10 00000066 E8 95 24 3F 67   call        673F2500 
11 
12 
13 
14 
15 
16  static void Foo(string  s)
17     82:         {
18 00000000 55               push        ebp  
19 00000001 8B EC            mov         ebp,esp 
20 00000003 57               push        edi  
21 00000004 56               push        esi  
22 00000005 53               push        ebx  
23 00000006 83 EC 30         sub         esp,30h 
24 00000009 33 C0            xor         eax,eax 
25 0000000b 89 45 F0         mov         dword ptr [ebp-10h],eax 
26 0000000e 33 C0            xor         eax,eax 
27 00000010 89 45 E4         mov         dword ptr [ebp-1Ch],eax 
28 00000013 89 4D C4         mov         dword ptr [ebp-3Ch],ecx 
29 00000016 83 3D E0 8C 7B 00 00 cmp         dword ptr ds:[007B8CE0h],0 
30 0000001d 74 05            je          00000024 
31 0000001f E8 1D 91 57 68   call        68579141 
32 00000024 90               nop              
33     83:             s = "bbb";
34 00000025 8B 05 90 20 C0 02 mov         eax,dword ptr ds:[02C02090h] 
35 0000002b 89 45 C4         mov         dword ptr [ebp-3Ch],eax 
36     84:         }
37 0000002e 90               nop              
38 0000002f 8D 65 F4         lea         esp,[ebp-0Ch] 
39 00000032 5B               pop         ebx  
40 00000033 5E               pop         esi  
41 00000034 5F               pop         edi  
42 00000035 5D               pop         ebp  
43 00000036 C3               ret              


可以看到第2行将字符串的地址写入到 eax,然后写到堆栈的【ebp-48h】处;
调用Foo方法前,放到ecx中。

在方法Foo中,可以看到又经ecx放到了【ebp-3Ch】处;
在执行s=“bbb”的时候,同样将新字符串的地址放到了【ebp-3Ch】处,但是原来的字符串并为更改,只是更改了临时变量s的引用。
所以在调用完方法Foo之后,原来的字符串还是“aaa”,没有改变。
所以这个时候我回答不变是对的,但是我不知道为什么string的传递是类似于值传递的,有点运气了。
接下来,他又问
A:那如果我有个类,里面有string成员,我同样改变他的值,外面的会变吗?这个时候我回答的是可以改变。
是不是这样呢?同样,上代码:

 1 class C1
 2     {
 3         public string s1="aaa";
 4     }
 5 
 6  static void Foo(C1   c1)
 7         {
 8             c1.s1  = "bbb";
 9         }
10 
11  C1 c1 = new C1();
12             Foo(c1);
13             Console.WriteLine(c1.s1 );


 

 1  Foo(c1);
 2 0000006c 8B 4D B8         mov         ecx,dword ptr [ebp-48h] 
 3 0000006f E8 94 AF 7F FF   call        FF7FB008 
 4 00000074 90               nop              
 5     93:             Console.WriteLine(c1.s1 );
 6 00000075 8B 45 B8         mov         eax,dword ptr [ebp-48h] 
 7 00000078 8B 48 04         mov         ecx,dword ptr [eax+4
 8 0000007b E8 80 24 52 67   call        67522500 
 9 
10 
11 
12  static void Foo(C1   c1)
13     82:         {
14 00000000 55               push        ebp  
15 00000001 8B EC            mov         ebp,esp 
16 00000003 57               push        edi  
17 00000004 56               push        esi  
18 00000005 53               push        ebx  
19 00000006 83 EC 30         sub         esp,30h 
20 00000009 33 C0            xor         eax,eax 
21 0000000b 89 45 F0         mov         dword ptr [ebp-10h],eax 
22 0000000e 33 C0            xor         eax,eax 
23 00000010 89 45 E4         mov         dword ptr [ebp-1Ch],eax 
24 00000013 89 4D C4         mov         dword ptr [ebp-3Ch],ecx 
25 00000016 83 3D E0 8C 13 00 00 cmp         dword ptr ds:[00138CE0h],0 
26 0000001d 74 05            je          00000024 
27 0000001f E8 AD 90 6A 68   call        686A90D1 
28 00000024 90               nop              
29     83:             c1.s1  = "bbb";
30 00000025 8B 05 90 20 D7 02 mov         eax,dword ptr ds:[02D72090h] 
31 0000002b 8B 4D C4         mov         ecx,dword ptr [ebp-3Ch] 
32 0000002e 8D 51 04         lea         edx,[ecx+4
33 00000031 E8 9A 16 45 68   call        684516D0 
34     84:         }
35 00000036 90               nop              
36 00000037 8D 65 F4         lea         esp,[ebp-0Ch] 
37 0000003a 5B               pop         ebx  
38 0000003b 5E               pop         esi  
39 0000003c 5F               pop         edi  
40 0000003d 5D               pop         ebp  
41 0000003e C3               ret              

在执行30行的时候eax是01DBC268,其内存的内容拷贝出来是:

54 0b a0 67 04 00 00 00 03 00 00 00 62 00 62 00 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

 

可以看出这是一个string的实例,前面的67a00b54是MT的地址,后面的00000004是字符串的实际长度,00000003是字符串有效内容的长度,
后面的3个0062是连着三个字符‘b’,看来确实是字符串“bbb”。再后面00的就不管了。
接着依次执行31和32行,则ecx是01D9EEC8,edx是01D9EECC;据猜测ecx应该是c1的地址,把内存考出来看一下:
 d0 99 41 00 94 ee d9 01 00 00 00 00 24 43 9d 67 0a 00 00 00 70 07 a0 67 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
而此时edx就应该是s1的地址,可以看出edx就比ecx相差4,所以01d9ee94就应该是字符串“aaa”的地址,同样考出来看看:
54 0b a0 67 04 00 00 00 03 00 00 00 61 00 61 00 61 00 00 00 00 00 00
可以看出,“aaa”和“bbb”的头几个部分完全是一样的,就是后面的一个是61,一个是62.
那么问题很简单了,知道把c1里的字符串地址从01d9ee94换成01DBC268就算OK了。事实上33行就是做这个事情的。
看一下执行完33行后的c1的内容:
d0 99 41 00 68 c2 db 01 00 00 00 00 24 43 9d 67 0a 00 00 00 70 07 a0 67 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
可以看出,确实是换了。
所以到这里,问题解决了。
接着这老大又问
A:有没有其他方法可以改变字符串?
ME:加ref或out关键字可以,或者用指针。
我们看一下加ref(或加out,其实是一样的)的为什么可以改变,更详细的看一下。
static void Foo(ref string  s)
        {
            s
= "bbb";
        }

string s = "aaa";
            Foo(
ref s);
            Console.WriteLine(s );
继续汇编:
 1  string s = "aaa";
 2 0000004c 8B 05 88 20 ED 02 mov         eax,dword ptr ds:[02ED2088h] 
 3 00000052 89 45 B8         mov         dword ptr [ebp-48h],eax 
 4     92:             Foo(ref s);
 5 00000055 8D 4D B8         lea         ecx,[ebp-48h] 
 6 00000058 E8 AB AF D0 FF   call        FFD0B008 
 7 0000005d 90               nop              
 8     93:             Console.WriteLine(s );
 9 0000005e 8B 4D B8         mov         ecx,dword ptr [ebp-48h] 
10 00000061 E8 9A 24 49 67   call        67492500 
11 
12 
13 
14   static void Foo(ref string  s)
15     82:         {
16 00000000 55               push        ebp  
17 00000001 8B EC            mov         ebp,esp 
18 00000003 57               push        edi  
19 00000004 56               push        esi  
20 00000005 53               push        ebx  
21 00000006 83 EC 30         sub         esp,30h 
22 00000009 33 C0            xor         eax,eax 
23 0000000b 89 45 F0         mov         dword ptr [ebp-10h],eax 
24 0000000e 33 C0            xor         eax,eax 
25 00000010 89 45 E4         mov         dword ptr [ebp-1Ch],eax 
26 00000013 89 4D C4         mov         dword ptr [ebp-3Ch],ecx 
27 00000016 83 3D E0 8C 6D 00 00 cmp         dword ptr ds:[006D8CE0h],0 
28 0000001d 74 05            je          00000024 
29 0000001f E8 1D 91 61 68   call        68619141 
30 00000024 90               nop              
31     83:             s= "bbb";
32 00000025 8B 05 90 20 ED 02 mov         eax,dword ptr ds:[02ED2090h] 
33 0000002b 8B 4D C4         mov         ecx,dword ptr [ebp-3Ch] 
34 0000002e 8D 11            lea         edx,[ecx] 
35 00000030 E8 A3 0E 3C 68   call        683C0ED8 
36     84:         }
37 00000035 90               nop              
38 00000036 8D 65 F4         lea         esp,[ebp-0Ch] 
39 00000039 5B               pop         ebx  
40 0000003a 5E               pop         esi  
41 0000003b 5F               pop         edi  
42 0000003c 5D               pop         ebp  
43 0000003d C3               ret              
同样,关注代码的32~34行:
eax:01DEC25C,内容:
54 0b a0 67 04 00 00 00 03 00 00 00 62 00 62 00 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
确实是字符串“bbb”
ecx和edx都是:05C7E778,内容:0x01dcee94,这个是字符串“aaa”的地址。
执行完35行之后,地址05C7E778的内容变成了01DEC25C,在之后第9行代码确实地址变成了01DEC25C,则可以推断05C7E778是上个堆栈
s引用的位置,则35行的代码则是将新“bbb”的地址写到原来的s引用处。
A继续问:ref和out有什么区别?
ME:我说两者没什么区别,就是out不要求变量初始化。
A:那要是初始化了呢,改变了之后是什么值?
ME:(这个我还真被问住了。不知道可以,但是不能乱说啊。)基于对out这个关键字的理解,我认为应该返回改变后的值。
如果将原来的ref改为out,汇编代码完全相似,区别就是变量是否初始化问题,如果不初始化,其实变量在栈中也是有位置的,只不过地址内容为0.
如果初始化,则和ref完全一样。代码我就不贴了,大家可以自己调式看一看。

问题:为什么默认的字符串作为参数传递是类似的值传递呢?请大家告诉我。
靠,弄了半天,才记得所有传递默认都是值传递,这才是问题的根源。老了,脑袋记不清了,以前看C语言的时候还特别注意了这点,结果还是忘记了。
问题的答案请看我最下面的留言。
在这里有些误导大家了,给大家致歉。

posted @ 2009-09-19 20:34  DiggingDeeply  阅读(3357)  评论(58编辑  收藏  举报