C语言 数据的存储

简介: C语言 数据的存储

一、数据与进制之间的关系



我们都知道,计算机存储的数据单位是二进制,要么是 0,要么是 1. 实际上,计算机就是用这种二进制序列来表示某个数值。


但我们也要理解与电子信息数据相关的其他表示方法:十进制、十六进制、八进制。因为在 C语言 中,常常需要用到将这些进制进行一定的转换。


十进制:    (0 - 9)
二进制:    (0 1)
八进制:    (0 - 7)
十六进制: (0 - 9 a b c d e f)


1. 十进制与二进制之间的转换


下图是数据为 11 的十进制与二进制之间的转换。此外,十进制与十六进制、十进制与八进制相互转换的过程也是同理。


b13043b0a2804ba6b9b0742b390177af.png


2. 二进制与十六进制之间的转换


1 个字节 8 位 二进制,恰好可以用两个十六进制数据表示。


a32d0b2faccc4a81bb57f48599d63522.png


二、整型数据存储



1. 原、反、补码


计算机中的整数有三种表示方法,即原码、反码和补码。

三种表示方法均有符号位和数值位两部分。符号位 0 表示正,1 表示负;数值位就是正常的 0/1 序列。


原码:直接将原数据按照正负数转换成二进制。

反码:原码的符号位不变,其他位依次按位取反。

补码:反码 +1.


2. 整型数据在内存中的存储


int 类型的 10,与 int 类型的 -10 在内存中的存储如下:


83cb800394a64dfc984ab456970d4598.png


从上面的图上看,我们可以得出结论:


① 整型数据存放内存中的是二进制补码。

② 正整数的原、反、补码是相同的;但负整数的原、反、补码则需要计算。

③ printf 格式化输出的是数据的原码。


3. 为什么整型数据存在内存中存储的是补码


注意: CPU只有加法器,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。这样一来,使用补码,则可以将符号位和数值域统一处理。


我们就拿 1 + (-1) = 0 来举例:


// 1 - 1 <==> 1 + (-1)
 00000000 00000000 00000000 00000001  -> 1的原、反、补码
 10000000 00000000 00000000 00000001  -> -1的原码
 11111111 11111111 11111111 11111110  -> -1的反码
 11111111 11111111 11111111 11111111  -> -1的补码


// 错误的算法(使用原码相加)
 00000000 00000000 00000000 00000001  -> 1的原码
 +
 10000000 00000000 00000000 00000001  -> -1的原码
 10000000 00000000 00000000 00000010  -> 数值为 -2
// 正确的算法(使用补码相加)
 00000000 00000000 00000000 00000001  -> 1的补码
 +
 11111111 11111111 11111111 11111111  -> -1的补码
100000000 00000000 00000000 00000000  -> 数值为 0(最前面的1 舍去)


从结果来看,CPU 加法器对原码直接运算产生的结果是错误的,而采用补码是正确的。


4. 有符号和无符号的数据类型


char
unsigned char
signed char
short <==> signed short   // 有符号短整型
unsigned short        // 无符号短整型
int <==> signed int     // 有符号整型
unsigned int        // 无符号整型
long <==> signed long     // 有符号长整型
unsigned long       // 无符号长整型


注意事项:


① unsigned 代表无符号类型,signed 代表有符号类型。如果没有特殊说明,一般就表示有符号类型。( 例如:int 就等价于 signed int 类型,即有符号整型。short、long 也默认为是有符号类型。但 char 官方并没有说明默认是有符号类型,这取决编译器的实现。)


② 有符号类型的二进制最高位是符号位,无符号类型的二进制最高位依然是数据位。


有符号和无符号的存储范围


我们以 char 类型的有符号和无符号对比, char 类型是一个字节,即 8 个比特位。


d40459dd85514bd393a0f033333245c1.png


通过上图分析,我们可以看到有符号 char 类型的数据存储范围:-128 ~ 127,而 无符号 char 类型的数据存储范围:0 ~ 255. 类比 short、int、long 类型的数据范围也是这么计算来的。


猜想


理解了上面的有符号和无符号原理后,如果我们将一个负数放进一个无符号类型中,那么结果会发生什么事情呢?


程序清单:


#include <stdio.h>
int main() {
  unsigned char a = -10;
  printf("%u\n", a); // %u 为无符号打印
  return 0;
}
// 输出结果:246

从输出结果来看,-10 依然是按照二进制补码 11110110 存储至内存中的,只是在最后输出的时候,程序将 -10 的补码视为 -10 的原码直接就打印出来了,因为无符号整型本身就是一个不存在负数的类型,所以程序就视为原、反、补码相同才输出的。


f8275d9e863d400c820456f22c42de21.png


所以输出结果并不是 -10,而是直接将其视为无符号二进制计算出的结果。


12a661dc5d774cea8a3ca639e34fa4e5.png


5. 关于 char 类型


char 类型占用内存的大小为 1 个字节,即 8 个比特位。而我们一般普遍认为 char 是字符类型。但实际上字符类型在底层存储字符的时候,存储却是字符对应的 ASCII 码值,所以我们依然可以将 char 类型视为整型。


字符 ’ A ’ 在内存中存储的二进制补码如下所示:实际上 ’ A ’ 的 ASCII 码值为 65,系统再转换成对应的二进制序列放入了内存中,为了方便显示,以十六进制显示在我们的面前。


1cbf53e490794dd7ac9d6faa67da9129.png


三、大小端存储



ff76487b58df4eb187aa06a6f9a2a007.png


在上图,我们可以看到局部变量 a,在内存中存储的倒序的字节数据。这是为什么呢?其实这是在 VS 底下的编译器,它采用的是小端存储模式。


注意: 计算机在内存中存储数据是二进制序列,但是 VS 编译器为了方便我们观察,采用了十六进制显示序列,2个十六进制位对应 8个二进制位,对应1个字节.


1. 两种存储方式的区别


大端存储方式:数据的低位字节保存在内存的高地址中,而数据的高位字节保存在内存的低地址中。


小端存储方式:数据的低位字节保存在内存的低地址中,而数据的高位字节保存在内存的高地址中。


299c6712d3aa43f98d79f7ac78dc08e6.png


2. 设计一个程序来判断当前编译器的字节序存储


设计思路:利用 char* 指针来找到整个数据的第一个字节,从而判断是否为对应的字节值即可。( 例如:0x 00 00 00 01,如果是小端存储,必然第一个字节取出的是低位 01;反之如果是大端存储,必然第一个字节取出的是高位 00. )


注意: 利用指针访问、解引用都是从低地址往高地址操作的。


775d9b71b7f34ec1a43f4b42e335c336.png

程序清单:


#include <stdio.h>
int main() {
  int a = 0x00000001;
  char* p = (char*) & a;
  if (*p == 1) {
    printf("小端存储\n");
  }else {
    printf("大端存储\n");
  }
  return 0;
}


四、整型提升



由于表达式的整型运算需要在 CPU 的相应运算器件内执行,而 CPU 内整型运算器(ALU) 的操作数的字节长度一般是 int 类型的字节长度,同时也是 CPU 的通用寄存器的长度。因此,即使两个 char 类型的变量相加,在 CPU 执行时也要先转换为 CPU 内整型操作数的标准长度。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int 或 unsigned int,然后才能送入 CPU 去执行运算。


注意:

① 对于有符号类型,整形提升是按二进制最高位补全。

② 对于无符号类型,整型提升直接按 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", a, b, c);
  return 0;
}
// 输出结果:-1 -1 255


运算过程:


① 变量 a 的运算过程 (变量 b 也是同理)


// 1. 将 -1 数据放入变量 a 的内存中
10000000 00000000 00000000 00000001   -> -1 原码
11111111 11111111 11111111 11111110   -> -1 反码
11111111 11111111 11111111 11111111   -> -1 补码
11111111 // 截断成 char 类型,放入变量 a 中(补码)
// 2. 整型提升,由于变量 a 是有符号类型,所以按最高位补全
11111111 11111111 11111111 11111111   -> 新的补码
// 3. %d 打印,输出一个有符号的整型数据
11111111 11111111 11111111 11111111   -> 新的补码 
11111111 11111111 11111111 11111110   -> 新的反码 
10000000 00000000 00000000 00000001   -> 新的原码 (最终输出 -1)


② 变量 c 的运算过程


// 1. 将 -1 数据放入变量 c 的内存中
10000000 00000000 00000000 00000001   -> -1 原码
11111111 11111111 11111111 11111110   -> -1 反码
11111111 11111111 11111111 11111111   -> -1 补码
11111111 // 截断成 char 类型,放入变量 c 中(补码)
// 2. 整型提升,由于变量 c 是无符号类型,所以按 0 补全
00000000 00000000 00000000 11111111   -> 新的原、反、补码
// 3. %u 打印,输出一个无符号的整型数据
00000000 00000000 00000000 11111111   -> 不存在负数,直接输出 255


程序清单2


#include <stdio.h>
int main() {
  char a = 3;
  char b = 127;
  char c = a + b;
  printf("%d\n", c);
  return 0;
}
// 输出结果:-126


计算过程:


// 1. 将 a + b 的结果数据放入变量 c 的内存中
00000000 00000000 00000000 00000011   -> 3 的原、反、补码
00000011 // 截断成 char 类型,放入变量 a 中(补码)
00000000 00000000 00000000 01111111   -> 127 的原、反、补码
01111111 // 截断成 char 类型,放入变量 b 中(补码)
// 2. 整型提升,由于变量 a, b 是有符号类型,所以按最高位补全
00000000 00000000 00000000 00000011   -> 3 的补码
+
00000000 00000000 00000000 01111111   -> 127 的补码
=
00000000 00000000 00000000 10000010   -> 新的补码
10000010  // a + b 的结果,截断成 char 类型,放入变量 c 中(补码)
// 3. 整型提升,由于变量 c 是有符号类型,所以按最高位补全
11111111 11111111 11111111 10000010   -> 新的补码
// 4. %d 打印,输出一个有符号的整型数据
11111111 11111111 11111111 10000010   -> 新的补码
11111111 11111111 11111111 10000001   -> 新的反码
10000000 00000000 00000000 01111110   -> 新的原码 (最终输出 -126)


程序清单3


#include <stdio.h>
int main()
{
  char a = -128;
  char b = 128;
  printf("%u %u\n", a, b); 
  return 0;
}
// 输出结果:4294967168  4294967168


运算过程:变量 a


// 1. 将 -128 数据放入变量 a 的内存中
10000000 00000000 00000000 10000000   -> -128 原码
11111111 11111111 11111111 01111111   -> -128 反码
11111111 11111111 11111111 10000000   -> -128 补码
10000000 // 截断成 char 类型,放入变量 a 中(补码)
// 2. 整型提升,由于变量 a 是有符号类型,所以按最高位补全
11111111 11111111 11111111 10000000
// 3. %u 打印,输出一个无符号的整型数据
11111111 11111111 11111111 10000000   -> 不存在负数,直接输出 4294967168


运算过程:变量 b


// 1. 将 128 数据放入变量 b 的内存中
00000000 00000000 00000000 10000000   -> 128 原、反、补码
10000000 //截断成 char 类型,放入变量 b 中(补码)
// 2. 整型提升,由于变量 b 是有符号类型,所以按最高位补全
11111111 11111111 11111111 10000000   -> 新的补码
// 3. %u 打印,输出一个无符号的整型数据
11111111 11111111 11111111 10000000   -> 不存在负数,直接输出 4294967168


程序清单4


#include <stdio.h>
int main() {
  int i = -20;
  unsigned int j = 10;
  printf("%d\n", i + j);
  return 0;
}


运算过程:


10000000 00000000 00000000 00010100   -> -20 原码
11111111 11111111 11111111 11101011   -> -20 反码
11111111 11111111 11111111 11101100   -> -20 补码
00000000 00000000 00000000 00001010   -> 10 原、反、补码
// 1. i + j 的计算过程
11111111 11111111 11111111 11101100   -> -20 补码
+
00000000 00000000 00000000 00001010   -> 10 补码
=
11111111 11111111 11111111 11110110   -> 新的补码
// 2. %d 打印,输出一个有符号的整型数据
11111111 11111111 11111111 11110110   -> 新的补码
11111111 11111111 11111111 11110101   -> 新的反码
10000000 00000000 00000000 00001010   -> 新的原码 (最终输出 -10)


程序清单5


#include <stdio.h>
#include <Windows.h>
int main() {
  unsigned int i;
  for (i = 9; i >= 0; i--)
  {
    printf("%u\n", i);
    Sleep(1000); // 休眠 1 秒
  }
  return 0;
}
// 输出结果:9  8  7  6  5  4  3  2  1  0  4294967295  4294967294...


运算过程:


00000000 00000000 00000000 00001001   -> 9 的原、反、补码
...
10000000 00000000 00000000 00000001   -> - 1 原码
11111111 11111111 11111111 11111110   -> - 1 反码
11111111 11111111 11111111 11111111   -> - 1 补码 (4,294,967,295)
// 当 -1 放入无符号变量 i 中时,此时程序就不将其视为负数了,
// 所以最终将其视为无符号直接输出,即所有的二进制补码序列全是数据位


程序清单6


#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\n", strlen(a));
  return 0;
}
// 输出结果:255


首先,我们得明白 strlen 是用来求字符串的长度的 ( ’ \0 ’ 之前的),而 ’ \0 ’ 的 ASCII 码也是 0. 所以对于上面的程序,当字符数组中间的元素出现 字符0 时, 就意味着 strlen 函数计算字符串长度已经到头了。


计算过程:


截断发生的过程:


// i = 0
10000000 00000000 00000000 00000001   -> -1 原码
11111111 11111111 11111111 11111110   -> -1 反码
11111111 11111111 11111111 11111111   -> -1 补码
11111111 // 截断放入 a[0] 中(补码)
// i = 1
10000000 00000000 00000000 00000010   -> -2 原码
11111111 11111111 11111111 11111101   -> -2 反码
11111111 11111111 11111111 11111110   -> -2 补码
11111110 // 截断放入 a[1] 中(补码)
// i = 2
10000000 00000000 00000000 00000011   -> -3 原码
11111111 11111111 11111111 11111100   -> -3 反码
11111111 11111111 11111111 11111101   -> -3 补码
11111101 // 截断放入 a[2] 中(补码)
...
...


经过上面的分析,可以总结出,字符数组中第 256 位被放入的是 0,所以在第 256 位之前,就是应该 strlen 所计算的字符串长度。


// 字符数组中每个字符存储的二进制补码
11111111    -> -1 补码
11111110    -> -2 补码
11111101    -> -3 补码
...
...
00000001    
00000000 // 第 256 位补码


或者我们也可以如下分析:

无符号的 char 类型的范围:-128 ~ 127


f290266bc4cb44fe91ecb979b1683909.png


程序清单7


#include <stdio.h>
#include <Windows.h>
unsigned char i = 0;
int main()
{
  for (i = 0; i <= 255; i++)
  {
    printf("hello world, %d\n", i);
    Sleep(100); // 休眠 0.1 秒
  }
  return 0;
}
// 输出结果:0 - 255, 0 - 255, 0 - 255 ...



运算过程:


有符号的 char 类型的范围:0 ~ 255


 00000000     -> 0 补码
 00000001     -> 1 补码
 ...
 ...
 11111111     -> 255 补码
100000000     -> 256 补码 -> 截断成 00000000
 00000001     -> 1 补码
 ...
 ...


目录
相关文章
|
2天前
|
存储 程序员 编译器
C 语言中的数据类型转换:连接不同数据世界的桥梁
C语言中的数据类型转换是程序设计中不可或缺的一部分,它如同连接不同数据世界的桥梁,使得不同类型的变量之间能够互相传递和转换,确保了程序的灵活性与兼容性。通过强制类型转换或自动类型转换,C语言允许开发者在保证数据完整性的前提下,实现复杂的数据处理逻辑。
|
6天前
|
存储 数据建模 程序员
C 语言结构体 —— 数据封装的利器
C语言结构体是一种用户自定义的数据类型,用于将不同类型的数据组合在一起,形成一个整体。它支持数据封装,便于管理和传递复杂数据,是程序设计中的重要工具。
|
3月前
|
存储 编译器 C语言
C语言存储类详解
在 C 语言中,存储类定义了变量的生命周期、作用域和可见性。主要包括:`auto`(默认存储类,块级作用域),`register`(建议存储在寄存器中,作用域同 `auto`,不可取地址),`static`(生命周期贯穿整个程序,局部静态变量在函数间保持值,全局静态变量限于本文件),`extern`(声明变量在其他文件中定义,允许跨文件访问)。此外,`typedef` 用于定义新数据类型名称,提升代码可读性。 示例代码展示了不同存储类变量的使用方式,通过两次调用 `function()` 函数,观察静态变量 `b` 的变化。合理选择存储类可以优化程序性能和内存使用。
161 82
|
2月前
|
存储 C语言 C++
深入C语言,发现多样的数据之枚举和联合体
深入C语言,发现多样的数据之枚举和联合体
深入C语言,发现多样的数据之枚举和联合体
|
2月前
|
存储 C语言
C语言中的浮点数存储:深入探讨
C语言中的浮点数存储:深入探讨
|
2月前
|
存储 C语言
深入C语言内存:数据在内存中的存储
深入C语言内存:数据在内存中的存储
|
2月前
|
C语言
回溯入门题,数据所有排列方式(c语言)
回溯入门题,数据所有排列方式(c语言)
|
3月前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
435 8
|
3月前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
3月前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。