Skip to content

Latest commit

 

History

History
225 lines (165 loc) · 9.77 KB

16.rst

File metadata and controls

225 lines (165 loc) · 9.77 KB
Authors: Kenneth Lee
Version: 0.2
Date: 2023-09-14
Status: Draft

:index:`c`

C语言速成

介绍

本书的目标读者马上要学操作系统了,很多操作系统,比如著名的Linux,都是用C写的。 所以这章我们来速成一下C语言。

C语言经常被称为中低级语言。我们近距离观察一下为什么会有这种说法的。

最早的系统都是用汇编语言写的,汇编语言和直接编码的数字几乎是一一对应的,你看见 汇编语言,就能大致猜到背后的程序是什么样的,好比这样::

00:   f3 0f 1e fa             endbr64
04:   55                      push   %rbp
05:   48 89 e5                mov    %rsp,%rbp
08:   48 83 ec 20             sub    $0x20,%rsp
0c:   bf 00 00 80 02          mov    $0x2800000,%edi
11:   e8 00 00 00 00          call   16 <main+0x16>
16:   48 89 45 f0             mov    %rax,-0x10(%rbp)
1a:   bf 00 00 80 02          mov    $0x2800000,%edi

冒号前面的是内存中的地址(这里是相对地址,等把文件放到内存中会加上一个偏移), 冒号后面的数字就是指令,指令后面的就是汇编语言的指令,我们就看最后1a位置上的那 条指令,我们还能隐约看到汇编中的0x2800000呈现在指令中的样子(最后是80 02)。

所以,看着汇编,基本上简单联想一下,我们就可以知道代码是什么样子的。汇编器只是 根据汇编代码的提示,给我们编码一个数字而已。甚至都不需要看代码的其他位置,不用 看什么全局变量之类的东西,汇编器就可以工作了。

现在我们再看C语言,我们用一个程序来做参考,假定程序是这样的:

int add(int a, int b) {
     return a+b-10;
}

现在我们看看它的汇编:

0000000000000000 <add>:
0:   f3 0f 1e fa             endbr64
4:   8d 44 77 a6             lea    -0xa(%rdi,%rsi,1),%eax
8:   c3                      ret

虽然复杂了一些,但我们开始可以看到具体的行为的,endbr64是x86汇编中放在每个函数 前面用来校验这是一个函数的,我们这里忽略。所以这里的第一条有功能的指令就是lea, 它表示Load Effective Address,用来计算地址用的,作用是把第一个寄存器加上第二个 寄存器左移0位(括号里的1),再加上-0xa(就是减去10)。第二个指令ret,就是程序 跳转回调用点。

你看,这个程序基本上还是一一对应的,我们基本上还是可以猜到它是怎么样的。如果这 是用c++编译的,它就是这样的了:

0000000000000000 <_Z3addii>:
0:   f3 0f 1e fa             endbr64
4:   8d 44 77 f6             lea    -0xa(%rdi,%rsi,2),%eax
8:   c3                      ret

这只有一个区别,名字从原来的add变成了_Zaddii。因为C++支持重载,所以需要在名字 中编码函数的参数,所以add后面要加上ii,表示这是add有两个int做输入参数时的版本。

如果你再加上模板,类,继承,异常这些特性。这个代码就更加难一一对应了。比如我们 把这个函数的int换成string,它就变成这个样子了:

section .text
0000000000000000 <_Z3addNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_>:
   0:   f3 0f 1e fa             endbr64
   4:   41 55                   push   %r13
   6:   41 54                   push   %r12
   8:   49 89 fc                mov    %rdi,%r12
   b:   48 83 c7 10             add    $0x10,%rdi
   f:   55                      push   %rbp
  10:   53                      push   %rbx
  11:   48 89 d3                mov    %rdx,%rbx
  14:   48 83 ec 18             sub    $0x18,%rsp
  18:   4c 8b 6e 08             mov    0x8(%rsi),%r13
  1c:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  23:   00 00
  25:   48 89 44 24 08          mov    %rax,0x8(%rsp)
  2a:   31 c0                   xor    %eax,%eax
  2c:   49 89 3c 24             mov    %rdi,(%r12)
  30:   48 8b 2e                mov    (%rsi),%rbp
  33:   48 89 e8                mov    %rbp,%rax
  36:   4c 01 e8                add    %r13,%rax
  39:   74 09                   je     44 <_Z3addNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_+0x44>
  3b:   48 85 ed                test   %rbp,%rbp
  3e:   0f 84 8e 00 00 00       je     d2 <_Z3addNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_+0xd2>
  44:   4c 89 2c 24             mov    %r13,(%rsp)
  48:   49 83 fd 0f             cmp    $0xf,%r13
  4c:   77 52                   ja     a0 <_Z3addNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_+0xa0>

section .unlikely_text
0000000000000000 <_Z3addNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_.cold>:
   0:   4c 89 e7                mov    %r12,%rdi
   3:   e8 00 00 00 00          call   8 <_Z3addNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_.cold+0x8>
   8:   48 89 ef                mov    %rbp,%rdi
   b:   e8 00 00 00 00          call   10 <_Z3addNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEES4_.cold+0x10>

由于string的各种构造行为的存在,这个代码会在很多地方增加额外的代码处理这些构造 和析构。你甚至会发现,它还多了一个额外的函数出来。

所以,我们一般可以认为C++是中高级语言。而Python之类的,你基本上没法和汇编一一 对应了(实际上Python的二进制根本就不是用来运行的,而是指导Python的解释器运行 的,它严格来说是和源代码才是基本一一对应的),这就是彻底的高级语言了。

操作系统这些底层的程序经常要控制函数怎么调用,系统怎么请求,线程怎么调度,需要 直接控制汇编的代码是怎么工作的,所以就会更喜欢用C来写。这相当于就是写汇编,只 是写得比较快而已。

所以,我们基本上可以认为,去掉高级功能以后的C++就是C。C的代码都可以直接用C++编 译器来编译的。只是生成的二进制不同,其实你甚至可以在C++中强行要求编译的结果也 是C形式的,这样就可以了:

extern "C" {
        int add(int a, int b) {
             return a+b-10;
        }
}

所以,其实你完全可以在C++语言中和C混着用,比如你不用C++的cout,在C++程序中这样 写,是没有问题的:

#include <stdio.h>
#include <string>

int main(void) {
     string s("hello world\n");
     printf(s.c_str());
     return 0;
}

这里混用了C++的string和C的printf,是没有问题的,stdio.h中已经声明printf是 extern "C"的,所以你在main中用这个函数,它是知道这是个C的函数的,就会用printf 这个名字去调用它,而不是C++重载版本的名字去调用它。

实际上,C++主程序的main函数,就是C形式的。

C++学下去,有很多功能,比如模板,标准模板库(STL),异常处理等等。其实我觉得在 工程上,不少这些功能其实不实用,所以在这个阶段,我个人是建议根本没有必要深入进 去,就用最基本的C++的类,重载两个功能就够了,其他高级功能,特别是设计模板的高 级功能,都不要用,要实现什么功能都用C来做,就够了。所以,我们这里就来学习一下 C是怎么用的。必要的时候,单独写C的代码也没有什么不可以。

C和C++的语言区别

C是基础版本的C++。只包含最简单的功能,所以你记得一组不要用的C++功能,就基本上 懂C了。

下面是明显的C不支持的C++功能(模板相关的天然不会支持,忽略):

  1. 不支持类,只支持struct,里面不能包含函数,它就是一个内存数据。定义了什么, 内存就有什么,基本上一一对应。

    因为不支持类,所以也不支持new。

  2. 不支持引用,比如func(int &a),这样不行,需要引用,就真的写成指针,比如 func(int *a),然后用*a这样的方式直接修改它的内容。

  3. const int SIZE = 10被看作“不能更改的”变量,而不是一个常数所以不能这样定义数 组int arr[SIZE],要定义常数,只能用#define SIZE 0这样的形式。

  4. 不支持重载,一个名字的函数只能有一个。当然更不支持操作符重载。

  5. 头文件都有.h扩展名,所以你会用#include <stdio.h>而不是#include <iostream>。

  6. 没有namespace的概念。

没有了,是不是很简单?(其实一些旧的标准还有更多的限制,比如不能用//写注释,变 量定义不能在函数中间之类的,但我们暂时不用考虑去考古。)

其实真正的麻烦是学习函数库。但学这个函数库不亏,因为C++很多时候也需要使用C的函 数库的。比如你要创建一个线程,pthread_create_thread就是一个C的函数,C++中就没 有对应的标准封装。

标准的C函数库可以直接上网查POSIX标准,这个标准是针对大部分Unix系统的,但 Windows也支持,所以它的标准化程度也比C++高。但大部分时候,在Unix系统中我们都可 以用man来查,如果你大概记得名字,比如要查thread有关的函数,你可以这样找::

man -k thread

下面是一批最基本的函数,都可以上网查例子或者用man学习:

  • scanf,对应cin
  • printf,对应cout
  • malloc,对应new
  • free,对应delete
  • open/close/read/write,对应iostream

编译

C的编译器用法和C++差不多,如果用GNU的工具链,C++用g++编译,而C用gcc编译。像这 样::

gcc test.c -o test
g++ test.cpp -o test

如果你使用更新潮的LLVM,那也差不多::

clang test.c -o test
clang++ test.cpp -o test

其他-O,-Wall的参数完全是一样的。