?? 寫在前面
經過上篇博客的學習,你已經知道了資料的運算, 那資料在記憶體中又是如何存盤的呢?
今天bug郭就帶你一起學習資料在記憶體中的儲存!
點擊目錄跳轉
- :scissors: 寫在前面
- :100: 本章重點
- :book: 資料型別介紹
- :eye:內置型別
- 型別的基本歸類
- :tm:整形在記憶體中的存盤
- :heavy_check_mark: 大小端
- :old_key:判斷大小端
- :punch: 小試牛刀
- **練習題目**
- **:pushpin: 練習講解**
- :star: 重點歸納總結
- :sweat_drops: 浮點型在記憶體中的存盤
- :thumbsup: 浮點數存盤方式介紹
- :trophy: 總結
💯 本章重點
- 資料型別詳細介紹
- 整形在記憶體中的存盤:原碼、反碼、補碼
- 大小端位元組序介紹及判斷
- 浮點型在記憶體中的存盤決議
📖 資料型別介紹
那些我們學過的C語言資料型別,你還記得多少,我們一起來整理一一下吧📖
👁內置型別
char //字符型 1byte
int //整型 4byte
short//短整型2byte
long //長整型4/8byte
long long //更長的整型8byte
float //單精度浮點型 4byte
double//雙精度浮點型8byte
//C語言中無字串型別
型別的意義
之前的博客中已經介紹過了
- 型別可以決定該型別的變數在記憶體中創建記憶體空間的大小
- 型別可以決定指標訪問的權限,加減指標的位移
- …
我們可以根據我們變數的大小合理選擇型別,創建空間大小,

不同的資料型別根據它們的位元組大小,需要占用不同空間大小的記憶體空間
型別的基本歸類
整型家族
char
signed char
unsigned char
short
signed short [int]
unsigned short [int]
int
signed int
unsigned int
long
signed long [int]
unsigned long [int]
long long
signed long long [int]
unsigned long long [int]
注意:字符型也歸類為整型家族,每個型別都有有符號型別和無符號型別,
浮點數家族
float
double
構造型別
//結構體型別
struct
//列舉型別
enum
//聯合型別
union
指標型別
char*
int*
float*
void*
空型別
void
void空型別
通常使用在函式的引數,回傳值,指標,
??整形在記憶體中的存盤
我們之前講過一個變數的創建是要在記憶體中開辟空間的,空間的大小是根據不同的型別而決定的,
那接下來我們來看看整型是如何存盤的,
例如:
int a=1;
int b=-3;
我們已經知道整型占用記憶體空間為4個位元組,那么是如何分配儲存的!
我們先來了解一下計算機中有符號數的三種表示方法:
原碼,反碼,補碼
- 計算機中有符號數有三種表示方法,原反補,
- 這三種表示方法,都是由符號位和數值位組成,符號位
1表示負數,0表示正數,數值位各不相同!
原碼
直接將資料通過二進制正負的形式翻譯過來的的二進制位
反碼
由原碼,符號位不變,數值位按位取反,
補碼
反碼
+1得到補碼!
正數的原反補相同
資料是以補碼的形式在記憶體中存盤
為啥是補碼呢?
學過計算機原理的同學肯定了解,因為計算機的CPU中運算器(ALU)只能進行加法!所以負數要轉化成加法運算,而補碼很好的解決了這個問題!
?? 大小端

根據我們之前博客的學習👁,避免bug,除錯技巧我們已經知道了,除錯視窗,可以查看變數的地址和記憶體,我們&x可以查看到x在計算機中記憶體的儲存,
int x=1;
//x為整型有32二級制位
//而每4個二進制位是一個16進制位,
//x=1的16進制表示方法:00 00 00 01
而我們看到vs下x的記憶體,低位01卻存在最左邊,
為啥會存到最左邊呢?
我們可以看到x占用4個位元組空間,地址從左往右依次遞增!低地址存低位位元組資料,高地址存高位位元組資料,
這就是我們所介紹的小端存盤,
而大端存盤,不言而喻就是,高地址存低位,低地址存高位!
總結:
大端(存盤)模式,是指資料的低位保存在記憶體的高地址中,而資料的高位,保存在記憶體的低地址中;
小端(存盤)模式,是指資料的低位保存在記憶體的低地址中,而資料的高位,,保存在記憶體的高地址中,
為啥會有大小端
為什么會有大小端模式之分呢?這是因為在計算機系統中,我們是以位元組為單位的,每個地址單元都對應著一個位元組,一個位元組為
8bit,但是在C語言中除了8bit的char之外,還有16bit的short型,32bit的long型(要看具體的編譯器),另外,對于位數大于8位的處理器,例如16位或者32位的處理器,由于暫存器寬度大于一個位元組,那么必然存在著一個如果將多個位元組安排的問題,因此就導致了大端存盤模式和小端存盤模式,
例如一個16bit的short型x,在記憶體中的地址為0x0010,x的值為0x1122,那么0x11為高位元組,0x22為低位元組,對于大端模式,就將0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中,小端模式,剛好相反,我們常用的X86結構是小端模式,而KEIL C51則為大端模式,很多的ARM,DSP都為小端模式,有些ARM處理器還可以由硬體來選擇是大端模式還是小端模式,
總結:
計算機暫存器寬度大于 一個位元組,那么就多個位元組型別資料的存盤就產生了不一樣的大小端存盤模式,
🗝判斷大小端
我們已經知道有大小端兩種存盤模式,而我們要如何判斷一臺機器是小端存盤,還是大端儲存呢?也就是判斷當前機器的位元組序?
我們可以設計幾個程式,來驗證該不同機器的位元組序,
設計思路
我們可以想辦法將某一地址處存的位元組資料拿出即可判斷,如果高地址低位元組位,說明是小端存盤,否者就是大端存盤模式,
//代碼一
//利用char*指標得到低地址的位元組資料
#include<stdio.h>
int main()
{
int a=1;
int *pa=&a;
//利用char*存盤a第一個位元組的低地址
char*pc=(char*)pa;
printf("%d",*pc);//訪問這個位元組的地址,列印資料
return 0;
}

低地址列印了低位元組位,說明bug郭的機器是采用小端存盤模式!
我們剛剛是說寫個程式,判斷位元組序,所以我們需要封裝一下!
//代碼1
#include <stdio.h>
int check_sys()
{
int i = 1;
return (*(char *)&i);
}
int main()
{
int ret = check_sys();
if(ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
我們之前還了解到了一個C語言自定義型別聯合體,我們后期還會詳細介紹!
聯合體就是一塊空間,多個變數聯合使用,共同占用一塊空間!當我們訪問其中一個變數,該空間就存盤著該變數!
我們可以利用聯合體這一特性來判斷位元組序
//代碼2
int check_sys()
{
union
{
int i;
char c;
}un;
un.i=1;
return un.c;
}

學會了嗎,這就是大小端的判斷!

👊 小試牛刀
到這里我們已經學習了整型在記憶體中如何存盤,我們來寫幾個練習鞏固一下吧!
練習題目
下面一共7道題目
大家可以試著練習一下,我會給大家一一講解
//練習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;
}
//練習2
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
//練習3
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
//練習4
#include <stdio.h>
int main()
{
int i= -20;
unsigned int j = 10;
printf("%d\n", i+j);
//按照補碼的形式進行運算,最后格式化成為有符號整數
}
//練習5
#include <stdio.h>
int main()
{
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
return 0;
}
//練習6
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
//練習7
#include <stdio.h>
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
📌 練習講解
//練習1
//輸出什么?
#include <stdio.h>
int main()
{
char a = -1;
//-1 : 補碼 11111111 11111111 11111111 11111111
//截斷放入 a: 11111111
signed char b = -1;
//-1截斷放入b中 b: 11111111
unsigned char c = -1;
// 同理 c: 11111111
printf("a=%d,b=%d,c=%d", a, b, c);
//a和b是有符號字符型,%d列印整型提升補充符號位后
//補碼 11111111 11111111 11111111 11111111
//得到原碼:10000000 00000000 00000000 00000001
//所以a和b列印結果是-1
//而c是無符號字符型,所以整型提升,補充0
//00000000 00000000 00000000 11111111
//轉換原碼 00000000 00000000 00000000 11111111
//所以c的列印結果是 225
return 0;
}
運行結果

//練習2
#include <stdio.h>
int main()
{
char a = -128;
//-128 原碼:00000000 00000000 00000000 10000000
//補碼: 11111111 11111111 11111111 10000000
//截斷存入char a 中 10000000
printf("%u\n", a);
//%u無符號的形式列印
//a是有符號char 整型提升補充符號位
// 11111111 11111111 11111111 10000000
//而%u默認該資料為無符號資料,所以認為a的原碼補碼相同
//列印結果 4294967168
return 0;
}
運行結果

//練習3
#include <stdio.h>
int main()
{
char a = 128;
//128 00000000 00000000 00000000 10000000
//截斷存入char a :10000000
printf("%u\n", a);
//整型提升char a有符號,補符號位
//11111111 11111111 11111111 10000000
//%u無符號的形式列印,認為該資料原碼補碼相同
//列印 4294967168
return 0;
}
看到練習3的結果和練習2的結果一樣,一個是-128,一個是128
但以%u列印了一樣的結果!

因為無論是128還是-128截斷后存盤到a都是相同的二進制位!
//練習4
#include <stdio.h>
int main()
{
int i = -20;
//-20 原碼:10000000 00000000 00000000 00010100
//補碼 : 11111111 11111111 11111111 11101100
unsigned int j = 10;
//10 : 00000000 00000000 00000000 00001010
printf("%d\n", i + j);
//i+j補碼:11111111 11111111 11111111 11110110
//原碼: 10000000 00000000 00000000 00001010
// 列印 -10
//按照補碼的形式進行運算,最后格式化成為有符號整數
}
運行結果

//練習5
#include <stdio.h>
int main()
{
unsigned int i;
//無符號int i 所以始終大于等于0
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
//無法退出回圈
}
return 0;
}
unsigned int范圍:0~2^32
代碼會發生死回圈!
運行結果

//練習6
#include<stdio.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
//a[i]是字符型,范圍為-128~127
//超過會進行截斷存入a[i]中
//當-129存入a[128]中截斷
//-129 :原 10000000 00000000 00000000 10000001
// 補碼 11111111 11111111 11111111 01111111
//截斷 后存入a[128]中 01111111
//此時a[128]符號位為0 故存入的為 127
//后面資料同理
//當a[255]=-256
// -256 原 10000000 00000000 00000001 00000000
//補碼 : 11111111 11111111 11111111 00000000
// 故此時a[255]存入的是 0
}
printf("%d", strlen(a));
//strlen 遇到'\0'停止計數,也就arr[255],所以回傳長度為255
return 0;
}

char中的范圍就是這樣的,所以但一個資料小于-128時下一個資料就是127 大于127下一個資料就是-128
運行結果

//練習7
#include <stdio.h>
unsigned char i = 0;
//unsigned char 范圍為0~255
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
//當i=255時,i++后,i回圈回到i=0
//所以該代碼會發生死回圈
}
return 0;
}

運行結果

這就是所以練習的答案了,是不是還有點意猶未盡!如果還沒學會可以多看幾遍!
?? 重點歸納總結
- 計算機中資料的存盤和計算都是以補碼的形式進行的!
- 整型提升還有截斷的物件也是針對補碼,
- 無符號整型提升,二級制位補充
0,有符號整型提升,二進制位補充符號位, %u(無符號列印)自動認為列印的資料是無符號資料,所以存盤的補碼也就是原碼,%d(有符號列印)認為列印的資料是有符號型別的,要將資料轉換成原碼列印輸出!
💦 浮點型在記憶體中的存盤
我們已經學會了整型在記憶體中的存盤,你肯定會好奇,浮點型資料該怎樣存盤在記憶體中呢?
常見的浮點數:
3.14159 1E10 2.7
浮點數家族包括:
float、double、long double 型別,
浮點數表示的范圍:
vs中float.h有詳細介紹浮點數的表示范圍,有興趣的伙伴可以期查閱一下,bug郭截取了一段供大家參考:
// float.h
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Implementation-defined values commonly used by sophisticated numerical
// (floating point) programs.
//
#pragma once
#ifndef _INC_FLOAT // include guard for 3rd party interop
#define _INC_FLOAT
#include <corecrt.h>
#pragma warning(push)
#pragma warning(disable: _UCRT_DISABLED_WARNINGS)
_UCRT_DISABLE_CLANG_WARNINGS
_CRT_BEGIN_C_HEADER
#ifndef _CRT_MANAGED_FP_DEPRECATE
#ifdef _CRT_MANAGED_FP_NO_DEPRECATE
#define _CRT_MANAGED_FP_DEPRECATE
#else
#ifdef _M_CEE
#define _CRT_MANAGED_FP_DEPRECATE _CRT_DEPRECATE_TEXT("Direct floating point control is not supported or reliable from within managed code. ")
#else
#define _CRT_MANAGED_FP_DEPRECATE
#endif
#endif
#endif
大家肯定會疑問,這是個啥,看不懂啊,其實bug郭也看不懂,哈哈哈,不過問題不大!

浮點型的其型別說明符有
float單精度說明符,double雙精度說明符,在Turbo C中單精度型占4個位元組(32位)記憶體空間,其數值范圍為3.4E-38~3.4E+38,只能提供七位有效數字,雙精度型占8個位元組(64位)記憶體空間,其數值范圍
1.7E-308~1.7E+308,可提供16位有效數字,
兄弟們,我們寫個代碼看看,你就了解了浮點型!
#include<stdio.h>
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值為:%d\n",n);
printf("*pFloat的值為:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值為:%d\n",n);
printf("*pFloat的值為:%f\n",*pFloat);
return 0;
}
輸出結果會是怎么樣的?會是像我們整形資料那樣分析嗎?
結果肯定是否定的!
運行結果:

可以看到列印結果完全出乎我們意料,num和*pFloat 在記憶體中明明是同一個數,為什么浮點數和整數的解讀結果會差別這么大?
要理解這個結果,一定要搞懂浮點數在計算機內部的表示方法,所以我們可以知道,浮點型資料和整型資料在計算機中有著不一樣的存盤方式!
👍 浮點數存盤方式介紹
根據國際標準
IEEE(電氣和電子工程協會)754,任意一個二進制浮點數V可以表示成下面的形式:
(-1)^S * M * 2^E(-1)^s表示符號位,當s=0,V為正數;當s=1,V為負數,M表示有效數字,大于等于1,小于2,2^E表示指數位,
舉例來說:
十進制的
5.0,寫成二進制是101.0,相當于1.01×2^2, 那么,按照上面V的格式,可以得出s=0,M=1.01,E=2,
十進制的-5.0,寫成二進制是-101.0,相當于-1.01×2^2,那么,s=1,M=1.01,E=2,
IEEE 754規定:
對于32位的浮點數,最高的1位是符號位s,接著的8位是指數E,剩下的23位為有效數字M,

對于
double64位的浮點數,最高的1位是符號位S,接著的11位是指數E,剩下的52位為有效數字M,

IEEE 754對有效數字M和指數E,還有一些特別規定,
資料的存入
前面說過,1≤M<2,也就是說,M可以寫成1.xxxxxx的形
式,其中xxxxxx表示小數部分,
IEEE 754規定,在計算機內部保存M時,默認這個數的第一位總是1,因此可以被舍去,只保存后面的xxxxxx部分,
比如保存1.01的時候,只保存01,等到讀取的時候,再把第一位的1加上去,這樣做的目的,是節省1位有效數字,
以32位浮點數為例,留給M只有23位,將第一位的1舍去以后,等于可以保存24位有效數字,
至于指數E,情況就比較復雜,
首先,E為一個無符號整數(unsigned int)這意味著,如果E為8位,它的取值范圍為0~255;如果E為11位,它的取值范圍為0~2047,但是,我們知道,科學計數法中的E是可以出現負數的,所以IEEE 754規定,存入記憶體時E的真實值必須再加上一個中間數,對于8位的E,這個中間數是127;對于11位的E,這個中間數是1023,比如,2^10的E是10,所以保存成32位浮點數時,必須保存成10+127=137,即10001001,
然后,指數E從記憶體中取出還可以再分成三種情況:
- E不全為
0或不全為1
這時,浮點數就采用下面的規則表示,即指數E的計算值減去127(或1023),得到真實值,再將有效數字M前加上第一位的1, 比如:0.5(1/2)的二進制形式為0.1,由于規定正數部分必須為1,即將小數點右移1位,則為1.0*2^(-1),其階碼為-1+127=126,表示01111110,而尾數1.0去掉整數部分為0,補齊0到23位
00000000000000000000000,則其二進制表示形式為:
00111111000000000000000000000000- E全為
0時
浮點數的指數E等于1-127(或者1-1023)即為真實值, 有效數字M不再加上第一位的1,而是還原為0.xxxxxx的小數,這樣做是為了表示±0,以及接近于0的很小的數字,- E全為
1
這時,如果有效數字M全為0,表示±無窮大(正負取決于符號位s);
好了,關于浮點數的表示規則,就說到這里,
學到這我們了解了浮點資料的存盤方式,就可以把剛剛的運行結果解釋清楚了!
決議
#include<stdio.h>
int main()
{
int n = 9;
//n 9 00000000 00000000 00000000 00001001
float* pFloat = (float*)&n;
//*pFloat :00000000 00000000 00000000 00001001
//利用存盤公式 (-1)^s * M *2^E
//所以 s 為 0為正數
//E 為 00000000 全為0 E無需減去127,M無需加上1.
//M 000000 00000000 00001001
//所以*pFloat 是一個無限接近0的數
printf("n的值為:%d\n", n);
//n的列印結果肯定是9
printf("*pFloat的值為:%f\n", *pFloat);
//所以列印結果為0.000000
*pFloat = 9.0;
//9.0 存入 float中
//9.0: 00001001.0 向左移動3位 00001.001*2^3
//s為0 E為3 3+127=130
// s 0
// E 10000010
// M 001后面添20個0補齊 00100000000000000000000
//所以存入 *pFloat 資料為
// 0 10000010 00100000000000000000000
printf("num的值為:%d\n", n);
//*pFloat解參考操作,將n的值也改變了
//補碼 01000001 00010000 00000000 00000000
//轉換成10進制 1091567616
printf("*pFloat的值為:%f\n", *pFloat);
return 0;
}

讓我們回到一開始的問題:
為什么0x00000009還原成浮點數,就成了0.000000?
首先,將0x00000009拆分,得到第一位符號位s=0,后面8位的指數E=00000000,最后23位的有效數字M=000 0000 0000 0000 0000 1001,
9->0000 0000 0000 0000 0000 0000 0000 1001
由于指數E全為
0,所以符合上一節的第二種情況,因此,浮點數V就寫成:V=(-1)^0×0.00000000000000000001001×2^(-126)=1.001×2^(-146)顯然,V是一個很小的接近于0的正數,所以用十進制小
數表示就是0.000000,
再看例題的第二部分,
請問浮點數9.0,如何用二進制表示?還原成十進制又是多少?
首先,浮點數9.0等于二進制的1001.0,即1.001×2^3,
那么,第一位的符號位s=0,有效數字M等于001后面再加20個0,湊滿23位,指數E等于3+127=130,即10000010, 所以,寫成二進制形式,應該是s+E+M,即
01000001 00010000 00000000 00000000
這個
32位的二進制數,還原成十進制,正是1091567616,
🏆 總結
- 浮點數存入時,由于指數有可能是負數,所以統一單精度指數加上
127,雙精度加上1023存入E中 - 當
M有二進制位不足時采用右邊補位補0補齊M E全為0和1時為特殊情況!
兄弟們看到這里那就收藏一下吧!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/305437.html
標籤:java
上一篇:從斐波那契到矩陣快速冪
