1. 程式人生 > >現代C語言程式設計之資料儲存(一)

現代C語言程式設計之資料儲存(一)

2.1 計算機資訊儲存

2.1.1 計算機常用儲存單位

在計算機最底層,資料都是以二進位制(01010)的補碼方式儲存,而計算機中最小的儲存單位是位(bit),用來表示0或者1。
計算機中最基本的儲存單位是位元組(Byte),1個位元組對應8個位(Bit)。
而日常應用中常使用的基本儲存單位包括KB,MB,GB,TB。它們之間都是以1024換算的,如下所示

1TB=1024GB
1GB=1024MB
1MB=1024KB
1KB=1024B
1B=8bit

2.1.2 計算機記憶體儲存

現在通常筆記本的記憶體通常是8G,16G,32G,64G等等,而執行在筆記本之上的作業系統普遍都是64位的,因為32位系統只能使用4G記憶體,下面是4G的記憶體換算

4G=2^2 * 2^10 * 2^10 * 2^10 =4*1024*1024*1024=2^32

因為4G只能夠定址到2^32,使用16進製表示就是0xFFFFFFFF,這裡可以藉助Visual Studio的除錯功能檢視記憶體的定址,如下圖所示
記憶體定址
源程式(memory_storage_32bit.c)如下所示

#include <stdio.h>
#include <stdlib.h>

/*
   	32位數系統定址
    @author tony [email protected]
    @version  2018/11/19 17:19:07
*/
void
main() { int age = 29; printf("整數變數age的地址是%p\n",&age); system("pause"); }

2.1.3 計算機網速換算

使用迅雷下載某些資源時的網速就是KB或者MB,而網路運營提供商(例如長城寬頻、移)聲稱的百兆頻寬實際上是100Mb(bit),但是網路下載速度是以位元組(KB)為單位的,因此真實的網速(下載速度)理論上只有100Mb/8=12.5MB

2.1.4 計算機磁碟容量換算

在購買記憶體或者買行動硬碟時,通常使用的儲存單位就是GB或者是TB,
但是在買4T的行動硬碟時,實際的可用容量卻只有3T多,因為計算機的儲存單位是以2的10次方(即1024)換算,而硬碟廠商們是以1000為換算單位。

4T的硬碟換算成位如下所示

4T=4*1024GB*1024MB*1024KB*1024B*8bit

而硬碟廠商的實際容量

4T=1000*1000*1000*1000*8

因此實際的可用容量是

4*1000*1000*1000*1000/1024/1024/1024/10243.63T

而在一些網際網路巨頭(例如國內的BAT,國外的亞馬遜、蘋果、微軟、谷歌,臉書)公司中,可能使用到比TB更大的海量資料,也就是PB或者EB,它們的換算單位如下所示。

1PB=1024TB
1EB=1024PB

2.2 變數

2.2.1 變數概述

記憶體在程式看來就是有地址編號的一塊連續空間,當資料放到記憶體中後,為了方便的找到和操作這個資料,需要給這個位置起名字,程式語言通過變數來表示這個過程。

2.2.2 變數的宣告和初始化賦值

在使用變數前必須先要宣告變數並初始化賦值,並且要遵守變數的命名規範

  • 變數名由字母數字下劃線組成,不能以數字開頭
  • 變數名區分大小寫。
  • 變數名不能是C語言的關鍵字(Visual Studio中的關鍵字都是藍色的)
  • 考慮到軟體的可維護性,建議變數見名知意

如下應用案例(Chapter2/variable/variable_declare.c)所示展示了C語言的變數命名案例

#include <stdio.h>
#include <stdlib.h>

/*
    變數的宣告賦值及其命名規範
    @author tony [email protected]
    @version  2018/11/19 17:21:07
*/
void main() {
    //合法的識別符號
    int number;
    //見名知意
    int age;
    char ch;
    double db;
    //變數名不能是關鍵字
    //int void;
    //變數名不能以數字開頭
    //int 1num;
    /****************************************編譯器特性*******************************/
    //VC支援中文變數,GCC不支援中文命名
    int 年齡 = 29;
    printf("年齡 =%d\n", 年齡);
    //在老版(C++11之前)的編譯器中,變數宣告必須放在函式呼叫之前
    /****************************************編譯器特性*******************************/
    //宣告多個變數
    int one, two, three;
    system("pause");
}

在宣告變數後,一定要給變數賦初始值,否者無法編譯通過,如下應用案例(Chapter2/variable/variable_not_init.c)所示

#include <stdio.h>
#include <stdlib.h>
/*
    變數初始化賦值
    在使用變數時必須手動初始化賦值,否則會得到一個隨機的垃圾值

    @author tony [email protected]
    @version  2018/11/24 17:58:07
*/
void main() {
    int num;
    //編譯錯誤錯誤	C4700	使用了未初始化的區域性變數“num”
    printf("num =%d\n", num);
    system("pause");
}

2.2.3 變數儲存

如下應用程式(Chapter2/variable/variable_storage.c)所示,通過"="可以給變數賦值,同時可以通過printf()函式傳遞%p引數來獲取變數在記憶體中的地址。

#include <stdio.h>
#include <stdlib.h>

/*
    變數在記憶體中的儲存
    @author tony [email protected]
    @version  2018/11/24 17:58:07
*/
void main() {
    int num = 20;
    //檢視num的記憶體地址
    printf("整數變數num的地址是%p\n", &num);
    printf("整數變數num = %d\n", num);
    num = 30;
    printf("修改之後整數變數num的值是%d\n", num);
    system("pause");

}

如下圖所示,還可以通過Visual Studio 提供的除錯功能通過斷點檢視變數在記憶體的儲存,通過輸入變數的記憶體地址便可以觀察變數對應的值。
變數在記憶體中的儲存
在同一時刻,記憶體地址對應的值只能儲存一份,如果修改地址對應的值,之前的值會被覆蓋,這個就是變數的特點,變數名是固定的,但是變數值在記憶體中是隨著業務邏輯在變化的,例如最常見的遊戲場景中,遊戲人物生命值的變化。

2.2.4 編譯器對變數的處理

當在程式中宣告變數並賦值時,編譯器會建立變量表維護變數的資訊,包括變數的地址,變數的型別以及變數的名稱。
而在記憶體中變數的記憶體地址和變數值是一一對應的,編譯器正是通過變量表的記憶體地址和記憶體中的變數地址關聯。因此在使用變數進行相關操作之前必須先宣告並賦值,否則程式會發生編譯錯誤,如下應用案例(Chapter2/variable/variable_compiler.c)所示。

#include <stdio.h>
#include <stdlib.h>

/*
    編譯器和記憶體對變數的處理
    @author tony [email protected]
    @version  2018/11/24 17:27:07
*/
void main() {

    int a, b, c;
    //這裡會發生編譯錯誤 不能使用未宣告的變數
    //printf(" %d\n", d);
    system("pause");
}

2.2.5 變數運算的原理

當兩個變數在執行相關運算(例如加法)時,系統會將把兩個變數地址對應的變數值移動到CPU內部的暫存器中執行運算後將運算結果返回給記憶體,如下應用案例(Chapter2/variable/variable_operator_principle.c)所示

#include <stdio.h>
#include <stdlib.h>
/*
    變數運算的原理
    @author tony [email protected]
    @version  2018/11/24 18:26:07
*/
void main() {
    int a = 1;
    int b = 2;
    //分配四個位元組的記憶體
    int c;
    printf("變數a的地址是%p\t,變數b的地址是%p\t,變數c的地址是%p\n", &a, &b, &c);
    //資料的運算是在CPU的暫存器完成的
    c = a + b;
    c = b - a;
    //對資料的操作是由CPU完成的
    //a + 1 = 4;
    printf("c=%d\n", c);
    system("pause");

}

如下圖所示,可以藉助VisualStudio的除錯功能來觀察EAX暫存器的變化的值。
EAX暫存器
為了能夠更加直接的理解暫存器的作用,這裡使用C語言嵌套匯編語言來通過指令操作暫存器完成變數的賦值運算和加法運算,應用案例(Chapter2/variable/variable_asm_assignment)如下所示。

#include <stdio.h>
#include <stdlib.h>
/*
    使用匯編語言實現變數的賦值以及運算來理解資料的運算是在CPU內部的暫存器完成的
    @author tony [email protected]
    @version  2018/11/24 18:30:07

*/
void main() {

    //申請四個位元組的記憶體
    int a;
    printf("整數變數a的地址是%p\n", &a);


    //變數的賦值都是通過CPU的暫存器來完成的
    //這裡藉助組合語言實現將10賦值給變數a
    _asm {

        mov eax, 10
        mov a, eax
    }

    printf("整數變數a的值等於%d\n", a);

    _asm {

        //把變數a的值賦值給暫存器eax
        mov eax, a
        //將eax的值加5
        add eax, 5
        //把eax的值賦值給a
        mov a, eax
    }
    printf("變數a加5之後的結果是%d\n", a);
    system("pause");
}

2.2.6 變數交換的實現

變數的交換,可以通過採用中間變數,算術(加減法或者乘除法)運算、異或運算
三種方式實現,其應用場景主要在使用在排序演算法中,每種實現變數交換方法的時空複雜度有不同的考量。

  1. 通過使用中間變數實現交換應用案(Chapter2/variable/variable_swap_with_tmp.c)
#include <stdio.h>
#include <stdlib.h>
/*
    使用臨時變數實現變數交換
    賦值運算三次
    增加空間
    @author tony [email protected]
    @version  2018/11/24 18:32:07
*/
void varriable_swap_with_tmp(int left, int right) {

    printf("使用臨時變數實現變數交換交換之前\t left=%d \t right=%d\n", left, right);
    int middle = left;
    left = right;
    right = middle;
    printf("使用臨時變數實現變數交換交換之後\t left=%d \t right=%d\n", left, right);
}

/*
    變數交換
    @author tony [email protected]
    @version  2018/11/24 18:32:07
*/
void main() {
    int left = 5;
    int right = 10;
    varriable_swap_with_tmp(left, right);
    system("pause");
}
  1. 使用算術運算實現變數交換應用案例(Chapter2/variable/variable_swap_with_algorithm.c)
#include <stdio.h>
#include <stdlib.h>
/*
    使用算術運算實現變數交換 考慮資料越界的問題
    不需要開闢額外的空間
    賦值運算三次,算術運算三次 總運算次數6次

    @author tony [email protected]
    @version  2018/11/24 18:38:07
*/
void variable_swap_with_algorithm(int left, int right) {
    printf("使用算術運算實現變數交換交換之前\t left=%d \t right=%d\n", left, right);
    left = left + right; // 加號變成乘號
    right = left - right;//減號變成除號
    left = left - right; //減號變成除號
    printf("使用算術運算實現變數交換交換之後\t left=%d \t right=%d\n", left, right);
}


/*
    使用算術運算實現變數交換
    @author tony [email protected]
    @version  2018/11/24 18:39:07

*/
void main() {

    int left = 5;
    int right = 10;

    variable_swap_with_algorithm(left, right);
    system("pause");
}
  1. 使用異或運算實現變數交換應用案例(Chapter2/variable/variable_swap_with_xor.c)
#include <stdio.h>
#include <stdlib.h>

/*
    使用異或運算實現變數交換
    不用考慮運算結果溢位的問題

    @author tony [email protected]
    @version  2018/11/24 18:40:07
*/
void variable_swap_with_xor(int left, int right) {
    printf("使用異或運算實現變數交換交換之前\t left=%d \t right=%d\n", left, right);
    left = left ^ right;
    right = left ^ right;
    left = left ^ right;
    printf("使用異或運算實現變數交換交換之後\t left=%d \t right=%d\n", left, right);

}
/*
    使用異或實現變數交換
    @author tony [email protected]
    @version  2018/11/24 18:41:07
*/
void main() {
    int left = 5;
    int right = 10;
    variable_swap_with_xor(left, right);
    system("pause");
}

2.2.7 自動變數與靜態變數

在函式中的形式引數和程式碼塊中的區域性變數都是自動變數,它們的特點是隻有在定義的時候才會被建立(即系統自動開闢記憶體空間),在定義它們的函式返回時系統自動回收變數佔據的記憶體空間,為了考慮到程式碼的可讀性,通常使用auto關鍵字來修飾自動變數,應用案例(Chapter2/variable/auto_variable.c)如下所示

#include <stdio.h>
#include <stdlib.h>
/*
    自動變數:
        只有定義它們的時候才建立,在定義它們的函式返回時系統回收變數所佔用的儲存空間,
        對這些變數儲存空間的分配和回收由系統自動完成
        一般情況下,不做專門說明的變數都是自動變數,自動變數也可以使用關鍵字auto說明
        塊語句中的變數,函式的形式引數都是自動變數

    @author tony [email protected]
    @version  2018/11/24 18:42:07

*/

void auto_varriable(auto int num) { //num就是自動變數,函式呼叫的時候就存在,函式結束,變數會被作業系統自動回收,地址都是同一個地址,但是值在不斷髮生變化

    printf("num的記憶體地址是%p\nnum的值是%d\n", &num, num);

    auto int data = num;

    printf("data的記憶體地址是%p\ndata的值是%d\n", &data, data);

}

/*
    多次呼叫自動變數

    @author tony [email protected]
    @version  2018/11/24 18:42:07
*/
void invoke_auto_varriable() {
    int num = 20;
    auto_varriable(num);
    printf("\n\n");
    auto_varriable(80);

}

/*
    自動變數測試入口

    @author tony [email protected]
    @version  2018/11/24 18:42:07
*/
void main() {

    invoke_auto_varriable();

    system("pause");
}

可以通過下斷點來除錯該程式,觀察當執行auto_varriable()函式完成以後,區域性變數data將會被回收,如下圖所示
自動變數的特性
同時可以通過觀察記憶體地址,發現當呼叫auto_varriable()函式時,num=20
變數銷燬之前
然後當執行完auto_varriable()函式後,num的值變數一個系統分配的垃圾值
函式執行完成之後

而靜態變數不會發生變化,即使函式執行完成也不會被作業系統回收,應用案例(Chapter2/variable/static_variable.c)如下所示

#include <stdio.h>
#include <stdlib.h>

/*
    靜態變數
    @author tony [email protected]
    @version  2018/11/24 18:43:07
*/
void static_varriable() {
    static int x = 99;

    printf("x的記憶體地址是%p,x的值是%d", &x, x);

    printf("\n\n");
}

/*
    多次呼叫靜態變數
    @author tony [email protected]
    @version  2018/11/24 18:43:07
*/
void invoke_static_varriable() {
    static_varriable();
    printf("\n");
    static_varriable();
}

/*
    靜態變數測試入口
    @author tony [email protected]
    @version  2018/11/24 18:43:07
*/
void main() {

    //invoke_auto_varriable();
    invoke_static_varriable();
    system("pause");
}

除錯以上應用程式,會發現直到main函式執行完成,靜態整數變數x都不會被作業系統回收。
靜態變數的特性

2.3 常量

常量表示一旦初始化之後便不能再次直接改變的變數,例如人的身份證編號一旦確定之後就不會再次改變。C語言支援使用const關鍵字和#define CONST_NAME CONST_VALUE 兩種方式來定義和使用常量。

2.3.1 const常量

如果想要使一個變數變成常量,只需要在變數前面使用const關鍵字即可,const常量雖然不能直接修改,但是可以通過C語言的指標來修改,因此不是真正意義上的常量。,應用案例(Chapter2/const/const.c)如下所示。

#include <stdio.h>
#include <stdlib.h>
/*
    const常量不能直接修改值,但是可以通過指標修改值
    @author tony [email protected]
    @version  2018/11/24 18:56:07
*/
void main() {

    //定義一個整數常量
    const long id = 10000;
    //不能直接修改常量
    //id = 10001;
    printf("常量id的地址是%p\n", &id);
    printf("常量id=%d\n", id);

    //通過指標修改
    //* 根據地址取內容
    //(int*) 型別轉換為非 常量型別
    * (int*)(&id) = 10001;

    printf("常量id=%d\n", id);
    system("pause");


}

2.3.2 #define常量

在C語言中使用const定義的變數不能直接修改,但是可以通過指標來修改,因此不是真正意義上的常量。
如果想要使用真正意義上的常量,可以使用#define CONSTA_NAME VALUE 來實現,應用案例(Chapter2/const/define.c)如下所示

#include <stdio.h>
#include <stdlib.h>

//#define語句不需要分號結尾,#define定義的常量值是在暫存器中產生,無法取記憶體地址,即無法通過C語言修改,
//因為C語言無法直接操作CPU的暫存器,只能操作記憶體。
#define CARD_NUMBER 88888888  

/*
    define常量
    @author tony [email protected]
    @version  2018/11/24 19:15:07

*/
void main() {
    printf("CARD_NUMBER=%d\n", CARD_NUMBER);
    system("pause");

}

使用#define定義常量的好處:

  1. 通過有意義的常量名,可以指定該常量的意思,使得開發人員在越多程式碼時減少迷惑
  2. 常量可以在多個方法中使用,如果需要修改常量,只需要修改一次便可實現批量修改,效率高而且準確。

應用案例如下所示(Chapter2/const/define_with_method_reference.c)

#include <stdio.h>
#include <stdlib.h>
#define CARD_NUMBER 88888888  

/*
   在自定義方法中使用常量
    @author tony [email protected]
    @version  2018/11/24 19:18:07
*/
void use_card_number_const() {

    printf("在自定義方法中使用CARD_NUMBER常量的值=%d\n", CARD_NUMBER);

}

/*
    define常量的好處
    定義後可以在任意的程式碼塊引用
    @author tony [email protected]
    @version  2018/11/24 19:18:07

*/
void main() {

    use_card_number_const();
    system("pause");
}

使用define定義常量實現程式碼混淆,應用案例(Chapter2/const/define_app.c)如下所示

首先在define_app.h標頭檔案中定義如下常量

#define _ void
#define __ main()
#define ___ {
#define ____ system("notepad");
#define _____ system("pause");
#define ______ }

然後定義define_app.c原始檔,內容如下

#include "define.h"

/*
    使用define實現程式碼混淆
    @author tony [email protected]
    @version  2018/11/24 19:18:07
*/
_ __ ___ ____ _____ ______

執行程式後,可以開啟記事本。

2.4 進位制

2.4.1 常用進位制及其應用場景

在計算機記憶體中,都是以二進位制(01001)的補碼形式來儲存資料的,而在生活中以十進位制方式計算的資料居多,例如賬戶餘額,薪水等等。而計算的記憶體地址通常都是使用十六進位制展示的,Linux系統的許可權系統採用八進位制的資料運算。相同進位制型別資料進行運算時會遵守加法:逢R進1;減法:借1當R,其中R就表示進位制

如下表格是它們的組成、示例和使用場景:

進位制名稱 組成 數值示例 典型使用場景
二進位制 0,1 0101 記憶體資料儲存
八進位制 0-7之間的8個整數 012(以0開頭) linux許可權
十進位制 0-9之間的10個整數 12 整數
十六進位制 0-9,a-f之間的10個數字加6個字母 12f 資料的記憶體地址

如下應用案例(Chapter2/datatype/datatype_radix_int.c)就是八進位制、十六進位制和十進位制的變數使用,需要注意的是C語言中的整數預設就採用十進位制來表示,而且c語言沒有提供二進位制的資料表現形式。

#include <stdio.h>

/*

    整數常見的幾種進位制型別
    @author tony [email protected]
    @date 2017/10/31 20:27
    @website www.ittimeline.net
*/

void main() {

    //八進位制:0開頭 0-7之間的八個數字表示
    int oct_val = 017; //15
    //十六禁止:0x開頭0-9,a-f之間的十個數字加上6個字母
    int hex_val = 0x12;//18
    //C語言不支援宣告2進位制的變數,這裡是十進位制的值
    int binary_val = 101001;

    printf("oct_val = %d\t hex_val =%d,binary_val=%d\n",oct_val,hex_val,binary_val);

    system("pause");
    
}

2.4.2 進位制之間的轉換

在某些場景下(例如面試)需要完成常用進位制之間的資料轉換

二進位制轉八進位制、十六進位制

根據小學數學的邏輯可以知道23=8,24=16, 它們三者之間可以這樣換算

二進位制轉八進位制:在轉換時,從右向左,每三位一組(不足三位用0補齊),轉換成八進位制。

二進位制和八進位制的對應關係如下表:

二進位制 八進位制
000 0
001 1
010 2
011 3
100 4
101 5
110 6
111 7

例如 1010轉換為八進位制的結果為12

二進位制和十六進位制的對應關係如下表:

二進位制 十六進位制
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 8
1001 9
1010 a
1011 b
1100 c
1101 d
1110 e
1111 f

二進位制轉十六進位制:從右向左,每4位一組(不足4位,用0補齊),轉換成十六進位制。

例如 1010轉換為八進位制的結果為a

二進位制、八進位制、十六進位制轉十進位制

二進位制、八進位制、十六進位制轉換成十進位制都是採用按權相加的形式計算的。

先看一個整數計算的案例:

1234=1*10^3+2*10^2+3*10^1+4*10^0=1000+200+30+4

因此可以採用按權相加的計算方式將二進位制、八進位制、十六進位制轉換為十進位制

二進位制轉換為十進位制:

100101=1*2^5+1*2^2+1*2^0=32+4+1=37

八進位制轉換為十進位制

27=2*8^1+7*8^0=16+7=23

十六進位制轉換為十進位制

20d=2*16^2+13*16^0=512+13=525
十進位制轉二進位制

十進位制整數轉換為二進位制:方法是除以2取餘,直到商數為0,逆序排列,以22為例:倒序的二進位制結果就是10110

計算過程如下:

22/2 餘 0
11/2 餘 1
5 /2 餘 1
2/2 餘 0
1/2 餘 1
0 商數為0

如下應用案例所示(Chapter2/datatype/datatype_radix_convert_app.c),採用程式設計實現十進位制轉二進位制案例

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
/*
     0-32767之間的十進位制轉換成二進位制:除以2取餘,直到商數為0,逆序排列
     @author tony [email protected]
     @version  2018/11/24 20:08:07
*/
void main() {
    int numbers[16] = { 0 }; //初始化一個16位整數陣列,用於儲存二進位制整數
    printf("請輸入一個需要轉換成二進位制的十進位制正整數,取值範圍在0-32767之間\n");

    int value = 0; //待轉換的十進位制整數
    scanf("%d", &value); //讀取使用者輸入的整數 
    for (int i = 0; i < 15; i++) { //十六位二進位制數,最高符號位為正整數

        int quotient = value / 2; //商數
        int remainder = value % 2; //餘數
        value = quotient; //將除以2之後的商數賦值給value
        numbers[i] = remainder;//將餘數儲存在陣列中

    }

    printf("