目录
[4.char 和 unsigned char 的取值范围](#4.char 和 unsigned char 的取值范围)
引言
在C语言的世界里,理解数据在内存中的存储方式是每个开发者必须掌握的基础知识。这不仅关系到程序的正确性,还直接影响到性能优化和系统级调试。无论是简单的变量,还是复杂的结构体和数组,它们在计算机内存中的表现方式都隐藏着丰富的细节和精妙的设计。本文将带你深入探索C语言中各种数据类型在内存中的存储规律,帮助你建立起对数据布局的直观理解,从而在日后的程序开发中更加游刃有余。
一、整数在内存中的存储
前面讲了,整数有原码、反码、补码,由符号位和数值位组成。那么对于整型来说,数据存放到内存中的其实是补码
二、大小端字节序判断
1.大小端
大端:高位字节数据存储到低地址处
小端:高位字节数据存储到高地址处
在VS中运行以下代码,并打开内存窗口:
cs
int main()
{
int r = 2;
return 0;
}

为什么在 r 的地址处显示的是 02 00 00 00 ?而不是一串补码?
因为内存中显示的是 16 进制,所以才会出现数字2
那为什么又是 02 00 00 00 而不是 00 00 00 02?
因为在VS中数据存放是采用小端进行的
举个例子:
cs
int main()
{
int r = 0x11223344;
return 0;
}

可以看到这次内存中放的是 44 33 22 11,再次验证了VS环境下 内存存储方式是按小端字节序排的
如果是大段字节序,这里应该是 11 22 33 44
2、练习:设计一个程序用来验证当存储的是大端还是小端
假设我们存放 1,在内存中无非是 00 00 00 01 和 01 00 00 00,所以我们只要创建一个整型变量,然后验证其第一个字节是否为 1 就可以了
cs
int main()
{
int r = 1;
if (*((char*)&r) == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
这里说明一下,对 r 取地址,此时的地址类型为 int* ,所以要强制类型转换为 char* 类型,然后在解引用,就可以访问 r 的第一个字节了
3、练习
cs
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 的原码:1000 0001,反码:1111 1110,补码:1111 1111
b 的原码,反码,补码与 a 一致
c 的原码:1000 0001 ,反码:1111 1111,补码:1111 1111
%d:打印十进制的整型。
所以在用 %d 打印 a 的时候,会认为 a 是一个整型,但整型有4个字节,而 a 只有一个字节,怎么办?这时会出现一个名词:整型提升
%d 会把 a 认为成 11111111 11111111 11111111 11111111,但这个是补码,对应的原码为
10000000 00000000 00000000 00000001,打印出来就是 -1 ,b 同理
c为无符号的,那么%d在整型提升的时候就会认为其是无符号的,认为c是
00000000 00000000 00000000 11111111 ,该数现在是正数,原码根补码是一样的,化为十进制就是255
所以改题目的答案为:

4.char 和 unsigned char 的取值范围
char 类型占一个字节,共8个比特位,从0000000开始,计数到11111111,换成十进制就是0~255,但这只是256个数,具体情况我们下面分析

从上图可以看到,char 类型的取值范围是 -128 ~127,共256个数。//这里画图有失误
至于unsigned char 类型,则只需把负数部分当成正数即可

所以unsigned char 的取值范围为 0 ~ 255
5、练习
cs
int main()
{
char a = -128;
printf("%u\n" ,a);
return 0;
}
a 的补码:1000 0000 ,%u 是用来打印无符号整型的 ,
整型提升后为:11111111 11111111 11111111 10000000,但%u认为该数是无符号的,为大于0的数,所以认为该码不是补码,而是原码,其转化为十进制为:4294967168
由上面两个练习可以看出,整型提升默认提升的是补码的最高位,最高位是几,整型提升的时候就补几,但是,如果这个数据本身的类型是无符号的,就只会补0,如果这个数本身有符号 ,但是打印的关键字为%u等无符号的,那么还是默认补最高位
6、练习
cs
int main()
{
char arr[1000];
int i;
for (int i = 0; i < 1000; i++)
{
arr[i] = -1 - i;
}
printf("%d" ,strlen(arr));
return 0;
}
arr 是一个字符数组,前面提到 char 类型的范围为 -128 ~ 127 ,
而且是从0~127 再到 -128 ~ -1 在到 0如此循环的
strlen 函数 只需要接收字符串的首地址,遇到 \0 截止,所以我们需要找到 \0 的位置
arr[0] = -1 ;arr[1] = -2; ...... ; arr[127] = -128; arr[128] = 127; arr[129] = 126......; arr[255] = 0;
可见,第一个 \0 出现在 arr[255] 处,所以strlen的长度是从 arr[0] 到 arr[254] 共255

7、练习
cs
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("I hate her\n");
}
return 0;
}
前面提到过 unsigned char 的范围为 0~ 255,到达255 后又回到 0,
所以这个题 i 永远都符合循环条件,本题为死循环
8、练习
cs
//x86环境下,小端字节序
int main()
{
int arr[4] = { 1,2,3,4 };
int* ptr1 = (int*)(&arr + 1);
int* ptr2 = (int*)((int)arr + 1);
printf("%x, %x" ,ptr1[-1] ,*ptr2 );
return 0;
}
//%x 是以16进制打印数据
&arr 取的是首元素地址,类型为:int (*) [4],+1 之后,指向数组末尾外的第一个空间,类型不变,然后强制类型转化为 int* ,所以 ptr[-1] 又指向了4 ,所以第一个打印(0x)4
强制类型转化的优先级比加法高一级,所以先进行强制类型转换
arr的类型为 int [4] ,,本身也是首元素 1 的地址,强制类型转化为 int ,然后再+1,相当于地址+1,(但是现在的地址只是一个普通变量),然后再强制类型转化为 int* 类型,现在变成一个int*类型的指针,指向 1 的第二个字节,如图:

所以 *ptr2 就是16进制下的 02000000 打印出来就是 (0x)2000000
三、浮点数在内存中的存储
常见的浮点数:3.1415926 ,1E10(1.0*10^-10)
在头文件 <flat.h> 中可以查看浮点数的表示范围
1、引例
cs
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n = %d\n",n);
printf("*pFlpat = %f\n" ,*pFloat);
*pFloat = 9.0;
printf("n = %d\n" ,n);
printf("*pFloat = %f\n" ,*pFloat);
return 0;
}
结果

这说明,浮点数的存储方式和整数的存储方式不一样
2、浮点数的存储
根据国际标准IEEE,任何浮点数可以表示为:
:表示符号位,当S = 0 时,表示正数, S = 1 ,表示负数
M :表示有效数字,M是大于等1,小于2的
:表示指数位
例如:
float 类型占4个字节,共32个比特位,在内存中的作用如下:

double类型也是与此相似,这里就不多说了
注意:E为无符号数,但是E会有负数的情况出现,所以存放一个E需要加上1271003 来达到所有的E都大于等于0,从而变为无符号数