有时候还需要多些自信(面试小记2)
今天遇到了个面试,其中有的问题我当时还真不能确定,遂发出来,大家分享。
先大致讲一下流程,一面还挺顺利,游刃有余;二面就有些紧张了,是个额头头发不多但是显得很精干的男士(下文简称为A)。
只摘录其中的部分我很“为难”的地方:
A:string是值类型是引用类型?
ME:(我心想string是class,肯定是)引用类型
A:那我有个方法,参数为string,我在里面改变他的值,原来的会变吗?
ME:(这个我当时很犹豫,虽说string平时用,但是还真考虑过这个。我要是说会不会变吧,岂不是自打嘴巴?String是引用类型,怎么还值专递呢?)
当时我就记得园子里有句话:String是引用类型,但是用起来像值类型。我就说的是不变。
下面上一段代码分析一下:
这个确实是不会变的,调用完之后还是“aaa”,这是为什么呢?先大致讲一下流程,一面还挺顺利,游刃有余;二面就有些紧张了,是个额头头发不多但是显得很精干的男士(下文简称为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);
{
s = "bbb";
}
string s = "aaa";
Foo(s);
Console.WriteLine(s);
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 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 );
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
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 );
继续汇编:{
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行: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
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语言的时候还特别注意了这点,结果还是忘记了。
问题的答案请看我最下面的留言。
在这里有些误导大家了,给大家致歉。