C语言数据在内存中的存储

目录

前言

本期内容介绍

一、数据类型的介绍

1.1类型的意义:

1.2C语言中是否有字符串类型?

1.3类型的基本归类

整型家族:

浮点型(实型)家族:

构造(自定义)类型:

指针类型:

空类型:

二、整型在内存中的存储

2.1原码、反码、补码

总结:

2.2大小端字节序介绍

2.3什么是大小端?

2.4为什么有大小端?

 四、几个笔试题详解

无符号整型详解:

五、浮点数在内存中的存储详解

浮点数的存储规则


前言

我们前期已经学习了C语言的数据类型,有整型、浮点型、字符型等,但他们在内存中是如何存的我们好像不太了解,还有我们以前在看内存的时候发现里面的数据是倒着存的,这是为什么呢?本期小编带您彻底弄清楚数数据在内存的存储规则!

本期内容介绍

1.数据类型的详细介绍

2.整型在内存中的存储

3.大小端字节序介绍以及判断

4.几个笔试题详解

5.浮点数在内存中的存储详解

一、数据类型的介绍

我们前面已经了解了数据的基本数据类型也就是内置类型(C语言自带的)如下:

char                字符类型                1个字节

short               短整型                    2个字节

int                   整型                        4个字节

long                长整型                    4/8个字节

long long        更长整型                 8个字节

float                单精度浮点型          4个字节

double            双精度浮点型          8个字节

这里要注意的是字符类型(char)在C语言中占1个字节,而在其他语言可能不占一个字节例如java中就占2个字节!(我们知道一个汉字是两个字节java中可以用char类型的变量存储性别!)

另外,long 占4/8字节是因为C语言标准只规定了long的大小范围是:

sizeof(int) <= sizeof(long) <= sizeof(long long)具体大小得看编译器

这里你是不是想要这么数据类型有啥意义吗?平时生活中我们不就整数和小数吗?他还真有意义!

1.1类型的意义:

计算机不同于生活中,计算机要管理很多数据。类型决定了它的使用范围(所能开辟和访问的大小)和如何看待这块空间!

例如:一个整型所访问的权限(大小)是4个字节,而字符类型的访问权限是1个字节,你把浮点数存储到整型中不强转他会给你报错精度丢失,这就说明了类型不一样看待类存的视角也就是不一样的有句话说的很好(锤子的眼里什么都是钉子)!

1.2C语言中是否有字符串类型?

答案是:没有!上一期我们刚说完C语言的字符串存储在字符数组或常量串字符串存储在字符指针里面(字符串首元素的地址)!

1.3类型的基本归类

整型家族:

char                unsigned char                    signed char

short               unsigned short                   signed short

int                   unsigned  int                      signed int

long                unsigned  long                   signed long

long long        unsigned  long long           signed long long

这里你肯定会想为什么把char归类到整型家族里面呢?

其实字符类型在存储的时候实质上存储的是对应的ASCII值,本质上是数字!因此归在整型家族!

另外,一般的类型都是有符号的,要想使无符号的就得加上unsigned !

浮点型(实型)家族:

float

double

构造(自定义)类型:

数组类型:type [const num];

结构体类型:struct

枚举类型:enum

联合(公用)类型:union

这里数组类型可能有的朋友不知道,把数组名去掉就是数组的类型!其他结构、枚举、联合后面的几期小编会介绍!

指针类型:

int  *p;

char  *pc;

float  *pf;

void  * pv;

这些都在前面的两期说过,这里不在多说,唯一要注意的是:void*这个指针,他可以接收任何类型的指针,用的时候强转为自己所需要的!

空类型:

void 表示空类型(如返回值)

通常用于函数的返回值、函数参数等!

这里无返回值要注意一下:可以不用返回,要是非要返回的话可以这样写: return ;

二、整型在内存中的存储

我们前面了介绍过创建一个变量要在内存中开辟空间,空间的大小是由其类型所决定的!但空间开辟好了数据是怎么在内存中存的?下面弄我们一起来探索一下!

2.1原码、反码、补码

计算机中整数存的是以二进制的形存的,而二进制的表示形式有三种:原、反、补码!

三种二进制表示方法均由符号位和数值位两部分组成!最高位是符号位其余位是数值位!符号位0表示正,1表示负!

正整数(无符号数)的原、反、补都相同!

负整数的三种形式不用要计算:

原码:将十进制直接翻译成二进制就是原码!

反码:符号位不变其他位按位取反!

补码:反码加1就时补码!

举个栗子:

到这里我们知道了整数在内存中是以二进制的形式存储的的,也了解了二进制的三种形式!那到底在内存中存的是二进制的哪一种呢?我们前面介绍操作符的时候位有关的操作符操作的都是操作其二进制的补码,我们猜想会不会存的也是相应的二进制的补码呢?我们下面通过一个栗子来验证一下:

int main()
{
	int a = -1;
	return 0;
}

我们知道正整数的原码、反码、补码都一样不能区分存的到底什么?所以我们来用负数检验!我们知道-1的补码是32个1,所以我们只要看一下内存中存的是不是全1就OK了,又因为内存的值是16进制的,我们知道1个16进制位相当于4个二进制位,也就是说如果内存中的结果是:ff ff ff ff就说明存的是补码,否则就是原码!

果然存的是补码!这个栗子也说明了,数据在内存中存的是补码,而打印的时候用的原码!

上面我们已经了解了整数是在内存中以补码的形式存储,那他是以原码计算的还是补码计算的呢?下面通过一个栗子来证明一下:


int main()
{
	int a = -3;
	int b = 5;
	int c = a + b;
	return 0;
}

假设存以原码进行计算的:

这显然是不合理的!所以排除了计算用的是原码!

假设存以补码进行计算的:

结果正确!所以数据在计算操作的是其二进制的补码! 

总结:

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

这解释了上面的所有问题!另外,注意这句话:“补码与原码相互转换,其运算过程是相同的,不需要额外使用硬件电路!”这句换 什么意思呢?我们知道:负整数从原码到补码要通过取反(符号位不变其他位按位取反) + 1得到,从补码到原码要先减1再取反!这句换其实有介绍了另一种方法:从原码到补码要通过取反 + 1,补码到原码也可以取法+1;

画个图理解一下:

2.2大小端字节序介绍

我们在观察内存的时候发现,数据是倒着存的:

例如:


int main()
{
	int a = 15;
	return 0;
}

15的二进制是00000000 00000000 00000000 00001111转换成16进制就是00 00 00 0f按理说内存中就应该是这个顺序存的!

看内存:

内存中是刚好倒的放的,这是为什么呢?其实这是大小端字节序问题!我们下面介绍一下什么大小端字节序! 

字节序:是以字节为单位,来讨论数据的存储形式的!

2.3什么是大小端?

大端(存储)模式:数据的低位保存在高地址中,而数据的高位保存在低地址中!

小端(存储)模式:数据的低位保存到低地址中,而数据的高位保存到高地址中!

OK!画个图理解一下:

我们的机器就是小端存储模式!

2.4为什么有大小端?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit位,但是在C语言中,除了8bit位的char之外还有,16bit位的short类型、32位的int或long(具体看编译器),另外,对于位数大于8位的处理器,例如16位的或32位的处理器,由于寄存器宽度大于一个字节,那么久必然存在如何安放多字节的问题!因此就产生了大小端字节序!

上面说的不知你看懂了没,这里小编用白话给您在来梳理一遍:

就是说一个字节的变量的值你无论怎么存都可以,无顺序而言!但超过一个字节的存储就有顺序问题了!例如:

为了存储完后容易拿出来,所以最后就留下了,上面两种 即大小端。下面两种由于存储无规则舍弃!

介绍到这里就得介绍一下2015年百度系统工程师的一道笔试题了:

请简述大端字节序和小端字节序的概念,并设计一个程序判断当前机器是大端还是小端。

这里概念就不在介绍了,上面刚刚说完!这里重点是第二问;如何判断当前是大端还是小端?

这里管着个问题编会两种方法:1.指针 2.联合 。下面小编就来介绍一下这两种方法:

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

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

	return 0;
}

这个代码应该都能看懂!线面我们来看另一种:

int check_sys()
{
	union
	{
		int i;
		char c;
	}un;//创建了一个联合体对象un,他有两个成员,一个是int i和char c;
	un.i = 1;
	return un.c;
}

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

	return 0;
}

这里要说一下的是:联合体的大小是其成员中最大的那个的大小!这里也即是int  i;联合的成员变量不可能同时使用,这里返回了char c的值,如果是小端里面就是1否则里面是0;画个图解释一下:

这里如果不明白的话也没有关系,后面的某一期马上我就会介绍!

看看两个结果:

这与我们上面所介绍的一致!

 四、几个笔试题详解

(1)

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和b的我们前面说过:char == signed char

 如果说这里这整形提升不理解的看一看一下我之前写过的这一篇文章 :

C语言整形提升http://t.csdn.cn/bCGN6这c我们来分析一下:

(2)

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

先思考一下,这道题的答案是多少?

看结果:

这个题估计答对得好一会,我就来解释一下:

 

(3) 


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

这道提的答案又是多少?

看结果:

做完上一道再做这个题其实应该很容易的,可以不用这样一步一步分析的!它里面存的和上面-128存的一样而有都是以%u打印,所以拿出来的结果应该一样!

(4)

int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
	return 0;
}

这道提的答案又是多少?

看结果:

 分析:

 (5)

int main()
{
	unsigned int i;
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
	}
	return 0;
}

这段代码的结果是多少?

看一下结果:

没错他陷入了死循环,为什么呢?小编给您分析一下:

其实原因很简单,i是无符号数,是恒大于等于0的,这里的循环判断条件是>=0的所以恒成立!所以就陷入利润死循环!但为什么一直打印这么大的数呢?这里就要详细的在说一下,无符号整型了!

无符号整型详解:

我们知道整型在VS上如果不加unsigned 就默认为是signed (有符号的),每个类型所能表示的范围是多少?我们好像还没有介绍过!下面小编详细介绍一下有符号和无符号的表示范围!

char        占一个字节(8个bit位,最高那一位是符号位)所以其取正值是的范围是

               0 ~ 2 ^ 7 - 1 = 0 ~ 127

               负数取值就是-1 ~  -2 ^ 7 = - 1 ~ -128,多cha的取值范围就是:-2 ^ 7 -- 2 ^ 7 - 1

那unsigned char 呢?由于最高一位不在是符号位了而是数值位,所以其范围就是0~2^8-1

short        占两个字节(16个bit位,最高那一位是符号位)所以其取正值是的范围是

                0 ~  2  ^  15 - 1

                负数取值就是 -1 ~ -2 ^ 15,所以取值范围就是:-2 ^ 15 -- 2  ^  15 - 1

那unsishort 呢?由于最高一位不在是符号位了而是数值位,所以其范围就是0~2^16 -1;

其他整型家族里面的和这个分析方法一样,int -2^31~2^31-1;unsigned int 就是0~2^31-1;这里我就不再一一列举了!下面小编解释一下为什么是这样的!以char为例(其他同理):

有符号char:

明白了这个就可以看看这么一张图了:

 无符号char:

了解到这里,就能解释为什么上面那个题为什么死循环后打印很大的数字了!

因为i是unsigned int类型的 恒大于等于0所以,当他等于0再减1之后,就变成了无符号int的最大值四十多亿!然后就循环一直减一直大于等于0!同理的还有前面的第二题和第三题!

(6)

#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;
}

这段代码多看一眼就会爆炸!思考执行结果是多少?

我们一起来看一看:

255肯定有很多人没想到!小编第一次做这个提的时候也是被秀的头皮发麻!哈哈,下面我来分析一下:

当a[i]中是0时,其对应的字符为'\0'我们知道strlen计算的是\0之前的内容,之前放进数组的元素有127+128=255个,所以是255,是不是这道题很坑!哈哈!

(7) 

unsigned char i = 0;
int main()
{
	for (i = 0; i <= 255; i++)
	{
		printf("hello world\n");
	}
	return 0;
}

这道题执行结果是多少?思考一下!

看结果:

这道提的死循环应该很容易理解了!这里我就不画图了!unsigned char 是永远小于等于255的)所以它就死循环了!

五、浮点数在内存中的存储详解

常见的浮点数:

3.14

1E10 ----->1.0 * 10 ^10

我们介绍了浮点数的家族里面有:float和double以及long double

浮点数到底在内存中是如何存储的?我们来一起探讨一下,先来看一个栗子:


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

	*pf = 9.0;
	printf("num = %d\n", n);
	printf("*pf = %f\n", *pf);

	return 0;
}

还是思考一下,您认为这段代码的结果是多少?

看结果:

我猜第一个和最后一个和你想的一样! 其他两个不一样,明明num和*pf里面是同一个数为什么打印出来不一样,这是为什么呢?要解决这个问题就要说一下,浮点数在内存中的存储规则了!

浮点数的存储规则

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

一、(-1)^ S * M * 2 ^ E

二、(-1)^ S表示符号位,当S = 0, V 为正数;当S = 1是时,V为负数;

三、   M表示有效数字,大于等于1,小于2;

四、   2 ^ E表示指数位

什么意思呢?我来画个图直观地理解一下:

OK!举个栗子:

int main()
{
	float f = 5.5;
	return 0;
}

我们可以用上面的这个标准来实践一下:

IEEE754规定:

对于32位的的浮点数(float),最高一位是符号位S,接着的8位是指数E,剩下的23位是有效数字!

对于64位的浮点数(double),最高以为是符号位S,接着的1是指数E,剩下的52位是有效数字!

IEEE754还对有效数字M和指数E还有一些特别的规定:

我们前面说过:1 =< M < 2,也就是说M可以写成1.xxxxxxxx的形式,其中xxxxx表示小数部分!

IEEE754规定:在计算机保存M时,默认这个数的第一位总是1,因此可以被舍去,只保留后面的xxxxx(例如:1.01只保存01)到用(读取)的时候再加上1,这样走的好处是:可以空出一位,而空出的这一位可以用来多存一位有效位!使得存储范围大一点!(64位的浮点数同理)

至于指数E,情况比较复杂:

注意:IEEE754认为E是一个无符号整数(unsigned int):

这就意味着,如果E为8位,它的取值范围为:0~255,如果是E为11位它的取值范围就是:0~2047

但我们知道指数其实是可以是负数的!而现在E是无符号的也即是恒大于等于0的,所以为了解决这个问题IEEE754规定,当存入内存时E的真实值必须再加上一个中间数!对于8位的E这个中间数即使127,而对于11位的E而言这个中间数是1023!

例如:2^10的E是10,你存的时候(假设32位),必须保存成10+127=137即:10001001

然后就是E读取得时候又分为三种情况:

(1)E不全为0或不全为1

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

例如:0.5(32位为例)

0.5的二进制为:0.1由于规定正数部分必须为1,即可以写成:1.0 * 2^-1,

存的时候E=-1是:-1+127=126(这就变成了正数了)即01111110,而1.0的1 存的省略,用的时候加上!所以只存0,而有效位有23位加上1的那一位就有24位所以补齐!00000000 00000000 00000000 00000000

这就是他存入内存的形式:

0 0111110 000000000000000000000000

你读取的时候,126(01111110)-127就得到真实的E了!同理后面的有效位在最前面加上1就是真是的有效位了,就可以还原了!

验证一下:

0 01111110 00000000000000000000000 这时二进制位,内存时16进制,我们换成16进制:

0011 1111 0000 0000 0000 0000 0000 0000-------》3f 00 00 00,而我们上面验证了当前机器是小端模式所以是倒着存的!即00 00 00 3f

看一下结果:

一模一样,这就说明了我们上面的完全正确! 

(2)E全为0

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

这里解释一下:当拿的时候发现E为全0的时候就说明,他存的真实值应该是-127/-1023,即他是这样的形式:

这是一个非常非常小的数几乎接近于0了,所以M前面的1加不加都无所谓了! 

(3)E为全1

这时有效数字M全为0,表示正负无穷大(正负取决于S);

这种情况也比较特殊,拿的时候时发现是全1,这就说,即255(1023),也就是存的时候的真实值是255-127/2047-1023而2^128/2^1024这是一个很大的数字!我们理解为无穷!

介绍到这里我们就可以解释刚刚上面那个题了!

OK!好兄弟本期分享就到这里!我们下期再见! 

猜你喜欢

转载自blog.csdn.net/m0_75256358/article/details/131806769