本节必须掌握的知识点:
基本数据类型
定义变量数据类型
示例七
代码分析
汇编解析
■C语言包含的数据类型如下图所示:
图3-1C语言数据类型
编译器定义的基本数据类型有整型、浮点型、字符型和枚举类型。常用的四种基本数据类型为char、int、float、double。
■整型
int 类型:有符号整数。VS编译器为32位有符号整数,取值范围-231~231-1。
unsigned int类型:无符号整数,VS编译器为32位无符号整数,取值范围0~232-1。
short (int) 类型:2个字节,16位有符号整数,可以简写为short。
unsigned short (int)类型:2个字节,16位无符号整数,可以简写为unsigned short。
long (int) 类型:4个字节,32位有符号整数,可以简写为long。
unsigned long (int) 类型:4个字节,32位无符号整数,可以简写为unsigned long。
long long(int) 类型:8个字节,64位有符号整数,可以简写为long long。
■浮点型
单精度浮点型 float :4个字节,32位浮点数,6位有效位。
双精度浮点型 double :8个字节,64位浮点数,15位有效位。
长双精度浮点型long double :10个字节,80位浮点数,19位有效位。
图3-2 ASCII码表
■字符型
char 类型:8位1个字节,取值范围0~255,VS编译器取值范围-128~127。
unsigned char 类型:8位1个字节,取值范围0~255,对应的整数称为字符的ASCII码值。如图2-10所示。
singned char类型:8位1个字节,取值范围-128~127。
定义变量的格式:类型 变量名
举例
int a;
定义一个整型变量a,定义变量时也可以赋值:int a = 1;
char ch=‘A’; //字符要加单引号。
或者先定义然后赋值:
char ch;
ch=65; //字符类型可以直接使用ASCII码值。
示例代码七
/*
输入输出基本数据类型
*/
#include
#include
int main(void)
{
/********整型*********/
int y; //准备变量
printf("请输入一个整型:");
scanf_s("%d", &y);
printf("用户输入的内容是%d\n", y);
/********字符型********/
char ch; //准备变量
getchar();//取出前一个scanf_s函数遗留在stdin的回车符
printf("请输入一个字符:");
scanf_s("%c", &ch,1);
printf("用户输入的内容是%c\n", ch);
/********单精度浮点型*********/
float a; //准备变量
printf("请输入一个float型:");
scanf_s("%f", &a);
printf("用户输入的内容是%f\n", a);
/********双精度浮点型*********/
double b; //准备变量
printf("请输入一个double型:");
scanf_s("%lf", &b);
printf("用户输入的内容是%f\n", b);
system("pause");
return 0;
}
●输出结果:
请输入一个整型:1
用户输入的内容是1
请输入一个字符:a
用户输入的内容是a
请输入一个float型:1.2
用户输入的内容是1.200000
请输入一个double型:1.3
用户输入的内容是1.300000
请按任意键继续. . .
■scanf_s函数的格式化说明符与printf函数稍有差异:
scanf_s函数中float浮点类型对应的格式化说明符为’%f’,double浮点类型对应的格式化说明符为’%lf’,long double类型对应的格式化说明符同样是’%lf’。
printf函数中float浮点类型对应的格式化说明符为’%f’,double浮点类型对应的格式化说明符同样是’%f’,long double类型对应的格式化说明符才是’%lf’。
■scanf_s函数遗留字符:
getchar();//取出前一个scanf_s函数遗留在stdin的回车符
printf("请输入一个字符:");
scanf_s("%c", &ch,1);
之所以在接收键盘输入一个字符前先调用getchar()函数,是因为前一个scanf_s函数接收键盘输入一个int类型整数值时按了回车键结束,因此stdin输入流中遗留了一个“回车符0ah”。如果不添加getchar()函数,获取到的字符将是回车符,程序出现错误。如果此处scanf_s函数接收的不是字符型数据,而是整型、浮点型或者其他类型数据,则会自动过滤掉遗留的回车符,不受遗留字符的影响。
■格式化说明符对应相应的数据类型
%d输入或者输出一个int类型数据;
%c输入或者输出一个char类型数据;
%f输入float类型数据,输出float类型或double类型数据;
%lf输入或者输出一个long double类型数据。
■输出符号
空格:输出正数的时候在前面补一个空格。
在用%o输出八进制的时候,在八进制前面补一个0。
在用%x输出16进制的时候,在16进制前面补一个0x。
■其他不常格式占用符
%hd输入或者输出一个short类型数据;
%ld 输入或者输出一个long类型数据;
%lld 输入或者输出一个long long类型数据;
%x 输入或者输出一个16进制整型数据;
%o 输入或者输出一个8进制整型数据;
%u 输入或者输出一个无符号整型数据;
%s 输入或者输出一个字符串类型数据;
%p 输入或者输出一个地址类型数据;
对于输入输出的格式和精度,我们将在第十二章详细讲述。
实验二十一:验证scanf_s函数执行后的遗留字符
第一步:屏蔽掉//getchar();//取出前一个scanf_s函数遗留在stdin的回车符。
第二步:在scanf_s("%c", &ch,1);这行下断点。
第三步:按F5执行调试。控制台窗口提示输入整型,键盘输入整数1,然后回车结束输入。接着提示输入一个字符。
请输入一个整型:1
用户输入的内容是1
请输入一个字符:
第四步:监视1窗口输入&ch,显示内容如下所示:当前ch变量地址处为无效字符。
五步:按快捷键F10单步执行,监视1窗口显示内容如下:
此时,会发现char型变量ch地址处存储的第一个字符为’\n’回车符,即输入整数1时遗留在stdin输入流中的回车符。
结论
当调用scanf_s接收输入一个字符时,请务必先清空stdin标准输入流,否则会造成错误!
■汇编代码
;FileName:3-2-1.asm
;例7:示例代码7-1 输入输出基本数据类型
;by:bcdaren
;2023.08.27
;===============================
;C标准库头文件和导入库
include vcIO.inc
.data ;全局区
y sdword 0
chr byte ? ;notepad++中ch变量名与关键词有冲突
a real4 ?
b real8 ?
.const ;常量区
iszMsg1 db "请输入一个整型:",0
iszMsg2 db "%d",0
iszMsg3 db "用户输入的内容是%d",0dh,0ah,0
cszMsg1 db "请输入一个字符:",0
cszMsg2 db "%c",0
cszMsg3 db "用户输入的内容是%c",0dh,0ah,0
fszMsg1 db "请输入一个float型:",0
fszMsg2 db "%f",0
fszMsg3 db "用户输入的内容是%f",0dh,0ah,0
dszMsg1 db "请输入一个double型:",0
dszMsg2 db "%lf",0
dszMsg3 db "用户输入的内容是%f",0dh,0ah,0
.code ;代码区
start:
;输入一个整数
push offset iszMsg1 ;格式化常量字符串偏移地址入栈
call printf ;调用printf函数输出结果
lea esi,y ;取变量y地址
push esi
push offset iszMsg2
call scanf
push y
push offset iszMsg3
call printf
;输入一个字符
call getchar;取出输入流中遗留的回车符,不可以用_getch()函数
push offset cszMsg1
call printf
lea esi,chr
push esi
push offset cszMsg2
call scanf
movsx eax,chr
push eax
push offset cszMsg3
call printf
;输入一个单精度浮点数
push offset fszMsg1
call printf
lea esi,a
push esi
push offset fszMsg2
call scanf
invoke printf,offset fszMsg3,a ;输出结果为错误
;输入一个双精度浮点数
push offset dszMsg1
call printf
lea esi,b
push esi
push offset dszMsg2
call scanf
invoke printf,offset dszMsg3,b
;
invoke _getch ;等待输入单个字符
ret ;结束返回
end start
●输出结果:
请输入一个整型:1
用户输入的内容是1
请输入一个字符:A
用户输入的内容是A
请输入一个float型:2
用户输入的内容是0.000000
请输入一个double型:3
用户输入的内容是3.000000
上述汇编代码需要注意两个问题:
_getch()函数会暂停控制台输出,直到按下一个按键为止,它不使用任何缓冲区来存储输入字符,输入字符后将立即返回,而无需等待回车键,输入的字符不会显示在控制台上。
getch()函数适用于接受隐藏的输入,例如密码、ATM账号等。
getchar()函数的函数原型:int getchar(void);
函数返回值是ASCII码,所以只要是ASCII码表里有的字符它都能读取出来。在调用getchar()函数时,编译器会依次读取用户键入缓存区的一个字符(注意这里只读取一个字符,如果缓存区有多个字符,那么将会读取上一次被读取字符的下一个字符),如果缓存区没有用户按键输入的字符,那么编译器会等待用户输入并回车后再执行下一步 (注意键入后的回车键也算一个字符,输出时直接换行)。
2.movsx eax,chr语句将chr8位扩展为32位,然后入栈。这是因为32位程序的堆栈空间是32位对齐的,X86指令不支持8位入栈。
3.输入一个float型数据2时,printf输出的值为0,显示这是错误的。在汇编语言中,数据类型real4为单精度浮点型,等同于C语言中的float类型,数据类型real8等同于C语言中的double类型。在printf函数中,float 、double都是%f输出,但 float 是32位的,double 是64位的,所以在参数传递的时候C语言的编译器统一将 float 类型数值传换为 double 类型再传入printf 函数。问题肯定出现在数据类型转换的环节,接下来我们使用调试器单步跟踪验证一下错误原因。
实验二十二:验证汇编语言中float类型输出错误原因
第一步:打开DtDebug调试器,将汇编生成的3-2-1.exe程序拖入调试器。
第二步:按Ctrl+F9,进入程序入口地址。
第三步:按F8单步执行,直到输出double型数据结束。控制台窗口显示内容如下:
请输入一个整型:1
用户输入的内容是1
请输入一个字符:a
用户输入的内容是a
请输入一个float型:2
用户输入的内容是0.000000
请输入一个double型:3
用户输入的内容是3.000000
第四步:查看下方的内存窗口,如图3-3所示。第一个输入的整数1位于0x00A53000地址处(每次运行地址不同),占用4个字节空间。接着在地址0x00A53004处存储输入的单个字符’a’(十进制数值61),占用一个字节空间。然后是输入的float型数据2,存储在地址0x00A53005处,占用4个字节空间,值为0x40000000。最后是输入的double类型数据3,存储在地址0x00A53009处,占用8个字节空间,低32位值0x00000000,高32位值为0x40080000。
图3-3 汇编代码中的float类型数据输出错误
第五步观察反汇编窗口,输出float型数据的printf函数有3条反汇编语句:
00A51078 PUSH DWORD PTR DS:[A53005] ; /<%f> = 2.000000
00A5107E PUSH 3-2-1.00A52086 ; |format =
00A51083 CALL 3-2-1.00A510C6 ; \printf
第一个PUSH语句将DS数据段DS:[A53005] 4个字节数据入栈0x40000000。
第二个PUSH语句将格式化字符串入栈。
第三条CALL指令调用printf。
【注意】执行printf函数时,会自动将0x40000000转换成double类型数据,低32位为0x00000000,高32位为0x40000000。但是使用ml.exe汇编器编译后,printf输出时只输出了低32位,因此输出结果为0,出现错误。
上述汇编代码,读者可以将real4类型变量a改为real8类型,尝试一下结果是否正确。
结论
1.数据类型的宽度和取值范围和编译器相关。
2.数据类型的转换有自动类型转换和强制类型转换两种方式。
3.我们可以观察汇编和反汇编代码,单步跟踪来分析C语言的实现过程和产生错误的原因。
■反汇编代码
/********整型*********/
int y; //准备变量
printf("请输入一个整型:");
00951952 push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xd2\xbb\xb8\xf6\xd5\xfb\xd0\xcd:" (0957B30h)
/********整型*********/
int y; //准备变量
printf("请输入一个整型:");
00951957 call _printf (095104Bh)
0095195C add esp,4
scanf_s("%d", &y);
0095195F lea eax,[y]
00951962 push eax
00951963 push offset string "%d" (0957B44h)
00951968 call _scanf_s (0951154h)
0095196D add esp,8
printf("用户输入的内容是%d\n", y);
00951970 mov eax,dword ptr [y]
00951973 push eax
00951974 push offset string "\xd3\xc3\xbb\xa7\xca\xe4\xc8\xeb\xb5\xc4\xc4\xda\xc8\xdd\xca\xc7%d\n" (0957B48h)
00951979 call _printf (095104Bh)
0095197E add esp,8
/********字符型********/
char ch; //准备变量
getchar();//取出前一个scanf_s函数遗留在stdin的回车符
00951981 mov esi,esp
00951983 call dword ptr [__imp__getchar (095B17Ch)]
00951989 cmp esi,esp
0095198B call __RTC_CheckEsp (095122Bh)
printf("请输入一个字符:");
00951990 push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xd2\xbb\xb8\xf6\xd7\xd6\xb7\xfb:" (0957B60h)
00951995 call _printf (095104Bh)
0095199A add esp,4
scanf_s("%c", &ch,1);
0095199D push 1
0095199F lea eax,[ch]
009519A2 push eax
009519A3 push offset string "%c" (0957B74h)
009519A8 call _scanf_s (0951154h)
009519AD add esp,0Ch
printf("用户输入的内容是%c\n", ch);
009519B0 movsx eax,byte ptr [ch] ;8位有符号数扩展为32位有符号数
009519B4 push eax
009519B5 push offset string "\xd3\xc3\xbb\xa7\xca\xe4\xc8\xeb\xb5\xc4\xc4\xda\xc8\xdd\xca\xc7%c\n" (0957B78h)
009519BA call _printf (095104Bh)
printf("用户输入的内容是%c\n", ch);
009519BF add esp,8
/********单精度浮点型*********/
float a; //准备变量
printf("请输入一个float型:");
009519C2 push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xd2\xbb\xb8\xf6float\xd0\xcd:" (0957B90h)
009519C7 call _printf (095104Bh)
009519CC add esp,4
scanf_s("%f", &a);
009519CF lea eax,[a]
009519D2 push eax
009519D3 push offset string "%f" (0957BA8h)
009519D8 call _scanf_s (0951154h)
009519DD add esp,8
printf("用户输入的内容是%f\n", a);
009519E0 cvtss2sd xmm0,dword ptr [a]
009519E5 sub esp,8
009519E8 movsd mmword ptr [esp],xmm0
009519ED push offset string "\xd3\xc3\xbb\xa7\xca\xe4\xc8\xeb\xb5\xc4\xc4\xda\xc8\xdd\xca\xc7%f\n" (0957BACh)
009519F2 call _printf (095104Bh)
009519F7 add esp,0Ch
/********双精度浮点型*********/
double b; //准备变量
printf("请输入一个double型:");
009519FA push offset string "\xc7\xeb\xca\xe4\xc8\xeb\xd2\xbb\xb8\xf6double\xd0\xcd:" (0957BC4h)
009519FF call _printf (095104Bh)
00951A04 add esp,4
scanf_s("%lf", &b);
00951A07 lea eax,[b]
00951A0A push eax
00951A0B push offset string "%lf" (0957BDCh)
00951A10 call _scanf_s (0951154h)
00951A15 add esp,8
printf("用户输入的内容是%f\n", b);
00951A18 sub esp,8
00951A1B movsd xmm0,mmword ptr [b]
00951A20 movsd mmword ptr [esp],xmm0
00951A25 push offset string "\xd3\xc3\xbb\xa7\xca\xe4\xc8\xeb\xb5\xc4\xc4\xda\xc8\xdd\xca\xc7%f\n" (0957BACh)
00951A2A call _printf (095104Bh)
00951A2F add esp,0Ch
注意,上述VS反汇编代码中关于浮点数的输出语句:
009519E0 cvtss2sd xmm0,dword ptr [a]
009519E5 sub esp,8
009519E8 movsd mmword ptr [esp],xmm0;将float类型转换为double类型入栈
009519ED push offset string "\xd3\xc3\xbb\xa7\xca\xe4\xc8\xeb\xb5\xc4\xc4\xda\xc8\xdd\xca\xc7%f\n" (0957BACh)
009519F2 call _printf (095104Bh)
反汇编代码中使用浮点指令cvtss2sd将float类型数据加载到浮点寄存器xmm0,然后再使用movsd指令将浮点寄存器xmm0中的64位值复制到堆栈中,实现了float类型到double类型的转换。而在汇编代码中直接使用高级伪指令语句:invoke printf,offset fszMsg3,a ;输出结果。在DtDebug调试器反汇编窗口使用的是PUSH DWORD PTR DS:[A53005]这样的语句,只是将32位值压入堆栈传参,没有事先实现float类型到double类型的数据转换。double类型的值通过两次PUSH分别把低32位值和高32值位入栈传参。
本文摘自编程达人系列教材《汇编的角度——C语言》。