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,恒满足循环条件,所以陷入了死循环。

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

目录
相关文章
|
23天前
|
监控 算法 应用服务中间件
“四两拨千斤” —— 1.2MB 数据如何吃掉 10GB 内存
一个特殊请求引发服务器内存用量暴涨进而导致进程 OOM 的惨案。
|
22天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
46 1
|
27天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
29天前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
69 1
|
1月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
37 2
|
23天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
19 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
7天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
23 6
|
27天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10
|
20天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。