学校上过数学系的 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 是一回事, 指针使用 ->, 非指针使用 .


题叶
17.3k 声望2.7k 粉丝

Calcit 语言作者