前言

本文章主要面向的是已经基本熟悉 C 语言的初学者,而目的则在于通过对 C 语言具体机制的介绍,使得读者可以理解其本质,面对问题时,能够通过语言的 设计思想 而非 强制记忆的规则 得出正确的答案。

所采取的方式是,通过对某个语言机制 存在的原因 ,来理解这个特性 为何这样设计 ,并得出一些 基本的原则 ,以至于 可以推测 在其他情景下这个特性会如何表现。适当的时候,会结合一些例子说明问题。

给楚楚。

静态

这一部分主要介绍 C 语言中的静态部分,即语法相关。

在开始之前,有一个基本原则可以记住:

基本原则

C 语言是一门直接面向内存操作的语言。

C 语言在本质上不过是在按规则不断读写内存而已。这一点在之后会不断体现。

数据类型

C 中常见的数据类型有 intlongfloatdouble 等等基础类型,和 structunionenum 等自定义类型。

数据类型的存在,是为了给内存里本身并 没有意义 的二进制数据,提供一个 理解这些数据的方式。因此,int 类型会导致 C 语言认为这个位置上的二进制们是一个特定长度的整数,并按照整数的加减乘除进行运算;而 float 类型会导致 C 语言把这些数据理解成一个浮点数来处理。

总的来说,内存里的数据本身没有意义,有了数据类型规定的理解方式后,它们才有了意义。

但是,另外需要注意的一点是,数据类型的存在,只是为了告诉 C 语言的编译器如何处理数据,由此来生成 CPU 指令。而在 编译之后 实际运行时,只有 CPU 对内存作数学运算,并 不会留有任何关于数据类型的信息,这也是一条重要的原则。

重要原则

C 语言编译后数据类型就不再存在,即数据类型只用于编译时帮助生成代码。

指针

关于指针最重要的一点:

重要原则

指针在本质上只是一个整数。

这一点请时刻牢记。

因此,指针存在的作用是,为了能够引用内存中任意位置的数据,需要将它的地址这个整数,保存在某种的类型变量里。这种类型就是指针类型,这种类型的 内容就是一个整数 ,不过是这个整数被 理解为内存地址 而已。

指针特有的操作是 解引用 。解引用的操作,就是把指针变量所存储的那个整数取出,作为内存地址理解,然后把对这个变量的取值或者赋值, 转化为对那个内存地址上内容的取值或赋值

由于指针的值不过是一个整数,因此可以像整数一样进行加减来计算地址,称为 指针运算 。与普通的整数运算不同,指针每次加 1,都是将指针所存储的地址的值,加上指针 所指向的那个类型占据的大小 ,这样新的地址值就会 指向下一个 这个类型的数据。即如下例:

1
2
long b[4] = {0}, *a = b;
a += 3; // a 存储的地址将增加 3 * sizeof(long),即指向 b[3]。

结构体

结构体的存在,是因为程序员表达一个事物可能需要多个基础数据类型的量,但将一个个量在函数等之间 单独传递过于麻烦 ,所以提供一种机制把多个量 打包一起处理

重要原则

结构体的本质是一个偏移量表(以及由此知道的总长度)。

这一点会在不同的场景下不断体现。

可以通过 .-> 访问结构体的成员 。在访问成员时,实际上,只是取得结构体在内存里的位置, 加上这个成员相对于结构体开始的偏移量 ,来 访问这个新位置上内存中的内容 而已。

// TODO: &(NULL->a)

类型转换

类型转换分为两种,一种是基础数据类型间的转换,一种是指针类型的转换。

基础数据类型有 intlongfloatdouble 等,在它们之间进行相互转换时,是将数据 按照原有类型理解 以后,重新 以另一种类型规定的方式把这个值存储 到另一个地方。

例如,在将 int 转换为 float 时,int直接采用补码表示一个整数,类型转换时按照补码理解这个数的值,然后通过运算,计算出 float 需要的 IEEE 754 格式的值(符号位、指数、尾数)给使用者。二者在内存中的表示其实完全不同,是编译器会生成代码帮助程序员完成这个转换。

重要原则

指针类型间的转换可以理解为只是为了 C 语言的类型检查,编译时不会产生实际代码。

也就是说,编译器需要知道指针所指向数据的类型,才能知道应该 怎样操作 指向的这块数据,以及 检查 出不合法的操作。类型转换,只是程序员明确告诉编译器指向的这块数据可以被 重新理解 为另一种类型而已,然后编译器就会按照这种类型操作。但实际上,这种重新理解只需要在编译的时候知道即可,所以编译后不会有相关的代码。

// TODO:结构体间类型转换。

实际情况下,不同指针类型的长度可能不同,所以有时会发生实际的扩展或压缩地址长度的操作,这一点知道即可。

指针与数组

// TODO: 正在写作。

函数调用

// TOOD: 正在写作。