【新书推荐】3.2 数据类型的分类

本节必须掌握的知识点:

  基本数据类型

   定义变量数据类型

   示例七

   代码分析

   汇编解析

       ■C语言包含的数据类型如下图所示:

【新书推荐】3.2 数据类型的分类_第1张图片

                                                图3-1C语言数据类型

3.2.1 基本数据类型

编译器定义的基本数据类型有整型、浮点型、字符型和枚举类型。常用的四种基本数据类型为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。

3.2.2 定义变量数据类型

定义变量的格式:类型 变量名

举例

int a;

定义一个整型变量a,定义变量时也可以赋值:int a = 1;
char ch=‘A’; //字符要加单引号。

或者先定义然后赋值:
char ch;
ch=65; //字符类型可以直接使用ASCII码值。

3.2.3 示例七

示例代码七

/*

   输入输出基本数据类型

*/

#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

请按任意键继续. . .     

3.2.4 代码分析

■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标准输入流,否则会造成错误!

3.2.5 汇编解析

汇编代码

       ;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

       上述汇编代码需要注意两个问题:

  1. 取出标准输入流stdin中遗留的回车符使用getchar()函数,而不是_getch()函数。

_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.2 数据类型的分类_第2张图片

                                图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语言》。

你可能感兴趣的:(《汇编的角度——C语言》,汇编,c语言)