0

C语言进阶—数据的存储

 9 months ago
source link: https://blog.51cto.com/u_16129148/6868040
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1.数据类型的介绍

内置类型(C语言本身就具有的):

char      //字符型                 1byte
short     //短整型                 2byte
int       //整型                   4byte
long      //长整型                 4/8byte
long long //更长的整型  C99引入的    8byte
float     //单精度浮点型             4byte
double    //双精度浮点型             8byte

注意:sizeof(long)>=sizeof(int)   4(32位/8(64位)>=4

类型的意义

  1. 类型决定了使用这个类型开辟(申请)内存空间的大小(大小决定了使用范围),变量的创建是要在内存中申请空间的,空间的大小是由类型决定的
  2. 如何看待内存空间的视角
int a;
float f;
//都申请4byte的空间,空间大小一样
//站在int变量a角度 它认为存储的是整型数据
//站在float变量f角度 它认为存储的是浮点型数据
//类型不同,视角不同

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] 
long long  //C99引入
    unsigned long long [int]
    signed long long [int]

为什么char也是整型家族?字符在内存中存的是ASCII码值,也就是说字符的本质是ASCII码值,ASCII码值也是整型,所以划分到整型家族。

char非常特殊,有三种类型:

char
signed char
unsigned char
//char到底是signed char还是unsigned char,C语言标准是未定义的
//取决于编译器的实现
signed int   有符号int
unsigned int 无符号int
eg:
int a;<===>signed int a;
//int 就是signed int

同理short/long/long long<===>signed short/long/long long

有符号和无符号的区别

生活中有些数据是没有负数的:身高 体重 长度……  

unsigned int high;
//32位中每一位都是有效位,没有符号位概念
//10000000 00000000 00000000 00001010  2^31+2^3+2^2
//最高位有数值意义的

生活中有些数据既有正数又有负数的:温度……   

int a = 10;
//a是一个有符号整型,一个整型4byte(32bit)
//00000000 00000000 00000000 00001010  +10
//10000000 00000000 00000000 00001010  -10
//最高位是符号位 0表示正数 1表示负数 最高位不是有效位

浮点型家族:

float
//float精度低,存储的数值范围较小 
double
//double精度高,存储的数据范围更大

构造类型: (自定义类型:自己创建出的新的类型)

数组类型 
结构体类型 struct
枚举类型 enum
联合类型 union
int arr1[5];//类型:int [5]
int arr2[8];//类型:int [8]
//两种不同类型的数组 
//去掉数组名剩下部分就是类型
char arr3[5];//类型:char [5]
//只要数组元素类型和元素个数不同 就是不同类型的数组
//根据不同需求可以创建不同类型数组

指针类型:

int* pi;
char* pc;
float* pf;
void* pv;

空类型:

void表示空类型(无类型) ;通常用于函数的返回类型,函数参数,指针类型。

void test(void)
//第一个void 表示函数不会(无)返回值
//第二个void 表示函数不需要传任何参数
void test(void):写了void明确不能传参,传了会报warning
void test():没有写void,可以传参也可以不传参

2.整型在内存中的存储

例题:20/-10如何存储?

//数值有不同表示形式 二进制 八进制 十进制 十六进制
//十进制的21:0b10101 025(8进制0开头) 21 0x15 (计算方法:每位权重 按权展开相加)
//整数的二进制表示有三种表示形式:
//正整数:原码 反码 补码 相同  
//负整数:原码 反码 补码通过计算得到
//原码:直接通过正负形式写出的二进制序列就是原码
//反码:原码符号位不变其他位按位取反
//补码:反码+1
//整数在内存中存放的是补码的二进制序列

#include<stdio.h>
int main()
{
	int a = 20;
	//00000000 00000000 00000000 00010100 原码 反码 补码
	//0x00 00 00 14 (十六进制)
	int b = -10;
	//10000000 00000000 00000000 00001010 原码
	// 0x80 00 00 0a
	//11111111 11111111 11111111 11110101 反码
	// 0xff ff ff f5
	//11111111 11111111 11111111 11110110 补码
	//0xff ff ff f6
	return 0;
}
//内存上本质上存放的是二进制,显示的是16进制
//说明整型在内存中存放的是补码

输入&a查看a变量内容:看不出来是补码还是原码或反码

C语言进阶—数据的存储_大小端字节序存储

输入&b,查看b的内容,发现是补码,说明整型在内存中存储的是补码的二进制序列

C语言进阶—数据的存储_整型家族_02

2.1 原码、反码、补码

计算机中整数(二进制)有三种表示形式:原码、反码、补码

三种表示形式均有符号位和数值位(有效位)两部分,符号位(最高位)都是用0表示“正”,用1表示"负“,而数值位:正数的原码 反码 补码相同 负数的三种表示方法各不相同,通过计算得到。

整数在内存中存放的是补码,为什么?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

1-1//CPU只有加法器===>1+(-1)
用原码计算:
00000000 00000000 00000000 00000001
+
10000000 00000000 00000000 00000001
————————————————————————————————————
10000000 00000000 00000000 00000010===>-2 结果不对
用补码计算:
 00000000 00000000 00000000 00000001
+
 11111111 11111111 11111111 11111111
————————————————————————————————————
100000000 00000000 00000000 00000000
整型运算最多有32位,最高位1丢失。
 00000000 00000000 00000000 00000000===>0 计算结果正确
 //使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理
原码------>补码  取反+1
补码------>原码  方法1:-1取反  方法2:取反+1
eg -10
11111111 11111111 11111111 11110110 补码
10000000 00000000 00000000 00001001 取反
10000000 00000000 00000000 00001010 +1 ===>原码
原码和反码相互转换的运算过程(计算逻辑)可以是一样的(都可以通过取反+1)
//补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路

为什么整型在内存中存放的补码是倒着存放的?

C语言进阶—数据的存储_原码反码补码_03

C语言进阶—数据的存储_大小端字节序存储_04

======>大小端

2.2 大小端介绍

什么是大端小端?

例:0x11223344

C语言进阶—数据的存储_整型存储规则_05

大端字节序存储:把一个数据的高位字节序的内容放在低地址处,把低位字节序的内容存放在高地址处,就是大端字节序存储。

小端字节序存储:把一个数据的高位字节序的内容放在高地址处,把低位字节序的内容存放在低地址处,就是小端字节序存储。

  1. 大小端字节序描述的是一个数据在内存中存储时以字节为单位来讨论以什么顺序进行存储。
  2. 内存中存放还是二进制,表示可以用16进制更方便。
  3. 字节序:以字节为单位来讨论顺序

为什么会有大小端模式之分呢?

这是因为在计算机系统中,是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如:一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即Ox0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的x86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

例题:当前编译器采用哪种存储方式存储0x00000014

C语言进阶—数据的存储_浮点型存储规则_06

小端字节序存储

  1. 浮点型也会有大小端字节序的存储
  2. 对于一个字节,没有大小端顺序可谈 eg:char;两个字节以上的数据存储,就有顺序,即就有大小端字节序的存储 eg:short int float 
  3. 大小端存储方式取决于硬件,与编译器无关。

例题:简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序

a=1  0x00 00 00 01

小端存储:低地址         01 00 00 00           高地址

大端存储:低地址         00 00 00 01           高地址

利用两种存储方式下首字节的差异来判断采用哪一种存储方式,&a首字节地址,我们只需要访问第一个字节即可,强制类型转换为char*(&a) ,再进行*char*(&a)就可以取出第一个字节的内容,为1就是小端存储,为0就是大端存储

#include <stdio.h>
int main()//方法一
{
	int  a = 1;
	if (*(char*)&a == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}
//方法二:函数
#include <stdio.h>

int check_sys()
{
	int a = 1;
	return *(char*)&a;
}

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

2.3 练习

1.下面程序输出?

#include <stdio.h>
int main()
{
	char a = -1;         //在VS中相当于signed char 范围:-128~127     
	signed char b = -1;  //范围:-128~127
	unsigned char c = -1;//范围:0~255
	printf("%d %d %d\n", a, b, c);//-1 -1 255
	return 0;
}
分析: -1 整数4byte(32bit) 
原码:10000000 00000000 00000000 00000001
反码:11111111 11111111 11111111 11111110
补码:11111111 11111111 11111111 11111111
补码放入字符型变量a中发生截断:只留下低8位11111111其他位丢失 
打印时%d(有符号整型):进行整型提升,char有符号类型,最高位看作符号位,高位补符号位
整型提升后:11111111 11111111 11111111 11111111 内存中的补码打印需要原码
%d认为是有符号整数,符号位1,负数,计算得到原码
原码:10000000 00000000 00000000 00000001===> -1

b与a同理 都打印-1

补码放入字符型变量c中发生截断:只留下低8位11111111其他位丢失 
打印时%d(有符号整型):进行整型提升,char无符号类型,高位补0
整型提升后:00000000 00000000 00000000 11111111 补码
打印时%d(有符号整型)认为是有符号数,符号位0,看作正数,正数的补码原码反码相同
原码:00000000 00000000 00000000 11111111===> 255

======>打印输出:-1 -1 255

有符号的char取值范围:-128~127

无符号的char取值范围:0~255

signed short取值范围:-215~215-1(-32768~32767)

unsigned short取值范围:0~216-1(65535)

注意:无符号数,最高位不是符号位,是有效位,无符号数没有负数

2.下面程序输出什么?

#include <stdio.h>
int main()
{
	char a = -128;//-128~127  有符号char范围  可以放下-128
	//10000000  00000000 00000000  10000000 原码
	//11111111  11111111 11111111  01111111 反码
	//11111111  11111111 11111111  10000000 补码
	//存入1byte大小的字符型变量a发生截断,只存了低8位
	//10000000 
	//%u打印无符号整型  需要发生整型提升 char有符号变量  最高位是符号位1 高位补1
	//整型提升后:11111111  11111111 11111111  10000000 补码
	//%u认为这个补码是无符号数  即正数  正数原码和补码一样  打印输出一个很大的数

	//%d认为这个补码是有符号数 
	//10000000  00000000 00000000  10000000 原码 ===> -128
	printf("%u\n", a);//4294967168
	printf("%d\n", a);//-128

	//%u打印无符号整数
	return 0;
}

3.下面程序输出?

#include <stdio.h>
int main()
{
	char a = 128;//有符号char范围:-128~127  放不下128
	//00000000 00000000 00000000 10000000 原码 反码 补码
	//字符型变量  整数存入会发生截断  低8位:10000000
	//%u 打印无符号整数 会发生整型提升 char有符号数  最高位符号位 1  高位补1
	//整型提升后:
	//11111111 11111111 11111111 10000000 补码
	//%u认为这个补码是无符号数 即正数  打印输出一个很大的数
	//%d认为这个补码是有符号数 即负数
	//10000000 00000000 00000000 01111111 反码
	//10000000 00000000 00000000 10000000 原码 ===> -128
	printf("%u\n", a);//4294967168
	printf("%d\n", a);// -128 
	return 0;
}

4 . 下面程序输出?

#include <stdio.h>

int main()
{
	int i = -20;
	//10000000 00000000 00000000 00010100 原码
	//11111111 11111111 11111111 11101011 反码
	//11111111 11111111 11111111 11101100 补码
	unsigned int j = 10;
	//00000000 00000000 00000000 00001010 原码 反码 补码
	printf("%d\n", i + j);//-10
	//11111111 11111111 11111111 11101100 
	// +
	//00000000 00000000 00000000 00001010
	//11111111 11111111 11111111 11110110 补码
	//%d认为这个补码是有符号数 转换为原码
	//10000000 00000000 00000000 00001010 原码  -10
	return 0;
}

5.下面程序输出?

#include <stdio.h>
#include <windows.h>

int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);//无符号数 没有负数,没有符号位,所有位都是有效位
		//i=0---> i--   ---> i=-1
		//10000000 00000000 00000000 00000001 原码
		//11111111 11111111 11111111 11111110 反码
		//11111111 11111111 11111111 11111111 补码
		//%u认为这个补码是一个无符号数, 即非常大的一个正数  >=0 依旧成立  会一直输出   i-- -2的补码%u认为是一个很大的正数
		//死循环输出
		Sleep(1000);//休眠1000ms,方便观察
	}
	return 0;
}
C语言进阶—数据的存储_整型家族_07

6.下面程序输出?

#include <stdio.h>
int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
  //-1 -2 -3 …… -1000 int
	//a[i] char  取值范围-128~127
	//-1 -2 -3 -4……-128 127 126 125 ……4 3 2 1 0 -1 -2……
	printf("%d", strlen(a));//128+127=255
	//strlen是求字符串的长度 关注的是字符串中'\0'(ASCII:数字0)之前出现多少个字符
	return 0;
}
C语言进阶—数据的存储_整型存储规则_08

分析:循环会进行1000次对字符型数组a赋初值: -1 -2 -3 …… -128,内存中存放的是补码,int型-128的补码-1存入字符型数组是 127 如图所示:-1 -2 -3 ………-128 127 126…… 3 2 1 0 -1…… strlen()求的是'\0'前面的字符个数,所以128+127=255

对图的理解:

数字:负数:顺时针方向+1 非负数:顺时针方向+1;整体数字在顺时针方向是+1 比较难理解的地方在于127+1为什么是-128

128
//00000000 00000000 00000000 10000000 原码 反码 补码
//存入char数组产生截断只存了低8位10000000
//如果要进行整型提升时 char有符号 符号位1 高位补1
//11111111 11111111 11111111 10000000 补码
//%d认为这个补码有符号数 
//10000000 00000000 00000000 01111111 
//10000000 00000000 00000000 10000000 原码:-128
====>127+1是-128
同理可以求出129在char变量中打印输出是-127……
int存入到char
1 
//00000000 00000000 00000000 00000001 原码 反码 补码
//存入char数组产生截断只存了低8位 00000001
127
//00000000 00000000 00000000 01111111 原码 反码 补码
//存入char数组产生截断只存了低8位 01111111
-1
//10000000 00000000 00000000 00000001 原码
//11111111 11111111 11111111 11111110
//11111111 11111111 11111111 11111111 补码
//存入char数组产生截断只存了低8位 11111111
-127
//10000000 00000000 00000000 01111111 原码
//11111111 11111111 11111111 10000000 
//11111111 11111111 11111111 10000001 补码 
//存入char数组产生截断只存了低8位 10000001
-128
//10000000 00000000 00000000 10000000 原码
//11111111 11111111 11111111 01111111
//11111111 11111111 11111111 10000000 补码
//存入char数组产生截断只存了低8位 10000000

注意:
127截断的补码+1就是-128截断的补码,这里虽然从整数上看不合逻辑,但是补码上看是符合逻辑的

7.下面程序输出?

#include <stdio.h>
unsigned char i = 0;//无符号char取值范围: 0~255
int main()
{
	for (i = 0; i <= 255; i++) //i<=255恒成立 死循环
	{
		printf("hello world\n");
		//256
		//00000000 00000000 00000001 00000000
		//存入字符型变量产生截断只保存低8位:00000000
		//进行整型提升:无符号 高位补0  
		//00000000 00000000 00000000 00000000  ===>0    <=255 
		//257同理 i<=255恒成立 死循环
	}
	return 0;
}

8.下面程序输出?

#include<stdio.h>
#include<string.h>
int main()
{
	if (strlen("abc") - strlen("abcdef") >= 0)
		printf(">\n");//>
	else
		printf("<\n");
	return 0;
}
//strlen()返回值类型是size_t--->unsigned int 求出的字符串长度不可能是负数
//3-6=-3  
//10000000 00000000 00000000 00000011 原码
//11111111 11111111 11111111 11111100
//11111111 11111111 11111111 11111101 补码
//这个补码被认为是无符号数 即一个很大的正数 >=0成立,输出>

//改进1:不相减 只比较 if(strlen("abc") > strlen("abcdef"))
//改进2:强制类型转换为int if((int)strlen("abc")-(int)strlen("abcdef")>=0)

注意:有符号数和无符号数计算时会先将有符号数转换为无符号数进行计算,下图的算数转换是向上进行转换的。

C语言进阶—数据的存储_原码反码补码_09

3.浮点型在内存中的存储

常见浮点数:

3.14159   
1E10:浮点数的科学计数法表示 1.0*10^10

浮点数家族:

float
double 
long double//C99 引入

浮点数表示的范围:float.h中定义           整型家族取值范围:limits.h中定义

3.1 一个例子

#include<stdio.h>
int main()
{
	int n = 9;
  //00000000 00000000 00000000 00001001 原码 反码 补码
	float* pFloat = (float*)&n;
  //0 00000000 00000000000000000001001 
  //+0.00000000000000000001001*2^(-126) 
	printf("n的值为: %d\n", n);//9
	printf("*pFloat的值为: %f\n", *pFloat);//0.000000
	*pFloat = 9.0;
  //1001.0=1.001*2^3
  //(-1)^0*1.001*2^3  3+127=130
  //0 10000010 00100000000000000000000
	printf("num的值为: %d\n", n);  //1091567616
  //01000001 00010000 00000000 00000000 补码 原码

	printf("*pFloat的值为: %f\n", *pFloat);//9.000000
	return 0;
}
//浮点数的存储和取出方式与整数不一样
//整数存储方式存入再以整数形式取出    结果是对的
//浮点数存储方式存入再以浮点数形式取出 结果是对的
//浮存整取 整存浮取 结果都是错的

C语言进阶—数据的存储_大小端字节序存储_10

3.2 浮点数存储规则

根据国际标准IEEE (电气和电子工程协会) 754, 任意一个二进制浮点数V可以表示成下面的形式:

  • (-1)^S*M* 2^E
  • (-1)^S表示符号位, 当S=0,V为正数;当S=1, V为负数。
  • M表示有效数字,大于等于1,小于2
  • 2^E表示指数位。

例题:5.0,9.5

十进制V=5.0f 二进制表示形式101.0(小数点前+小数点后)
二进制科学计数法表示形式:1.01*2^2(第一个2表示二进制,第二个2表示向左移动两位)
(类比十进制的123.45科学计数法表示形式1.2345*10^2)
===>(-1)^0*1.01*2^2  S=0 M=1.01 E=2

十进制V=9.5f 二进制表示形式1001.1
二进制科学计数法表示形式:1.0011*2^3
===>(-1)^0*1.0011*2^3  S=0 M=1.0011 E=3

有些数值是没法计算的
十进制V=9.6f 二进制表示形式1001.1
2^(-1)=0.5    1
2^(-2)=0.25   0
2^(-3)=0.125  0
2^(-4)=0.0625 1 
……
往下计算 有可能总是差一点出现精度丢失 
也有可能位数太多了,存放不下,出现精度丢失

float 4byte(32bit)存放位数有限 有些数无法精确保存
double 8byte(64bit)精度更高了 但是仍然有些数无法精确保存
===>浮点数在内存中有可能是无法精确保存的

注意:浮点数小数部分二进制表示(每一位的权重)2^(-1)=0.5 

C语言进阶—数据的存储_整型存储规则_11

浮点数存储规则

C语言进阶—数据的存储_整型存储规则_12
C语言进阶—数据的存储_大小端字节序存储_13

IEEE 754对有效数字M和指数E,还有一些特别规定。

前面说过,1<=M<2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂。首先,E为一个无符号整数(unsigned int),这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2410的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

V = 0.5f
  = 0.1
  = 1.0*2^-1
  =(-1)^0*1.0*2^(-1) S=0 M=1.0 E=-1
  
float--->E(真实值)+127(中间值)--->126(存储值)
double--->E(真实值)+1023(中间值)--->1022(存储值)

例题:5.5在内存中怎么存储?

#include <stdio.h>
int main()
{
	float f = 5.5;
	//5.5
	//101.1
	//1.011*2^2
	//S=0 M=1.011 E=2
	//0 10000001 01100000000000000000000 ===>0x40 B0 00 00
	//2+127=129 10000001
	//M只保存小数点后面的部分 不够23位补0
	//float 4byte(>1byte)涉及大小端问题
	//VS 小端字节型存储
	return 0;
}
C语言进阶—数据的存储_大小端字节序存储_14

浮点数的取出规则

根据指数E,从内存中取出浮点数还可以再分成三种情况:

1.E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1

0.5 (1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:0 01111110 00000000000000000000000

float f = 5.5f;
存:0 10000001 01100000000000000000000 
取:+1.011*2^2
    129-127=2

2.E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示+0,以及接近于0的很小的数字。

3.E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK