C语言函数参数传递之痛
文章原地址:点击打开链接
首先先讲一下表达式中“类型提升”,来自《C专家编程》
整型提升就是char,short(无论unsigned,signed),位段类型,枚举类型都将提升为int类型。前提是int类型能完整容纳原先的数据,否则提升为unsigned int类型。
char c1,c2;
c1=c1+c2;
在运算时,c1和c2都先被提升为int类型进行运算,运算结果再裁减为char类型。
类似的
float f1,f2;
f1=f1*f2;
在运算时,f1和f2都先提升为double类型,然后运算结果再裁减为float类型。
函数参数时的“类型提升”。
其实就是函数传递时的一种策略:方便编译器的设计。
例子:
file1.c中的函数:
void newpf(char a,char b)
{
printf("%f,%d/n",a,b);
}
file2.c中调用:
extern void newpf(char ,char);
int main()
{
char a=42;
char b=41;
newpf(a,b);
return 0;
}
对这两个文件分别反汇编:
gcc -S file1.c
gcc -S file2.c
file2.s如下(只摘取调用函数的语句):
movb $42, -1(%ebp) ==》char a=42;
movb $41, -2(%ebp) ==》char b=41;
subl $8, %esp
把b压入栈,压入的是4byte (虽然形参是char类型)==》可以认为是“类型提升为int”
movsbl -2(%ebp),%eax
pushl %eax
把a压入栈,压入的是4byte (虽然形参是char类型)==》可以认为是“类型提升为int”
movsbl -1(%ebp),%eax
pushl %eax
调用函数
call newpf
file1.s如下(只摘取获取实参的语句):
movl 8(%ebp), %eax ==》提取实参a (4byte)
movl 12(%ebp), %edx ==》提取实参b (4byte)
movb %al, -1(%ebp) ==》重新把实参a进行裁减为char类型
movb %dl, -2(%ebp) ==》重新把实参b进行裁减为char类型
subl $4, %esp
在调用printf函数时,参数类型也会被提升为4byte
movsbl -2(%ebp),%eax
pushl %eax
movsbl -1(%ebp),%eax
pushl %eax
pushl $.LC0
call printf
所以在32bit系统上,函数参数不管是char,short,int都是以4byte压入栈中。函数在根据定义,把实参裁减为定义的类型。
所以file2.c修改成如下,也可以运行。
注掉原型防止编译不通过
//extern void newpf(char ,char);
int main()
{
a,b都修改成int类型
int a=42;
int b=41;
newpf(a,b);
return 0;
}
gcc -o file file1.c file2.c
./file
结果如下:42,41
如果把file2.c修改成如下:
注掉原型防止编译不通过
//extern void newpf(char ,char);
int main()
{
a,b都修改成int类型
int a=65535;
int b=65535;
newpf(a,b);
return 0;
}
运行结果如下:-1,-1
原因:
在newpf函数中,实参会被重新裁减为char类型。
所以65535被转换成signed char 就变成了-1
很显然,如果遵守编程规范不了解“类型提升”似乎没什么问题?
但是当下面的情况出现时,如果不了解“类型提升”,就很难找到BUG了。
file1.c
void newpf(float a,char b)
{
printf("%f,%c/n",a,b);
}
file2.c
int main()
{
float a=40.0;
char b=41;
newpf(a,b);
return 0;
}
看起来没什么问题。但是编译运行结果如下:0.000000,
通过反汇编我们找下答案:
gcc -S file1.c file2.c
file2.s
movl $0x42200000, -4(%ebp)==》a=40.0
movb $41, -5(%ebp) ==》b=41
subl $4, %esp
把b转换成4byte压入栈中
movsbl -5(%ebp),%eax
pushl %eax
flds 指令意为把单精度value的值放入FPU的st7寄存器(64bit)中
flds -4(%ebp) ==》float a==》double a
leal -8(%esp), %esp
接着fstpl 指令把FPU的寄存器中的值以双精度的形式出栈,并存储在(%esp)处
fstpl (%esp)
调用函数
call newpf
所以栈中此时景象如下:
地址高
b 4byte
a 8byte
addr 4byte的函数返回地址
ebp 4byte
地址低
我们在看看file1.c中的newpf函数如何取实参的:
file1.s
movl 12(%ebp), %eax ==》本意是想取实参b,但是实际上取的是实参a的高4byte
movb %al, -1(%ebp)
movsbl -1(%ebp),%eax
pushl %eax
flds 8(%ebp) ==》该指令只能取实参a的低4byte,flds指令只能取单精度值--》即float类型
leal -8(%esp), %esp
fstpl (%esp)
pushl $.LC0
所以调用printf的时候,悲剧发生了。
call printf
为何会这样?
C标准中的参数“类型提升”。
在没有“原型”声明时,
float a=40.0;
char b=41;
调用newpf时,实参a从float类型自动提升为double类型
newpf(a,b);
所以a变成了8Byte。所以堆栈为:
地址高
b 4byte
a 8byte
addr 4byte的函数返回地址
ebp 4byte
地址低
而在函数newpf中,取实参时,根据newpf的“定义”,a为float类型,
函数newpf认为堆栈如下:
地址高
b 4byte
a 4byte
addr 4byte的函数返回地址
ebp 4byte
地址低
所以造成了不一致的情况。
如何解决?
在file2.c中加入 newpf函数的“原型”,如下:
加入原型
void newpf(float a,char b);
int main()
{
float a=40.0;
char b=41;
newpf(a,b);
return 0;
}
我们此时在看一下file2.c的反汇编:
file2.s
movl $0x42200000, -4(%ebp)==》float a=40.0
movb $41, -5(%ebp) ==》char b=41
subl $8, %esp
压入b参数
movsbl -5(%ebp),%eax
pushl %eax
pushl -4(%ebp) ==》直接压入4byte的a,不在对float a进行扩展。
call newpf
再次运行下:
gcc -o file file2.c file1.c
./file
运行结果如下:40.000000,)