zhyDaDa的个人站点

* 对应课程: 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) | -(1218) |

二进制负数

有三种方案:
+ 仿照十进制, 取一个特殊标记表示负数
+ 取中间数为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运算得到了浮点数的结果, 显然类型不一致, 会报错

Avatar photo
我是 zhyDaDa

前端/UI/交互/独立游戏/JPOP/电吉他/游戏配乐/网球/纸牌魔术

发表回复