好的书就是你一看就会,不看还真就不会。
Chapter 1 -- 导言
遗漏知识点
- 赋值操作的值是变量被保存的值
- 建议extern显式地声明全局变量(放在头文件中)
困惑的知识点
- 什么是流?
- 什么叫“函数定义可以以任意次序出现在一个源文件或多个源文件中,但同意函数不能分割存放在多个文件中。”
章节小结
这本书有点牛逼,开始看导言就发觉这本书绝非一般。C语言的基础知识至少过了五六遍了,但看了导言还是有些震惊。并不是说它讲的有多复杂,而是知道C语言基础知识的架构以后看它的讲述方法,确实很有逻辑性,适合初学者,内容编排的十分合理,是一本相当好的教材。说回我自己,起初我还是比较如鱼得水,后面就有点吃力了,习题的难度非常有创造性,甚至有些题目是Leecode上的题目了(这可是序言啊)。可不敢怠慢,多做题点习题巩固一下。Of course, without AI.
打算做的习题
: 1-7, 1-11, 1-13, 1-14, 1-16, 1-18, 1-20, 1-21, 1-22, 1-23, 1-24.
Chapter 2 -- 类型、运算符与表达式
遗漏知识点
- 常量表达式在编译时求值
- strlen返回值不包括字符串结尾的'\0'
- ‘x’是一个字符常量,“x”是一个字符串数组
- expr1 [op]= expr2 等价于 expr1 = expr1 [op] (expr2),这样的设计免去了检查expr1是否一致
- C语言没有指定函数参数的求值顺序
- 在任何一种编程语言中,如果代码的执行结果与求值顺序相关,那都是不好的程序设计风格。很自然,有必要了解哪些问题需要避免,但是,如果不知道这些问题在各种机器上是如何解决的,就最好不要尝试运用某种特殊的实现方式。比如说printf("%d %d\n", ++n, power(2, n));
困惑的知识点
- #define与enum的优劣
- 什么叫“尽管可以声明enum类型的变量,但编译器不检查这种类型的变量中存储的值是否为该枚举的有效值。”
章节小结
这本书很薄,但也很厚。看这章的时候想起以前看过的林锐所写的《高质量C++编程》,书中给出了很多很好的设计规范,理解他们并不需要什么项目经验,而一旦掌握了这些规范,写代码就会变得很有条理。比如说对于指针是否为空的比较,const的使用方法,显式的使用括号突出优先级等。这些规范有的也在这本经久不衰书中也有提到,并给出很多良好的工程实践(实现系统函数,标准库函数)以及对危险和不好的用法的警告。总之,这本书真是一本好书!
打算做的习题
: 2-6, 2-7.
Chapter 3 -- 控制流
遗漏知识点
- 注意if-else的歧义性,else总是与离它最近的if匹配,在嵌套时要用{}明确块的范围
- 对于switch语句,应当尽量减少从一个分支直接进入下一个分支执行的情况,在不得不这样做时,应当在注释中加以说明
- 注意区分逗号在分割函数参数和分割声明时的区别
- do-while语句至少执行一次循环体,并且应该显式地使用{}来强调循环体
困惑的知识点
- “Shell”算法
章节小结
第三章介绍的是流程控制,流程控制对于各个语言都是相通的,记得曾经看郝斌的C语言课程的时候,他就这么说。不同语言的流程控制学一编就够了。流程控制就是让有些语句不执行,有些语句多次执行。而当任何编程语言来到流程控制以后,强度一下子就上来了,掌握了流程控制以后,就可以做很多有趣的事情了,很多复杂的事情了。就比如说排序算法,查找算法等等。
打算做的习题
: 3-1
Chapter 4 -- 函数与程序结构
遗漏知识点
- 声明与定义必须严格一致(考虑单独编译的情况)
- 变量可以武断地分为external和internal两种
- 作用域:可以使用该名字的部分
- “\”可以延续#define定义的宏到下一行
- 条件包含就是在预处理时根据条件选择性地包含文件
- 形式参数不能用带引号的字符串替换。但是,如果在替换文本中,参数名以#作为前缀则结果将被扩展为由实际参数替换该参数的带引号的字符串。#define dprint(expr)printf(#expr " = %g\n", expr);
- 仔细考虑一下max的展开式,就会发现它存在一些缺陷。其中,作为参数的表达式要重复计算两次,如果表达式存在副作用(比如含有自增运算符或输入/输出),则会出现不正确的情况,这是危险的。#define max(a, b) a > b ? a : b然后调用max(x++, y++)
- 如果初始化表达式的个数比数组元素数少,则对外部变量、静态变量和自动变量来说没有初始化表达式的元素将被初始化为0。如果初始化表达式的个数比数组元素数多,则是错误的。不能一次将一个初始化表达式指定给多个数组元素,也不能跳过前面的数组元素而直接初始化后面的数组元素。
- 变量i的作用域是if语句的“真”分支,这个1与该程序块外声明的i无关。每次进入程序时,在程序块内声明以及初始化的自动变量都将被初始化。静态变量只在第一次进入程序时被初始化一次。if(n>0){ int i; i = 1; printf("%d\n", i); }else printf("%d\n", i);
- 在不进行显式初始化的情况下,外部变量和静态变量都将被初始化为0,而自动变量和寄存器变量的初值则没有定义(即初值为无用的信息)
- 对于外部变量与静态变量来说初始化表达式必须是常量表达式,且只化一次(从概念上讲是在程序开始执行前进行初始化)
- static也可用于声明内部变量。static类型的内部变量同自动变量一样,是某个特定函数的局部变量,只能在该函数中使用,但它与自动变量不同的是,不管其所在函数是否被调用,它一直存在,而不像自动变量那样,随着所在函数的被调用和退出而存在和消失。换句话说,static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量
- 在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern声明)。外部变量的定义中必须指定数组的长度,但extern声明则不一定要指定数组的长度。外部变量的初始化只能出现在其定义中
- 下面我们来考虑把上述的计算器程序分割到若干个源文件中的情况。如果该程序的各组成部分很长,这么做还是有必要的。之所以C多个文件,主要是考虑在实际的程序中,它们分别来自于单独编译的库。除此以外,还必须考虑定义和声明在这些文件之间的共享问题。
- 作用域关心的问题:如何进行声明才能确保变量在编译时被正确声明?如何安排声明的位置才能确保程序在加载时各部分能正确连接?如何组织程序才能保证中的声明才能确保只有一份副本?如何初始化外部变量?
- 我们对下面两个因素进行了折衷:一方面是我们期望每个文件只能访问它完成任务所需的信息;另一方面是现实中维护较多的头文件比较困难。我们可以得出这样一个结论:对某些中等规模的程序,最好只用一个头文件存放程序中各部分共享的对象。较大的程序需使用更多的头文件,我们需要精心地组织它们。
困惑的知识点
- 全局变量的生存周期?(既然是外部定义的,那如何知道它的作用域,仍然是考虑单独编译的情况,作用域是main函数吗,凭什么)
- 逆波兰表达式(图灵完备性)
- 快速排序算法是C.A.R.Hoare于1962年发明的。对于一个给定的数组,从中选择一个元素,以该元素为界将其余元素划分为两个子集一个子集中的所有元素都小于该元素,另一个子集中的所有元素都大于或等于该元素。对这样两个子集递归执行这一过程,当某个子集中的元素数小于2时,这个子集就不需要再次排序终止递归
- 某些变量,它们仅供其所在的源文件中的函数使用,其他函数不能访问。用static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。通过static限定外部对象,可以达到隐藏外部对象的目的
章节小结
神!这本书真的是太好了。快排和逆波兰的例子很好地融入了C语言的函数与程序结构的讲解中。作者的教学方式真的很循序渐进,很多东西也引发了我的思考,这薄薄的一本书含金量真的太高了。并且时常在教授我们如何写出好的代码,如何设计好的程序结构,避免危险的程序设计以及良好的工程实践是什么。神!
打算做的习题
:4-0, 4-3, 4-6, 4-7, 4-14.
Chapter 5 -- 指针与数组
遗漏知识点
- 在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的惟一途径,另一个原因是,同其他方法比较起来,使用指针通常可以生成更高效、更紧凑的代码
- ip++等价于(ip++),是将ip的值加1,然后取加1之前ip所指向的值;而(*ip)++是将ip所指向的值加1
- 虽然数组名和指针在某些方面是等价的,但应当注意数组名是一个名字,而指针是一个变量
- 0永远不是一个有效的地址,因此可以作为返回值为指针的函数的错误标志
- NULL也就是0,是唯一一个支持整型常量与指针类型之间隐式转换的整型常量
- 有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算:指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。其他所有形式的指针运算都是非法的,例如两个指针间的加法、乘法、除法、移位或屏蔽运算:指针同float或double类型之间的加法运算;不经强制类型转换面直接将指向一种类型对象的指针赋值给指向另一种类型对象的指针的运算
- 下面两个定义有很大差别:char amessage[] "now is the time"; /定义一个数组/; char *pmessage "now is the time"; 定义一个指针 */
- strcpy函数可以这样写while ((*s++ = *t++) |= '\0’);也可以这样写while(*s++ = *t++);
- 指针数组的一个重要优点在于,数组的每一行长度可以不同
- 对于下标从0开始的问题,如果不习惯可以显式地将第零个元素赋值为零,这在某些求和问题中有时是很方便的
- 逻辑表达式的结果只会是0或者1,因此有时可以用它们来为数组的索引
- C语言规定argc的值总是大于等于1,因为arg[0]总是程序的名字,argv[argc]的值总是一个空指针
- printf的格式化参数也可以是表达式,如printf(argc > 1 ? "%s ": "%s", *++argv);
- UNIX系统中的C语言程序有一个公共的约定:以负号开头的参数表示一个可选标志或参数,并且可以与其他参数组合在一起。
- 排序程序通常包括3部分:判断任何两个对象之间次序的比较操作、颠倒对象次序的交换操作、一个用于比较和交换对象直到所有对象都按正确次序排列的排序算法。由于排序算法与比较、交换操作无关,因此,通过在排序算法中调用不同的比较和交换函数,便可以实现按照不同的标准排序。这就是我们的新版本排序函数所采用的方法。
- int(*comp)(void *a, void b);它表明comp是一个指向函数的指针,该函数具有两个void类型的参数,其返回值对为int。
困惑的知识点
- 针对上面的第七条,amessage是一个仅仅足以存放初始化字符串以及空字符‘\0'的一维数组。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。另一方面pmessage是一个指针,其初值指向一个字符串常量,之后它可以被修改以指向其他地址,但如果试图修改字符串的内容,结果是没有定义的。
- 数组的行数为什么无关紧要
- 函数内static变量的作用
- 当使用argv来捕获命令行参数时,是如何做到分割各个参数的,为什么可以用%s输出,难道空格='\0'?(P99)
章节小结
这本书实在是太太太牛逼了,手搓各种系统函数(各种字符处理函数,malloc,free),手搓命令行参数解析,还手搓了一个语法解析器。这些例子都非常经典,它们的的确确可以用本章所新学的知识来完成,并不需要更进阶的知识,但是其他的教材往往不会这么做,这让我不得不佩服作者的能力,我相信他是一个对操作系统的理解很深的人,并且有很强的编程能力,否则,他不会能把知识和实践结合的这么好。而且这本书经常能在我有困惑的时候,在后面几段或是后面一两页给出答案,让我有了和作者对话的感觉,这也许是好书的一个特征吧。就比如说malloc,free两个函数,没看这本书之前,觉得这两个函数要直接操作内存,偏底层,不用说手搓了,连看都不想看一眼,但是作者通过声明了一个10000个字节的静态数组,然后通过指针操作来实现了这两个函数,虽然是基础版,但是真正的内存又何尝不是一个“非常大”的内存块呢,只是比10000大了很多而已。作者的这种思路真是太棒了,佩服佩服。还有命令行参数的解析,作者通过对argv的解释,介绍了如何做到像命令行那样分割参数的。从前的我觉得-x,又可以-abcx,难以想象这个代码得多复杂,然而实际上用本章所学的知识就能实现。什么叫深入浅出,这他妈就叫深入浅出。这他妈就叫一看就会,不看还真就不会。我看这本书的时候已经不止一次在心里拍手叫好,真是太妙了。这本书一定是我会首先推荐给初学者的C语言书籍。
打算做的习题
: 5-5, 5-10, 5-13, 5-14, 5-15, 5-16, 5-17, 5-18, 5-20.
Chapter 6 -- 结构
遗漏的知识点
- 结构体的合法操作只有几种:作为一个整体复制和赋值
- 函数参数名和结构体同名不会引起冲突。事实上,使用重名可以强调两者之间的关系
- 在所有运算符中,下面四种运算符的优先级最高:结构运算符(.和->)、函数调用运算符()、数组下标运算符[]。因此,他们同操作数之间的结合也最紧密
- 对于常量字符串数组的计数,尽管可以手工计算,但由机器实现会更简单、更安全,当列表可能变更时尤其如此。一种解决方法是,在初值表的末尾添加一个空指针,然后循环直到遇到该空指针为止
- 计算数组中元素的个数时,常见的方法是使用sizeof运算符,用数组的长度初除以单个元素的长度。例如#define NELEMS(x) (sizeof(x) / sizeof((x)))或#define NELEMS(x) (sizeof(x) / sizeof((x)[0])),第二种方法更好一些,即使类型改变了也不需要改动程序
- 两个指针的加法是非法的,但两个指针的减法是合法的,所以mid = (low + high) / 2;是非法的,而mid = low + (high - low) / 2;是合法的
- 一个包含其自身实例的结构体是不合法的,但包含一个指向其自身类型的指针是合法的
- 从任何意义上讲,typedef声明并没有创建一个新类型,它只是某个已存在的类型增加了一个新的名称而已。typedef声明也没有增加任何新的语义:这种方式声明的变量与通过普通声明方式声明的变量具有完全相同的属性。实际typedef类似于#define语句,但由于typedef是由编译器解释的,因此它的文本替能要超过预处理器的能力
- 除了表达方式更简洁之外,使用typedef还有另外两个重要原因。首先,它可以使程参数化,以提高程序的可移植性。如果typedef声明的数据类型同机器有关,那么,当程移植到其他机器上时,只需改变typedef类型定义就可以了。一个经常用到的情况是,对于各种不同大小的整型值来说,都使用通过typedef定义的类型名,然后,分别为各个不同宿主机选择一组合适的short、int和1ong类型大小即可。标准库中有一些例子,例如size_t和ptrdiff_t等。typedef的第二个作用是为程序提供更好的说明性--Treeptr类型显然比一个声明为指向复杂结构的指针更容易让人理解。
- 实际上、联合就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间大到足够容纳最“宽”的成员并且,其对齐方式要适合于联合中所有类型的成员。对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问期中一个成员。联合可以用其第一个成员类型的值进行初始化。
困惑的知识点
- 条件编译语句#if中不能使用sizeof,因为预处理器不对类型名进行分析。但预处理器并不计算#define语句中的表达式。因此在#define中使用sizeof是合法的
- ungetch是如何实现将多读的一个字符放回输入流的
- 哈希表的意义
章节小节
这个章节主要介绍结构体,然后还有一些哈希表、二叉树、链表的内容。这种把数据结构和算法恰到好处的融入C语言的讲解中,我还挺喜欢的。那种填鸭式的教学我挺容易忘记的,但是像作者这样根据实际应用案例来引入推荐能更好地帮助我理解记忆这些知识点。同时,就像之前那样,作者也会在讲解中穿插一些好的编程实践和设计理念。这章的习题也像之前那样,难度不小,汗流浃背;比起有些教材无意义的重复习题不知道高到哪里去了。神中神!
打算做的习题
: 6-2, 6-3, 6-4, 6-5, 6-6.
Chapter 7 -- 输入与输出
遗漏的知识点
- ANSI标准精确地定义了一些输入输出库函数,所以在任何可以使用C语言的系统中都有这些函数的兼容形式。如果程序的交互部分仅仅使用了标准库函数的功能,则可以不经过修改地从一个系统移植到另一个系统。
- 在“stdio.h”、“ctype.h”等头文件中,定义了一些常用的输入输出宏。其中一部分都是宏函数,这样就避免了函数调用的开销。
- 无论"ctype.h"中的函数在给定的机器上是如何实现的,使用这些函数的程序都不必了解字符集的知识。
- 在转换说明中、宽度或精度可以用星号表示、并且相应的值将从参数列表中获取。例如、printf("%.*f", width, precision, x);表示以width指定的宽度和precision指定的小数位数打印浮点数x。
- scanf函数返回成功赋值的项数,如果没有成功赋值任何项则返回0,如果在读取任何项之前就遇到文件结尾则返回EOF。
- printf函数的每个转换说明由一个百分号(%)开始、并以一个转换字符结束。在二者之间依次可以有下列可选项:负号,数字,小数点,数字,字母h或l。
- 变长参数表的函数声明以省略号(...)结尾。省略号只能出现在参数表的尾部。省略号表示函数参数的数量和类型都是可变的。
- 如果要读取格式不固定的输入,最好每次读入一行,然后再用sscanf将合适的格式分离出来读入。
- 格式串通常都包含转换说明,用于控制输入的转换。格式串可能包含下列部分:空格或制表符,在处理过程中将被忽略;普通字符(不包括),用于匹配输人流中下一个非空白符字符;转换说明,依次由一个%、一个可选的赋值禁止字符*、一个可选的数值(指定最大字段宽度)、一个可选的h、1或L字符(指定目标对象的宽度)以及一个转换字符组成。
- 启动一个C语言程序时,操作系统环境负责打开3个文件,并将这3个文件的指针提供给该程序。这3个文件分别是标准输人、标准输出和标准错误,相应的文件指针分别为stdin、stdout和stderr,它们在stdio.h中声明。在大多数环境中,stdin指向键盘,而stdout和stderr指向显示器。stdin和stdout可以被重定向到文件或管道。
- getchar和putchar可以被这样定义:#define getchar() getc(stdin) #define putchar(c) putchar((c), stdin)
- 当程序正常终止时,程序会自动为每个打开的文件调用fclose函数。
- 标准库函数exit()被调用时将终止调用程序的执行。任何调用该程序的进程都可以获取exit的参数值,因此可以用来输出是否执行成功。按照惯例返回0表示一切正常,返回非0表示出现异常情况。
- 在main中,语句return expr;与exit(expr);的作用是相同的
- 对于任何重要的程序来说,都应该让程序返回有意义且有用的值。
- gets函数在读取字符串时将删除结尾的换行符,并在字符串末尾添加一个空字符'\0',而puts函数在输出字符串时将自动在字符串末尾添加一个换行符
- 释放一个不是通过malloc、calloc函数得到的指针所指向的存储空间,将是一个很严重的错误;使用已经释放的存储空间同样是错误的
- #define frand() ((double)rand() / RAND_MAX + 1.0) 该宏定义了一个生成大于等于0但小于1的随机浮点数的表达式
困惑的知识点
- 假设s是一个字符串,那么printf("%s", s);和printf(s);有什么区别?前者是安全的,后者是不安全的,因为s中可能包含格式化字符,这会触发printf的转换机制,从而使得输出结果不可预知,甚至可能引起程序崩溃
- va_list类型用于声明一个变量,该变量将依次引用各参数。宏va_start将ap初始化为指向第一个无名可变参数。在使用ap之前,该宏必须被调用一次。参数表必须至少包括一个有名参数,va_start将最后一个有名参数作为起点
- 什么叫:当scanf返回0时,下一次调用scanf时,scanf函数将从上一次转换的最后一个字符的下一个字符开始继续搜索
- 什么叫:转换说明控制下一个输人字段的转换。一般来说,转换结果存放在相应的参数指向的变量中。但是,如果转换说明中有赋值禁止字符*,则跳过该输人字段,不进行赋值。输人字段定义为一个不包括空白符的字符串,其边界定义为到下一个空白符或达到指定的字段宽度这表明scanf函数将越过行边界读取输人,因为换行符也是空白符。(空白符包括空格符、制表符、换行符、回车符、纵向制表符以及换页符)。
- 相比于return,exit()函数有一个优点,它可以在函数中调用,立即终止整个程序。而return只能结束当前函数,而不能直接终止整个程序(除非是在main函数中)
章节小结
叹为观止,以前就好奇过想printf和scanf是怎么实现的,能根据格式化字符串来决定输出什么东西,不知道是如何实现。最关键的一点,如何识别变长的参数表。这章就好似庖丁解牛一般,解答了我的困惑,把原理讲的清清楚楚;而且,就像前面的章节一样,会给你介绍库函数的实现方法,手搓了一些常用的字符处理函数。这章也介绍了一些好的编程实践,比如说使用stderr来输出错误信息,而不是stdout。以及exit函数和return的区别等。神!真他妈的牛逼。
打算做的习题
: 7-3, 7-4, 7-9.
Chapter 8 -- UNIX系统接口
遗漏知识点
- 在UNIX操作系统中,所有的外围设备(包括键盘和显示器)都被看作是文件系统中的文件,因此,所有的输入/输出都要通过读文件或写文件完成。也就是说,通过一个单一的接口就可以处理外围设备和程序之间的所有通信。
- 通常情况下,在读或写文件之前,必须先将这个意图通知系统,该过程称为打开文件。如果是写一个文件,则可能需要先创建该文件,也可能需要丢弃该文件中原先已存在的内容。系统检查你的权力(该文件是否存在?是否有访问它的权限?),如果一切正常,操作系统将向程序返回一个小的非负整数,该整数称为文件描述符(fd)。任何时候对文件的输入/输出都是通过文件描述符标识文件,而不是通过文件名标识文件。(文件描述符类似于标准库中的文件指针或MS-DOS中的文件句柄。)系统负责维护已打开文件的所有信息,用户程序只能通过文件描述符引用文件。
- 因为大多数的输人/输出是通过键盘和显示器来实现的,为了方便起见,UNIX对此做了特别的安排。当命令解释程序(即“shell”)运行一个程序的时候,它将打开3个文件,对应的文件描述符分别为0、1、2,依次表示标准输人、标准输出和标准错误。如果程序从文件0中读,对1和2进行写,就可以进行输入/输出而不必关心打开文件的问题。
- read函数和write函数的返回值是实际读写的字节数。
- 除了默认的标准输人、标准输出和标准错误文件外,其他文件都必须在读或写之前显式地打开。系统调用open和creat用于实现该功能。
- 一个程序同时打开的文件数是有限制的(通常为20)。相应地,如果一个程序需要同时处理许多文件,那么它必须重用文件描述符。函数close(intfd)用来断开文件描述符和已打开文件之间的连接,并释放此文件描述符,以供其他文件使用。close函数与标准库中的fclose函数相对应,但它不需要清洗(fush)缓冲区。如果程序通过exit函数退出或从主程序中返回,所有打开的文件将被关闭。
- lseek函数用于重新定位文件读写位置。lseek函数的第三个参数指定了从哪里开始计算偏移量:0表示文件的开头,1表示当前位置,2表示文件的结尾。这与fseek函数的第三个参数相同。
- 标准库中的文件不是通过文件描述符描述的,而是使用文件指针描述的,文件指针是一个指向包含文件各种信息的结构的指针,该结构包含下列内容:一个指向缓冲区的指针,通过它可以一次读人文件的一大块内容;一个记录缓冲区中剩余的字符数的计数器;一个指向缓冲区中下一个字符的指针;文件描述符;描述读/写模式的标志;描述错误状态的标志等。
- 尽管fsize程序非常特殊,但是它的确说明了一些重要的思想。首先,许多程序并不是“系统程序”,它们仅仅使用由操作系统维护的信息。对于这样的程序,很重要的一点是,信息的表示仅出现在标准头文件中,使用它们的程序只需要在文件中包含这些头文件即可,而不需要包含相应的声明。其次,有可能为与系统相关的对象创建一个与系统无关的接口。标准库中的函数就是很好的例子。
- malloc并不是从一个在编译时就确定的固定大小的数组中分配存储空间,而是在需要时向操作系统申请空间。因为程序中的某些地方可能不通过malloc调用申请空间(也就是说通过其他方式申请空间),所以,malloc管理的空间不一定是连续的。
- 函数morecore用于向操作系统请求存储空间,其实现细节因系统的不同而不同。因为向系统请求存储空间是一个开销很大的操作,因此,我们不希望每次调用malloc函数时都执行该操作,基于这个考虑,morecore函数请求至少NALLOC个单元。这个较大的块将根据需要分成较小的块。
- 一般来说,不指向同一个数组的两个指针不应该进行比较;但sbrk函数返回的指向不同块的多个指针之间可以作有意义的比较,因为他们都来自同一个连续的堆区。
- 对指针进行强制类型转换可以使得他们进行比较操作,通过ANSI的标准;但是前提是,这些指针都指向同一个数组(或同一个堆区)中的元素时,这样的比较才有意义。
困惑的知识点
- 什么叫“在任何情况下,文件赋值的改变都不是由程序完成的,而是由shell完成的。只要程序用文件0作为输入、文件1和2作为输出,它就不会知道程序的输入从哪里来,并输出到哪里去。”--比如说我写的程序内部并不知道输入其实来自文件而不是键盘,输出其实去了文件而不是屏幕。
- lseek的l是什么意思?--lseek中的l表示“long”,即长整型,表示它支持 long 类型的偏移量。
- 什么叫fopen不分配任何缓冲区空间,缓冲区的分配是在第一次读文件时由函数_fillbuf完成的?到底什么是缓冲区?为什么有的时候为了获取正确的键盘输入需要加如fflsuh(stdin)?--缓冲区是提高输入输出效率的内存区域,它让数据的读写更高效、更平滑。fopen真的只是创建了一个文件指针,打开了文件。
- 什么是UNIX操作系统的i节点表?-- inode(index node),每个文件(包括目录、设备等)在磁盘上都有一个唯一的i节点,i节点中保存了文件的元信息(如文件大小、权限、所有者、时间戳、数据块位置等),但不包含文件名。这样的分离设计便于操作系统高效地管理文件系统。可以集中检查维护文件系统,可以实现多对一的文件名映射(硬链接),可以更高效地进行文件操作(如删除、移动等),可以免去遍历整个目录结构来查找文件的开销。
- 上面第九条中“使用它们的程序只需要在文件中包含这些头文件即可,而不需要包含相应的声明”是什么意思?-- 许多程序其实并不是“系统程序”,它们只是利用操作系统已经维护好的信息,比如文件的大小、权限、时间戳等,而不需要自己去实现底层的功能。对于这类程序来说,最重要的是只需要在代码中包含标准头文件(如 stdio.h、sys/stat.h 等),就能直接访问和使用这些结构体和信息,无需自己再声明这些结构体或变量。这样做的好处是,如果操作系统或编译器对这些结构体做了修改,程序只需重新编译即可适配,无需手动同步声明,从而提升了可移植性和安全性。此外,标准库函数为系统相关的操作提供了统一的、与平台无关的接口,不管底层操作系统如何实现,程序员都可以用同样的方式进行文件操作,这大大方便了程序的移植和维护。这种接口与实现分离”的设计,使得程序具有良好的可移植性,只需重新编译即可在不同平台上运行,无需修改源代码。
- malloc从哪里获取内存空间,那里的内存空间还可以干什么用?程序还可以通过什么方式申请内存空间? -- malloc 申请的内存空间来自操作系统为进程分配的“堆区”(heap),这是进程虚拟地址空间中专门用于动态分配内存的一部分。堆区的内存除了可以被 malloc、calloc、realloc 等标准库函数分配给程序使用,还可以被操作系统用于其他用途,比如内存映射(mmap)、共享内存等。除了 malloc 这类标准库函数,程序还可以通过系统调用(如 mmap)或第三方内存管理库来申请内存空间。堆区的内存是动态分配和释放的,适合存储生命周期不确定、大小不固定的数据。
章节小结
神!这本书实在是太好,用深入浅出来形容这一章的讲解再合适不过了。原本对于库函数,文件操作以及流相关的知识一直迷迷糊糊,不太明白。读完这一章一下子清澈了许多,虽然还是有一些地方不动,但是能明显地感觉到自己的认知在增加。另外,我之前也学了一些Linux相关的知识,也难怪Linux是基于UNIX的了,很多概念都是相通的,比如说一切皆文件,文件描述符,硬链接,shell等等,像是原先孤立的几个知识点,连接了起来,形成了一个块儿。这章也是这本书的最后一章,随后是关于对附录A和附录B的学习,以及之后习题的完成也要提上日程了。我能明显感觉到老一辈程序员它们“惜墨如金”的代码风格,代码量少,功能却非常强大,相对应的,理解起来有一些难度。能用前缀++和后缀++完成的事情绝对不拖泥带水多写一行;充分利用函数的返回值,例如用返回值的比较作为分支条件和循环条件;还有利用返回值返回有效操作的个数等等。这种风格非常的帅,恐怕我得沉淀个几年才能达到这种境界吧。
打算做的习题
: 8-1, 8-2, 8-3, 8-4, 8-6, 8-7, 8-8.