├── .gitignore ├── README.md ├── c ├── README.md ├── call-stack │ ├── README.md │ └── call.c ├── control-flow │ ├── README.md │ ├── branch.c │ ├── func.c │ └── loop.c ├── hello-world │ ├── README.md │ └── main.c ├── pointer-references │ ├── README.md │ └── pass-by-ref.js ├── structure-heap │ ├── README.md │ ├── buffer-alloc.c │ ├── return-local.c │ └── struct.c └── variable-types │ ├── README.md │ ├── basic-types.c │ └── implicit-conversion.c └── objective-c ├── README.md └── objects ├── README.md └── main.m /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.out 3 | *.s 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 面向 Web 前端的原生语言总结手册 2 | 3 | 这一系列文章旨在让具有 Web 前端背景的开发者快速上手原生语言。 4 | 5 | 6 | ## 背景与动机 7 | 从 WebView 到 Hybrid 再到 React Native,移动端主流技术方案中前端同学的施展空间越来越大。但传统 Web 前端背景的同学所熟悉的编程语言主要是 JavaScript,在与 Native 协作的边界上很容易遇到掌控范围之外的坑,这也是 RN 等方案经常被诟病的理由之一。 8 | 9 | 然而,某一门具体的编程语言并不应该成为生涯的瓶颈或阻碍。已经熟悉某门主流语言的同学,学习新语言的速度可以是非常快的。在这方面,C++ 领域的《Essential C++》就是一个很好的例子:它假定读者已经熟练掌握了一门编程语言,从而忽略了入门编程初期大量琐碎的新手向知识点,直接向读者展示 C++ 的核心特性,让读者能够非常迅速地上手 C++ 语言(注意这和精通是两回事)。对于这份教程而言,让已有 JavaScript 背景的同学能够迅速上手原生语言而迈过跨端开发的一个坎,就是我们的初心。 10 | 11 | 12 | ## 要求与目标 13 | 这份教程对读者的要求只有一点:**熟悉** JavaScript。而在学习目标层面,请首先明确这份教程**不能做到**什么: 14 | 15 | * 让你达到**精通**水平:请慎用这个词。 16 | * 让你成为移动端开发者:特定的编程语言只是平台开发的**子集**。 17 | * 让你熟悉 IDE:这份教程会使用最简单的**命令行**编译配置,无需 IDE。 18 | 19 | 与之相对地,这份教程的定位,是在这些场景下能够让你更快地达成目标: 20 | 21 | * 你在基于 RN 等方案开发,需要整合原生 SDK 或类库。 22 | * 你在 RN 等方案下踩到了 Native 的坑,希望能够独立调试解决。 23 | * 你需要大致理解现有的 Objective-C 等应用代码,或进行小修改。 24 | 25 | 如果这些场景命中了你,那么就别犹豫了,上车继续吧😉 26 | 27 | 28 | ## Getting Started 29 | 如何阅读呢?从下面的链接开始就行了: 30 | 31 | * [**C**](./c) 32 | * [**重温 Hello World**](./c/hello-world) - 介绍编译环境外与编码风格等基础。 33 | * [**变量与类型**](./c/variable-types) - 介绍常常被 JS 程序员忽略的类型系统到底有多么重要。 34 | * [**控制流**](./c/control-flow) - 介绍日常司空见惯的 for 和 while 循环是怎样和底层机制联系起来的。 35 | * [**函数调用与栈**](./c/call-stack) - 介绍原生语言是如何复用代码段的。 36 | * [**指针与引用**](./c/pointer-references) - 介绍我们需要区分基本类型和引用类型的理由。 37 | * [**结构体与堆**](./c/structure-heap) - 介绍对象的雏形与内存管理的概念。 38 | * [**Objective-C**](./objective-c) 39 | 40 | 为什么从 C 开始呢?一方面,WASM 和 WebGL 中少不了 C 的影子,而更主要的是,C 的内容**其实非常少**,并且有一个非常好的思维模型,能够帮助你理解编程语言的核心特性,从而更容易地通过类比来掌握其它语言。例如作为 C 的超集,Objective-C 中就有许多 C 的影子。从 C 开始能够让你更好地理解它的特性为何这么设计,从而更好地理解其它编译型的原生语言。当然,如果你已经熟悉了 C,你也可以直接跳过它,阅读其它部分。 41 | 42 | 43 | ## 贡献 44 | 非常欢迎各种形式的参与,包括但不仅限于问题讨论、勘误指正与新增内容🙏使用 GitHub 的 Issue 和 PR 来参与吧。 45 | 46 | 47 | ## 致谢 48 | 本系列文章的组织结构参考了《Objective-C Programming The Big Nerd Ranch Guide》一书。 49 | 50 | 51 | ## 许可 52 | [CC 署名-禁止演绎](http://creativecommons.org/licenses/by-nd/4.0) 53 | -------------------------------------------------------------------------------- /c/README.md: -------------------------------------------------------------------------------- 1 | # C 语言 2 | 3 | 这一部分内容旨在快速覆盖 C 的特性,帮助有 JavaScript 等脚本语言基础的同学触类旁通地理解它,其中许多概念对理解其它原生语言有重要的作用。 4 | 5 | 6 | ## 目录 7 | 8 | * [重温 Hello World](./hello-world) 9 | * [变量与类型](./variable-types) 10 | * [控制流](./control-flow) 11 | * [函数调用与栈](./call-stack) 12 | * [指针与引用](./pointer-references) 13 | * [结构体与堆](./structure-heap) 14 | -------------------------------------------------------------------------------- /c/call-stack/README.md: -------------------------------------------------------------------------------- 1 | # 函数调用与栈 2 | 3 | 从汇编中的子程序到 C 语言中的函数,再到面向对象语言中对象的方法,编程语言中复用代码段的能力在不断进步。但语法的演进背后的基础原理总是相似的,借助 C 语言理解函数调用的原理之后,能帮助你更轻松地以触类旁通的方式理解更高级语言中类似的机制。 4 | 5 | 6 | ## 函数的意义 7 | 为什么需要函数?在[控制流](../control-flow)一节中,我们已经能够编写各种复杂的逻辑判断流程,但还对于功能相近的代码,我们尚且没有一种稳定的方式来**复用**它们。而在 C 语言中,实现这个能力的概念就是所谓的函数了。 8 | 9 | 除了复用自己编写的代码之外,复用第三方库也是函数的重要用途之一。回想我们对 `printf` 函数的调用,它实际上就是一个库函数。通过将可复用的代码段封装为函数后作为库发布的方式,我们就不需要重复发明轮子,在大量库函数的基础上构建我们的上层应用了。 10 | 11 | 12 | ## 函数的背后 13 | 熟练的前端开发者会编写大量的函数。函数最基本的能力有哪些呢?不外乎下面这几点: 14 | 15 | * 函数可以接受参数。 16 | * 函数内可以定义局部变量。 17 | * 函数可以返回值。 18 | * 函数中可以调用其它函数。 19 | 20 | 从 C 到 JavaScript,函数的这几个基本能力是一致的。当然了,作为函数一等公民的 JavaScript,其闭包、匿名函数等能力是 C 原生不具备的。但要搞清楚 Objective-C 等 C -like 语言的方法、消息等概念,从 C 语言函数的原理出发是完全可行的。 21 | 22 | 另一方面,虽然 JavaScript 和 C 有着相近的函数语法和函数调用的机制,但巨大的区别在于 C 是一个非常贴近底层的语言,它的功能可以非常简单地对应到汇编代码中,而 JavaScript 则几乎屏蔽了全部的底层复杂度。因而在各类 JavaScript 的介绍性文章中,你多半只能对背后的执行机制有个道听途说的认识,无法获得直观而切实的感受。但在 C 里要讲清楚这些内容就容易得多了。让我们从 C 的函数语法开始吧: 23 | 24 | ``` c 25 | // 声明 26 | int add (int a, int b); 27 | 28 | // 定义 29 | int add (int a, int b) { return a + b; } 30 | ``` 31 | 32 | C 的函数并非一等公民,函数需要先声明,后使用。函数声明既可以放到头文件中以便于代码的复用,也可以简单地写在 `.c` 代码的顶部。 33 | 34 | 我们已经知道,if 和 for 等语句编译成汇编后,只相当于在其中的代码块首尾加上几条判断和跳转指令而已。函数编译成汇编时也可以这么简单地实现吗?我们需要注意一个重要的机制:作用域。 35 | 36 | C 中的函数和 if for 等控制流语句的一大区别,在于函数内部有自己的作用域。譬如这样的代码是完全合法的: 37 | 38 | ``` c 39 | int x; 40 | 41 | if (...) { 42 | x = 10; 43 | } 44 | ``` 45 | 46 | 但对于函数而言,下面的代码就违背了作用域机制: 47 | 48 | ``` c 49 | int x; 50 | 51 | void fn () { 52 | x = 10; 53 | } 54 | ``` 55 | 56 | 当然了,JavaScript 中通过闭包可以很方便地访问到函数外层的变量。但为什么在 C 中函数的大括号里就找不到外层的变量呢?我们如果查看 [call.c](./call.c) 编译成的汇编码,会发现函数调用时所使用的指令不是 `JMP` 而是 `CALL`,这有什么不同呢? 57 | 58 | 和 `JMP` 纯粹地跳转到某一个代码段继续执行不同,`CALL` 不仅仅能跳转到另一个代码段位置,更能够在接下来遇到 `RET` 指令时,直接回到 `CALL` 所在的位置继续执行。联想一下 C 中函数代码段尾部的 `return`,不难发现一个 C 的函数体对应到汇编,也就是被 `CALL` 和 `RET` 指令包裹起来的一块代码而已。 59 | 60 | 这和作用域有什么关系呢?之前的控制流结构里,`JMP` 这样的指令基本上都是在相邻的几段代码之间来回跳跃,不会跳转到一个很遥远的位置。这也就带来一个问题:在 `CALL` 过去的位置,怎么样传递函数的参数呢?汇编的寄存器只有那么几个,但 C 中的函数至少支持 127 个参数,这是怎么做到的呢?并且,`CALL` 过去的代码段里完全可以继续 `CALL` 其它地方,然后再用多个 `RET` 逐次返回。这里面也没有对函数参数长度的限制。并且,后 `CALL` 的代码段会先被 `RET` 回去,有什么数据结构能够满足这种需求呢? 61 | 62 | 等等,可变长度和后进先出,这不就是经典的**栈**吗?基于栈的模型,我们需要一段连续的地址空间,每次函数调用的多个参数依次 push 到栈上,在函数体内部也通过栈上的偏移量来访问这些参数,并在函数返回时 pop 出栈,将内存空间释放。这样一来,多个嵌套的函数调用,就转化为了在栈上对地址线性地来来回回的操作。这样一个逐层存储函数调用参数的栈,就是我们耳熟能详的**调用栈**了。譬如 A 函数中调用了 B,而 B 中调用了 C,那么调用栈应该看起来像这样: 63 | 64 | ``` nasm 65 | --- 66 | C 67 | --- 68 | B 69 | --- 70 | A 71 | --- 72 | ``` 73 | 74 | 关于调用栈有一个反直觉的地方:A 虽然在调用栈的底部,但它在内存中的地址一般却是最大的。这背后有些历史原因,但更接近车道是靠左行还是靠右行一样只是一个约定,故而虽然有所谓的“栈内存从高地址向低地址增长”这一说,但这个定义并不是特别准确。 75 | 76 | 既然我们已经引出了调用栈的概念,那么调用栈上放着的内容又是什么呢?调用栈上的每一项,都装着为一个函数所传入的实际参数,每个函数所对应的项,我们称之为**栈帧**。每个栈帧里放置的实际参数,其实也就是函数里能够访问得到的局部变量了。 77 | 78 | 现在我们可以把函数的能力和底层的原理对应起来了: 79 | 80 | * 函数可以接受参数 - 参数逐个存在栈帧里。 81 | * 函数内可以定义局部变量 - 函数内只能访问栈帧上的数据。 82 | * 函数可以返回值 - 对应汇编的 `RET` 指令。 83 | * 函数中可以调用其它函数 - 调用函数,相当于 push 一个新帧;函数返回,相当于 pop 出栈顶的帧。 84 | 85 | 走通这些概念后,不妨思考这个问题:函数调用栈有专门的位置存放吗?有的,这段空间用 `RSP` 和 `RBP` 寄存器表示(**R**eserved **S**tack **P**ointer 与 **R**eserved **B**ase **P**ointer)。`RSP` 指向当前栈顶,`RBP` 指向当前栈帧底部。函数调用时的简化汇编代码大致形如: 86 | 87 | ``` nasm 88 | PUSHQ %RBP ; 保存上一个栈帧地址 89 | MOVQ %RSP, %RBP ; 设置 RBP 为当前栈顶 90 | SUBQ $16, %RSP ; 为局部变量留出 16 字节 91 | 92 | ; ...函数体 93 | 94 | MOVQ %RBP, %RSP ; 重置 RSP 到栈底 95 | POPQ %RBP ; 恢复上一个栈帧 96 | ``` 97 | 98 | 到这里,相信你已经可以理解调用栈的概念了。看起来虽然有些费劲,但在接下来的内容中它对我们很重要 :) 99 | -------------------------------------------------------------------------------- /c/call-stack/call.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int add(int a, int b); 4 | 5 | int add(int a, int b) { return a + b; } 6 | 7 | int main(int argc, char *argv[]) { 8 | int x = add(1, 2); 9 | printf("%d\n", x); 10 | 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /c/control-flow/README.md: -------------------------------------------------------------------------------- 1 | # 控制流 2 | 3 | 控制流指的是程序运行时,每条指令运行或求值的顺序。不同编程语言会提供的不同的流程控制指令,大致可以分为以下几种: 4 | 5 | * 跳转到另一位置继续运行指令 - `goto` 6 | * 若满足条件,则运行紧接的一段指令 - `if` / `else` 7 | * 运行紧接的一段指令若干次,直到满足条件为止 - `while` / `for` 8 | * 运行另一位置的一段指令,完成后回到原位置继续运行 - `function` 9 | 10 | 如此简单的几种控制流类型已经足够程序员使用 C 写出 Linux 复杂度的程序。并且这一套关键字体系影响非常深远,以至于今天的主流编程语言中几乎都多少存在它的影子。下面我们简要地展示 C 中与控制流相关的若干语言特性。由于 `goto` 存在争议,在目前的主流语言中也已经很少出现,因此我们着重讨论上面的四种情况中剩下的三种。 11 | 12 | > 下文中涉及汇编的进阶介绍可以加深理解,在你需要了解 WASM 等字节码标准时或许也能对你有所帮助。但如果你的目标是复习 C 的语法,大可以略过它们 :) 13 | 14 | 15 | ## 分支 16 | 17 | ### 基础 18 | [branch.c](./branch.c) 中这个形式的控制流可能是我们最熟悉的之一: 19 | 20 | ``` c 21 | if (expression) { 22 | /* ... */ 23 | } else if (expression) { 24 | /* ... */ 25 | } else { 26 | /* ... */ 27 | } 28 | ``` 29 | 30 | 我们会依次根据每个表达式的求值结果是否 `!= NULL`,判断是否走入相应的代码块。 31 | 32 | 33 | ### 进阶 34 | 如果你有兴趣观察 `gcc -S` 获得的汇编码,会发现一个 if 的判断逻辑背后不过是一条形如 `CMP` 的比较指令和 `JNE` 条件跳转(**J**ump if **N**ot **E**qual)指令而已。对于复杂的逻辑判断,我们只需要串联或嵌套多个 else if 子句即可。这样看来,这个语法兼顾了使用时的便利和实现上的简洁,是一个非常经典的语言特性。 35 | 36 | 37 | ## 循环 38 | 39 | ### 基础 40 | [loop.c](./loop.c) 中的 for 循环也是非常经典的控制流。当然也别忘了 while 语句: 41 | 42 | ``` c 43 | for (int i; i < 10; i++) { 44 | /* ... */ 45 | } 46 | 47 | while (expression) { 48 | /* ... */ 49 | } 50 | ``` 51 | 52 | ### 进阶 53 | C 语言中 while 循环背后的原理较为简单:在循环体代码顶部放置一个 `JMP` 无条件跳转指令,它会始终跳转到循环体底部一对上文提及的 `CMP` 和 `JNE` 指令位置,由它们判断是否退出循环体: 54 | 55 | ``` nasm 56 | JMP LOOP ; 首先跳到底部以开始循环 57 | BEGIN: NOP ; 空指令占位符 58 | ; ...此处开始放置循环体中代码 59 | ; ... 60 | ; ... 61 | ; ...执行完循环体内代码 62 | LOOP: CMP ... ; 检查条件 63 | JNE BEGIN ; 若不满足则跳转到 BEGIN 位置 64 | ``` 65 | 66 | 67 | 而 for 循环所编译成的汇编代码执行流程和它接近,但多了循环的初始化过程和一个计数器临时变量,故而要退出 for 的检查实际上是针对这个计数器的。 68 | 69 | 别忘了 `break` 和 `continue` 关键字,它们可以允许有条件地跳出整个循环,或忽略循环体中的一部分代码。这是通过在循环体中添加更多的标号,从而更加灵活地 `JMP` 来实现的。 70 | 71 | 72 | ## 子程序 73 | 74 | ### 基础 75 | 子程序是一个概括性的术语,用来指代编程语言中可复用的一段代码块。C 和 JavaScript 中的函数就相当于子程序。子程序中可以调用其它子程序,从而实现代码的模块化。如果子程序调用了自己,我们将这种情况称为**递归**。在 [func.c](./func.c) 中有一个 C 语言函数递归调用的简单示例: 76 | 77 | ``` c 78 | int fib(int n) { 79 | if (n == 0) 80 | return 0; 81 | else if (n == 1) 82 | return 1; 83 | else 84 | return (fib(n - 1) + fib(n - 2)); 85 | } 86 | ``` 87 | 88 | ### 进阶 89 | 函数调用时发生了什么呢?这是个非常有意思的话题。函数作为编程语言中最基础的可复用元素,理解函数对于理解各种编程语言复用代码的原理会有很大的帮助。接下来我们将着重讨论这块内容。 90 | -------------------------------------------------------------------------------- /c/control-flow/branch.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main(int argc, char *argv[]) { 6 | srand(time(NULL)); 7 | int r = rand(); 8 | 9 | if (r % 2 == 1) { 10 | printf("odd number!\n"); 11 | } else { 12 | printf("even number!\n"); 13 | } 14 | 15 | return 0; 16 | } 17 | -------------------------------------------------------------------------------- /c/control-flow/func.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int fib(int n); 4 | 5 | int fib(int n) { 6 | if (n == 0) 7 | return 0; 8 | else if (n == 1) 9 | return 1; 10 | else 11 | return (fib(n - 1) + fib(n - 2)); 12 | } 13 | 14 | int main(int argc, char *argv[]) { 15 | int x = fib(10); 16 | printf("%d\n", x); 17 | 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /c/control-flow/loop.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char *argv[]) { 4 | for (int i = 0; i < 10; i++) { 5 | printf("%d\n", i); 6 | } 7 | 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /c/hello-world/README.md: -------------------------------------------------------------------------------- 1 | # 重温 Hello World 2 | 3 | 编写 Hello World 是使用一门语言最简单的方式。在这里,我们除了编写出最简单的 C 代码以外,还会介绍与**编译环境**相关的基础知识,以及我们的**编码风格**所对应的约定。 4 | 5 | 6 | ## 运行示例 7 | 在 [main.c](./main.c) 中 Hello World 的代码示例如下: 8 | 9 | ``` c 10 | #include 11 | 12 | int main(int argc, char *argv[]) { 13 | printf("Hello World!\n"); 14 | return 0; 15 | } 16 | ``` 17 | 18 | 在终端里,我们可以通过 `gcc` 命令编译 C 代码: 19 | 20 | ``` bash 21 | gcc main.c 22 | ``` 23 | 24 | 这会默认在同目录下生成名为 `a.out` 的文件。如何运行呢? 25 | 26 | ``` bash 27 | ./a.out 28 | ``` 29 | 30 | 这就足够了!这样看来 C 语言并不比 `npm run dev` 更麻烦吧 :) 31 | 32 | 33 | ## 编译环境 34 | 和 JS 这样可通过解释器直接执行源码的**解释型语言**不同,C 语言是需要通过编译器编译到到机器码的**编译型语言**。这个过程和 babel 的转译过程有些接近,但一大区别是 C 编译器生成的是平台相关的**原生机器码**。故而与 babel 生成的 JS 代码可以在各类兼容的 JS 解释器里运行不同,但 C 机器码只能在编译的目标平台下运行:Linux 下的 `a.out` 和 macOS 下的 `a.out` 是不兼容的。 35 | 36 | Linux 上的 `gcc` 命令背后是 GCC 编译器,而 macOS 上同样的命令背后其实是 Clang 编译器。不过对于我们的简单示例,这点区别可以忽略。 37 | 38 | 当然,编译过程从简单的 `.c` 源码到可执行文件的背后,是有大量的幕后工作的。如果对这个过程感兴趣,下面这条命令会是个不错的开始: 39 | 40 | ``` bash 41 | gcc main.c -S 42 | ``` 43 | 44 | 45 | ## 基础概念 46 | 在我们的 Hello World 中,除了 `"Hello World"` 字符串之外的内容初看起来也许会有些让人摸不着头脑,但对于一个最小可用的 C 应用而言,它们都是必要的。下面整理出我们现在所涉及到的关键语法: 47 | 48 | ### Include 与库函数 49 | 在第一行代码中,我们使用 include 指令导入了 stdio.h 头文件: 50 | 51 | ``` c 52 | #include 53 | ``` 54 | 55 | 这很自然地会让我们联想到 ES6 的 import 语法: 56 | 57 | ``` js 58 | import fs from 'fs' 59 | ``` 60 | 61 | 二者有何异同呢?它们确实有着相近的目标,即让用户导入并复用库函数。但 C 的 include 语法并没有 import 那么方便。目前我们只需要知道,include 导入的是 `.h` 格式的**头文件**,其中仅包含了各个库函数的**声明**,而没有**定义**。如何区分二者呢?不妨类比下面的代码段: 62 | 63 | ``` js 64 | // 函数声明 65 | const fn 66 | 67 | // 函数定义 68 | fn = () => 123 69 | ``` 70 | 71 | 你可以粗略地认为,C 的库函数名已经**声明**在了头文件中,只需 include 后即可在你的代码中使用。而 stdio.h 就是 C 标准库中与 IO 相关的头文件(stdio 即 **St**andar**d** **I**nput **O**utput 的缩写),其中包含了 `printf` 函数的声明。因此在 include 后我们就能够使用这些函数,进而在终端输出 Hello World 了。 72 | 73 | 思考题:为什么在 JavaScript 中不需要导入任何模块就能调用 `console.log`,而 C 要把 `printf` 设计成导入库函数后才能使用呢? 74 | 75 | ### Main 函数 76 | 对于 JS 文件而言,它的内容即便只有一行简单的 `console.log(123)`,也能在浏览器和 Node 中顺利执行。但 C 语言中你并不能这么写: 77 | 78 | ``` c 79 | #include 80 | 81 | printf("Hello World!\n"); 82 | ``` 83 | 84 | 而是需要将代码放在 `main` 函数之中。这又是为什么呢? 85 | 86 | 请注意我们现在使用 C 语言编写的,实际上已经是原生的**应用**了。在浏览器中进行应用开发时,我们有 React 和 Vue 等框架提供的一套生命周期 API:例如在 Vue 中,我们可以将组件的 `mounted` 方法作为应用代码的入口;类似地在 React 中,这样的入口则是 `componentDidMount` 方法。对原生应用而言,POSIX 规范就相当于一套这样的编程框架,而 C 中的 `main` 函数则是我们的原生应用完成初始化后的入口位置。 87 | 88 | 就好像前端框架的初始化过程中会在背后做大量杂活一样,C 应用在 `main` 函数执行前也会做许多工作。如果希望对原生应用的编译和执行过程有更深的了解,推荐参阅 CSAPP 和《程序员的自我修养:链接、装载与库》等经典书籍。 89 | 90 | 91 | ## 代码风格 92 | 熟悉某门语言的开发者在编写这门语言的代码时,一般都有个人所偏好的代码风格。而在学习一门新语言时,相应的代码风格常常会带来一些困惑:这样的命名符合这门语言的约定吗?这门语言代码的组织与排版的最佳实践是怎样的呢?网上搜索到的代码片段往往风格各异,这更加大了代码风格上的困惑。 93 | 94 | 在 JavaScript 社区,已经有 ESLint 等工具能帮助我们自动化格式化代码。在 C 中,我们选择 `clang-format` 作为自动化格式代码的工具,相应的风格则是默认的 [LLVM Coding Standard](https://llvm.org/docs/CodingStandards.html)。在前端同学熟悉的 VSCode 中,这个工具也有现成的[插件支持](https://marketplace.visualstudio.com/items?itemName=xaver.clang-format)。在插件安装后即可通过 option-shift-F 快捷键格式化 C / C++ / Objective-C 代码。 95 | 96 | > ClangFormat 的 2 空格缩进和目前的前端主流风格很接近,并且类似的风格已经在 Google Chrome 等大型项目中应用。希望它不会让你感觉太别扭 :) 97 | -------------------------------------------------------------------------------- /c/hello-world/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char *argv[]) { 4 | printf("Hello World!\n"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /c/pointer-references/README.md: -------------------------------------------------------------------------------- 1 | # 指针与引用 2 | 3 | 在前面的章节中,我们已经介绍了与基本的数据类型和控制流相关的语言特性,它们看起来工作得也很好。那么为什么我们还需要引入指针这个概念呢?让我们从上一节最后提及的调用栈说起吧… 4 | 5 | 6 | ## 指针与取址 7 | 很多 C 语言的初学者对于理解指针的概念感到吃力,但如果你按照我们从类型系统出发的方式来理解,那么你就能够使用同样的思维模型来轻松理解指针了。我们已经知道 int 类型变量的值是个整数、char 类型变量的值是个 ASCII 码、float 类型变量的值是个浮点数。而指针也是一种数据类型,这个类型变量的值是个**内存地址**。 8 | 9 | 作为一种类型,指针可以使用 `*` 和 `&` 符号被声明并赋值: 10 | 11 | ``` c 12 | int x = 123; 13 | int *p = &x; 14 | 15 | printf("%d %d\n", x, &p); 16 | ``` 17 | 18 | 我们可以这样声明一个**指向整型变量 x 的指针 p**。注意这里的语法有一个稍微反直觉的地方:C 的语法中,p 的类型初看起来更接近 `Int`,但实际上其类型应当理解为 `Pointer`。并且 `*` 符号虽然和 `p` 紧贴,但其实它并不是变量名的一部分。 19 | 20 | 你可以把 `*` 理解为将 `Type` 类型封装在 `Pointer` 指针变量的操作符,它可以让你得到一个 `Pointer` 类型的指针变量。这样我们就能把数据的地址装在指针里了。在使用的时候,我们需要相应地使用 `&` 符号来将数据从指针里拿出来,即所谓的**解引用**。这就是指针的基本用法了。 21 | 22 | 然而既然我们已经有了完善的控制流和模块化的函数,为什么还需要引入这种并不和数据直接相关的类型呢?在我们之前的介绍中,C 的语法其实都只是在汇编上加了一层壳。而指针也不例外:它直接对应于汇编中非常常见的**取地址**操作。 23 | 24 | 取地址操作在 C 程序中有什么意义呢?将数据当做内存地址的操作一定程度上模糊了数据和代码段之间的界限,但在**将程序和数据一视同仁**的冯诺依曼体系结构中,这其实是一种司空见惯的手法。例如,在之前的章节中我们已经介绍过,函数调用时参数会放在调用栈上。但这就带来了一个问题:不管参数是几个简单的 int 还是复杂庞大的数据格式,在发生函数调用时都需要把这些参数全部**复制**一份到栈帧上。而冯诺依曼体系结构的瓶颈就在于内存的读写速度远远跟不上 CPU,故而这个复制的开销经常是难以容忍的。如果将传入函数的参数从值变成传指针,那么我们只需要复制一份指向数据的内存地址即可,从而避免了大量数据重复性读写的开销。 25 | 26 | 27 | 28 | 29 | 30 | ## 引用传递 31 | 在上面我们已经提及,我们可以通过在调用栈上复制指针的方式,来减少函数调用时参数传递的开销。这种方式称之为 Pass by Reference 引用传递,一直到当今的编程语言中都非常常见。 32 | 33 | 但你可能会有疑问,高级的编程语言里不是没有指针了吗?这个概念确实已经在 JavaScript 等语言里被淡化了,但你仍然可以抓住指针的小尾巴。比如,我们可以在 [pass-by-ref.js](./pass-by-ref.js) 示例里观察到 JavaScript 中修改函数参数时行为的不一致性。首先让我们考察这个函数: 34 | 35 | ``` js 36 | function setX (obj) { 37 | obj.x = 1 38 | } 39 | 40 | const a = { x: 0 } 41 | setX(a) 42 | 43 | console.log(a.x) // 1 44 | ``` 45 | 46 | 这个 `setX` 函数能够将外部的变量 `a` 属性修改掉(即所谓的副作用)。但下面的这个函数则不能达到预期的效果: 47 | 48 | ``` js 49 | function setNull (obj) { 50 | obj = null 51 | } 52 | 53 | const b = { x: 0 } 54 | setNull(b) 55 | 56 | console.log(b) // { x: 0 } 57 | ``` 58 | 59 | 为什么同样是传入函数的参数 `obj`,修改其属性能影响外部的变量,但将其置为 null 则不生效呢?在这方面,JavaScript 和 C 语言一样,是将函数参数通过调用栈传递的。而引用类型所传入的参数就是一个指针,故而通过指针去存取对象属性就能够对“外部”变量生效,但将指针本身置为 null 是不影响对象本身的。理解了这一点,就能够理解 Pass by Reference 引用传递的机制了。 60 | -------------------------------------------------------------------------------- /c/pointer-references/pass-by-ref.js: -------------------------------------------------------------------------------- 1 | function setX (obj) { 2 | obj.x = 1 3 | } 4 | 5 | const a = { x: 0 } 6 | setX(a) 7 | 8 | console.log(a.x) // 1 9 | 10 | function setNull (obj) { 11 | obj = null 12 | } 13 | 14 | const b = { x: 0 } 15 | setNull(b) 16 | 17 | console.log(b) // { x: 0 } 18 | -------------------------------------------------------------------------------- /c/structure-heap/README.md: -------------------------------------------------------------------------------- 1 | # 结构体与堆 2 | 3 | 在上一节的最后,我们已经演示了指针和引用传递的作用,但示例所用的 JavaScript 代码中所涉及的对象,在 C 语言中有什么对应的概念呢?让我们从结构体开始吧。 4 | 5 | 6 | ## struct 语法 7 | 对象最基本的用途之一,是封装若干种相互关联的数据。我们可以用结构体来定义这样的数据结构。假设我们在 [struct.c](./struct.c) 里需要一个具备宽度和高度属性的“矩形”概念,就可以定义出相应的结构体: 8 | 9 | ``` c 10 | struct Rect { 11 | float width; 12 | float height; 13 | }; 14 | ``` 15 | 16 | 基本的使用也很符合直觉: 17 | 18 | ``` c 19 | struct Rect rect; 20 | rect.width = 16.0; 21 | rect.height = 9.0; 22 | 23 | printf("Size: %f - %f\n", rect.width, rect.height); 24 | ``` 25 | 26 | 我们可以用 `typedef` 语法来简化声明和使用: 27 | 28 | ``` c 29 | typedef struct { 30 | float width; 31 | float height; 32 | } Rect; 33 | 34 | // ... 35 | Rect rect; 36 | ``` 37 | 38 | 39 | ## 堆内存 40 | 到目前为止,我们所声明的变量都是分配在调用栈上的,栈上分配的变量会在函数调用结束时被自动回收,很多场景下这是一个强大而优雅的特性。但如果单纯基于这个特性,我们难以**在应用的多个函数之间之间复用较大的一段内存空间**。尤其在我们需要使用较大的结构体数据的场景下,每次函数调用时将整个结构体复制一份传入的方式也存在着性能问题。 41 | 42 | 我们已经提及过,基于指针和引用传递方式,我们能够将函数调用时参数的全量复制优化为对指针的传递。但这时我们会遇到另一个麻烦:对于在函数中声明的局部变量,我们不应该返回指向它们的指针:局部变量会在函数调用结束后被回收,故而指向它们的指针,其内容可能被覆写。在 [return-local.c](./return-local.c) 中,我们在函数里初始化了一个结构体,再将指向它的指针返回。它虽然在简单的示例场景下能够运行,但编译时会产生一个警告。 43 | 44 | 有没有不受调用栈机制控制的内存空间呢?这就是所谓的**堆内存**了。我们一般使用**缓冲区**的概念来表达这样的一段内存空间。在 C 语言中,我们可以使用 `malloc` 函数分配堆内存,并用 `free` 函数释放之: 45 | 46 | ``` c 47 | // ... 48 | Rect *rect; 49 | rect = malloc(sizeof(Rect)); 50 | 51 | // 对指向结构体的指针,可用 `->` 替代 `.` 来获取其成员 52 | rect->width = 16.0; 53 | // ... 54 | 55 | free(rect); 56 | rect = NULL; 57 | ``` 58 | 59 | 在 [buffer-alloc.c](./buffer-alloc.c) 中我们可以看到对于堆内存的使用示例:在函数中动态地分配堆内存空间,这样声明的缓冲区不会在函数调用结束后被回收。但由于 C 语言没有垃圾收集机制,我们需要手动管理内存,故而这时保存在堆内存上的结构体需要通过 `free` 来释放。 60 | 61 | 这就是结构体与堆内存的基本概念了。在 C 语言中,手动的内存管理很容易造成内存泄漏。另一方面,结构体适合存取纯粹的数据,难以与函数相关联,实现这样的语法: 62 | 63 | ``` c 64 | rect.getSize(); 65 | rect.rotate(); 66 | ``` 67 | 68 | 这就是面向对象的编程语言所要解决的问题了,而结构体则正是对象的雏形。 69 | -------------------------------------------------------------------------------- /c/structure-heap/buffer-alloc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | typedef struct { 5 | float height; 6 | float width; 7 | } Rect; 8 | 9 | Rect *initRect(float height, float width); 10 | 11 | Rect *initRect(float height, float width) { 12 | Rect *rect; 13 | rect = malloc(sizeof(Rect)); 14 | rect->width = 16.0; 15 | rect->height = 9.0; 16 | return rect; 17 | } 18 | 19 | int main(int argc, const char *argv[]) { 20 | Rect *rect = initRect(16.0, 9.0); 21 | printf("Size: %.0f - %.0f\n", rect->width, rect->height); 22 | free(rect); 23 | rect = NULL; 24 | 25 | return 0; 26 | } 27 | -------------------------------------------------------------------------------- /c/structure-heap/return-local.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef struct { 4 | float height; 5 | float width; 6 | } Rect; 7 | 8 | Rect *initRect(float height, float width); 9 | 10 | Rect *initRect(float height, float width) { 11 | Rect rect; 12 | rect.width = 16.0; 13 | rect.height = 9.0; 14 | return ▭ 15 | } 16 | 17 | int main(int argc, const char *argv[]) { 18 | Rect *rect = initRect(16.0, 9.0); 19 | printf("Size: %.0f - %.0f\n", rect->width, rect->height); 20 | 21 | return 0; 22 | } 23 | -------------------------------------------------------------------------------- /c/structure-heap/struct.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct Screen { 4 | float height; 5 | float width; 6 | }; 7 | 8 | int main(int argc, const char *argv[]) { 9 | struct Screen screen; 10 | screen.width = 16.0; 11 | screen.height = 9.0; 12 | printf("Size: %.0f - %.0f\n", screen.width, screen.height); 13 | 14 | return 0; 15 | } 16 | -------------------------------------------------------------------------------- /c/variable-types/README.md: -------------------------------------------------------------------------------- 1 | # 变量与类型 2 | 3 | 不论是 GUI 还是命令行,计算机程序的处理的内容总是**数据**。在编程语言中,我们可以通过**变量**的概念来存取数据。 4 | 5 | 对 JavaScript 这样灵活的脚本语言,编写形如 `var x = 123` 和 `var y = 'abc'` 的变量赋值代码是非常简单而直观的。但这也带来了潜在的问题:脚本语言用户在定义并使用变量时,往往只关注**变量名**,而忽略了变量的**类型**。 6 | 7 | 类型有多重要呢?类型系统可以成为划分编程语言的主要维度之一。字符串、整数、浮点数、布尔值、集合、字典……类型的背后是编程语言的种种设计决策,它直接关系到一门语言所写出代码的: 8 | 9 | * **安全性** - 完善的静态类型检查能避开空指针等暗坑。 10 | * **抽象能力** - 抽象能力强的语言通常能够支持更复杂的类型。 11 | * **可维护性** - 动态类型一时爽,重构代码火葬场。 12 | * **运行效率** - 能够静态推导出的类型更利于性能优化。 13 | 14 | 正是因为类型系统如此重要,因而在跑通 Hello World 之后,我们希望首先从类型系统出发来重新了解 C。 15 | 16 | 17 | ## 一等公民的类型 18 | 我们经常能够听见这样的说法:“JavaScript 中函数是一等公民。”那么一等公民有何定义,这样的划分在 C 语言中又是如何体现的呢? 19 | 20 | 可以宽泛地认为,一等公民相当于一门编程语言中这样的实体: 21 | 22 | * 可以被存入变量。 23 | * 可以作为函数参数传递。 24 | * 可以作为函数返回值返回。 25 | * 可以在运行时创建,无需编译期静态声明。 26 | * 可以匿名存在。 27 | 28 | 面向对象语言中的对象类型满足上面的每一条规则,因而我们可以认为这些语言中对象是一等公民。类似地,JavaScript 的函数也满足这些规则,故而有 JavaScript 中函数一等公民的说法。而在 C 中,四种基本的运算类型 **char**、**int**、**float** 和 **double** 都属于一等公民,至于剩下的布尔值、数组、指针、结构体和函数呢?它们都有着各自的局限: 29 | 30 | * C 没有原生的布尔值类型,TRUE 和 FALSE 不过是 0 和 1 的语法糖。 31 | * C 不支持对数组重新赋值,只能通过指针间接操作。 32 | * C 没有匿名指针。 33 | * C 的结构体、枚举和联合类型不能够动态创建,只能静态指定。 34 | * C 的函数不能像 `new Function()` 那样动态创建。 35 | 36 | 现在我们已经知道了 C 有哪些类型,其中又有哪些是一等公民。但要根据类型来快速了解一门编程语言,其维度显然不止这一点。下面我们不妨从动态类型和静态类型的区别,来看看 C 与 JavaScript 的异同。 37 | 38 | 39 | ## 动态类型与静态类型 40 | 在 JavaScript 这样的脚本语言中,我们通常习惯只提供**变量名**来声明变量: 41 | 42 | ``` js 43 | let x = 123; 44 | let y = 'Hello World' 45 | let z = [1, 2, 3] 46 | ``` 47 | 48 | 而在 C 里,我们需要同时为变量指定**变量名**和**类型**: 49 | 50 | ``` c 51 | int x = 42; 52 | float y = 1.5; 53 | ``` 54 | 55 | 这种区别的背后,实际上涉及一门编程语言是动态类型语言还是静态类型语言,这对其使用体验会产生很大的影响: 56 | 57 | * **静态语言** - 变量类型在**编译期**指定,类型问题会造成编译失败。它的开发效率相对较低,但运行时更不容易出现类型错误。 58 | * **动态语言** - 变量类型在**运行时**才能判断。它的开发效率相对较高,但更有可能出现运行时的类型错误。 59 | 60 | 很显然,C 属于静态语言,而 JavaScript 属于动态语言。静态语言能够避免什么错误呢?譬如这样的代码: 61 | 62 | ``` js 63 | let fn // undefined 64 | fn() 65 | ``` 66 | 67 | 将任意的变量当做函数调用,这样的语法在 JavaScript 中都是合法的。但正如上面所看到的,被当做函数调用的变量,其值完全有可能是一个未定义的 `undefined`,这所带来的报错相信前端同学一定不会陌生: 68 | 69 | > TypeError: undefined is not a function 70 | 71 | 那么,C 这样的静态语言就能通过编译期检查来杜绝运行时的类型错误了吗?这也是不准确的。在这方面,编程语言的强类型和弱类型之分是另一个重要的影响因素。 72 | 73 | 74 | ## 强类型与弱类型 75 | 静态类型和动态类型的区别可以说泾渭分明,但强类型和弱类型则没有一个非常明确的边界。记得化学中熵的概念吗?熵没有单位,但可以比较。类似地,编程语言没有绝对的强类型和弱类型,只有相对的强弱之分。 76 | 77 | 在类型的强弱方面,有两个非常普遍的错误观点: 78 | 79 | * Python 是脚本语言,所以它是弱类型的。 80 | * C 是静态语言,所以它是强类型的。 81 | 82 | 我们可以用一行代码的实验,来分辨出 Python、JavaScript 和 C 中类型的强弱: 83 | 84 | * 终端运行 Python,输入 `1 + '1'` 查看结果。 85 | * 终端运行 Node,输入 `1 + '1'` 查看结果。 86 | * 编译 [implicit-conversion.c](./implicit-conversion.c) 并运行,查看 C 中 `1 + '1'` 的结果。 87 | 88 | 为什么同样是 `1 + '1'`,在 JavaScript 中得到 `'11'`,在 C 中得到 50,而在 Python 中会报错呢? 89 | 90 | 一门语言的类型越强,则意味着它越不容忍隐式类型转换;反之类型越弱,则越倾向于容忍隐式类型转换。作为例子,我们可以解析这三门语言在上面这个场景(将整数和字符串两种类型相加)下的处理策略: 91 | 92 | * Python 没有隐式转换,解释器认为这是一个类型错误。作为替代,你可以选择 `str(1) + 1` 或 `1 + int('1')`。 93 | * JavaScript 选择将数字隐式转换成字符串,然后执行字符串的相加操作,相当于 `'1' + '1'`。 94 | * C 的设计是先将 char 类型隐式转换为整数,再执行相加操作。C 的 char 类型是基于 ASCII 码表示的,这个编码中每个字符使用 0~256 中的一个数字表示,`'1'` 对应十进制的 49,故而我们得到了 49 + 1 = 50 的整数。 95 | 96 | 所以,C 和 JavaScript 其实都属于弱类型语言,而常常被认为非常灵活的 Python 却是一门正经的强类型语言。对 C 来说,在后面的篇幅中我们会介绍的指针,甚至可以任意指定其所指的类型,这也是 C 中最灵活和最不容易掌握的特性了。 97 | 98 | 99 | ## 代码示例 100 | 对于上文中提及的几种 C 基本数据类型,可以在 [basic-types.c](./basic-types.c) 中查看它们的定义和使用方式,注意 `printf` 中用于打印不同类型变量的标识符哦。 101 | 102 | 在明白了变量的类型后,在 C 中定义并使用它们相信不会是一件难事。但如何处理程序逻辑的复杂度呢?接下来让我们看看 C 中的控制流语句吧。 103 | -------------------------------------------------------------------------------- /c/variable-types/basic-types.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char *argv[]) { 4 | int x = 42; 5 | printf("x has value: %d\n", x); 6 | 7 | float y = 1.5; 8 | printf("y has value: %f\n", y); 9 | 10 | char z = x; 11 | printf("z has value: %c\n", z); 12 | 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /c/variable-types/implicit-conversion.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char *argv[]) { 4 | int x = 1 + '1'; 5 | printf("1 + '1' = %d\n", x); 6 | 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /objective-c/README.md: -------------------------------------------------------------------------------- 1 | # Objective-C 2 | 3 | > WIP 4 | 5 | 这一部分内容将覆盖 Objective-C 的主要特性。 6 | 7 | 8 | ## 目录 9 | 10 | * 对象与消息 11 | * 类与继承 12 | * 对象生命周期 13 | * 集合类 14 | * 常量 15 | * 回调 16 | * 协议与代理 17 | * 类别 18 | * 块 19 | -------------------------------------------------------------------------------- /objective-c/objects/README.md: -------------------------------------------------------------------------------- 1 | # 对象与消息 2 | 3 | ## 第一段 Objective-C 4 | TODO 5 | 6 | 7 | ## 对象 8 | TODO 9 | 10 | 11 | ## 消息 12 | TODO 13 | -------------------------------------------------------------------------------- /objective-c/objects/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, const char *argv[]) { 4 | @autoreleasepool { 5 | NSDate *now = [NSDate date]; 6 | NSLog(@"The date is %@", now); 7 | } 8 | return 0; 9 | } 10 | --------------------------------------------------------------------------------