个人最近读到 CSAPP 这本书,由于书中的 Labs 都使用 C 语言,所以最近个人打算开始先学习一下 《The C Programming Language》这本书,这本 K&R 书在国外很畅销,上图中就为原版,目前已经修订到第三个版本了。这本书是最为经典的 C 语言教材,作者是 C 语言的设计者所编写,使用一本好的教材去入门也会轻松很多,本人看的是 Pearson 引入的中文第二版,封面图👇:


如果有一些其他语言经验例如 Java 来学习可能前期的一些语言基础部分花上一点时间就能轻松掌握,因为 C 没有自动垃圾回收器实现也没有更高级的多道程序设计并发协程操作所以 C 是一门小而简单的语言。如果涉及到内存分配和指针,内存分配和指针操作的花需要更多的时间,因为这是比较难掌握的。当前这个版本完全使用的 ANSI 标准,现在更多都是基于这个版本的 C 编译器实现,使用这个标准一大特点就是函数声明采用了声明参数信息,例如 C99 中的函数原型,这些信息能帮助编译器很容易得检测到参数不匹配导致的后果,这特性与一些弱类型或者脚本语言相比要写出的程序更加健壮。

目前 C 语言编译器标准有很多延伸出来的版本,这里作为初学者更加应该是关注如何编写 C 语言程序的代码和风格,书中大部分采用的一个完整代码实例来讲解 C 语言程序设计,从简单逻辑程序将其不断得循序渐进增加程序功能,以增加程序的功能来讲解 C 语言一些语言特征和特性,每个特性都是有前面一个例子程序功能需求所引出来的。这就使得新手朋友不会一上来就觉得写程序很困难情况,从而把人劝退。目前 C 语言还在发展,最新的标准是 C17 的,添加了很多新的特性例如字节对齐说明符、泛型机制(generic selection)、对多线程的支持、静态断言、原子操作以及对 Unicode 的支持,而现在笔者读的这本书采用的可能比较老的标准,但是不妨碍而去学习 C 语言的精髓。


Part 1

对应新学习一门语言学习者而言,更多是养成编写符合对应规范的代码,这里我更关注的是一些 C 语言细节,我整理第一章注意事项:

  1. 注意数据类型使用,和数据类型范围,例如默认 int 类型范围。
  2. 要通过缩进的方式突出程序逻辑结构。
  3. 运算符号和多个表达式之间要保留空格。
  4. 使用良好格式化方式输出调试信息,例如有小数部分的。
  5. 不要在程序写过多的幻数,要通过符号常量的方式编写,符号常量是没有分号的。
  6. 如果能把多条语句放到一起可以放到一起简写。
  7. 变量如果可以在定义的时候初始化就直接初始化。
  8. C 语言中的字符和字符表有对应关系,char类型是小整数。
  9. 通过使用函数来提高代码复用率,在函数实现之前先写函数原型定义,让编译器检查。
  10. 函数原型的参数名称可以与具体实现函数参数名称不相同。
  11. 被调用的函数不能修改外部的变量,而只是一个临时自动变量。
  12. 能使用数组多用数组,数组传递的时候只是传递数组的启始位置,可以通过指针访问偏斜访问。
  13. 大多数 C 语言中字符串采用 \0 表示字符串分隔符结尾。
  14. 非必要不要使用全局变量,如果要使用则用 extern 关键字来引入外部变量。
  15. 如果变量在函数之前出现,不需要使用 extern 关键字引入变量。
  16. 新版本的 ANSI C 标准要兼容,例如无参数要写 void 关键字。

第一章部分主要是对应 C 语言做了一个全局的讲解,主要对 C 语言的传统核心部分进行了介绍,通过很多小例子来展示 C 语言相关的基础知识,笔者还是建议能把书中例子全部敲一遍,例子放到本文后面链接中。


Part 2

第二章主要讲解的 类型和运算符与表达式 ,在 C 语言中有一些基础的数据类型,默认使用 int 定义的变量大小是取决于机器架构的,因为 C 语言大部分是嵌入式开发对应类型的使用一定要格外注意!而浮点数也有单精度和双精度,此外就是限定符的使用,限定符可以限制任何类型,如果是 unsigned 为无符号限定则对应的类型总数值范围是正值或为 0 ,不会出现负数情况;用 signed 限定的类型就可以为负数情况,这得看具体限定的什么类型,例如 signed char 范围是 -2^7 到 (2 ^ 7) - 1 ,下面为我整理一张思维导图:

为什么要有类型这概念可以查看我之前写过的一篇文章:基础数据类型作用是干什么的?

第二章的一些编写程序的注意事项:

  1. 变量名称起名必须以字母开头,大部分都是小写字母,不能以数字开头。
  2. 下划线 _ 被看作字母,通常用于比较长的变量名,变量名是区分大小写。
  3. 不要使用 C 语言默认关键字,关键字都是小写。
  4. 局部变量名应该比较短,外部变量名可以比较长,循环变量名要短小。
  5. 在使用常量的时候,常量名应该是全大写,并且放到源文件头部。
  6. 日常使用字面量书写的数字 1234 默认则为 int 类型常量,而如果结尾带 l 和 L 结尾则是 long 类型,例如1213121241L 的长整型。
  7. 类型常量也可以通过有符号和无符号表示,例如 0xful 则为十六进制无符号 Long 类型,其对应的十进制值为 15 。
  8. 人类使用的是十进制,计算机是二进制,但是 C 语言支持多种进制书写,带 0 的表示八进制,带 0x 表示十六进制,
  9. 字符常量一般都是用来做比较操作,也可以与整数参与数值运算操作。
  10. 常量表达式是只包含常量的表达式,常量表达式只能在编译时求值,而不是运行时。
  11. 在 C 语言中书写一个字符串常量,双引号是控制字符串范围的。
  12. 在 C 语言中常量字符串底层存储是通过数组的方式,大小是不确定的,但是是以 \0 结尾。
  13. 在 C 语言中 \`x\` 定义一个字符其底层存储是一个数值类型对应着字符集,而 "X" 是字符串常量,底层存储是一个以 \0 结尾的数组。
  14. 在 C 语言中枚举是常量整数,枚举类型通过 enum 关键字声明,如果没有指定值就从0开始递增,如果指定了值其后面字段值会按照前面的值基础之前递增。
  15. enum 枚举值可以自动化生成,而 #define 为字符常量也不能被编译器检查。
  16. 为变量分配地址和存储空间的称为定义,不分配地址的称为声明。
  17. 变量定义只的是分配空间和初始化值,而声明只是表明变量的类型和名字,定义一个变量就包含声明的操作。
  18. 在 C 语言中提倡先声明一个变量在使用,然后对其操作分配空间和值。
  19. 变量类型一致的可以放到一起声明,也可以分开声明,分开声明可以方便些注释。
  20. 使用 const 限定符的变量值是不能被修改的,const 限定符可以用在函数形参上限制。
  21. 运算符中的 % 不能用于 float 和 double 类型,注意类型防止运算发生溢出,+ 和 - 的运算符优先级低于 * 、/ 、% 等符号,这和日常数学规则一致,这些都属于算术运算操作。
  22. 逻辑运算符 = 、 >= 、<= 、!= 、== 属于逻辑运算符,逻辑运算符优先级比算术运算符低,例如 i < n + 1 程序在运算的时候会先处理 n + 1 等价于 i < (n + 1)。
  23. 逻辑运算符特殊的 && 和 || 属于短路运算符,&& 优先级比 || 高,&& 运算符的表达式会从左到右,如果遇到条件为真则停止计算,而 || 表达式两侧任意一方为真则停止计算。
  24. 需要注意的是 != 运算符优先级大于 = 赋值运算符,如果要先赋值在做比较的场景需要使用 () 来提高表达式的优先级,例如 (c = getchar()) != '\n' 的场景。
  25. 在数值运算的过程中一定要注意参入运算的数值类型,不能把长整型赋值给短整型,浮点不能赋值给整型,这些类型在编译的时候编译器会检查。
  26. 在使用 char 类型存储非字符数据,最好指定 signed 或 unsigned 限定符。
  27. 使用单精度的浮点数类型可以提高运算速度,但是如果想精确计算还是推荐精度更高的类型。
  28. 在 C 语言中函数形参传递参数时,char 和 short 类型会被转换成 int 类型,而 float 类型会转换成 double 类型,我们可以通过函数原型提前声明参数类型。
  29. 在不同的场景下使用自增和自减时要注意运算符是在变量前面还是后面。
  30. 在不同的类型参入运算的过程中注意类型转换,无符号和有符号类型做数值运算,可能会被隐式转换成有符号类型,但是如果特殊情况例如 -1L > 1UL 那么 -1L 会被提升为无符号类型。
  31. 在 C 语言中只提供 6 中位操作符,能操作的范围是 char 、short 、 int 、long 类型,只能在整数数值类型之上操作,分别为:& (按位与 AND)、| (按位或 OR)、^ (按位异或 XOR)、<< >> (左移和右移)、~ (按位求反,一元运算符)。

下面是笔者画一张位操作运算和二进制位关系图:

第二章主要介绍了表达式和变量操作运算符相关的内容,在实际编程要注意这些细节,运算符在表达式上优先级关系,注意字符常量和枚举的区别,数据值运算过程中注意变量的类型大小关系,以避免出现溢出安全问题。最为需要注意的是同一级别的计算顺序,取决于编译器先优先计算谁?例如 x = f() + g() 这个例子中 x 变量依赖于右边的两个函数,那么 x 变量的最终的值会被这两个函数的计算顺序所影响到,如果要强行优化的化,必须把这两个函数通过中间变量来做二次计算。


Part 3

第三章主要讲解逻辑分支控制流,在计算机中运行会出现需要逻辑分支,例如生活在满足某个条件就去做什么什么,不做什么什么,这就是控制执行流程。在计算机中也是一样的,它可以用来决定当程序指定的布尔运算值为真或假时,程序接下来将会采取的行动。 第三章主要是针对的程序逻辑快控制和执行次序,注意事项:

  1. 一对花括号可以把一组声明和语句写在一起构建成复合语句,也称之为程序块。
  2. 一元运算符只对一个表达式执行操作,并产生一个新值;二元运算符将两个表达式合成一个稍复杂的表达式,换而言之,他们的操作数均是两个
  3. 编写多层嵌套 if else 语句时需要注意缩进,或者使用一对 { } 花括号分割。
  4. 在编写多路 if else 时程序会依次从各个表达式中求值,一旦某个表达式结构为真,则执行相关的语句,并且停止整个语句序列的执行。
  5. 在 if else 语句中 else 永远是用于执行处理所有条件均不成立的情况,当然 else 也不需要编写。
  6. 在 C 语言中没有直接获取数组大小的函数,因为数组底层是连续的空间元素也是固定的,所以可以通过 sizeof(arr) / sizeof(arr[0]) 公式获得数组元素大小。
  7. 如果是多个 if else 语句快,可以替换成 switch case 语句进行编写,在 switch 语句中有一个默认的分支为 default ,default 分支可以写和可以不写。
  8. 在 while 和 for 中可以使用 break 和 return 强制语句中立即退出,在 switch case 语句可以控制执行流程,不使用 break 可以做到多个语句块串通。
  9. 在 C 语言中 for 语句体的逻辑可以在运算的时候修改循环条件变量,循环结束之后条件表达式中的变量会一直保留,for 语句可以有任意表达式组成,所有不要把无关紧要的计算表达式放到 for 语句中。
  10. 在 C 语言中 for 循环语句中的变量可以是多个,可以使用 , 逗号分割,可以将多个表达式放在各个语句成分中,比如同时处理两个循环控制变量。
  11. 在 C 语言中 for 和 while 语句都是在循环体执行前对某个条件表达式进行计算,查看是否满足某个条件来执行语句体,而 do while 则会在是先执行语句体,在判断某个控制循环的条件表达式。
  12. 在 C 语言中 break 语句是跳出当前执行程序的状态,而 continue 关键字则是会让程序停止执行当前循环语句进入下一次循环体中执行。

上面则是第三章的内容主要介绍了 C 语言如何控制程序执行流程和分支,还有循环的多种形式。需要注意的是 C 语言中还提供了 goto 语句,该语句会使得代码的执行逻辑跳转到指定的位置上执行逻辑代码,常用与跳出多层次的循环体结构,实际开发中不建议使用。


Part 4

第4章主要介绍如何结构化程序的逻辑,每个程序都是不同的函数调用组成的,一个程序可以被分成多个模块,是分而治之的方法应用,在计算机科学界的所有的问题都可以使用这个方法,把一个大的问题拆解小的问题去解决,隐藏操作细节从而使整个程序结构更加清晰,降低修改程序的难度。在 C 语言中函数设计是为了高效性与易用性这两个因素,C 语言提倡的是小的函数体,而不是由少量较大的函数组成,一个程序可以将函数存放在多个源文件中,各个文件可以单独编译一起加载。在 C 语言中如果函数签名没有写返回值类型,而默认的返回值类型是 int ,函数之间的通信可以通过参数和函数返回值以及外部变量进行。如果函数签名有返回值那么必然有返回值,如果在分支里面有返回值而其他分支里面没有返回值,说明该函数是不合法的。

组成程序的多个函数可以分散在不同的文件中,如果程序由多个源文件组成的话,需要将其多个源文件分别编译生成出 .o 文件然后再合并 3 个 .o 文件生成可执行文件 .out ,这个过程叫编译与加载。函数原型声明必须和函数实现一致,这样编译器才能检测到一些不匹配的参数类型和返回值类型错误,而如果把函数原型和函数实现单独存放和单独编译就会出现不匹配错误无法检测出来的情况。

  1. 如果函数在没有函数原型的情况下,函数直接编写在表达式中例如 n = atof(line) 那么编译器可以根据上下文推断出返回值可能是 int 类型,但是参数类型就无法推断处理,这种做法是不合理的,所有建议编写函数原型。
  2. 在 C 语言中变量分别为 外部变量内部变量,内部变量之前已经说过为函数调用栈所产生的变量,随着调用栈回收而针对回收,而外部变量则是全局使用的,可以在多个函数之间直接使用不需要通过函数的参数列表传递。如果程序中大量需要共享一些数据,可以尝试使用外部变量,这样可以让参数类别变得更短而不是更长。外部变量作用域比内部变量作用域更大并且生命周期更长,如果多个函数只是共享变量数据,而不涉及到互相调用情况建议使用外部变量。
  3. 在一个程序的所有源文件中外部变量只能被声明一次,而如果多个文件中需要使用该变量需要通过 extern 关键字引入该变量。
  4. 一个正常的 C 语言程序所组成他的功能应该被分配到多个源代码文件中,每个源文件只负责它所能访问的任务信息,每个 .c 源文件应该只负责对应的功能函数;而如果多个源文件需要共享一些信息,可以将这些信息抽离到 .c 头文件中,如果一个程序太多需要多个头文件应该要合理精心设计划分规则。
  5. 在 C 语言中中默认定义的全局变量是可以被多源文件中函数进行访问的,如果想让其不被其他源文件中函数访问可以使用 static 关键字进行修饰,即内部变量,这样就让其修饰的变量只能本源文件中被访问,即使其他源文件中有同名的变量也不是同一个。
  6. 如果 static 修饰的是局部变量是修改其生命周期,而 static 修饰全局变量只是修改作用域范围,而 static 修饰的是函数则这个函数为内部函数,也是修改其函数作用域范围。
  7. 在 C 语言中还提供一种寄存器变量,寄存器变量可以适用于访问频率比较高的变量,执行速度会更快,因为在 CPU 中寄存器,寄存器的存储空间是最为接近顶端的。使用 register 关键字声明变量就为寄存器变量,可以在函数参数上使用。
  8. 在函数里定义的变量是自动变量,每次调用都会被初始化,生命周期会跟随着调用栈所回收,而静态变量只会在第一次使用函数时被初始化,早期的 C 语言中可以使用 auto 关键字来声明自动变量,但是目前新版本的标准已经废弃,可以不再使用,默认在函数内部创建的变量即可为自动变量。
  9. 在编写函数形参类型时,需要注意不要让函数内部的变量名隐藏外部变量的类型,例如函数内部使用的变量名和函数外部的变量名类型不一致。
  10. 外部变量和自动变量如果光声明没有进行初始化则默认值为 0 ,寄存器变量和自动变量的值是在函数调用时或者程序块时进行初始化的,建议在声明变量的时候就进行初始化变量的默认值。
  11. 在 C 语言中通过一个预处理器,一般分为 3 种 文件包含、条件编译、宏定义。分为两种一种是 #define 指令和 #include 指令, #include 指令所制定的文件在编译器处理的时候编译器会去相应的目录查找到源文件,将其替换成目标文件中的内容。需要注意是平时使用的 <stdio.h> 这种形式,会有相应的规则,缺点很明显如果多个文件依赖于这个包含的文件,如果源文件修改了那么所有的文件都需要进行重新编译。
  12. 宏处理器对应就是 #define 指令所声明的变量,程序如果使用了宏名称记号的地方都会被替换成对应的文本,如果内容是多行需要再末尾处添加一个反斜杠符号 \ ,宏会直接替换程序中代码文本,替换插入到代码中。
  13. 在宏中定义预定义函数时,一定要主要运算符优先级,例如 define mul(a, b) ((a)*(b)) 等价于 (a * b),因为宏最基本设计就替换字符串,并没有针对运算符优先级做出来,如果需要设置计算表达式优先级你可以通过 () 处理。

Part 5

在 C 语言中指针概念最为重要的,也是 C 语言的灵魂,指针是一种能保存变量地址的变量,可以认为指针就是一个变量,而与其他变量不同的是它的值是可以存储其他变量的地址,指针操作在 C 语言中使用的非常广泛,但是指针和 goto 语句一样如果使用不规范会导致程序难以阅读和理解,使用者粗心大意会导致指针指向错误地方导致程序崩溃。

合理的使用指针也会使得程序变得更加简单清晰,在 C 语言中最为重要的就是编译器部分,编译器的一些标准定制了操纵指针的规则,熟悉这些规则对于开发者来说能写出更好的 C 语言程序代码。指针和数组有一种关系,因为计算机内存的连续一块空间,如果想要分配内存,那么就向这块内存地址申请一块连续的空间,将这块空间的指针返回即可使用。计算机中的存储单元都是有编号的和有编址的,一个 char 类型的变量可以占用 1 byte 大小,那么两个相邻的 char 类型可以组织一个 short 类型,指针就是起到索引这块内存的作用。

  1. 指针是用存储变量的地址的,指针变量的值是另外一个变量的地址,可以通过指针来间接操作某个变量的值。
  2. 指针变量在没有初始化时,操作它是很危险的,因为只是声明了指针变量,而值是空的,操作空的存储单元非常危险。
  3. 指针变量的类型必须和被存储的变量类型的一致,例如 int a = 100int *p = &a 这是符合规范的。
  4. 在同一种编译器下指针所占用的内存空间是规定的,例如在 16 位编译器下占用 2 字节,而在 32 位编译器下占用 4 字节,在 64 位下占用 8 字节,所有指针所存储的变量类型不会影响到其占用大小,影响它的是编译器相关的规则。
  5. 在 C 语言中操作指针的运算符有 & 获取某个变量的地址,* 获取指针变量的值,这两个一算运算符比其他运算符的优先级要高。
  6. 在 C 语言中函数调用传递的参数都是以值传递的方式进行的,被调用的函数不能直接修改外部的变量的值,如果想修改必须以指针的方式传入。
  7. 在 C 语言中数组和指针关系十分密切,通过数组下标能操作的也可以通过指针来操作,通过指针访问比通过数组的下标去访问程序执行速度更快。
  8. 在 C 语言中数组在函数间传递时,函数的形参是数组那么实参是可以是数组名,如果是形参是指针那么就可以传递数组名和也可以传递指针。
  9. 在 C 语言中字符串在存储时也是以数组的方式存储,操作字符串也可以通过数组的方式来操作,字符串变量中的每个元素也有自己的地址。
  10. 如果使用指针指向一个字符串位置,例如 char *p = "Hello" 这个 p 则为指针,只能通过 p 访问这块内存字符串不能修改,因为这样书写的字符串是常量不可变,严格意义上等于 const char *p = "Hello" 常量。
  11. 数组和指针最大差异就是,数组名不是变量,而指针是变量,指针变量可以存储数组中的元素地址然后通过偏移访问,而如果把指针赋值给数组名这是非法的。
  12. 在函数形参是建议使用指针,数组传递可以使用指针,使用指针的好处是可以传递数组的任意位置的元素作为启始位置,例如 slen(&arr[2]) 从第二元素开始。
  13. 地址算术运算是指在 C 语言中可以通过操作指针的偏移量来访问某个变量中的值,例如 int *pi = arr; 是一个指向数组的指针,那么就可以通过 sum = *(pi += 5); 的方式来计算某个值和另外某个值的和了,指针指向的值可以通过指针算术运算符来访问。
  14. 在 C 语言中 0 可以代表一个指针的地址,如果一个指针的值是 0 则表示这个地址是无效的或者说发生了异常,指针和整数之间是不能转换的,但是 0 是除外的,常量 0 可以赋值给一个指针,一个指针可以和 0 进行比较,常量 NULL 代表常量 0 ,常量 0 是一个特殊的值可以用带代表 NULL 值。
  15. 指针在算术运算过程中,指针每次所参入运算的长度大小,是取决于指针类型的,这是什么意思?一个 short 类型的指针 2 字节,那么每次则以 2 的倍数来计算。
  16. 指针有效的运算有,相同类型的指针之间赋值运算,指针同整数之间的加法或减法运算,指向同一个数组中两个指针间的减法或比较运算是合法的。
  17. 不符合指针运算有,两个指针做加减乘除运算,指针同浮点数做运算,不经过强制类型转换的指针从一个对象类型指向另外一种类型对象的指针的运算。
  18. 在 C 语言中字符串底层是数组存储的,如果使用一个指针变量指向第一个元素,想把它赋值给另外一个指针的话,就需要拷贝其元素的值,而不是赋值指针里面的地址。
  19. 在 C 语言中两个字符串在运算的时候,如果是通过指针来复制其位置上的值例如 while ((*s++ = *t++) != 0) 时,这个可以简写成 while ((*s++ = *t++)) 省去 \0 部分,因为在运算过程中字符值同时也用来和空字符串中的 \0 进行比较,所有默认到达字符串尾巴就会停止循环。
  20. 在 C 语言中如果一个逻辑表达式的算术运算结构可以为 1 和 0 ,那么就可以充当 BOOL 类型来使用。
  21. 在 C 语言中如果使用多维数组进行函数传递,必须要在函数参数上声明列数,例如 func(int arr[2][13]) 当然也可以简写成 func(int (*arr)[13]) 这种声明是参数指针形式,指向一个 13 个整数元素一维数组的对象。
  22. 在 C 语言中二维数组的存储方式是以一维的方式存放的,每个元素启始位置都是一维数组的大小作为其实偏移量。
  23. 在 C 语言中指针还有一个特殊的用处就是 “函数的指针” ,函数本身不是变量但是可以通过指针指向函数的地址,可以定义指向函数的指针,可以将函数的地址保存到数组中也可以传递给函数作为参数,也能作为函数的返回值使用。
  24. 函数指针格式 int(*p)(int,int) 则表示返回值类型为 int 参数类型为 int 的函数,当然排除一个个例通用类型的指针 void *,任何类型的指针都可以转成 void * 类型并且转换回原类型时不会丢失信息。

指针是 C 语言最为重要的一个部分,如果指针没有掌握好,那么 C 语言也就没有掌握好,指针是 C 语言精华,特别是多级指针复杂声明这部分,当然这本书并没有深入指针部分,笔者建议如果先要学习更深入指针部分建议阅读其他技术书籍。


Part 6

第六章主要针对基础数据结构组成的结构体相关的内容,基础的数据类型只能描述单一的数据,数据类型也是单一的,如果有一个需求就是通过代码结构描述一个人的基本信息,这些基础数据类型就不能很好来描述一个整体了,这是就需要通过基础类型组成的结构体类型描述这些信息。结构可以认为是一个或者多个变量的集合,这些变量可以有不同的数据类型,由这些变量组成的一个整体的结构,而这些结构就可以在其他地方单独的使用。在 ANSI 标准中对于结构规范是可以像不同类型变量一样能拷贝、赋值、传递给函数、也可以作为函数返回值使用,编译器必须满足这个标准。

  1. 在 C 语言中通过 struct 关键字来声明一个结构体,在结构里面使用大括号 {} 来描述成员变量,成员变量可以是基础数据类型组成。
  2. 部分结构体页不需要成员变量列表,也不会为其分配内存存储空间,它仅仅描述了一个结构的模板和轮廓。
  3. 给一个结构体的成员变量进行初始化可以通过一堆大括号安装字段进行赋值,变量类型名称要声明,例如 struct Person people = { 22, "Leon Ding" }; 类型为 Person 的结构类型,变量名称为 people 。
  4. 结构的成员变量也可以是结构体,结构可以嵌套在结构中,如果结构变量需要操作内部的变量表,可以通过 . 进行访问,例如 people.name 进行访问操作,每个点代表一个层级。
  5. 结构可以作为一个整体进行赋值和取地址,访问其成员,但是结构之间是不能直接进行比较的,可以通过常量成员列表进行初始化结构。
  6. 结构在函数之间传递的时候需要注意如果是通过赋值给参数是拷贝整个结构,而如果使用指针的方式相比复制整个结构的效率要高。
  7. 如果是结构指针的情况,在访问其结构成员的时候需要注意 *pp 为该结构,而 (*pp).x 则为成员变量,需要解引用取值操作,在 C 语言中为了方便取值采用了新的语法糖 pp->x 来替换。在一些特殊运算场景下,对其成员操作可以使用 ++pp->x 等于 ++(p).x,对其成员 x 进行自增操作,对该结构指针自增操作再加一 (++p)->x ,在 C 语言中指针操作时一定要注意运算符优先级关系。
  8. 在 C 语言如果想获取某个变量的内存大小可以使用标准库内置的 sizeof 函数,可以被计算大小类型有例如 short 、int 、 double 、long 、数组 、结构 、派生的类型和指针类型都可以用来作为 sizeof 的参数返回值为一个int类型,但是 sizeof 不能在预处理器中的 #if 语句块中使用,因为预处理器不会对类型名进行分析,这和预处理器相关规则有关,#define 语句表达式中使用是合法的。
  9. 结构里面成员变量大小不会影响到实际结构的大小,例如 struct { char c; int i; }; 其中 c 是字符类型为 1 字节,而 i 为 int 类型为 4 字节,但是实际上编译器会为结构做类型填充 8 字节,这个填充可能发生在 c 成员变量身上。
  10. 在 C 语言中如果函数原型和函数实现返回的是一个指针结构函数名不能容易阅读,也不容易被编辑器搜索匹配到,这是可以将函数返回值类型和函数名分开写,例如 struct key *binsearch(char *word, struct key *tab, int n); 这里的返回值就可以另外单独占据一行编写,这是在 C 语言中允许的 struct key *
  11. 在 C 语言结构中也允许自引用结构,这是什么意思?换句话说成员变量也是是自己的类型成员变量,也可以指针类型的,这在 C 语言中通称之为 自引用结构
  12. 在 C 语言中如果想要分配一个结构对齐存储空间,需要使用到标准库中的 malloc 函数,返回值是一个指向 void 通用类型的的指针,然后可以显示地将该指针强制转换为所需的类型,函数参数可以通过 sizeof 函数的返回值来计算,如果存储空间不够时 malloc 函数返回值是 NULL 。
  13. 在 C 语言中基础类型和结构都可以通过 typedef 关键字来定义类型的别名,可以适用于枚举、基础数据类型、结构、函数指针,typedef 并没有给已知的类型创建新的类型,而只是对其已存在的某个类型增加了一个新的名称而已,通常采用 typedef 定义一个新类型名称为了方便阅读和书写方便.
  14. 使用 typedef 比 #define 功能要强大,#define 只能算是普通字符串预处理器,而 typedef 是由编译器解释的,最为常见的使用常见就是通过在不同架构的普通上数据类型通用型,通过 typedef 基于对应普通的数据类型定义别名,代码逻辑中则用别名,当需要更改的时候更改 typedef 基础类型对应的类型即可。
  15. 在 C 语言中还有一种类似于结构的类型,称之为 unions 联合体,在运行过程中可以保存不同类型和不同长度的对象变量,一个变量可以合法地保存多种数据类型中任何一种类型的对象,联合体声明的方式和结构相似,通过联合创建的变量的值可以是联合体中类型的值,在运行过程中必须保证读取的类型必须是最近一次存入的类型,程序员要负责跟踪当前保存在联合中类型。
  16. 内存对应计算机来说很宝贵,有一定经验的开发者可能需要位运算的方式对某个变量的二进制位进行操作来标识某个条件,在 C 语言中也有提供这个功能 bit-field(字段位), 可以允许像定义一个结构一样来定义一个变量位,唯一区别就是每个字段 unsigned int 类型并且通过 :的方式设置字段的宽度,例如 struct { unsigned int is_keyword : 1 } flags; 的方式,这样需要访问位就可以使用 . 进行访问,比通过 & 和 | 位运算符号要高效简洁。

最后几章都是标准库和 Unix 编程接口的一些讲解,C 语言的发展随着计算机发展而发展起来的,所以很多历史遗留的软件都是采用 C 去编写的,稍微涉及到一点偏向系统层次的开发时就需要使用到 C 语言,很多语言编译器也是 C ,比如 Python 第一个解释器就是。所以这也是我为什么把学习 C 的列入我学习范畴,K&R 这本书只是算是带一个完全没有 C 语言经验的开发者入门,而更深入还是看读者自己去摸索和学习。

其他资料

便宜 VPS vultr
最后修改:2023 年 07 月 05 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !