* 对应课程: 6.1.1\~ 7
目录
- 基础数据类型
- 不同之处:
- sizeof
- 整数类型
- 整数的内部表达
- 数的范围
- 整数的格式化
- 选择整数类型
- 浮点类型
- 浮点的输入输出
- 浮点的范围和精度
- 浮点数的内部表达
- 选择浮点数类型
- 逻辑类型
- 逻辑运算
- 类型转换
- 自动类型转换
- 强制类型转换
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, 如10000000
为0
, 比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
, 在其后面加上u
或U
+ 例如 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
整数的格式化
整数的输入输出
只有两种形式: int
或 long 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
为例子
+ 最前面可选的+
或-
符号
+ 小数点.
也是可选的
+ 可以用e
或E
+ 符号可以是+
或-
也可以省略(表示+
)
+ 整个词不能有空格
输出精度
在%
和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 需要用f
或F
后缀来表明身份
+ 在浮点计算中, f1==f2
这类的关系判断可能失败
+ 如果迫不得已, 可以通过fabs(f1-f2)<1e-12
来代替
+ 其实一般1e-10
就可以了
+ 不能用浮点数来计算金额, 因为其误差会累积起来
+ 只能在一定的范围内相信浮点数的计算结果, 其误差很大
浮点数的内部表达
浮点数在内存中以编码形式储存
+ 1bit 用与判断正负号
+ 11bit 用于记录指数部分
+ 52bit 用于记录分数部分
事实上实际用不了这么多位
浮点数在计算时是由专门的硬件部件实现的
计算 double和float要用的硬件部件是一样的
选择浮点数类型
如果没有特殊需要, 只使用 double
现代CPU能够直接对double做硬件运算
计算速度和存储速度都不比float慢
逻辑类型
C语言原先是没有布尔类型的, 用0
和非0
就可以判断
后来引入了布尔类型, 由于其不是原生类型, 因此使用前要先加上#include <stdbool.h>
之后才能使用bool和true/false
但事实上, 在输入输出时, 布尔类型仍旧当做整数使用, 无法输出为true/false
逻辑运算
| 运算符 | 描述 |
| :—-: | :—-: |
| !
| 逻辑非 |
| &&
| 逻辑与 |
| \|\|
| 逻辑或 |
数学中的
4<x<6
在程序中, 应当表示为x>4&&x<6
考虑这样一个例子:
!age < 20
由于逻辑非优先级最高,
!age
被结合在一起, 其结果仅可能是0
或1
, 那么整个表达式的结果永远为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
运算得到了浮点数的结果, 显然类型不一致, 会报错