【C】C系列二 - 数据类型

C 语言的数据类型有很多,可以分为 4 大类型:

  • void 类型
  • 基本类型
    • 字符类型
    • 有符号整数类型
    • 无符号整数类型
    • 浮点类型
  • 枚举类型
  • 派生类型
    • 数组类型
    • 结构体类型
    • 联合体类型(也叫共同体类型)
    • 函数类型
    • 指针类型

一、基本类型

  • 有符号整数类型

    • char(等价类型:signed char
    • short(等价类型:signed shortshort intsigned short int
    • int(等价类型:signed intsigned
    • long(等价类型:signed longlong intsigned long int)(C99 起)
    • long long(等价类型:signed long longlong long intsigned long long int`)(C99 起)
  • 无符号整数类型

    • unsigned char
    • unsigned short(等价类型:unsigned short int
    • unsigned int(等价类型:unsigned
    • unsigned long(等价类型:unsigned long int)(C99 起)
    • unsigned long long(等价类型:unsigned long long int)(C99 起)
  • 字符类型

    • char
  • 浮点类型

    • float
    • double
    • long double

1.1. 整数类型

1.1.1. 整数类型大小

C 标准规定

1
2
3
4
5
1 == sizeof(char)
<= sizeof(short)
<= sizeof(int)
<= sizeof(long)
<= sizeof(long long)

1.1.2. 数据模型

关于基本类型大小的是实现方案,统称为:数据模型。有 4 种常见的数据模型

  • 32bit 系统(指针为 32 位)

    • LP32
      • Win16 API
    • ILP32
      • Win32 API
      • Unix、类 Unix 系统(Linux、Mac OS X)
  • 64bit 系统(指针为 64 位)

    • LLP64
      • Win64 API
    • LP64
      • Unix、类 Unix 系统(Linux、Mac OS X)
  • 比较少见的数据模型

    • ILP64:int、long、指针均为 64 位
      • 仅出现在早期 64 位 Unix 系统(例如 Unicos on Cray)

1.1.3. 有符号整数类型和无符号整数类型区别

整数类型可分为:有符号整数类型、无符号整数类型。

1.1.4. 有符号整数 char、无符号整数 unsigned char

1
2
3
4
// 0xBD <=> 0b10111101
char c1 = 0xBD;
unsigned char c2 = 0xBD;
printf("%d %d", c1, c2); // 输出:-67 189

1
2
3
4
// 0x43 <=> 0b01000011
char c1 = 0x43;
unsigned char c2 = 0x43;
printf("%d %d", c1, c2); // 输出:67 67

  • c1、c2 变量在内存中存放的二进制数据是完全一样的;
  • 对于同一份二进制数据,分别以有符号数形式、无符号数形式解读出来的含义可能是不一样的。

1.1.5. 整数的取值范围

charunsigned char 都只占用一个字节,能够存放的二进制数据范围都是[0b0000 0000, 0b1111 1111]

  • char的取值范围:$[-128, 127] = [-2^7, 2^7-1]$
  • unsigned char的取值范围:$[0, 255] = [0, 2^8-1]$
  • 有 n 个二进制位的有符号数的取值范围$[-2^{n-1}, 2^{n-1}-1]$
  • 有 n 个二进制位的无符号数的取值范围$[0, 2^n-1]$
位数 符号 最小值 最大值
8 有符号 $$-2^7(128)$$ $$2^7-1(127)$$
8 无符号 0 $$2^8-1(255)$$
16 有符号 $$-2^{15}(-32768)$$ $$2^{15}-1(32767)$$
16 无符号 0 $$2^{16}-1(65535)$$
32 有符号 $$-2^{31}(-2147483648)$$ $$2^{31}-1(2147483647)$$
32 无符号 0 $$2^{32}-1(4294967295)$$
64 有符号 $$-2^{63}(-922337203685477808)$$ $$2^{63}-1(922337203685477807)$$
64 无符号 0 $$2^{64}-1(18446744073709551615)$$

1.1.6. 溢出(Overflow)

溢出指的是内容超过了已知容器的容量。

例:short s;

  • short类型的变量 s 有 2 个字节的内存空间
  • 如果要将超过 2 个字节(比如 4 个字节)的数据存储到变量 s 中去,就会产生内存溢出

案例一:

1
2
3
4
5
6
int i = 16909292;
short s = 16909292;
char c = i;
printf("%d\n", i); // 输出:16909292
printf("%d\n", s); // 输出:1004
printf("%d\n", c); // 输出:-20

  • 当出现溢出时,会优先保留低字节的数据,舍弃高字节的数据;
  • 所以在给取值范围小的变量赋值时要注意防止数据溢出,否则结果可能会跟预期不符合。

思考:那是不是以后都统一使用取值范围比较大的变量就好了?
应该根据已知的数据范围选择合适大小的变量,不然会造成内存空间的浪费

案例二:

1
2
3
4
5
int i = 16909292;
char c = i;
i = c;
printf("%d\n", i); // 输出:-20
printf("%d\n", c); // 输出:-20

案例三:

1
2
unsigned char c1 = 255;
char c2 = -1;

1
2
3
c1 = c1 + 1;
c2 = c2 + 1;
printf("c1=%d, c2=%d", c1, c2); // 输出:c1=0, c2=0

案例四:

1
2
unsigned char c1 = 0;
char c2 = 0;

1
2
3
c1 = c1 - 1;
c2 = c2 - 1;
printf("c1=%d, c2=%d", c1, c2); // 输出:c1=255, c2=-1

案例五:

1
2
unsigned char c1 = 127;
char c2 = 127;

1
2
3
c1 = c1 + 1;
c2 = c2 + 1;
printf("c1=%d, c2=%d", c1, c2); // 输出:c1=128, c2=-128

案例六:

1
2
unsigned char c1 = 128;
char c2 = -128;

1
2
3
c1 = c1 - 1;
c2 = c2 - 1;
printf("c1=%d, c2=%d", c1, c2); // 输出:c1=127, c2=127

案例七:

1
2
unsigned char c1 = 255;
char c2 = -1;

1
2
3
c1 = c1 + 1;
c2 = c2 + 1;
printf("c1=%d, c2=%d", c1, c2); // 输出:c1=0, c2=0

案例八:

1
2
unsigned char c1 = 0;
char c2 = 0;

1
2
3
c1 = c1 - 1;
c2 = c2 - 1;
printf("c1=%d, c2=%d", c1, c2); // 输出:c1=255, c2=-1

1.1.7. unsigned char、char 的取值范围

1.2. 浮点类型

浮点类型可以用来表示小数(比如 3.14),包括float,double,long double类型

最常用的浮点类型是floatdouble,一般在数值后面加上 f 或者 F 表示十 float 类型的数值

1
2
3
4
float f = 3.14F;
double d = 10.24;
printf("%f %f", f, d); // 输出:3.140000 10.240000

  • float:单精度(Single)浮点类型,占用 32bit,可以保证精确到小数点后 6 位
    • 最小值:$$-3.4*10^{38}$$
    • 最大值:$$3.4*10^{38}$$
  • double:双精度(Double)浮点类型,占用 64bit,可以保证精确到小数点后 15 位
    • 最小值:$$-1.8*10^{308}$$
    • 最大值:$$1.8*10^{308}$$

1.2.1. 浮点类型的存储细节

思考:为什么 32bit 的unsigned int最大值是 2^32-1,而 32bit 的float最大值是 3.4 * 10^38?
因为他们的存储方式不一样,浮点数在计算机中是按照 IEEE 754 标准存储的。


1.3. 整数、浮点数字面量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
char c = 10;
unsigned char uc = 10;
short s = 10;
unsigned short us = 10;

int i = 10;
unsigned int ui1 = 10u;
unsigned int ui2 = 10U;

long l1 = 10l;
long l2 = 10L;
unsigned long ul1 = 10ul;
unsigned long ul2 = 10UL;

// 不能是lL或Ll
long long ll1 = 10ll;
long long ll2 = 10ll;

float f1 = 10.24F;
double d1 = 10.24;

// 小数部分可选:1.0
float f2 = 1.F;
double d2 = 1.;

// 整数部分可选:0.1
float f3 = .1F;
double d3 = .1;

// 十进制小数 1.2 * 10^3 = 1200.0
float f4 = 1.2e3F; // 1.2E3F
double d4 = 1.2e3; // 1.2E3

// 十六进制小数 1.125 * 2^10 = 1152.0
float f5 = 0x1.2p10F; // 0x1.2P10F
double d5 = 0x1.2p10; // 0x1.2P10

// 十进制 1.875 * 2^3 = 15.0
printf("%f", 0x1.ep+3); // 可写:0x1.EP+3 输出:15.000000

// 十进制 1.875 * 2^(-2) = 0.46875
printf("%f", 0x1.ep-2); // 可写:0x1.EP-2 输出:0.468750

int v 0xE+2; // 错误
int x = 0xe+2; // 错误
int y = 0xA+2; // OK
int z = 0xE +2; // OK
int q = (0xE)+2 // OK

1.4. printf 中的转换格式指定符

1.4.1. 案例一

1
2
3
4
5
6
7
8
9
10
11
printf("%zd\n", sizeof(int)); // 输出:4

long long age = 10LL;
printf("%lld\n", age); // 输出:10

unsigned long no = 8UL;
printf("%lu\n", no); // 输出:8

char *name = "idbeny";
char *place = "1024星球";
printf("我是%s,欢迎来到%s\n", name, place); // 输出:我是idbeny,欢迎来到1024星球

1.4.2. 案例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 用%%来显示一个%
printf("%d%%%d\n", 10, 20); // 输出:10%20

int i1 = 10;
// %6d表示占用6个字符位置,默认靠右对齐(左边空白)
printf("welcome%6d24星球\n", i1); // 输出:welcome 1024星球
// %-6d表示占用6个字符位置,靠左对齐(右边空白)
printf("welcome%-6d24星球\n", i1); // 输出:welcome10 24星球

int i2 = -6;
// %+6d:加号(+)表示显示正负号
printf("%+6d和%+-6d\n", i1, i2); // 输出: +10和-6

double d = 3.1415926;
// 四舍五入保留2位小数
printf("%.2f\n", d); // 输出:3.14

/*
* 占用10个字符位置
* 四舍五入保留4位小数
* 显示正负号
* 靠左对齐
*/
printf("1%+-10.4f2\n", d); // 输出:1+3.1416 2

1.5. scanf 中的转换格式指定符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 用%%来匹配一个%
int age = 10;
scanf("%%%d", &age); // 输入:%30
printf("age is %d\n", age); // 输出:age is 30
/*
输入:30
输出:age is 10

输入:%30
输出:age is 30
*/

// 在scanf中,float类型用%f
float f;
scanf("%f", &f);

// 在scanf中,double类型用%lf
double d;
scanf("%lf", &d);

// 在printf中,float、double都可以用%f、%lf
printf("%f %lf %f %lf", f, f, d, d);
/*
输入:10(回车)20
输出:10.000000 10.000000 20.000000 20.000000
*/

int i1 = 1;
int i2 = 2;
// %*3d是跳过长度为3的整数
scanf("%2d%*3d%4d", &i1, &i2);
printf("i1=%d,i2=%d", i1, i2);
/*
输入:10 20 40
输出:i1=10,i2=40

输入:10111140
输出:i1=10,i2=140
*/

1.6. 类型转换

格式:

1
2
类型1 v1 = xx;
类型2 v2 = (类型1)v1;

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char c = 'A';
int i = (int)c;
printf("%d\n", i); // 输出:65

int i2 = 200;
short s = (short)i2;
printf("%d\n", s); // 输出:200

// 会丢失精度
double d = 3.14;
int i3 = (int)d;
printf("%d\n", i3); // 输出:3

int i4 = 333;
double d2 = (double)i4;
printf("%f\n", d2); // 输出:333.000000

其实在很多时候,编译器都会进行隐式类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char c = 'A';
int i = c;
printf("%d\n", i); // 输出:65

int i2 = 200L;
short s = i2;
printf("%d\n", s); // 输出:200

double d = 3.14;
int i3 = d;
printf("%d\n", i3); // 输出:3

int i4 = 333;
double d2 = i4;
printf("%f\n", d2); // 输出:333.000000

大类型转小类型的时候,可能会丢失精度:

1
2
3
4
5
6
7
8
9
10
11
double d = 3.14;
printf("%f\n", d); // 输出:3.140000

int i = d;
printf("%d\n", i); // 输出:3

double d2 = 1.1234567890123456789123;
printf("%.20f\n", d2); // 输出:1.12345678901234569125

float f = d2;
printf("%.20f\n", f); // 输出:1.12345683574676513672