重學 C 的筆記

題葉發表於2019-05-10

學校上過數學系的 C 課程, 上機 VC 操作做課堂作業過, 後來就一直動態型別語言.. 直到學 Go 型別卡住了.. 被學長帶著重學 C, 以下是開頭的筆記:


10 個人的性別資訊, 最少用多少 bits 來儲存?

實際計算機最小的處理單位是 8bits.

最少是 16bits, 順序 10bits 的 01 來表示性別.

假如還有變性人, 還有每個人性別位置, 多少位?

每個人 5 種情況 (5^10)bits,

> 5 ^ 10 / 8 / 8 / 8 / 8 / 8 / 8 / 8
4.656612873077393

(8*8)bits


程式語言裡只能操作四種位數, 8, 15, 32, 64bits. 就算用到只需要 20bits, 還是會用到 32 bits. 屬於硬體設計範圍.


表示 34 個數字, 需要多少 bits, 34 種不同的情況, 只要能區分?

34 個不同情況需要 64bits


8bits 能表示多少數字?

256, 能夠表示 0~255, 非負是 -127~128

-1 的表示是 11111111, -1 會用來表示最大


未知大小的資料, 通過一個字典, 每個字串一個需要, 然後按序號取. 空間可以動態重新申請, 然後獲取地址, 這樣就可以表示很大的資料.


34bits 的記憶體不能表示 4G 以上的地址. 因為 32bits 能表示 1024 * 1024 * 4.


儲存 100*32bits 的資料, 先在堆裡動態申請記憶體, 然後記錄地址, 每次 new 新建物件, 都申請到固定大小的內容, 然後儲存地址, 引用, 指標.

32bits 電腦的指標是 32bits 的, 引用物件的值表示地址. 如果引用物件的值是 0, 那麼就是 null

JS undefined 是有指向記憶體, 但記憶體沒有資料, 而 null 是地址都是 0.

系統會提供虛擬地址. 程式的虛擬地址是從 1 開始的, 0 在各種語言裡都是 null.


列印一下 Go 裡的地址…

package main

import (
  "fmt"
)

func main() {
  a := 1
  fmt.Println(&a)
}

C 不儲存型別的資訊.

值型別比引用型別更快, 因為引用型別都會涉及到記憶體申請. 坑爹的 Python 全是引用型別…


申請的記憶體要記得釋放.. 或者垃圾回收..


記憶體有四塊區域

  1. 程式區

存放程式本身, 彙編程式碼, 作業系統管理的記憶體. 程式不可修改, 可以讀取. 程式的記憶體是連續的, 可以通過計算推測.

存放指令和運算元. 比如 1 + 1ld 1, ld 1, add.

變數就是暫存器的地址. 1 + a 是載入 1 和 a 的地址. 基礎型別可以直接在程式裡用, 不需要記憶體. ld 1, lda 00001, add

  1. 靜態資料區

存放程式一定會用到的內些固定資料. 比如字串常量, 但數字可以直接在程式區用, 而字串不能出現在程式區.

需要在編譯時能得到. 執行時動態的資料沒法知道.

存放程式函式和區域性變數. 棧的大小固定, 一般為 2m, 從棧空開始, 先進後出的. 是人為劃分的一塊連續虛擬記憶體. 操作類似 push pop.

int i = 4 這樣就會進棧 4.

函式執行完畢函式內的棧會被清空, 不同的函式之間不會干擾.

{} 表示一個塊級作用域, 其中標記了進棧和清棧的範圍. for 迴圈的 {} 是有塊級作用域的.

一個程式只有一個棧. 函式執行時, 引數按照逆序先進棧, 函式執行完畢後包括引數一起退棧, 但寫入一個返回值進棧. 逆序是約定為了讀取方便.

變數賦值會在原先的資料上寫, 而不會重新進棧.

棧是編譯器管理的, 程式設計師不能處理, 但用 api alloca 可以申請記憶體

  1. 堆是完全給程式控制的, 動態記憶體

使用 API allocfree.

alloc 的引數是申請記憶體的位元組數. 返回值是申請的記憶體


比如申請字串 "hello world", 程式碼是:

char[] c = alloc(11*2)

這時 char 型別的長度對應是 2, 採用 utf16 編碼. 然後進行賦值:

cc[0]  =  `h`;
cc[1]  =  `e`;

然後拷貝字串:

char[] c1 =alloc (11*2)
cc[0]  =  `h`;
cc[1]  =  `e`;
// ...
char[] c2 =alloc (11*2)
strncpy(c2,  11,  c1)

最後釋放記憶體:

free(c1);
free(c2);

所以字串拼接很慢.. 涉及到建立和拷貝.

拷貝字串就…

char[]  r1="ss"
char[]  r2="ss"

char[] r3 = alloc (2 * (strlen(r1) + strlen(r2)))

strcopy(r3, strlen(r1), r1)
strcopy(r3 + strlen(r1), strlen(r1), r2)

申請一段記憶體, 然後擴充套件長度的虛擬碼:

char[] a = alloc (2)
char[] b = alloc (6)

strncpy(b, 2, a)

free(a)
a = b

更通用的 API memcpy(dest,size,src)


如何遍歷輸出當前函式下所有變數的值?

思路, 在開頭結尾取記憶體地址, 然後遍歷:

int* start =alloca(4)
int* end =alloca(4)

指標可以加減進行偏移, 根據不同的型別不同大小的便宜:

int 型別的移動 4bits, char 移動是 2bits.

沒有具體型別的 void* 不能移動, 因為沒有大小資訊.


char 可能有多種長度, wchar_t.

常用的長度和型別的對應:

8 bool byte sbyte
16 char short ushort
32 int float uint
64 long double ulong

? void object string

單位是 bits.

字串表示有 ASSIC, UTF8, UTF16 多種型別不同的長度.


clang 使用, clang code.c 對程式碼進行編譯.


unsigned int 在 C 裡可以賦值 -1, 而不會報錯; 對應 Go 的 uint 賦值 -1 將會報錯.
因為 C 沒有做多餘的檢查, 對應機器真實的操作.


寫盜號是通過分析記憶體中資料的位置, 推算變數佔用的空間, 通過手動操作指標獲取資料得到的.
安全的程式語言會關閉這種可能性.


通過指標實現多返回值.


Point p = {1, 2, 3};

Struct 型別, 資料是存在棧上的, 因為 p 是變數, 變數是存放在棧上的.
這裡的 p 對應資料長度固定, 整個都是存在放棧上的.

Stuct 類似陣列, 名稱很大程度是語法糖, *pb 就是 pb[0]


p.x(&p)->x 是一回事, 指標使用 ->, 非指標使用 .

相關文章