C语言 数据存储之结构类型 万字讲解#数据类型详细介绍 #整型在内存中的存储 #大小端字节序 #浮点型在内存的存储解析

文章目录

目录

前言

一、数据类型详细介绍

类型的意义:

1、类型的基本归类

a.整型家族

b.浮点型家族

c.构造类型

数组类型

[结构体类型 struct](#结构体类型 struct)

[枚举类型 enum](#枚举类型 enum)

[联合类型 union](#联合类型 union)

d.指针类型

e.空类型

二、整型在内存中的存储

三、大小端字节序

什么是大端字节序、小端字节序?

为什么会有大小端字节序?

练习题:设计一个程序来判断一个当前机器的字节序

例1:(输出什么?)

例2:(输出什么?)

四、浮点型在内存的存储解析

[1、十进制 - 二进制 - 科学计数法](#1、十进制 - 二进制 - 科学计数法)

2、对将M、E存入内存中的规定:

3、浮点数的读取:

1、E不全为0或者不全为1

2、当E全为0时

3、当E全为1时

总结


前言

万字讲解

赶紧收藏起来!


一、数据类型详细介绍

在前面我们已经学过很多的内置类型:

char //字符数据类型 1 byte

short //短整型数据 2 byte

int //整型 4 byte

long //长整型 4byte (32位 )\ 8 byte (64位)

long long //更长的整型 8 byte

float //单精度浮点数 4 byte

double //双精度浮点数 8 byte

类型的意义:

1、使用这个类型所开辟内存空间的大小(大小决定了使用范围)

2、如何看待内存空间大小的视角

分析,类型决定了所开辟内存空间的大小;即,若我创建一个char 类型的变量,那么它向内存申请 1 byte 的空间来存储数据;若我创建一个short 类型的变量,那么它向内存申请了 2 byte 的空间来存储数据......而如何看待内存空间的大小的视角呢?例如:int a = 1; --> 变量a 的类型是int ,站在a 的视角便认为a 里面放的 1 是整型数据 ; float b = 1.0f ; --> 变量b 的类型是float ,站在b的视角便认为 b里面放的是 1.0 是浮点型类型;

即使a、b 在内存中所占的空间为 4byte ,但是由于a、b 的类型不同它们的立场也不同,所以它们看这 4 byte 中的内容数据便有所差异;

1、类型的基本归类

a.整型家族

char**(默认可能是 unsigned char,也有可能是 signed int ;取决于编译器)**

unsigned char

signed char

注:字符在内存中本质上存的是ASCII码值,故而char 类型属于整型家族;

short (默认是 signed short (int))

unsigned short (int)

signed short (int)

int (默认是 signed int )

unsigned int

signed int

long (默认是 signed long (int))

unsigned long (int)

signed long (int)

long long (默认是 signed long long (int) )

unsigned long long (int)

signed long long (int)

注:long long 是C99 标准引入的类型

思考: signed 与 unsigned 到底有什么用?它们二者之间有什么区别?

生活中年龄、身高等数据等是没有负数的;故而signed 类型的数据表示有正数有负数,但unsigned类型的数据不表示负数;数据在内存中存储的是二进制的补码,而signed 类型的数据的最高位为符号位代表着这个数据是正还是负;而unsigned 即无符号类型,也就是说这些数据在内存中存的二进制的补码中的最高位不为符号位而是有效位 ;以char 类型为例子,那么显然signed 类型的数据可存放在内存中只有7bit 位,即signed char 可表示范围为 [-128,127] ; 而unsigned char 中在内存中的补码的最高位的不为符号位,所以便有8 bit位来存放数据,即unsigned char 可表示的范围为 [0,255];

b.浮点型家族

浮点型家族:只要是表示小数就可以使用浮点型;float 的精度低,存储的数据范围较小;double 的精度高,存储的类型的范围更大。

float

double

c.构造类型

构造类型即自定义类型(即我们可以自己创建的类型)

数组类型

数组的类型:去掉数组名剩余的便为数组的类型;只要数组元素的类型与个数在发生变化,数组的类型就在发生变化,故而数组类型是构造类型;

结构体类型 struct
枚举类型 enum
联合类型 union

d.指针类型

指针类型即通过创建指针变量

int* p1;

float* p2;

void* p3;

e.空类型

空类型即无类型

void 表示空类型,void 通常运用于函数类型的返回值、函数的参数、指针类型

当void 应用于函数类型的返回值,以表示此函数不用返回任何东西;

当void 应用于函数的参数时,即代表在这个函数明确不需要给此函数传参,即此函数不需要参数;但是当void 应用于函数的参数时,仅仅只是是说这个函数不需要参数,但如果你硬要是传参给这个函数,它也没有办法只有看着你,不过此函数仍然不会使用此参数;

当void 用来表示指针类型的时候,例如:void* p; 即创建了一个void* 类型的指针变量 p;

二、整型在内存中的存储

通过前文我们可知,当一个变量创建的时候便会根据此变量的类型向内存申请空间来存放数据,空间的大小是根据不同的类型决定的;

那么数据在开辟空间的时候到底是怎样存储的呢?

例如:

int a = 20;

int b = -10;

我们知道变量 a、 b 的类型为 int 类型,在创建的时候回向内存空间申请 4byte 大小的空间来存储数据,那么这些数据是怎样的形式存储在空间中呢?可以看此详细讲解的文章:链接在此:http://t.csdnimg.cn/DiyC2

数据可以有很多种表示形式即二进制、八进制、十进制、十六进制等;然而当整数以二进制的形式来表示的时候,又会有三种表现形式:原码、反码、补码;

注:1、正整数的原码、反码、补码相同

2、负整数原码、反码、补码需要计算才能得到;

原码、反码、补码均有符号位和数值位两部分,符号位即即二进制序列最左边的一位,用0来表示正数;用 1 来表示负数;

原码:根据此数值的正负直接写出来的二进制序列

反码:在原码的基础上,符号位不变,其他位按位取反

补码:在反码的基础上+1

根据以上例子:int a = 20; int b = -10;

变量a 的原码、反码、补码:00000000 00000000 00000000 00010100

变量b 的原码:10000000 00000000 00000000 00001010

十六进制表现形式:0x1000000a

反码:11111111 11111111 11111111 11110101

十六进制表现形式:0xfffffff5

补码:11111111 11111111 11111111 11110110

十六进制表现形式: 0xfffffff6

注:想要详细了解负整数原码、反码、补码的计算过程看此链接:http://t.csdnimg.cn/DiyC2

调试之后观察变量b 的地址,便可以得知数据在内存中存储的是原码还是反码或者是补码;

此处 &b 之后我们发现,在内存窗口中(这里是调试小技巧,想要了解可以点此来链接:http://t.csdnimg.cn/JDy0E);地址实际上是二进制序列,为了方便查看,呈现的时候我们看到的是十六进制(博主用的是x64 位) ;同理,在内存中存放的数据也是如此,本质上存的是二进制序列,但是为了便于查看,是以十六进制呈现的;从上图,我们可以得知,变量b 在内存中存储的是 0xfffffff6,即变量b 补码的十六进制数据,

故而数据在内存中存放的是二进制序列的补码;

为什么在内存中存放的是补码呢?

1、使用补码可以将符号位和数值位统一处理

2、加法和减法可以统一处理(CPU只有加法器)

3、补码与原码的相互转换,其运算过程是相同的,故而不需要额外的硬件电路;

注: 负整数中,将原码转换成补码:在原码的基础上符号位不变,其他位按位取反,得到反码,再在反码的基础上+1

那么将补码转换成原码?显然将原码转换成补码的步骤反过来就行了,即在补码的基础上-1得到反码,而在反码的基础上符号位不变其他位按位取反得到了原码; 然而实际上,将补码转换成原码也可以这样:在补码的基础上符号位不变其他位按位取反,得到反码,然后在反码的基础上+1,便得到了原码;

我们来看一下图解:(还是以 int b = -10 为例子)

方法一:

方法二:

三、大小端字节序

例子: int b = -10;

下图为在x64环境下调试后 &b 的内存窗口:

我们先来梳理一下原理:**内存单元的大小为 1 byte, 而 1 byte = 8 bit 也就是说可以存放 8个二进制位,一个十六进制位相当于四个二进制位;故而 1 byte 存放了两个十六进制位;所以0x00000014B076F884 这个地址(内存单元)中存放的是 f6 ,0x00000014B076F885 这个地址中存放的是 ff ;0x00000014B076F886 这个地址中存放的是 ff ; 0x00000014B076F887 这个地址中存放的是 ff ;**但是变量b 实际在内存中补码的十六进制表示是0xfffffff6,想必你提出一个疑问:数据是倒着存放的?为什么呢?

内存是一块连续的空间,由于每个内存单元的大小为1byte,当一个数据所占内存空间的大小大于 1 byte 的时候,就会有个"小尾巴",这个"小尾巴"到底是向左摆呢还是向右摆(在这里将内存看作了横向的连续空间),就会存在一个顺序问题,即大小端字节序;

大小端的概念来源于《格列佛游记》中一个吃鸡蛋的问题,即一个吃鸡蛋到底是从鸡蛋的小端开始吃还是从鸡蛋的大端开始吃;同理,"大小端字节序"中的"大小端"可以理解为数据在内存中排列顺序的问题,而"字节序"则是由于内存单元的大小为 1byte,当数据大于 1byte 的时候,就存在每一个大小为1字节数据之间顺序问题(以字节为单位讨论数据在内存中的存储顺序);

为什么只有大端、小端这两种顺序呢?不是说只要怎么存放都可以,只要当数据使用的时候,我按照原理展开就行了吗?理论上是可行的,但是如果存放规则没有定下来而去解决于怎么存(非大端和小端的方法)就怎么取,各种各样的方式就非常复杂,太麻烦了,于是乎后来就渐渐地变得之后大端和小端这两种数在内存中存放的顺序;

什么是大端字节序、小端字节序?

当数据顺着地址由低到高的顺序存放进去就为大端字节序,即把一个数据的高位字节序放在低地址,而其低位字节序放在高地址。

当数据逆着地址由低到高的顺序存放进去就为小端字节序,即把一个数据的高位字节序放在高地址,而其低位字节序放在低地址;

为什么会有大小端字节序?

这是因为在计算机系统中,内存被分为一个个内存单元,而一个内存单元的大小为1byte,一个字节为8比特位,但是在C语言除了8bit 的char 类型以外,还有18 bit 的short 类型、32 bit 的long 类型(要具体看其编译器),且在位数大于8 位的处理器,例如 16位、32位的处理器,由于寄存器的宽度大于一个字节,那么就必然存在多个字节安排的问题;即内存是一块连续的空间,由于每个内存单元的大小为1byte,当一个数据所占内存空间的大小大于 1 byte 的时候,就会有个"小尾巴",这个"小尾巴"到底是向左摆呢还是向右摆(在这里将内存看作了横向的连续空间),就会存在一个顺序问题;因此就导致了大端存储模式和小端存储模式;

注:只要是在内存中申请的空间大于 1 byte 的类型,都存在大小端字节序的问题;

练习题:设计一个程序来判断一个当前机器的字节序

代码如下:

#include<stdio.h>

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

int main()
{
	int ret = is_check_says();
	if (ret)
		printf("小端\n");
	else
		printf("大端\n");

	return 0;
}

分析:封装成一个函数专门来判断当前机器是大端还是小端;用 Int a = 1;就非常巧妙;变量a 补码的十六进制的写法:0x00000001; 如果当前机器是大端字节序那么此数据在内存中存储的顺序就是:00 00 00 01;而若是小端字节序,那么此数据在内存中存储的顺序就是 01 00 00 00 ; &a ,取出的地址是变量a 存储在内存空间中4字节中第一个字节的地址,由于变量 a的类型为Int 类型,所以变量a地址的类型为int* ,若是相对此地址进行解引用,因int* 的权限为 4字节,所以会访问变量a 存放在内存中4字节的数据;而这里想要区分大小端字节序只需要访问变量a 存放在内存中4个字节中第一个字节的内容,故而应该先对变量a 的地址进行强制类型转换而后再解引用;所以需要这样写:* ( char* ) &a ; 若是当变量a 存储在内存中的数据为1 的时候,取其第一个字节的内容,如果是1 ,则说明是小端字节序;如果是0,则说明是大端字节序;

例1:(输出什么?)

代码如下:

#include<stdio.h>

int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("a=%d,b=%d,c=%d\n", a, b, c);

	return 0;
}

代码运行结果如下:

分析:在vs中,char 类型默认为 signed char 类型,signed char 类型的取值范围为[-128,127]; -1是整型,为32bit,其反码为:11111111 11111111 11111111 11111111 ,而 signed char 在内存空间中占1 byte即8 bit , 所以将-1存入 signed char 会发生截断,即signed char在内存空间存放的是 11111111;在printf() 函数中占位符 %d 的对象是有符号整型,所以此处会发生整型提升,有符号的的整型提升,高位补的是其符号位,即 :11111111 11111111 11111111 11111111,符号位是1代表此数为负数,而负整数的原码、反码、补码需要计算才能得到;经计算得其原码为 :10000000 00000000 00000000 00000001 , 所以 a = -1; b = -1 ;

而unsigned int 类型的取值范围为 [0,255];故而unsigned char不能用来表示负数;-1是整型,为32bit,其反码为:11111111 11111111 11111111 11111111 ,而 unsined char 在内存空间中占1 byte即8 bit , 所以将-1存入 unsined char 会发生截断,即unsigned char在内存空间存放的是 11111111;由于%d 打印的有符号整型,变量c 的类型为unsigned char ,便会发生整型提升;无符号整型的提升直接在高位补0,即00000000 00000000 00000000 11111111,符号位为0说明该数为正整数,正整数的原码、反码、补码相等;所以 c = 255 ;

例2:(输出什么?)

代码如下:

#include<stdio.h>

int main()
{
	char a = -128;
	printf("%u\n", a);

	return 0;
}

代码运行结果如下:

分析:在vs编译器中 char 默认为 signed char 类型,其值的表示范围为 [-128 , 127];但是 printf() 函数中的占位符是 %u,来打印无符号整型的占位符;

-128为整型,其补码: 11111111 11111111 11111111 1000000,存放到 类型为 signed char 类型的变量a 中,便会发生截断即 1000000 ;由于%u是用来打印无符号整型的占位符,故而此处会发生整型提升,有符号的整型提升高位补其符号位,即得到补码:11111111 11111111 11111111 1000000,但是无符号整型无符号位则说明该值位正整数,即此32为均为有限位,即11111111 11111111 11111111 1000000 的十进制值为:4294967168

四、浮点型在内存的存储解析

1、十进制 - 二进制 - 科学计数法

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

即 V = ( -1 )^ S * M * 2^E

(-1)^S 为符号位,当S为0时,V为整数;当S为1时,V为负数;

M表示有效数字,[ 1, 2)

2^E 表示指数位;

接下来,我将以举例子的形式来让你理解这个式子: V=( -1 )^S * M * 2^E;

例3:

V = 5.5f;

此处的 5.5为浮点数,f 表示它的类型为float 类型;

5.5 为十进制的表示形式,怎么用二进制来表示呢? --> 小数点前二进制+小数点后二进制,即其二进制写作:101.1 (请注意:看其权重)--> 写成科学计数法:1.011 * 2 ^ 2 (可自行类比十进制数的科学计数法),那么此浮点数的二进制序列便可以写成 : V = ( -1 )^0 * 1.011 * 2^2 ,其中 S = 0 , M = 1.011 , E = 2 ;

但是用二进制序列存储浮点数,由于是用权重,可能就会出现有些浮点数无法被精确存储,或者即使可以精确存储但是由于位数太多(总位数大于32位或者64位,即float 类型与 double 类型)也会导致存储不精确的问题。这也就是浮点数为什么可能会存在存储不精确的原因;

由于任意一个浮点数均可以用 V = ( -1 )^S * M * 2^E 来表示,那么只要将S,M,E这三个数据信息存储在内存中即可,等到要使用该浮点数的时候,计算机再将这三个数据解读便可以了;

2、对将M、E存入内存中的规定:

有效数M属于[ 1 , 2) ,M总是大于1,故而将其存入内存的时候便不存第一位的1,在读取的时候加上就行了;这样做便可以多一个比特位来存储数据。 而指数位 2^E (按理说科学计数法可以向左移、向右移,故而E有正有负),但是E的类型为 unsigned int ,如何解决这个问题呢?--> IEEE754规定存入的E的真实值需要加上其取值范围的中间值;当此浮点数为float 类型时,E 占8bit ,取值范围为 [0 , 255] --> 中间值为127 ; 当此浮点数类型为double 类型时,E占11 bit, 取值范围为 [0 , 2027] --> 中间值为 1023;

对于float 类型的浮点数,有32比特位,S占了1bit 位,E 占了8bit 位,M占了 23bit 位;

eg.V=0.5;为float 类型的数存入内存中:(S+E+M)

0 01111110 00000000000000000000000

注:内存中存储M时绿色的0是补的,由于M存储的时候没有存第一位的1,所以在解读的时候计算机便会加上,故而0补在后面;

对于double 类型的浮点数,有64比特位,S占了1bit 位,E 占了11bit 位,M占了 52bit 位;

eg.V=0.5;为double 类型的数存入内存中:(S+E+M)

0 00001111110 0000000000000000000000000000000000000000000000000000

注:内存中存储M时绿色的0是补的,由于M存储的时候没有存第一位的1,所以在解读的时候计算机便会加上,故而0补在后面;

3、浮点数的读取:

指数E从内存中有三种情况:

1、E不全为0或者不全为1

此时,浮点数就采用以下规则表示,即指数E的计算值(E存储在内存中的数值)减去127(或1023),得到真实值,再将有效值M前加上第一位的1;

eg.还是以float类型的0.5为例;

其补码为: 0 01111110 00000000000000000000000

S = 0; E = 01111110 ; M=00000000000000000000000;真实值:S = 0 ; E = 126 - 127 = -1 ; M= 1.0 --> ( -1 )^0 * 1.0 * 2^( -1 ) ;算出来也是0.5 ;

2、当E全为0时

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

2^E 为指数位,当存入内存中的E全为0时,当浮点数位float类型即为32bit时,则E的真实值 =1-中间值127 ,代表着E真实值为-126,那么2^E 算出来的结果就非常小,无限接近于0;浮点数为double类型时同理;

3、当E全为1时

规则:此时,如果有效数字M全为0,表示+\- 无穷大(正负号决定于符号位S)

2^E 为指数位,当存入内存中的E全为1时,当浮点数位float类型即为32bit时,则E的真实值 =内存中存放的 (255)- 中间值(127),即代表着E真实值为128,那么2^E 算出来的结果就非常大;浮点数为double类型时同理;

例3:(浮点数存储)

代码如下:

#include<stdio.h>

int main()
{
	int a = 9;
	float* p = (float*)&a;
	printf("a=%d\n", a);
	printf("*p=%f\n", *p);

	*p = 9.0;
	printf("a=%d\n", a);
	printf("*p=%f\n", *p);

	return 0;
}

代码运行结果如下:

分析:变量a 的类型为int 类型,故而其地址为 int* 类型,所以若想要将变量a 的地址存入float* 类型的指针变量p 中,便要将&a 进行强制类型转换,所以得到 float* p = (float*)&a; 而printf() 函数中的占位符 %d 表示打印整型数据,变量a 的类型为int 类型,所以 printf("a=%d\n", a); 的输出为 9; 而printf() 函数的占位符 %f 表示打印浮点型float 类型的数据,也就是说时按照float 类型的读取方式来读取内存中的补码;变量a 在内存中的存储的二进制序列( 补码 ) :00000000 00000000 00000000 00001001 ;以浮点数的方式进行读取,即S=0; E= 00000000 ; M=0000000 00000000 00001001; 此时E为全0,所以E的真实值=1-127=-126,而M=0.0000000 00000000 00001001;,表示一个很小的值,无限接近于0;即 (-1)^0 * 2^(-126) * 0.0000000 00000000 00001001 ,故而输出为 0.000000.
*p = 0; 就是对指针p 指向的对象赋值为 浮点型9.0,即此处是以 float 类型存入变量a 所处的内存空间之中;9.0的二进制 --> 1001.0 --> 科学计数法:1.0010 * 2^ 3 --> (-1)^0 ^ 1.0010 * 2^ 3 ;则S=0;E=3;M=1.0010;存入内存当中,S占 1bit ,E占8bit ,E存入内存中的值=真实值+中间值(127) = 130;M占23 bit,存入内存中不存入第一位的1 ,即存入0010,位不够便在其后补0;所以变量a存入内存中的二进制序列为:0 10000010 00100000000000000000000 ; 而printf() 函数中的占位符 %d 打印的是整型数据,所以读取内存中的数据是以整型的读取方式来读取的,因符号位为0代表着此数为正整数,正整数的原码、反码、补码相同;二进制序列:0 10000010 00100000000000000000000的十进制表示形式为:1091567616所以printf("a=%d\n", a);的输出结果为:1091567616

而 printf() 函数的占位符为 %f 打印的是浮点型类型,所以读取内存的方式是以浮点型的读取方式来读取的;即存入内存中的S=0;E=10000010;M=00100000000000000000000; ,而打印出来 S=0; E的真实值为= 内存中的数值 - 中间值(127)=3 ; 在读取M的时候加上1,即M=1.00100000000000000000000; --> ( -1)^0 * 1.00100000000000000000000 *2^3 -- > 1001.00000000000000000000 --> 十进制:9.00000000000000000000,由于float 类型--> 9.000000;


总结

1、类型的意义:使用这个类型所开辟内存空间的大小(大小决定了使用范围);如何看待内存空间大小的视角

**2、**类型的基本归类:整型家族;浮点型家族;构造类型:数组类型、结构体类型、枚举类型、来联合类型。指针类型;空类型;

3、大端字节序当数据顺着地址由低到高的顺序存放进去就为大端字节序,即把一个数据的高位字节序放在低地址,而其低位字节序放在高地址。

小端字节序**:当数据逆着地址由低到高的顺序存放进去就为小端字节序,即把一个数据的高位字节序放在高地址,而其低位字节序放在低地址;**

**4、**二进制浮点数V可以表达成下面的形式: V = ( -1 )^ S * M * 2^E

(-1)^S 为符号位,当S为0时,V为整数;当S为1时,V为负数;

M表示有效数字,[ 1, 2)

2^E 表示指数位;

5、对将M、E存入内存中的规定;

6、浮点数的读取:

E不全为0或者不全为1:此时,浮点数就采用以下规则表示,即指数E的计算值(E存储在内存中的数值)减去127(或1023),得到真实值,再将有效值M前加上第一位的1;

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

当E全为1时,规则:此时,如果有效数字M全为0,表示+\- 无穷大(正负号决定于符号位S);

相关推荐
爱吃生蚝的于勒37 分钟前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
ChoSeitaku6 小时前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表
DdddJMs__1356 小时前
C语言 | Leetcode C语言题解之第557题反转字符串中的单词III
c语言·leetcode·题解
娃娃丢没有坏心思7 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
ahadee8 小时前
蓝桥杯每日真题 - 第11天
c语言·vscode·算法·蓝桥杯
No0d1es9 小时前
2024年9月青少年软件编程(C语言/C++)等级考试试卷(九级)
c语言·数据结构·c++·算法·青少年编程·电子学会
Che3rry10 小时前
C/C++|关于“子线程在堆中创建了资源但在资源未释放的情况下异常退出或挂掉”如何避免?
c语言·c++
kuiini11 小时前
C 语言学习-02【编程习惯】
c语言·学习
木辛木辛子11 小时前
L2-2 十二进制字符串转换成十进制整数
c语言·开发语言·数据结构·c++·算法