C++ 鍊氣期之數據是主角

1. 前言

數據在程式中的重要性,怎麼強調都不為過,程式的本質就是通過提供數據處理邏輯,把數據從一種狀態變成另一種狀態的過程。處理邏輯一定是有針對性的,針對的是數據本身的特性。

只有了解了數據本身的內在邏輯含義以及數據間的邏輯關係,才能提供恰到好處的處理邏輯。如,根據麵粉的特性適用於製作麵包、麵條的處理邏輯,並不適合辣條的製作邏輯。

數據是程式的主角,邏輯是程式的劇本。本文將從如下幾個方面聊聊C++中的數據這個主角。

  • 數據的存儲。
  • 數據的類型。
  • 數據的來源。

2. 數據的存儲

談論數據存儲之前,先要知道數據是什麼?

數據是電腦世界對現實世界中資訊的映射,映射數據的過程也是電腦認知現實世界的過程。

映射還有一個專業概念:數字建模

認知過程包括:

  • 識別: 比如:文字資訊還是影片資訊或是圖片資訊或是數字資訊……識別過程就是對現實世界中的資訊進行分類的過程。因為類型不同,其映射模型將不同、給其分配的劇本(處理邏輯)也不同。
  • 採集: 電腦只能識別二進位數據,現實世界中的任一類型資訊在電腦中都只能以二進位形式存儲,所謂採集就是把現實世界中的資訊以二進位式的形式描述,此過程也稱為編碼
  • 存儲: 以二進位的數據格式存儲在電腦中。

數據的存儲包含靜態存儲和動態存儲,本文只講解動態存儲,也就是程式運行時是如何存儲數據。程式運行時所需要的數據會存儲在變數中。

什麼是變數?

變數是指位於記憶體中的一個存儲塊。這個存儲塊又是由一個或多個基本存儲單元格組成。一個基本存儲單元格的大小一般為 1位元組(1 B)

比特(bit)是電腦的最小存儲單位。此單位太小,引入了位元組單位,1 位元組等於 8 個比特(1B=8bit)
6.png

因存儲塊中的數據可以根據邏輯的需要隨時發生變化,變數一詞由此而來。

變數的詞義強調了存儲塊中數據的動態性、靈活性。

什麼是變數名?

為了方便訪問變數,開發者需要給變數起一個名字,這便是變數名。

C++運行系統根據開發者的請求指令開闢了存儲空間後,便會把變數名和變數進行關聯。如此便可以在程式中通過變數名這唯一的變數標識符號訪問變數中的數據了。

由開發者提供的變數名,也稱為變數的邏輯名。C++底層機制會建立一張映射表,用來保存變數名和對應存儲塊的映射關係。

變數名由開發者指定,由系統關聯。開發者在給變數命名時,需要遵循變數名命名的語法規則。

變數名命名規則:

  • 首字母只能以字母、下劃線開頭。
  • 除首字母之外的其它部分只能是字母、下劃線、數字組成。
  • C++語言區分大小寫,所以NAMEname是 2 個不同的變數名。

變數名命名規範:

如果說規則是法律約束,則規範就是道德約束。規則遵循的是語法標準,不能不遵守,規範遵循的是事實標準。所謂事實標準指行業里的傳承或約定。你可以不遵守,但會破壞程式碼的閱讀性和格式一致性。

  • 編寫C++程式時,要求變數名遵循駱駝命名法則,如 myName。如果變數名由 2 個以上的英文單片語成,則從第二個英文單詞開始首字母大寫。

  • 還有一點,變數名儘可能能描述其存儲的數據的含義。或者叫知名達義,通過名字便能知道變數中數據的含義。

    類似於爸爸媽媽給自己的孩子起名字,都會起一個有寓意的名字。

在需要存儲數據時,需要向C++運行系統提出變數的申請。這裡會有一個常識,申請時需要告之變數的實際使用大小,類似於做衣服時,你對老闆說,給我做件衣服,僅這樣的資訊還是不夠的。你必須告訴老闆衣服的尺寸,這樣老闆才能合理使用布料。

那麼,申請變數時,如何告訴底層機制你所需的變數的大小?

答案是通過數據類型。

//在C++ 中需要變數時,一定要指定數據類型
數據類型 變數名;

數據類型在聲明變數語法中有 2 個作用:

  • 確定變數的大小。
  • 確定變數中數據的用途。

之於數據類型的具體概念是什麼?以及為什麼指定數據類型便能讓底層運行機制知道開發者所需的變數大小,下文將詳細介紹。

3. 數據類型

什麼是數據類型?

所謂數據類型,就是電腦世界對現實世界中資訊的分類。

為什麼要對數據分類?

分類是對數據識別的過程,分類的過程也是了解各種數據特徵的過程,只有了解了數據的特性方能擬定行之有效的解決方案。

自動駕駛汽車系統最複雜的地方在於:汽車在行駛過程中要實時對周邊的數據進行分類,是石頭還是人類還是花花草草或是一隻小狗小貓……只有在類別清楚的情況才能給出對應的處理方案。是人,停下來,是花花草草可以開過去,是石塊,還要區分其大小。

電腦對現實世界的資訊分類越精細,其處理領域以及處理能力會越強。如果人類對化學元素周期表中的元素僅了解其 1/3 ,則人類的科技文明將要遠遠低於現在的科技成就。

化學周期表中的元素有限,但是可以利用元素之間的關係,進行複合創造。這點很重要。在C++語言體系中,同樣能根據基礎分類構建出更複雜的類型,如結構體、類、枚舉……

C++把現實世界的資訊分為 2 大基礎類:

  • 數字型數據。
  • 非數字型數據。

3.1 數字型數據

數字型數據又分為整型數據浮點型數據整型數據通俗理解就是不帶小數點的數字,浮點數據可理解為帶小數點的數字。

2.1.1 整型數據

C++int統稱整型數據,又以 int為邊界根據數字的範圍大小分為:

  • short int:短整型。
  • long int:長整型。
  • long long int:長長整型。

存儲不同類型的數據時,C++會根據類型分配相應的存儲空間,導致所描述的數字大小也不一樣。

那麼!上述各種數據類型所描述的數字範圍到底有多大?

C++與其它的高級語言有所不同,如 JAVA中嚴格規定了 int4 個位元組大小。但是 C++標準中對 int只做了一個抽象規定,其描述的數字範圍大小與機器字相同。

  • int 是一個機器字。

  • short int是半個機器字。

  • long int12 個機器字。

  • long long int2 個機器字。

機器字,就是電腦的運算單元在單位時間內能處理的數據位數。如我們經常會說 16位處理器,32 位處理器。

16位處理器單位時間內能處理 16位也就是 2 位元組的數據。

32處理器單位時間內能處理 32位也就是 4 位元組的數據。

所以,同一個程式,運行在不同的電腦平台上時,int 所能描述的數據範圍是不一樣的。現假設本程式運行在 32 位的電腦上,在編寫如下變數聲明以及賦值程式碼時,請注意其中的細節。

  • 默認情況下,所有數字字面常量都是 int 類型。如下常量 34 就是 int。 數字字面值默認情況下十進位格式,也可以使用八進位或十六進位度。
//十進位
int num_1=34;
//八進位,前面使用 0 作為前綴
int num02=023;
//十六進位,前面使用 0X 作為前綴
int num03=0x12;
  • 在使用 short int 保存數據時,不要保存超過 short int 類型描述的數字大小。如下是正確的。在 32 位處理平台上,short int能保存的數字範圍是-32768~3276723在這個範圍之內。
short int num_a=23;

如下是錯誤的賦值操作,因為常量 100000已經超過了 short int描述的數字範圍。

short int num_a=100000;
  • 使用 long int 時,如果存儲的數字沒有超過 long int所描述的範圍,可以直接賦值,如下是正確的。
long int num_3=45;

最好在數字後面添加 Ll後綴。根據測試,編寫本文時測試程式碼用的電腦上的 long intint描述的數字範圍是相同的,都是 4 B

long int num_3=10000000000L;
  • 使用 long long int時,請在賦值的數字後面添加後綴 LLll。經過測試,本機 long long int8 B。當然如果不指定 LL特定描述符,C++也能自動轉換。
long long int num_3=10000000000LL;

int 類型大小的不確定性,C++程式在跨平台使用時,存在移植問題

什麼是移植問題?

這裡必然會出現一個問題,我在 32 位電腦編寫程式時,使用 int 描述了一個32 位的數據。如果讓此程運行在 16 位的電腦上,則會出現編譯無法通過或丟失數據的情況。

類似於我在一家銀行存儲物件時,此銀行給了我 4 個存儲櫃用來存儲我的物件,我也把 4 個柜子存滿了。

轉到另一家銀行時,人家說最多只能給我 2 個柜子,這肯定是存不下我所有的物件,會發生數據丟失。

如果情形反過來,倒沒有多大影響。

1.png

問題出現了,必然是要解決的,一種解決方案就是程式級解決,在編寫程式時,獲取到程式運行時的電腦的機器字,然後根據電腦的機器字採用不同的數據類型存儲。

2.png

在程式邏輯中,還要隨時獲取到底層硬體的工作狀態,這與高級語言的理念相矛盾,且增加了開發者的負擔,且易出現忽視的地方,導致程式在移植時 bug滿天飛。

當然,C++也可以讓開發者可以統一使用 int描述數據,在編譯器中,由編譯器根據電腦的機器字,然後採用是否拆分存儲的方案。也就是把上述邏輯由開發層面移到編譯器層面。

這是常規解決方案,但是會增加編譯器的工作負擔,影響編譯的速度。

另一種解決方案,C++在語法層面提供了明確描述數字範圍的類型關鍵字,可以由開發者根據自行選擇。這樣在語法層面和編譯層面有了統一的協議,編譯器不需要進行條件判斷。

  • __int8:表示8位。
  • __int16:表示16位。
  • __int32:表示32位。
  • __int64:表示64位。

有符號和無符號的問題:

默認情況下,int是有符號,意味著可以存儲正數,也能存儲負數。如下 2 行程式碼的語義是一樣的。

signed int num_1=34;
int num_2=34;

如果需要表示無符號的整型數據類型,則需要使用 unsigned 關鍵字。使用此關鍵字後變數中不能存儲負數。如下程式碼從語法上沒有錯誤,但是,從變數 num_1並不能獲取數據 -34,而是垃圾數據。

	unsigned int num_1=-34;

C++ 語言有一個讓讓人頭大的地方。

如下程式碼,很明顯,1000000000098788已經遠遠超過了 int描述的範圍,語法上沒有任何提示,並且能正確編譯運行,只是從變數num_3中獲得的數據是垃圾數據。

int num_3=1000000000098;

C++的語法較為寬鬆,編譯器較”圓滑”,一切就靠開發者自己步步驚心了。

可以說是缺點,但也是優點,正因為不設防,才能讓其編譯速度較快。

無符號數據可以在數據中添加 uU作為無符號數據的標識符號。

unsigned int num_3=34u;

有符號 int和無符號 int 所表示的數字範圍並不相同。32系統中,無符號 int 類型範圍如下圖,也就是 0~4294967295

3.png

unsigned int num_1=4294967295;
unsigned int num_2=num_1+1;
cout<<num_1<<endl;
cout<<num_2<<endl;
return 0;
//輸出結果
4294967295
0

int類型默認是有符號,只是省略signed ,在 32 位的平台上, int的範圍是-2147483648~2147483647

在有符號描述中,最高位並不表示有效的數據位,而是標誌位:

4.png

  • 當此位置設為 0 時,表示存儲的是正數。

    最大值求解表達式:1X230+1X229……1X20=2147483647

  • 當此位置設為 1 時,表示存儲的是負數。

    最小值的求解可理解為無符號位的最大值減去有符號位的最大值再取反,-(4294967295-2147483647)=-2147483648

2.1.2 浮點數據

浮點數據指帶小數點的數據,C++floatdouble表示浮點數據類型。

  • float表示單精度浮點數據。C++標準約定 float的有效位是一個機器字(32 位平台是 32 位)。
  • double表示雙精度浮點數據。C++標準約定 double 的有效位是 2 個機器字(32 位平台是 64 位)。double 還有一個兄弟:long double,其有效位至少應該和 double大小一樣(可以是 8096128 位)。

什麼是有效位?

有效位指數據中的有意義的位數。

  • 如數字 14567其有效位為 5位。
  • 如數字 14500 ,則有效位為 3,後面的 0 為被認為是佔位符,不計算到有效位中。
  • 245.89有效位也是 5。有效位與是否有小數點以及小數點位置無關。

默認情況下,字面浮點常量是double數據類型。如下的 34.0就是double類型。

double num=34.0;

站在數學的角度,34.0 後面的 0 是沒有意義的,但是C++依然把它當成浮點數字。

在浮點常量後面添加fF後綴。則表示為 float數據類型。

float num=34.5f;

在浮點常量後面添加L後綴,則為 long double數據類型 。

long double num=34.5L;

當浮點型常量後綴fFlL時,只能用在十進位開式中。C++在描述浮點型數據時,還可以使用科學計數法開式。科學計數法指數字中帶有指數表示方式。

如下程式碼,表示的是 3*102

double num=3e2;

這裡 2 稱為指數,3 稱為尾數。

如下程式碼,表示的是 3.4*10-2

double num=3e-2;

在電腦底層,存儲整型數據和浮點數據的方式是不同的。整型數據可以直接存儲,浮點數據則是將數據分成 2 個部分分別存儲。

  • 一部分用來存儲數值。
  • 一部分用來存儲放大或縮小因子。

舉一個例如,保存 3.457 十進位時,可以分成下面 2 個部分保存 :

  • 保存數值 3457
  • 縮放因子 1000

當讀取數據時,通過縮放因子縮小數值,就能得到 3.457。縮放或放大因子的作用是移動小數點的位置。上面是以十進位為例子說明問題,事實是電腦底層以二進位存儲,縮放因子是以 2 為冪。

正因為小數點可以移動,所以稱這類數據為浮點類型。

但是要知道,原理是這麼一回事,而事實是浮點數據的底層存儲結構要比整型存儲結構複雜的多。

3.2 非數字類型

C++非數字類型有 charbool

3.2.1 字元類型

char用來表示單個字元或小整數,char常量需要使用單引號括起來。

char myChar='A';

電腦能直接存儲數字類型數據,只需要把數字轉換成二進位便可。電腦不能直接存儲字元,所以需要遵循一種統一的標準,把字元轉換成一個數字後再存儲,這個過程叫字元編碼

電腦最早使用的是 ASCII編碼標準,主要是用於編碼英文中使用的字元。因英文字元並不多,所以 1B的存儲空間就夠用了,C++最初對 char類型的存儲標準就是 1 位元組的存儲空間。

但對於其它國家的語言來講,則遠遠不夠,默認情況下,char是不能存儲中文字元的,因為中文至少需要 2 個位元組的存儲空間。

中文編碼標準有 gb2312GBK, 這 2 種標準僅只能對中文字元編碼。

另有國際統一的 uncode 標準,用來對全世界所有語言的字元進行統一編碼。

如下的程式碼看似能存儲,但其真正存儲的是一個垃圾數據。正如前文所說,C++並不會在語法層面 檢查數據是否合理,編譯器採用原則是能存儲存則存儲,不能存儲就存儲能存儲的一部分。

char myChar='中';

C++還有一種 wchar_t 字元數據類型,叫寬字元類型,其存儲大小為 2 位元組。

wchar_t myChar='中';

C++ 11標準中還有 char16_tchar32_t類型描述,主要支援 unicode編碼標準,都是無符號類型。字面意思便能知道一個支援16位存儲,一個支援32位存儲。

無符號字元型

char在默認情況下既不是沒有符號,也不是有符號,因為並沒有編碼為負數的 ASCII字元。算是留了一個可擴展餘地。

C++有無符號的字元類型(unsigned char),其取值,除了包括 ASCII碼錶上的所有字元外,還包括一個擴展 ASCII碼錶上的字元。擴展字元指通過鍵盤無法輸入的字元。但可以通過字元與整數的關係,來初始化或賦值無符號字元型變數。

unsigned char myChar=128;

有符號和無符號char所表示的範圍是不相同的:

  • signed char表示範圍為 -128~127
  • unsigned char表示範圍是 0~255

3.2.2 bool類型

bool類型用來表示truefalse。在C++中可以把非零值當成 true。零值當成 false

bool exist=true;
bool exist_=1;
	
bool exist01=false;
bool exist01_=0;

4. 數據的獲取

程式中數據的源頭有多種途徑:已知數據,交互數據,資料庫中數據、網路數據、文件中的數據……

已知數據,指直接出現在程式中的字面數據,也稱為常量數據,可以直接參与到運算中,一般用來賦值。

交互數據,也稱為輸入數據。在程式運行時,通過交互機制獲取到用戶輸入的數據。

int num=0;
cout<<"請輸入一個數字";
cin>>num;
cout<<"你剛輸入的數字是"<<num<<endl;

C++通過 cin和重定向指令完成交互數據的獲取。

如果要獲取資料庫中的數據則需要依靠資料庫驅動 API。如果要獲取文件中的數據則需要使用文件讀寫API,需要網路上數據則需要網路相關的API`。這已經超過本文要聊的主題,有興趣者可查閱相關文檔。

5. 總結

本文試圖從數據的存儲、數據的類型、數據的源頭上講解數據的本質。程式這個劇本要開始,數據這個主角先要到位,對數據的了解多少,決定了邏輯的精彩度。

本文內容雖然很基礎 ,但尤為重要,基礎建設是否牢固決定了高層構建的高度。