1.数据类型介绍
前面我们已经学习了基本的内置类型:
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
以及它们所占存储空间的大小依次为:1,2,4,4/8,8,4,8(单位是字节),其中的长整型(long),C语言中只规定了sizeof(long)>sizeof(int),但是具体是4个字节还是8个字节由编译器决定。
那为什么整型又要分为长整型、短整型等呢?
我们说每个类型开辟的内存空间的大小不同,大小决定了使用范围,例如:要定义一个年龄的变量,年龄最大就只能到3位数了,而一个short类型的大小在-32768~32767之间,远远足够了。
1.1类型的基本归类:
整型家族:
char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
因为字符在存储时是ASCIl值,所以把字符型也归类于整型家族。
浮点数家族:
float
double
构造类型:
> 数据类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
构造类型可以自己创建,数组类型也算是构造类型,比如我们构造如下三个数组:
int arr1[10]; int arr2[5]; char arr3[10];
它们的类型都不相同,分别是:int [10]、int [5]、char[10]。
指针类型:
int *pi
char *pc
float *pf
void *pv
空类型:
void表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
2.整型在内存中的存储
我们之前讲过一个变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的。
那接下来我们来谈谈数据在所开辟的内存中到底是如何存储的?
比如:
int a = 20; int b = -10;
创建两个整型变量,内存为它们分别开辟4个字节的空间。
那到底是如何存储的呢?
下面来了解一下:
2.1原码、反码、补码
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”
正数的原、反、补码都相同。
负整数的三种表示方法各不相同:
原码:
直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码:
将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:
反码+1就得到补码。
关于原码、反码、补码,之前的章节中讲过,下面我们再来举两个例子:
int num1 = 10;//创建一个整型变量,在内存中开辟4个字节 //4个字节--32个bit位 //00000000000000000000000000001010 - 原码、反码、补码 int num2 = -10; //负数 //10000000000000000000000000001010 - 原码 //11111111111111111111111111110101 - 反码 //11111111111111111111111111110110 - 补码
对于整型来说,数据存放在内存中其实存放的是补码。
下面我们可以打开监视窗口观察一下:
本质上来说数据在内存中是以二进制的形式存储,但是VS中为了方便展示,显示的是16进制。学过计算机组成原理,大家应该都知道,4位二进制数可以化为1位十六进制数,而十六进制中的10~15用字母a~f表示。
其实我们将补码11111111111111111111111111110110化为16进制数就是:ff ff ff f8.
这时我们就可以发现内存中存储的是补码,但是内存存储的是f8 ff ff ff ,与我们转换出来的刚好是倒着存储的,这又是为什么呢?
后文我们会讲到。
那为什么数据在内存中存放的是补码呢?
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
例如我们要算 1 - (-1)
如果我们用原码计算就会发现,算出来的结果是-2,显然不对。
而用补码计算就能算出正确的值:
//计算 1 -(-1) //原码计算: //00000000000000000000000000000001 1的原码 //10000000000000000000000000000001 -1的原码 //10000000000000000000000000000010 相加结果是-2 //补码计算: //00000000000000000000000000000001 1的补码 //11111111111111111111111111111111 -1的补码 //00000000000000000000000000000000 相加结果是0
接下来,我们就讲一讲为什么会倒着存储。
2.2大小端介绍
什么是大端小端:
大端(存储)模式,是指将数据的低位保存在内存的高位地址中,而数据的高位,保存在内存的低地址中。
小端(存储)模式,是指将数据的高位保存在内存的低位地址中,而数据的高位,保存在内存高地址中。
如下图所示:
注意上图中我们讨论的是字节序存储,即以字节为单位讨论存储顺序。两位十六进制数就是一个字节。
有人对数据的高位和低位不太明白,其实很简单,就像十进制中的123,个位的3就是其低位,百位的1就是其高位,类比一下,上面的十六进制数0x11223344,44就是低位的字节,11就是高位的字节。
那这里有个问题,一个char类型的数据它有存储顺序吗?
答案是没有的,因为char类型的数据只有一个字节,不管怎么存储,顺序都是一样的,它不需要存储顺序。
学了大端小端的概念后,我们来设计一个小程序来判断当前的机器的字节序是大端还是小端。
我们先来分析一下设计思路,在这里我们可以定义一个变量a,令其初始化为1,它的十六进制应该是:0x00 00 00 01,如果是大端字节序存储,存储顺序应该是00 00 00 01,而小端字节序存储,存储顺序应该是01 00 00 00 ,我们只要看它的在内存中第一个字节是不是等于1,如果等于1就是小端字节序存储,如果等于0就是大端字节序存储。
那要判断第一个字节是不是等于1,就要先将其取出来,前面我们学过,可以将&a赋给一个指针,然后解引用指针就可以得到数据,但是因为该指针是int*型,通过解引用,一次取出的是4个字节,我们只需要判断一个字节,这时可以将它强制类型转换为char*,就取出第一个字节了。然后进行解引用并判断,即*(char*)&a == 1
代码实现如下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int check_sys() { int a = 1; int* p = &a; if (*(char*)p == 1) return 1; else return 0; } int main() { int ret = check_sys(); if (ret == 1) printf("小端"); else printf("大端"); return 0; }
当然,我们也可以简单优化一下:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int check_sys() { int a = 1; return *(char*)&a; } int main() { int ret = check_sys(); if (ret == 1) printf("小端"); else printf("大端"); return 0; }
2.3练习
上文讲到整型家族,对于整型家族的类型来说,有:有符号和无符号的区分。
例如:short == signed short(有符号短整型),而无符号短整型就是 unsigned short。
int == signed int (有符号整型),而无符号整型就是 unsigned int。
但是char到底是 signed char 还是 unsigned char 不确定,C语言并没有明确规定,但是通常我们见到的VS编译器上是signed char。
而有符号数和无符号数的区别就是:有符号数的第一位二进制位会被看成符号位,其他的位是数值位。而无符号数没有符号位,全部的二进制位都是数值位。(如下图所示)
以上是char和unsigned char 的取值范围,以此类推,我们也可以知道short的取值范围是:-32768~32767,unsigned short的取值范围是:0~65535。
这就是有符号和无符号的区别,下面我们来做几道练习:
练习1:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char a = -1; signed char b = -1; unsigned char c = -1; printf("a=%d,b=%d,c=%d", a, b, c); return 0; }
运行结果:
为什么会输出这样一个结果呢?
下面我们来分析一下:
a在内存中的补码形式是:11111111111111111111111111111111,由于是char类型所以会发生截断:11111111,而打印时的%d打印的是十进制的有符号整型整数,此时要进行整型提升(有符号数根据符号位补足32bit位),11111111111111111111111111111111,接着将补码化为原码是:10000000000000000000000000000001(即十进制的-1)。
上文讲过,char就是signed char,所以b输出的也是-1。而c是unsigned char型,整型提升时无符号数直接补0,00000000000000000000000011111111,此时输出就是255。
整型提升前面讲过,再来总结一下:
整型提升根据数据本身的类型,有符号型按符号位补足32个bit位,无符号型直接补0
练习2:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char a = -128; printf("%u\n", a); return 0; }
运行结果:
下面我们也来分析一下:
a的补码形式是:11111111111111111111111110000000 。char型发生截断:10000000。因为%u输出的是十进制的无符号整型整数,所以这里要整型提升,11111111111111111111111110000000 此时虽然依然是补码形式,但是我们要打印的是无符号数,这里就可以直接将其当做无符号数的原码,那此时输出的无符号整数就是4394967168。
练习3:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char a = 128; printf("%u\n", a); return 0; }
这次将a由-128换成了128,那么打印出的结果是什么呢?
分析一下会发现,在128的补码发生截断之后,保留的8位和-128截断之后的一样,都是10000000,那么它们最终的结果应该相同,也是4394967168
练习4:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int i = -20; unsigned int j = 10; printf("%d\n", i + j); return 0; }
运行结果:-10
i的补码形式是:11111111111111111111111111101100
j的补码形式是:00000000000000000000000000001010
相加后的补码:11111111111111111111111111110110
化为原码:10000000000000000000000000001010,即十进制的-10
练习5:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%u\n", i); } return 0; }
运行结果:
我们可以发现上述代码打印完1~0之后,开始陷入死循环,为什么会陷入死循环呢?为什么后面打印的数字这么大?
因为i是unsigned int 型,无符号数永远不可能小于0,它一直满足循环条件,所以会一直循环下去。而当循环到 i = -1的时候,-1的补码是11111111111111111111111111111111,它会被当做一个无符号整数看待,32bit的1化为十进制数就是4294967295
练习6:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> int main() { char a[1000]; int i; for (i = 0; i < 1000; i++) { a[i] = -1 - i; } printf("%d", strlen(a)); return 0; }
运行结果:
为什么是这个结果呢?
首先来看循环体,本来循环1000次后应该会将-1 ~ -1000的数字赋给数组a[1000],但是此时的数组是char型的,上文讲过,char的范围是-128~127,所以不可能将所有的数都赋给数组,-1截断后应该是11111111,第一次循环a[1]的值是11111110,而每次循环相当于在上次的基础上减一操作,直到a[127] = -128时,它的补码是10000000,再次减一后会变成01111111,即127,接着循环,126 125 124......直到a[255] = 0,减一之后又变成 -1,接着-2 -3 -4......-127 -128 127 126......1 0
而strlen是计算 \0 之前的字符串的长度,\0 和 0 的ASCII值都是0,所以在上述代码中计算的只是循环第一次到0之前的长度。
下图给出char类型数值的轮回过程,以及上述代码的解析图。
练习7:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> unsigned char i = 0; int main() { for (i = 0; i <= 255; i++) { printf("hello world\n"); } return 0; }
运行结果应该是hello world 死循环打印。
原因也很简单,上文我们说过unsigned char 的取值范围是0~255,恒满足循环条件,所以陷入了死循环。
今天就学到这里,未完待续。。。