C语言进阶-数据在内存中的存储(1)

简介: C语言进阶-数据在内存中的存储(1)

1.数据类型介绍

前面我们已经学习了基本的内置类型:

char        //字符数据类型

short       //短整型

int         //整形

long        //长整型

long long   //更长的整形

float       //单精度浮点数

double      //双精度浮点数

以及它们所占存储空间的大小依次为:1,2,4,4/8,8,4,8(单位是字节),其中的长整型(long),C语言中只规定了sizeof(long)>sizeof(int),但是具体是4个字节还是8个字节由编译器决定。

那为什么整型又要分为长整型、短整型等呢?

我们说每个类型开辟的内存空间的大小不同,大小决定了使用范围,例如:要定义一个年龄的变量,年龄最大就只能到3位数了,而一个short类型的大小在-32768~32767之间,远远足够了。

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]

因为字符在存储时是ASCIl值,所以把字符型也归类于整型家族。

浮点数家族:

float

double

构造类型:

> 数据类型

> 结构体类型 struct

> 枚举类型  enum

> 联合类型  union

构造类型可以自己创建,数组类型也算是构造类型,比如我们构造如下三个数组:

int arr1[10];
  int arr2[5];
  char arr3[10];

它们的类型都不相同,分别是:int [10]、int [5]、char[10]。

指针类型:

int *pi

char *pc

float *pf

void *pv

空类型:

void表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型

2.整型在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的。

那接下来我们来谈谈数据在所开辟的内存中到底是如何存储的?

比如:

int a = 20;
  int b = -10;

创建两个整型变量,内存为它们分别开辟4个字节的空间。

那到底是如何存储的呢?

下面来了解一下:

2.1原码、反码、补码

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

三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”

正数的原、反、补码都相同。

负整数的三种表示方法各不相同:

原码:

直接将数值按照正负数的形式翻译成二进制就可以得到原码。

反码:

将原码的符号位不变,其他位依次按位取反就可以得到反码。

补码:

反码+1就得到补码。

关于原码、反码、补码,之前的章节中讲过,下面我们再来举两个例子:

int num1 = 10;//创建一个整型变量,在内存中开辟4个字节
  //4个字节--32个bit位
  //00000000000000000000000000001010 - 原码、反码、补码
  int num2 = -10;
  //负数
  //10000000000000000000000000001010 - 原码
  //11111111111111111111111111110101 - 反码
  //11111111111111111111111111110110 - 补码

对于整型来说,数据存放在内存中其实存放的是补码。

下面我们可以打开监视窗口观察一下:

本质上来说数据在内存中是以二进制的形式存储,但是VS中为了方便展示,显示的是16进制。学过计算机组成原理,大家应该都知道,4位二进制数可以化为1位十六进制数,而十六进制中的10~15用字母a~f表示。

其实我们将补码11111111111111111111111111110110化为16进制数就是:ff ff ff f8.

这时我们就可以发现内存中存储的是补码,但是内存存储的是f8 ff ff ff ,与我们转换出来的刚好是倒着存储的,这又是为什么呢?

后文我们会讲到。

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

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;

同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

例如我们要算 1 - (-1)

如果我们用原码计算就会发现,算出来的结果是-2,显然不对。

而用补码计算就能算出正确的值:

//计算 1 -(-1)
  //原码计算:
  //00000000000000000000000000000001   1的原码
  //10000000000000000000000000000001   -1的原码
  //10000000000000000000000000000010  相加结果是-2
  //补码计算:
  //00000000000000000000000000000001   1的补码
  //11111111111111111111111111111111   -1的补码
  //00000000000000000000000000000000   相加结果是0

接下来,我们就讲一讲为什么会倒着存储。

2.2大小端介绍

什么是大端小端:

大端(存储)模式,是指将数据的低位保存在内存的高位地址中,而数据的高位,保存在内存的低地址中。

小端(存储)模式,是指将数据的高位保存在内存的低位地址中,而数据的高位,保存在内存高地址中。

如下图所示:

注意上图中我们讨论的是字节序存储,即以字节为单位讨论存储顺序。两位十六进制数就是一个字节。

有人对数据的高位和低位不太明白,其实很简单,就像十进制中的123,个位的3就是其低位,百位的1就是其高位,类比一下,上面的十六进制数0x11223344,44就是低位的字节,11就是高位的字节。

那这里有个问题,一个char类型的数据它有存储顺序吗?

答案是没有的,因为char类型的数据只有一个字节,不管怎么存储,顺序都是一样的,它不需要存储顺序。

学了大端小端的概念后,我们来设计一个小程序来判断当前的机器的字节序是大端还是小端。

我们先来分析一下设计思路,在这里我们可以定义一个变量a,令其初始化为1,它的十六进制应该是:0x00 00 00 01,如果是大端字节序存储,存储顺序应该是00 00 00 01,而小端字节序存储,存储顺序应该是01 00 00 00 ,我们只要看它的在内存中第一个字节是不是等于1,如果等于1就是小端字节序存储,如果等于0就是大端字节序存储。

那要判断第一个字节是不是等于1,就要先将其取出来,前面我们学过,可以将&a赋给一个指针,然后解引用指针就可以得到数据,但是因为该指针是int*型,通过解引用,一次取出的是4个字节,我们只需要判断一个字节,这时可以将它强制类型转换为char*,就取出第一个字节了。然后进行解引用并判断,即*(char*)&a == 1

代码实现如下:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int check_sys()
{
  int a = 1;
  int* p = &a;
  if (*(char*)p == 1)
    return 1;
  else
    return 0;
}
int main()
{
  int ret = check_sys();
  if (ret == 1)
    printf("小端");
  else
    printf("大端");
  return 0;
}

当然,我们也可以简单优化一下:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int check_sys()
{
  int a = 1;
  return *(char*)&a;
}
int main()
{
  int ret = check_sys();
  if (ret == 1)
    printf("小端");
  else
    printf("大端");
  return 0;
}

2.3练习

上文讲到整型家族,对于整型家族的类型来说,有:有符号和无符号的区分。

例如:short == signed short(有符号短整型),而无符号短整型就是 unsigned short。

int == signed int (有符号整型),而无符号整型就是 unsigned int。

但是char到底是 signed char 还是 unsigned char 不确定,C语言并没有明确规定,但是通常我们见到的VS编译器上是signed char。

而有符号数和无符号数的区别就是:有符号数的第一位二进制位会被看成符号位,其他的位是数值位。而无符号数没有符号位,全部的二进制位都是数值位。(如下图所示)

以上是char和unsigned char 的取值范围,以此类推,我们也可以知道short的取值范围是:-32768~32767,unsigned short的取值范围是:0~65535。

这就是有符号和无符号的区别,下面我们来做几道练习:

练习1:

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

运行结果:

为什么会输出这样一个结果呢?

下面我们来分析一下:

a在内存中的补码形式是:11111111111111111111111111111111,由于是char类型所以会发生截断:11111111,而打印时的%d打印的是十进制的有符号整型整数,此时要进行整型提升(有符号数根据符号位补足32bit位),11111111111111111111111111111111,接着将补码化为原码是:10000000000000000000000000000001(即十进制的-1)。

上文讲过,char就是signed char,所以b输出的也是-1。而c是unsigned char型,整型提升时无符号数直接补0,00000000000000000000000011111111,此时输出就是255。

整型提升前面讲过,再来总结一下:

整型提升根据数据本身的类型,有符号型按符号位补足32个bit位,无符号型直接补0

练习2:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
  char a = -128;
  printf("%u\n", a);
  return 0;
}

运行结果:

下面我们也来分析一下:

a的补码形式是:11111111111111111111111110000000 。char型发生截断:10000000。因为%u输出的是十进制的无符号整型整数,所以这里要整型提升,11111111111111111111111110000000 此时虽然依然是补码形式,但是我们要打印的是无符号数,这里就可以直接将其当做无符号数的原码,那此时输出的无符号整数就是4394967168。

练习3:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
  char a = 128;
  printf("%u\n", a);
  return 0;
}

这次将a由-128换成了128,那么打印出的结果是什么呢?

分析一下会发现,在128的补码发生截断之后,保留的8位和-128截断之后的一样,都是10000000,那么它们最终的结果应该相同,也是4394967168

练习4:

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

运行结果:-10

i的补码形式是:11111111111111111111111111101100

j的补码形式是:00000000000000000000000000001010

相加后的补码:11111111111111111111111111110110

化为原码:10000000000000000000000000001010,即十进制的-10

练习5:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
  unsigned int i;
  for (i = 9; i >= 0; i--)
  {
    printf("%u\n", i);
  }
  return 0;
}

运行结果:

我们可以发现上述代码打印完1~0之后,开始陷入死循环,为什么会陷入死循环呢?为什么后面打印的数字这么大?

因为i是unsigned int 型,无符号数永远不可能小于0,它一直满足循环条件,所以会一直循环下去。而当循环到 i = -1的时候,-1的补码是11111111111111111111111111111111,它会被当做一个无符号整数看待,32bit的1化为十进制数就是4294967295

练习6:

#define  _CRT_SECURE_NO_WARNINGS 1
#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", strlen(a));
  return 0;
}

运行结果:

为什么是这个结果呢?

首先来看循环体,本来循环1000次后应该会将-1 ~ -1000的数字赋给数组a[1000],但是此时的数组是char型的,上文讲过,char的范围是-128~127,所以不可能将所有的数都赋给数组,-1截断后应该是11111111,第一次循环a[1]的值是11111110,而每次循环相当于在上次的基础上减一操作,直到a[127] = -128时,它的补码是10000000,再次减一后会变成01111111,即127,接着循环,126 125 124......直到a[255] = 0,减一之后又变成 -1,接着-2 -3 -4......-127 -128 127 126......1 0

而strlen是计算 \0 之前的字符串的长度,\0 和 0 的ASCII值都是0,所以在上述代码中计算的只是循环第一次到0之前的长度。

下图给出char类型数值的轮回过程,以及上述代码的解析图。

练习7:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
unsigned char i = 0;
int main()
{
  for (i = 0; i <= 255; i++)
  {
    printf("hello world\n");
  }
  return 0;
}

运行结果应该是hello world 死循环打印。

原因也很简单,上文我们说过unsigned char 的取值范围是0~255,恒满足循环条件,所以陷入了死循环。

今天就学到这里,未完待续。。。

目录
相关文章
|
10天前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
24 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
14天前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
35 6
|
21天前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
81 12
|
21天前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
46 11
|
13天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
37 1
|
15天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
21天前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
45 11
|
18天前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
2月前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
82 1

热门文章

最新文章