學校上過數學系的 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
是 ld 1, ld 1, add
.
變數就是暫存器的地址. 1 + a
是載入 1 和 a 的地址. 基礎型別可以直接在程式裡用, 不需要記憶體. ld 1, lda 00001, add
- 靜態資料區
存放程式一定會用到的內些固定資料. 比如字串常量, 但數字可以直接在程式區用, 而字串不能出現在程式區.
需要在編譯時能得到. 執行時動態的資料沒法知道.
- 棧
存放程式函式和區域性變數. 棧的大小固定, 一般為 2m, 從棧空開始, 先進後出的. 是人為劃分的一塊連續虛擬記憶體. 操作類似 push pop.
int i = 4
這樣就會進棧 4.
函式執行完畢函式內的棧會被清空, 不同的函式之間不會干擾.
{
和 }
表示一個塊級作用域, 其中標記了進棧和清棧的範圍. for 迴圈的 {
和 }
是有塊級作用域的.
一個程式只有一個棧. 函式執行時, 引數按照逆序先進棧, 函式執行完畢後包括引數一起退棧, 但寫入一個返回值進棧. 逆序是約定為了讀取方便.
變數賦值會在原先的資料上寫, 而不會重新進棧.
棧是編譯器管理的, 程式設計師不能處理, 但用 api alloca
可以申請記憶體
- 堆是完全給程式控制的, 動態記憶體
使用 API alloc
和 free
.
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
是一回事, 指標使用 ->
, 非指標使用 .