|
C语言中,未初始化的局部变量到底是多少? 答案往往是: 与编译器有关。 可能但不保证初始化为0。 未确定。
; v& W; i+ S- a" x5 B/ O
总之,全部都是些一本正经的形而上答案,这很令人讨厌。 但凡一些人给你滔滔不绝地扯编译器,C库,处理器体系结构却给不出一个实际场景复现问题的时候,这人大概率在扯淡。
其实,这个问题本身就是错误的问法,说全了能讲10万字,我们只要能在特定场景下确定其特定行为就OK了,当然,这就需要设计一个比较OK的实验。 在演示一个实际代码行为之前,先给出一个知识,CPU不认识变量,更无法识别变量的名字,CPU只会从特定的内存位置取值或者将值存到特定的内存位置,因此当问一个变量的值是多少的时候,必须要知道这个变量对应的值被保存在什么地方。 来看下面的代码:
, W, c& Z/ S2 m1 h; u; ]/ A7 k- #include <stdio.h>3 y A( n6 D( Z! s
c$ Z! S: r7 f0 t% j! B* [- void func1()
+ I( z- a. ^( o5 l; q; b5 ` - {
+ k& ~6 n- |3 y2 b - int a;
: [; q) r' }0 t: X - printf("func1:%d\n", a);1 @+ ]7 c% r. |, i8 ?& T
- a = 12345;
& d. a/ T( r7 w1 K0 B, \, x6 ] - }
1 |% r' }$ Z1 j) ^( I
2 r3 e4 a& b x/ O) E" l- void func2()
" R1 O( B# M' ~4 a3 v7 c - {4 Y. [! `- ^/ K0 K; F4 R8 x
- int b;
5 h% s3 \* Z4 e7 ~/ F1 R: F& k. K/ [) } - printf("func2:%d\n", b);
. T( D3 k* Q: e) Z$ q+ S1 O - }
% @5 v& n) W. _. k' }* o - ) v- }6 |9 F' o' [
- void func4(). C- }9 r8 h$ X; r, F# @
- {9 J4 |% Y3 V# S( w C- v# w( H
- int d;4 A s7 D" E: ^! j7 C5 @, \
- printf("func3:%d\n", d);
# e- N/ x5 g# T! v& c2 L - }$ P) a# p$ X) [% V/ f
- : }! U: ]( N. U8 F
- void func3()
0 B- M# m9 l) n8 }4 J3 ` - {
2 k! W' U* X+ ]) p: W6 R - int c;( }! F5 s# d3 Z& u" U5 I" ?
- printf("func3:%d\n", c);4 C1 \* Z2 R y, I: ^+ ]6 C1 U
- c = 54321;
* e6 R2 E0 f4 r3 x3 [" J/ k( J - func4();9 s5 E/ S, R/ m* c, A$ h
- }1 ]( ?- l* x2 J; K! p
. H! E, a5 Q' H; p) i- void test_call()+ B9 Y2 Q! F' p& M* ~' X
- {+ e- \8 e/ S! F4 i: f9 I
- func3();% c! {2 l: E! X0 j1 X$ K
- }
! b: D% {% {8 i
+ T4 W# ~' m$ I- }9 o( y- int main(int argc, char **argv)1 w, U/ [1 n5 k8 K( x" g
- {# m5 Z% q ]( D3 [7 [8 v0 F& k
- func1();
# H4 a) B+ A# ^ F4 v* C - func2();) X0 \, J6 p) z, ~/ }# f
- 2 { J1 h0 j( o/ ~& W
- test_call(); ]0 N# h* W+ f5 c
- }
复制代码我们有func1~func4一共4个函数,其内部均有一个未初始化的局部变量,它们的值到底是多少呢? 对于这种局部变量,它们的值取决于: 变量在栈中的位置。 变量对应的栈位置在 之前 有没有被store过。
( y: @5 D* u) _
可以看到,上述第一点标记了一个内存位置,第二点则是代码的行为,也就是说,只要有代码去store对应的位置, 且后续的代码没有reset该位置的值的话,该位置就会保留着原先被store后的值。 验证非常简单,试一下就知道了:
$ Z1 h1 k+ A$ s7 p0 {% Y7 m- [root@localhost test]# ./a.out* R Z- V% S& J: H( d
- func1:0) L( d! A5 A! P8 {
- func2:12345/ s) k5 j& o" J8 a
- func3:0
7 k$ w* ~1 u0 x+ S. d6 E5 T) N - func3:0
复制代码
8 b9 }9 G0 v6 F- T- 按照函数调用栈帧的变化,func1的局部变量a和func2的局部变量b显然是位于同一个位置的,在func1被调用时,这是一块新的内存(可能在进入main之前有栈帧到达过这个位置),a的值取决于调入内存该位置的页面对应偏移的初始值,这取决于操作系统:
4 r5 i$ u t( r8 G2 T
栈的分配不会涉及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:
0 {+ u+ t9 q' S7 a7 b+ Q- // int a;& ~0 w, |0 X1 I. H: l5 h
- 00000000004005ad <func1>:
% G f4 V4 w+ _- t4 w$ f - 4005ad: 55 push %rbp2 @3 I6 h( U5 _
- 4005ae: 48 89 e5 mov %rsp,%rbp6 L8 O0 x8 w1 s3 {6 E
- 4005b1: 48 83 ec 10 sub $0x10,%rsp
6 {/ E' o, `8 C' f. ^ - 4005b5: 8b 45 fc mov -0x4(%rbp),%eax! R7 @% _9 U% F9 e" Z' l
- 4005b8: 89 c6 mov %eax,%esi3 K. t& V/ o ~3 W- _0 R6 H
- 4005ba: bf 90 07 40 00 mov $0x400790,%edi' q) Z; _: L/ X: F- ^! D
- 4005bf: b8 00 00 00 00 mov $0x0,%eax* T2 D$ {5 J) y( p' L+ U$ A
- 4005c4: e8 b7 fe ff ff callq 400480 <printf@plt>
$ F- @3 w B! ?+ t+ W - 4005c9: c7 45 fc 39 30 00 00 movl $0x3039,-0x4(%rbp)6 ^6 v: p) }2 w$ j
- 4005d0: c9 leaveq
' Z7 A5 K% Q2 ~' W! P$ Q - 4005d1: c3 retq
复制代码 * M) g {$ l4 T: m2 _+ i T
- 再看初始化局部变量a为2222的版本:
# E, X4 R9 M$ B: p0 U' l @, r4 E- // int a = 2222;& g5 q& o2 P4 ]5 k z) W/ M8 _4 Z0 X) B
- 00000000004005ad <func1>:
* Q+ R$ D, u6 J5 ?! h6 J - 4005ad: 55 push %rbp
( T* D) ?0 d1 O - 4005ae: 48 89 e5 mov %rsp,%rbp
1 D( \# T- ^" t4 W* v. v - 4005b1: 48 83 ec 10 sub $0x10,%rsp0 T( E/ Y, @. T( ~. G
- 4005b5: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
# w F! V) Z* W+ ^7 d - 4005bc: 8b 45 fc mov -0x4(%rbp),%eax5 O1 k% g3 ]+ a Q1 L5 w* z P1 M
- 4005bf: 89 c6 mov %eax,%esi6 Q. e% f* u6 W* ^6 G( N$ V5 _
- 4005c1: bf 90 07 40 00 mov $0x400790,%edi
- L& |( ]- ?* ^. D - 4005c6: b8 00 00 00 00 mov $0x0,%eax
- o3 v7 |. K: a! l" m - 4005cb: e8 b0 fe ff ff callq 400480 <printf@plt>
- T+ b6 E$ u4 _2 u! K) U. K; y - 4005d0: c7 45 fc 39 30 00 00 movl $0x3039,-0x4(%rbp)
. Z* d9 ^/ w7 c) z) V4 p) T: p - 4005d7: c9 leaveq
9 Y6 S# L7 m$ N5 g: S& W5 \& G - 4005d8: c3 retq
复制代码- 仅仅差了一条指令:
+ |1 q" h" P" j- 4005b5: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
复制代码
! H8 @$ S" q9 @3 ^9 w" C - Q7 B6 D; r! a3 W; _0 O
初始化的操作是依靠实实在在的指令完成的。 总结一句, 函数返回在pop出当前栈帧的时候,并不会清理它遗留在栈帧里的数据,下个函数调用再次重用到该栈帧的内存时,未初始化的局部变量将会被遗留数据影响,从而变得不确定! 所以,记得初始化你的局部变量。
& Z* M+ p6 e3 M# g: T2 o |