L5_数据类型___2022-10-28

* 对应课程: 6.1.1~ 7
目录


C是有类型的语言, 对于其变量, 必须:

  • 在使用前定义
  • 确定类型

早期语言 以及 面向底层的语言 更强调类型

  • 强类型: 有助于发现简单错误
  • 弱类型: 看重事务逻辑

C语言需要类型, 但是对类型的安全检查并不足够

基础数据类型

大致分为四个大类(逻辑和整数看做一类)

类别 包括
整数 char short int long long long
浮点数 float double long double
逻辑 bool
指针
自定义

前四种被称为 C语言的基础类型 (即本身就有的)

不同之处:

  • 名称: int long double
  • 输入输出时的格式化: %d %ld %lf
  • 表达的范围: char<short<int<float<double
  • 内存中占据的大小: 1~16字节
  • 内存中的表现形式: 整形是二进制(补码) 浮点数是编码形式(不能直接计算)

sizeof

sizeof是一个运算符, 给出某个类型或者变量在内存中占据的 字节数

  • sizeof(int)
  • sizeof(i)

注意: sizeof静态的, ta的括号内不会产生实际运算, 其结果在编译时已经确定下来

#include <stdio.h>
int main()
{
    int a = 6;
    printf("sizeof(a)=%ld\n", sizeof(a));
    printf("sizeof(a+1.0)=%ld\n", sizeof(a + 1.0));
    printf("a=%d", a);
    return 0;
}

其输出结果如下

sizeof(a)=4
sizeof(a+1.0)=8
a=6

整数类型

大小比较

类型 大小
char 1字节(8比特)
short 2字节
int 取决于编译器(CPU), 通常的意义是"1个字"
long 取决于编译器(CPU), 通常的意义是"1个字"
long long 8字节

1Byte = 8bit

上图中 字长 是说在寄存器和总线中, 一份数据的大小

整数的内部表达

计算机内部一切都是二进制

  • 18 –> 00010010
  • 0 –> 00000000
  • -18 –> ?

一切数据类型的意义在于我们怎么看待ta

负数的表达

计算机在处理负数时, 会把-视作为一种运算特殊处理, 并不看做负数

表达式 实际运算
12+(-18) 12-18
12-(-18) 12+18
12*(-18) -(12*18)

二进制负数

有三种方案:

  • 仿照十进制, 取一个特殊标记表示负数
  • 取中间数为0, 如100000000, 比ta小就是负数
  • 补码

第一种方案在计算时不能按照常规二进制计算, 要求特殊处理, 太复杂
二方案每次在获取数据的时候总要减去10000000, 也很复杂

补码

考虑-1, 希望能实现-1 + 1 = 0

  • 0 –>00000000
  • 1 –>00000001
  • -1–>11111111

这里-1 + 1的结果实际上是1 00000000
但由于表示数字的字节仅仅8bit, 所以多出来的那位被舍去, 成了0

11111111被当做纯二进制看待时, 是255
但被当做补码看待时, 是-1

同理, 对于-a, 补码就是0-a
实际上是 $2^n-a$, 其中n是这种类型的位数

补码的意义就是拿补码和原码可以加出一个溢出的
回到之前的三种方案, 补码的优势在于不用变化符号, 直接做加法就可以得出结果

数的范围

对于一个字节(8位 即一个char), 可以表达的是:

  • 00000000-11111111

其中:

内存表示 纯二进制角度 整数角度
00000000 0 0
11111111 ~ 10000000 255 ~ 128 -1 ~ -128
00000001 ~ 01111111 1 ~ 127 1 ~ 127

注意: 这里所说的"整数角度"是针对char来说的
不同类型有不同的角度, 下面就是个例子

#include <stdio.h>
int main()
{
    char c = 255;
    int i = 255;
    printf("c=%d; i=%d", c, i);
    return 0;
}

其结果为:

c=-1; i=255

分析如下:

  • 对于char来说,
    • 大小是 8bit
    • 在内存中表现为 11111111
    • 解读为补码(因为首位为1)
    • 输出为-1
  • 对于int来说,
    • 大小是 4Byte
    • 在内存中表现为 00000000 00000000 00000000 11111111
    • 解读为正整数
    • 输出为255

关键字 unsigned

在定义变量的时候在类型前面添上 关键字unsigned

#include <stdio.h>
int main()
{
    unsigned char c = 255;
    printf("c=%d", c);
    return 0;
}

运行结果:
c=255

如果一个字面量常数要想表达自己是unsigned, 在其后面加上uU

  • 例如 255U

unsigned的含义是说: 该数被视作一个正数, 不会被视作补码 其副作用就是将一个整数能表示的正数部分扩大两倍
但同样地, ta就无法表示负数

但是, unsigned关键字设计的初衷并不是为了扩大正数部分
而是为了做纯二进制计算, 主要是为了移位

整数越界

如果不断的做+1运算, 到了范围的边界时
考虑到之前的理论, 正整数将"绕一圈"变为负数
用程序来说明:

#include <stdio.h>
int main()
{
    char c = 128;
    c = c + 1;
    printf("c+1=%d\n", c);
    unsigned char u = 0;
    u = u - 1;
    printf("u-1=%d", u);
    return 0;
}

其结果是:

c+1=-127
u-1=255

整数的格式化

整数的输入输出

只有两种形式: intlong long

格式 类型
%d int char short
%u unsigned
%ld long long
%lu unsigned long long

可以通过以下代码理解 “数据类型重在如何看待ta”

#include <stdio.h>
int main()
{
    char c = -1;
    int i = -1;
    printf("c=%u, i=%u\n", c, i);
    return 0;
}

其结果为:

c=4294967295, i=4294967295

printf在接收数据时, 会把
小于等于int大小的整数类型转化为int传入
大于int大小的整数类型转化为long传入

在计算机内存的数据是同样的, 但以不同的方式看待就会有不同的结果
这和计算机内部数据是什么无关, 而取决于是否以正确的方式来使用数据, 使之成为人能读懂的表示

8进制和16进制

字面量整数前加0表示 八进制
0x表示 十六进制

#include <stdio.h>
int main()
{
    char c = 012;
    int i = 0x12;
    printf("c=%d, i=%d\n", c, i);
    return 0;
}

其结果为:

c=10, i=18

同样的, 这里的进制只是我们的视角
编译器仍旧会换算为二进制

进制只是表示如何把数字表达为字符串
这与内部如何表达数字无关

要想输入输出:

  • 八进制的格式为 %o
  • 十六进制的格式为 %x

注意: 在十六进制中,
%x会输出带有小写字母的十六进制数字
%X会输出带有大写字母的十六进制数字

16进制很适合表达2进制数据
因为4位二进制刚好是一个16进制位
8进制的一位数字正好表达3位二进制

因此早期计算机的字长是12的倍数, 而非8

选择整数类型

为什么那么多类型?

  • 为了直接和硬件打交道(16位的类型控制芯片上的16个引脚)
  • 早期语言的风格

建议: 没有特殊需要, 就选int

  • 如今计算机CPU的字长普遍是32/64bit, 一次内存的读写正好是一个int的大小, 一次计算也是一个int
  • 选择更短的类型不会更快, 甚至可能更慢
  • 考虑到现代编译器一般会设计 内存对齐, 所以更短的类型在内存中可能实际占据的也是一个int的大小
    (即便 sizeof 告诉你更小)

unsigned与否只影响输出的结果, 内部计算是一样的

浮点类型

类型 字长 范围 有效数字
float 32 $\pm(1.20\times10^{-38} \sim 3.4\times10^{38})$
以及$0,\pm \inf,nan$
7
double 64 $\pm(2.2\times10^{-308} \sim 1.79\times10^{308})$
以及$0,\pm \inf,nan$
15

浮点的输入输出

类型 scanf printf
float %f %f, %e
double %lf %f, %e

其中%e是用科学计数法输出

#include <stdio.h>
int main()
{
    double ff = 1234.56789;
    printf("%e, %f, %E", ff, ff, ff);
    return 0;
}

其输出结果为:

1.234568e+003, 1234.567890, 1.234568E+003

科学计数法

-5.67E+16 为例子

  • 最前面可选的+-符号
  • 小数点.也是可选的
  • 可以用eE
  • 符号可以是+-也可以省略(表示+)
  • 整个词不能有空格

输出精度

%f之间加上.n可以指定输出小数点后的n位(做四舍五入)

#include <stdio.h>
int main()
{
    printf("%.3f\n", -0.0049);
    printf("%.30f\n", -0.0049);
    printf("%.3f\n", -0.00049);
    return 0;
}

其结果为:

-0.005
-0.004899999999999999800000000000
-0.000

浮点的范围和精度

超过范围的浮点数

  • inf表示无穷大(即超出范围的浮点数)
  • nan表示不存在的浮点数
#include <stdio.h>
int main()
{
    printf("%f\n", 12.0 / 0.0);
    printf("%f\n", -12.0 / 0.0);
    printf("%f\n", 0.0 / 0.0);
    return 0;
}

其输出结果为:

inf
-inf
nan

也可以是:

1.#INF00
-1.#INF00
-1.#IND00

考虑到现在讨论的范畴是浮点数
如果用整数输出12/0会发生编译错误
这是因为nan/±inf为浮点数独有

浮点运算的精度

注意: 浮点运算是没有精度

#include <stdio.h>
void main()
{
    float a, b, c;
    a = 1.345f;
    b = 1.123f;
    c = a + b;
    if (c == 2.468)
        printf("相等\n");
    else
        printf("不相等! c=%.10f, 或%f\n", c, c);
}

其输出结果为:

不相等! c=2.4679999352, 或2.468000

输出的结果中, 前者才是c真正的数值, 后者已经由于浮点数的7位有效数字做了四舍五入处理

注意:

  • 带小数点的字面量是double 而非 float
  • float 需要用fF后缀来表明身份
  • 浮点计算中, f1==f2这类的关系判断可能失败
  • 如果迫不得已, 可以通过fabs(f1-f2)<1e-12来代替
  • 其实一般1e-10就可以了
  • 不能用浮点数来计算金额, 因为其误差会累积起来
  • 只能在一定的范围内相信浮点数的计算结果, 其误差很大

浮点数的内部表达

浮点数在内存中以编码形式储存

  • 1bit 用与判断正负号
  • 11bit 用于记录指数部分
  • 52bit 用于记录分数部分

事实上实际用不了这么多位

浮点数在计算时是由专门的硬件部件实现的
计算 doublefloat要用的硬件部件是一样的

选择浮点数类型

如果没有特殊需要, 只使用 double

现代CPU能够直接对double做硬件运算
计算速度和存储速度都不比float

逻辑类型

C语言原先是没有布尔类型的, 用0非0就可以判断
后来引入了布尔类型, 由于其不是原生类型, 因此使用前要先加上#include <stdbool.h>
之后才能使用booltrue/false
但事实上, 在输入输出时, 布尔类型仍旧当做整数使用, 无法输出为true/false

逻辑运算

运算符 描述
! 逻辑非
&& 逻辑与
|| 逻辑或

数学中的4<x<6在程序中, 应当表示为x>4&&x<6

考虑这样一个例子:
!age < 20

由于逻辑非优先级最高, !age被结合在一起, 其结果仅可能是01, 那么整个表达式的结果永远为1

优先级

优先级 运算符
1 ()
2 ! ++ --和单目的+ -
3 * / %
4 + -
5 < <= > >=
6 == !=
7 &&
8 ||
9 a?b:c
10 所有的赋值运算符

短路

逻辑运算是自左向右进行的, 若左侧的结果已经可以决定结果, 就不会执行右侧的计算

  • 对于&&, 左侧有false不做右侧
  • 对于||, 左侧有true不做右侧

条件运算符

ans = a ? b : c等价于

if(a){
  ans = b;
}else{
  ans = c;
}

这种写法看似简洁, 但当嵌套起来就会出现麻烦
注意: 条件运算符是自右向左结合的
w<x ? x+w : x<y ? x:y

尽量不要使用嵌套的条件表达式!!!

逗号运算

逗号是一个运算符, 用来连接两个表达式, 并以右侧表达式的值作为其结果
逗号的优先级是所有运算符中最低的, 所以其两侧的表达式会先计算
逗号的组合关系是自左向右, 左侧先算, 右侧后算, 取右侧的结果作为整体的结果
主要用途是for
for(i=0,j=10; i<j; i++,j--)

类型转换

当运算符两边出现不一致的类型时, 会自动转换成较宽/大的类型
宽/大是指能表达的数的范围更大

自动类型转换

整数: char → short → int → long → long long
浮点: int → float → double

对于printf, 任何小于int的类型都会被转化成int
float会被转化成double
但是, scanf不会

  • 要输入short, 需要使用%hd
  • 要输入long, 需要使用%ld
  • 若要以整数的形式输入char, 必须先得到整数再交给char

强制类型转换

要把一个量强制转换成另一个类型(通常是较小的类型), 需要采用(类型)值的写法, 例如:

  • (int)10.2
  • (short)32

注意:

  • 要留意安全性, 即大的数不一定能转换为小的数
    或者说, 小的变量不总能表达大的量
  • 这种转换只是计算出了一个新的量, 并不会改变原来的量的值或者类型
  • 强制类型转换的优先级高于四则运算
double a = 1.0;
double b = 2.0;
int i = (int)b / a;

上述的例子中, 先处理(int)b得到2, 再与浮点数a运算得到了浮点数的结果, 显然类型不一致, 会报错