C语言中,未初始化的局部变量到底是多少? 答案往往是: 与编译器有关。 可能但不保证初始化为0。 未确定。
+ a( u# U- u( e$ d% r
总之,全部都是些一本正经的形而上答案,这很令人讨厌。 但凡一些人给你滔滔不绝地扯编译器,C库,处理器体系结构却给不出一个实际场景复现问题的时候,这人大概率在扯淡。
其实,这个问题本身就是错误的问法,说全了能讲10万字,我们只要能在特定场景下确定其特定行为就OK了,当然,这就需要设计一个比较OK的实验。 在演示一个实际代码行为之前,先给出一个知识,CPU不认识变量,更无法识别变量的名字,CPU只会从特定的内存位置取值或者将值存到特定的内存位置,因此当问一个变量的值是多少的时候,必须要知道这个变量对应的值被保存在什么地方。 来看下面的代码:
2 ~% h/ f: a0 D$ d/ [9 g- #include <stdio.h>
! T4 P0 @5 ^7 s# g' y+ r) e5 `( p/ U - ) ?; X( } }$ j- ~' I
- void func1()
0 P$ T3 Q0 s- N6 e; S# h - {
3 B" W7 P: V7 F# H& c R5 @ - int a;
5 [" X, Q+ f# P5 o* S# G" Z - printf("func1:%d\n", a); e# ]- A+ P$ g4 d) ^) i
- a = 12345;$ @% {- {) n6 ]6 R( a5 b, o
- }
$ |. b/ d( Z3 f9 m% m) x, q - " D/ M! P; _4 @) @ U, J
- void func2(). ?' J2 c& V2 y) A" j
- {) e7 w6 D/ N, I9 [! L5 `" x
- int b;
( H( ?) `% B* M) Y' |8 P - printf("func2:%d\n", b);% c" U. c0 y+ W+ n/ A0 b) X
- }- d2 d* c7 q/ R8 h- j( x
- 1 K: n. ~( s4 s$ l
- void func4()+ M+ w3 c+ ^, o( S8 d
- {
2 [4 V0 s1 P6 @$ `, @ - int d;
; q* d& R$ e& H: s+ m7 d6 y5 e - printf("func3:%d\n", d);
2 v+ M/ A# s8 q. } - }- b% Q( T6 C. v+ k: a7 |, r* P; {
* o/ G6 {+ G9 U6 f" \- void func3()
, F0 U" @# `" {) h6 d4 O( N - {
. w8 W! M3 h; R" `' ] - int c;
2 l) y4 @6 T7 O, J. h - printf("func3:%d\n", c);
" B4 O! E2 x0 i; n/ K' w - c = 54321;
8 P4 g2 q0 ?3 W9 W5 V, x# f. A - func4();
; c: E+ r T6 R, ~( z5 V - }
3 G4 J9 A( u; O- P - 6 ^7 \2 P# O* R" o( g# S: v' B
- void test_call()5 q) |1 ~! F6 G$ h
- {: H# H* i* c' g+ k$ b
- func3();. k% S) N$ `' y# W+ |: Y: }" k; O! l
- }
! r1 M3 }& R0 X+ J0 M1 @ - 5 Y. {* B# H. g' e
- int main(int argc, char **argv)$ k* F p+ B0 E; z+ M7 |6 g/ q
- {
5 Z2 g1 K; B1 E: p8 J - func1();
$ ]0 c9 d0 l: O% Y' j. i6 _9 k - func2();
) B" Q( m! W* v# q0 f
1 p+ D& `% h8 L+ U7 ^- u- test_call();
. x- p: J; j# p - }
复制代码我们有func1~func4一共4个函数,其内部均有一个未初始化的局部变量,它们的值到底是多少呢? 对于这种局部变量,它们的值取决于: 变量在栈中的位置。 变量对应的栈位置在 之前 有没有被store过。
! ?' B1 |8 S- E! q
可以看到,上述第一点标记了一个内存位置,第二点则是代码的行为,也就是说,只要有代码去store对应的位置, 且后续的代码没有reset该位置的值的话,该位置就会保留着原先被store后的值。 验证非常简单,试一下就知道了: - 9 E: g: T, b) O g
- [root@localhost test]# ./a.out
: ]! o$ g# x4 a; ?+ o* I5 m3 J - func1:0
0 e2 ~$ O% g4 m. N1 T( A - func2:12345
: W1 p3 I; ?. w1 O* W' c - func3:0# ?: @ C* G& Y0 P7 i
- func3:0
复制代码 - g: W. {: p# b" c6 [
- 按照函数调用栈帧的变化,func1的局部变量a和func2的局部变量b显然是位于同一个位置的,在func1被调用时,这是一块新的内存(可能在进入main之前有栈帧到达过这个位置),a的值取决于调入内存该位置的页面对应偏移的初始值,这取决于操作系统:
9 E1 R/ \: H% a! x+ u
栈的分配不会涉及C库,这里显然并不涉及C库的行为,但类似malloc分配的内存则涉及C库了。
打印结果,a的值为0,我们认为操作系统返回给了应用程序零页。接下来在func1中将其赋值12345之后函数返回,接下来调用func2的时候,在之前func1已经退出的栈帧位置重建栈帧,对应位置依然还是12345。 我没有看到func1的ret操作后面有stack清0的代码指令。效率考虑,也不该有这样的指令。
再看test_call函数,很明显,func3和func4调用使用的并不是同一个栈帧,因此即便是在func3中对c赋值了54321,也不会影响在其栈帧之上的func4的栈帧对应位置的值d。因此c和d的初始值均保持为0。 那么,初始化一个局部变量和不初始化一个局部变量,在指令层面上,区别在哪里呢? 很简单,亲眼看一下就知道,先看未初始化局部变量的func1:
) K9 Y# g! j" n, g r& }- // int a;' I$ T+ Q% |1 {3 I8 R- V6 L
- 00000000004005ad <func1>:' x, O/ a2 j3 x
- 4005ad: 55 push %rbp
% q, c" }/ C+ `, d2 E& Y* x - 4005ae: 48 89 e5 mov %rsp,%rbp
& h5 V* |. o: \9 r) K9 ^ - 4005b1: 48 83 ec 10 sub $0x10,%rsp0 i. u# X$ W' k7 j& A4 {& m4 r8 d
- 4005b5: 8b 45 fc mov -0x4(%rbp),%eax
3 v, b5 v0 m8 e - 4005b8: 89 c6 mov %eax,%esi
; O( p7 F# W& [+ R5 D - 4005ba: bf 90 07 40 00 mov $0x400790,%edi* L2 y1 [7 t0 V
- 4005bf: b8 00 00 00 00 mov $0x0,%eax& F+ R8 P% d+ }/ z
- 4005c4: e8 b7 fe ff ff callq 400480 <printf@plt>
5 q) K5 T( d- K* b - 4005c9: c7 45 fc 39 30 00 00 movl $0x3039,-0x4(%rbp)
% A6 j% o/ d3 {# g; u - 4005d0: c9 leaveq% I+ _+ G0 @0 i9 [/ d' r
- 4005d1: c3 retq
复制代码 ! s$ A$ Z: A9 W8 W9 I6 D5 s
- 再看初始化局部变量a为2222的版本:
, p$ L* N. K& @/ x8 p% `6 E- // int a = 2222;! {: s& u$ W2 u i" u) q0 ^8 G
- 00000000004005ad <func1>:
# e) w; F! p z8 w - 4005ad: 55 push %rbp
4 g% ?3 @ F8 r) B - 4005ae: 48 89 e5 mov %rsp,%rbp( t9 N+ `8 O! ^: T3 a
- 4005b1: 48 83 ec 10 sub $0x10,%rsp! J' _, f& g% x6 s3 K7 K7 W4 G$ J8 j; }
- 4005b5: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
2 g" \( V3 {$ u: v$ k- P5 x - 4005bc: 8b 45 fc mov -0x4(%rbp),%eax
: A9 @& u1 ]. j( V% H% I. d, |: { - 4005bf: 89 c6 mov %eax,%esi$ O; J4 w f( y8 [
- 4005c1: bf 90 07 40 00 mov $0x400790,%edi
7 N/ o9 b" _8 h0 N3 a - 4005c6: b8 00 00 00 00 mov $0x0,%eax* G: H2 x: H0 n3 W, k
- 4005cb: e8 b0 fe ff ff callq 400480 <printf@plt>5 Z/ u `0 o U# O5 G" }
- 4005d0: c7 45 fc 39 30 00 00 movl $0x3039,-0x4(%rbp)
3 T* o) H& [% g8 x - 4005d7: c9 leaveq4 t: k$ k. j- @$ ^/ \6 A( A8 I
- 4005d8: c3 retq
复制代码- 仅仅差了一条指令:
8 ~7 r+ B1 M8 U2 m8 a5 [- 4005b5: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
复制代码 7 `6 V( d3 P1 u. x
: G9 m5 y. I9 t% ?4 n' K
初始化的操作是依靠实实在在的指令完成的。 总结一句, 函数返回在pop出当前栈帧的时候,并不会清理它遗留在栈帧里的数据,下个函数调用再次重用到该栈帧的内存时,未初始化的局部变量将会被遗留数据影响,从而变得不确定! 所以,记得初始化你的局部变量。
- F( r/ M4 @1 W# R1 D |