第7章 基本数据类型

作者: 橡树人 | 来源:发表于2020-02-26 06:54 被阅读0次

    英文原版:P125

    截止目前,我们仅使用过两种内置的基本数据类型:int和float。
    本章将描述剩余的基本数据类型,并讨论有关数据类型的重要主题。

    本章的主要内容有:

    • 7.1节展示了整数类型的取值范围,包括长整型、短整型、无符号整数。
    • 7.2节介绍了double和long double类型,其数值范围比float更大,精度比float更高。
    • 7.3节介绍了char类型,后面将用来处理字符数据。
    • 7.4节介绍类型转换相关主题。
    • 7.5节展示如何使用typedef来定义新的类型。
    • 7.6节描述了sizeof运算符,可用来判断一个类型所占的存储大小。

    7.1节 整数类型 Integer Types

    C语言有哪些整数类型?6种

    • short int
    • unsigned short int
    • int
    • unsigned int
    • long int
    • unsigned int

    取值范围
    32位机器:
    short int -2^15 2^15-1
    unsigned short int 0 2^16-1
    int -2^31 2^31-1
    unsigned int 0 2^32-1
    long int -2^31 2^31-1
    unsigned int 0 2^32-1

    64位机器:
    short int -2^15 2^15-1
    unsigned short int 0 2^16-1
    int -2^31 2^31-1
    unsigned int 0 2^32-1
    long int -2^63 2^63-1
    unsigned int 0 2^64-1

    所有的编译器都必须遵守2个规则:

    1. short int、int、long int必须能覆盖最小的数值范围。
    2. int不小于short int,long int不小于int,有可能short int跟int有相同的取值范围,int有可能跟long int有相同的取值范围。

    如何判断某种整数类型具体实现的整数范围?
    检查标准库里的头文件<limits.h>,在这个头文件里定义了宏,表示每种整数类型的最小值和最大值。

    整数常量

    C语言规定:

    • 整型常量可被写成10进制、8进制、16进制;
    • 十进制常量不能以0开头,可包含0到9中的数字;
    • 8进制常量必须以0开头,只能包含0到7中的数字;
    • 16进制必须以0x开头,只能包含0到9中的数字和a到f中的字母;

    注意:

    • 8进制和16进制只是书写数字的一种替代方式,对数字实际如何存储没有影响(整数通常都是以2进制存储的,跟使用任何进制来表示数字无关)。
    • 使用8进制和16进制来编写底层程序是最方便的。

    编译器是如何判断不同进制整数常量的类型的?

    • 十进制整数常量
      编译器会按照从int->long int->unsigned long int的顺序来遍历直到查找到一个合适的类型为止。
    • 八进制整数常量和十六进制整数常量
      编译器会按照从int->unsigned int->long int->unsigned long int的顺序来遍历直到查找到一个合适的类型为止。

    可强制编译器将一个常量当做长整型long int:

    15L 0377L 0x7fffL
    

    可强制编译器将一个常量当做无符号的:

    15U 0377U 0x7fffU
    

    整数溢出

    什么时候会出现整数溢出?
    对整数进行算术运算时,有可能结果太大而无法表示。比如,当对两个int整数执行算术运算,结果必须能使用int来表示。如果不能,则表示发生了溢出

    发生整数溢出时该怎么办跟操作数是有符号还是无符号有关。

    当对有符号整数执行运算后产生了溢出,结果是没有定义的。最可能的情形就是运算的结果是错的,但程序可能因此崩溃或者表现出其他的未知的行为。

    当对无符号整数执行运算后产生了溢出,结果是有定义的:正确结果对2^n取模,其中n是用来存储正确结果的位数。比如对无符号16位数65,535(2^{16}-1)加1,其结果为0。

    整数的读取和写入

    假设一个程序因为它的某个int变量值溢出而无法工作了。
    解决方法:

    1. 将该变量的数据类型修改为long int。
    2. 必须检查该变量是否被用在printf或者scanf的调用里;

    读写无符号整数、短整型数、长整型需要几条新的转换说明:

    • 读写无符号数时,%u表示读写十进制数,%o表示读写八进制数,%x表示读写16进制数。
    • 读写短整型数时,%hd表示读写十进制数,%ho表示读写八进制数,%hx表示读写16进制数,%hu表示读写无符号10进制数。
    • 读写长整型数时,%ld表示读写十进制数,%lo表示读写八进制数,%lx表示读写16进制数,%lu表示读写无符号10进制数。
    • 读写长长整型long long int数时,%lld表示读写十进制数,%llo表示读写八进制数,%llx表示读写16进制数,%llu表示读写无符号10进制数。

    程序示例:计算一系列数的和

    源文件sum2.c

    /* Sums a serial of numbers (using long variales) */
    
    #include <stdio.h>
    
    int main(void)
    {
            long n, sum = 0;
    
            printf("This program sums a serial of integers.\n");
            printf("Enter integers (0 to terminate): ");
    
            scanf("%ld", &n);
            while(n!=0){
                    sum += n;
                    scanf("%ld", &n);
            }
            printf("The sum is: %ld\n", sum);
    
            return 0;
    }
    

    7.2节 浮点数

    整数类型并不能满足所有应用。有时需要一些变量,这些变量可存储带小数点的数,或者非常大的数,或者非常小的数。像这样的数是按照浮点数格式存储的。

    C语言提供了3种浮点数类型:
    单精度浮点数float
    双精度浮点数double
    拓展精度浮点数double double

    精度 有效位数

    当对精度要求不高时,使用float

    double提供的精度可满足大部分应用

    double double极少被使用

    由于不同的计算机存储浮点数的方式不同,所以C语言标准并没有描述float double double double能提供多高的精度。

    大部分计算机都遵循IEEE 754标准

    7.3节 字符型char

    char类型值跟计算机有关,因为不同的计算机默认的字符集不一样。

    可把任意单个字符赋值给char类型变量,比如

    chat ch;
    
    ch='a';
    ch='A'
    ch='0';
    ch=' ';
    

    注意

    • 字符常量是用单引号包起来的,不是双引号

    字符操作

    基本事实:

    • C语言把字符当做小整数来处理
      比如,在ASCII码中,字符的编码范围为00000000到11111111,对应的十进制范围为0到127。字符'a'的数值为97,'A'的数值是65,'0'的数值为48,空格字符' '的数值为32。
    • 字符常量实际上是int类型,而不是char类型

    例1 字符运算

    char ch;
    int i;
    
    i = 'a';
    ch = 65;
    ch = ch + 1;
    ch++;
    

    例2 字符比较

    if ('a' <= ch && ch <= 'z') {
      ch = ch + 'A' - 'a';
    }
    

    例3 字符变量作为循环变量

    for(ch = 'A'; ch <= 'Z'; ch++){
      ...
    }
    

    有符号字符和无符号字符

    有符号字符的取值范围为-128到127。
    无符号字符的取值范围为0到255。

    由于C语言标准没有规定普通字符串是有符号的还是无符号的,所以有些编译器把普通字符当做有符号类型,其他则当做无符号类型。

    建议:

    别假设char类型是有符号还是无符号。如果有区别,则使用unsigned char或者signed char来代替char

    转义序列

    • 有两类转义序列:字符转义序列和数字转义序列;
    • 当做字符常量使用时,必须使用单引号括起来,比如'\33'或者'\1b'
    • 可通过查表来获取转义字符信息;

    由于存在不能从键盘上输入的或者不可见的字符,为了方便程序统一处理默认字符集里的每个字符,C语言标准提供了一种特殊的表示法:转义序列

    有两类转义序列:

    • 字符转义序列
      比如,\a表示警报服,\b表示回退符等;
    • 数字转义序列

    字符转义序列不能表示所有的无法打印的字符,只包含了最常用的字符。

    数字转义序列解决了字符转义序列存在的问题,可表示所有的字符。

    如何使用数字转义序列来表示特殊字符?
    首先,从表中查询该字符对应的八进制表示或者十六进制表示。
    然后,使用前述八进制码或者十六进制码来表示特殊字符。
    最后,要注意,使用十六进制码来表示数字转义序列时,其中的x必须小写

    作为字符常量使用时,转义字符必须用一对单括号括起来,比如'\33'或者'\x1b'。如有必要,可以使用#define来定义,比如:

    #define ESC '\33'
    

    例1 数字转义序列
    假设某个特殊字符对应的十进制编码为27,八进制编码为33,十六进制编码为1B,则该特殊字符对应的字符转义序列为\33或者\033或者\x1B或者\x1b

    处理字符的函数

    C语言的库函数toupper

    要调用该函数,需在程序开头包含如下预处理指令:

    #include <ctype.h>
    

    例1 将一个小写字符转换成大写的两种方式:

    if ('a' <= ch && ch <= 'z') {
      ch = ch + 'A' - 'a';
    }
    

    ch = toupper(ch);
    

    使用scanf和printf来读写字符

    • %c:不跳过空格
    • %c:跳过空格
      例1 使用%c转换符来读取单个字符
    char ch;
    
    scanf("%c", &ch);//在读取一个字符前,不会跳过空格符
    printf("%c", ch);
    
    scanf(" %c", &ch);//在读取一个字符前,跳过空格符
    printf("%c", ch);
    
    

    例2 利用scanf通常不会跳过空格的性质来检测输入的结束

    char ch;
    
    do {
      scanf("%c", &ch);
    }while (ch != '\n');
    

    使用getchar和putchar来读写字符

    1. 程序执行时,使用getchar和putchar要比scanf和printf快,理由有2个:
      • getchar和putchar比scanf和printf更简单;
      • 为了额外的速度提升,getchar和putchar是作为宏来实现的;

    函数getchar

    • 返回值是int值,而不是char值,为什么?
    • 不会跳过读取到的空格符
    • 查找某个字符或者跳过某个字符

    例1 跳过所有的换行符:

    char ch;
    
    do {
      scanf("%c", &ch);
    }while (ch != '\n');
    

    答:

    char ch;
    
    do {
      ch = getchar();
    }while (ch != '\n');
    

    或者

    char ch;
    
    while ((ch=getchar()) != '\n') {
      ;
    }
    

    例2 跳过所有的空白

    while ((ch = getchar()) == ' ') {
      ;
    }
    

    程序示例:输出一条消息的长度

    需求:用户输入一条消息,程序输出这条消息的长度
    范例输出:

    Enter a message: china
    Your message was 5 character(s) long.

    源文件length.c

    #include <stdio.h>
    
    int main(void){
        char ch;
        int len = 0;
    
        printf("Enter a message: ");
        ch = getchar();
        while (ch != '\n') {
            len++;
            ch = getchar();
        }
        printf("Your message was %d character(s) long.\n", len);
    
        return 0;
    }
    

    优化版length2.c

    #include <stdio.h>
    
    int main(void){
        char ch;
        int len = 0;
    
        printf("Enter a message: ");
        ch = getchar();
        while (ch != '\n') {
            len++;
            ch = getchar();
        }
        printf("Your message was %d character(s) long.\n", len);
    
        return 0;
    }
    

    7.4节 类型转换

    C语言有哪些算术类型?

    • 整数类型
      • char类型
      • 有符号整数(signed char, short int, int, long int)
      • 无符号整数(unsigned char, unsigned short int, unsigned int, unsigned long int)
      • 枚举类型
    • 浮点数类型(float, double, double double)

    C语言有哪两种类型转换?

    • 隐式转换
    • 显式转换

    什么是隐式转换?
    计算机要比C语言对待算术更严格。

    让计算机做算术运算,两个操作数必须有相同的大小,相同的存储方式。计算机允许两个16位的数相加,但不允许一个16位整数和一个32位整数相加、一个32位整数和一个32位浮点数相加。

    C语言允许表达式中出现基本类型的混合。我们可将整数、浮点数、甚至字符整合到一个单独的表达式中。编译器可能要生成一些指令,将某些操作数转换成不同类型,以便硬件能对该表达式求值。比如,将一个16位的短short整型数和一个32位的int整数相加,编译器会安排16位的短整型转换成32位。将一个int数和float数相加,编译器将安排int转换成float。

    由于编译器会自动地处理这些转换,所以被称为隐式转换

    有哪些场合需要执行隐式地类型转换?

    • 当在算术表达式或者逻辑表达式中的操作数类型不同时;
    • 当赋值运算符的右表达式的类型跟左表达式的类型不匹配时;
    • 当函数调用的实际参数的类型跟函数定义里的形式参数类型不匹配时;
    • 当return语句例的表达式的类型跟函数的返回值类型不匹配时;

    算术运算类型转换

    普通的算术类型转换可应用到大部分二目运算符的操作数上,比如算术运算符、关系运算符、判等运算符等。比如,我们假设f代表类型float,i代表类型int。表达式f+i的转换规则是将int转换成float。

    提醒:尽可能多地使用无符号数,永远不要混合使用无符号数和有符号数

    C语言中是如何处理有符号数和无符号数混合情形的?

    • 将有符号数转换成无符号数,给有符号数加上(n+1),其中n是无符号数能表示的最大数。比如
    it i = -10;
    unsigned int u = 10;
    
    i < j;//该表达式的值是0,理由负10不能被表示成无符号数,则将负10转换成-10+2^31+1=4294967286,显然大于10
    
    • 当比较一个无符号数和一个有符号数时,有些编译器会生成警告消息,比如“有符号和无符号比较”等

    例1 算术类型转换

    char c;
    short int s;
    int i;
    unsigned int u;
    long int l;
    unsigned long int ul;
    float f;
    double d;
    long double ld;
    
    i = i + c;
    i = i + s;
    u = u + i;
    l = l + u;
    ul = ul + l;
    f = f + ul;
    d = d + f;
    ld = ld + d;
    

    赋值过程中的转换规则

    规则:将赋值运算符右边表达式的值转换成左边表达式的值

    例1 赋值类型转换

    char c;
    int i;
    float f;
    double d;
    
    i = c;
    f = i;
    d = f;
    

    例2 浮点数赋值给整数

    int i;
    
    i = 842.97;//i现在是842
    i = -842.97;//i现在是-842
    

    例3 错误示例

    char c;
    int i;
    float f;
    
    c = 10000;//出错
    i = 1.0e20;//出错
    f = 1.0e100;//出错
    

    注解

    • 如果变量的类型至少跟表达式一样宽,则转换就不会有问题
    • 其他情形都是有问题的,比如将一个浮点数赋值给整型数,会丢失小数部分。
    • 如果将一个值超出了变量类型的范围,则将该值赋给一个更窄类型的变量会产生没有意义的结果

    强转符

    强转表达式形式:

    (类型名) 表达式

    • 单目运算符
    • 优先级高于任何一个二目运算符。

    例1 计算一个float数的小数部分

    float f, frac_part;
    
    frac_part = f - (int) f;
    

    例2 强制编译器做转换

    float quotient;
    int dividend, divisor;
    
    quotient = dividend / divisor;//将int转换成float
    
    quotient = (float)dividend / divisor;
    

    例3 使用强转来防止溢出

    long i;
    int j = 1000;
    
    i = (long) j * j;
    

    7.5节 类型定义

    • 使用的关键字是typedef
    • 格式:typedef int Bool;
    • 效果:编译器将该类型加入其可识别的类型名列表中;
    • 优点:提高程序的可读性和可移植性、方便程序修改等;
    • 比较宏macro类型定义type definition

    格式

    typedef int Bool;

    注解:

    • 在类型定义中,类型名是在最后定义的。
    • 使用的是首字母大写的Bool,这是一些C程序员的习惯。

    作用

    使用type def来定义Bool会导致编译器将Bool类型加入其能识别的类型名列表中。

    使用type def来定义Bool后,跟内置类型名一样,Bool可被用在变量声明,类型转换表达式,及其他地方。比如:

    Bool flag;//等价于 int flag
    

    优点

    1. 类型定义可使一个程序更容易理解。
    2. 类型定义可使一个程序修改起来更容易。
    3. 类型定义可提高一个程序的可移植性。

    例1 假设使用变量cash_in和cash_out来存储美元量。
    先声明Dollars为

    typedef float Dollars;
    

    然后声明:

    Dollars cash_in, cash_out;
    

    float cash_in, cash_out;
    

    更有实际意义。

    例2 后面我们要修改Dollars为double,仅需修改类型定义行,声明为Dollar类型变量的地方就不用修改。

    typedef double Dollars;
    

    例3 如果i是一个int变量,则虽然赋值语句

    i = 100000;
    

    在32位机器上运行正常,但在16位的机器上就运行失败(2^15-1=32767)。
    此时,可考虑使用typedef来定义整数类型

    例4 假设我们正在编写一个程序,该程序需要变量来存储产量,其中产量的数值范围为0-5000。
    方法一:使用long来定义该变量;
    方法二:使用int定义该变量;
    方法三:使用typedef来定义产量类型

    typedef int Quantity;
    

    使用Quantity类型来声明变量:

    Quantity q;
    

    当移植到较短整数机器上时,只需修改产量的类型定义即可:

    typedef long Quantity;
    

    例5 C语言库使用typedef来为那些因C语言实现不同而改变的类型来创建类型名,比如_tptrdiff_tsize_twchar_t等,可参考头文件<stdint.h>

    typedef long int ptrdiff_t;
    typedef unsigned long int size_t;
    typedef int wchar_t;
    

    7.6节 sizeof运算符

    作用:计算存储一个类型的值需要多大的内存

    格式

    sizeof(类型名)

    解释:
    该表达式的值是一个无符号整数,表示要存储属于类型名的值需要的字节数。

    sizeof运算符了可用在哪些类型名上?

    • 算术类型
    • 常量
    • 变量
    • 表达式

    • 编译器自身就能计算sizeof表达式的值
    • 对表达式使用sizeof运算符时需要注意括号的使用,比如假如本意是计算sizeof(i+j),如果没有括号,则编译器会将表达式sizeof i + j解释为(sizeof(i)) + j

    打印输出sizeof表达式值时要小心,因为size of表达式的类型是与C语言实现有关size_t类型。建议在输出前,将表达式的值转换成已知类型,比如

    printf("Size of int is %lu\n", (unsigned long) sizeof(int));
    

    相关文章

      网友评论

        本文标题:第7章 基本数据类型

        本文链接:https://www.haomeiwen.com/subject/uvxpchtx.html