“温故而知新可以为师也”
最近又在翻读《C Primer Plus》,虽然以前也看一遍,但基本上是囫囵吞枣,简单的东西以为很简单,没有进行深入的思考,复杂的东西更是被直接的略过。现在又翻起这书,一方面多看了一些C代码,多了一些自己的理解在里面,另一方面知道如何去分层次的阅读,补充自己以前漏掉的知识点。虽然可能以后也没有太多编写代码的工作,但是想要更多地去了解计算机,更多的了解一下偏低级语言的东西,好好学习C语言是很有必要的。尽管C有些缺陷,但是它仍是一款非常优秀的编程语言。
下面就是我在这次阅读中,随手记录的一些笔记,希望以后自己看见这些笔记的时候,不再是空洞的文字,而是C的细节知识。笔记正文:
第三章 C和数据
- float 类型至少能表示6位有效数字,取值范围至少为10^-37 到 10^37,系统用32位存储一个浮点数。
- double 类型至少能表示10位有效数字,一般地double使用64位而不是32位长度。
- 关于float类型在内存中的二进制存储方式
- [1位符号位][8位指数位][23位尾数位] --这里尾数是指一个浮点数被转成科学计数法后除了整数1和小数点后的部分,而整数1是被默认隐藏的。
- float的计算方式
- 符号 有效数字 * 2^指数,由于8位可表示的范围是 -128 ~ 127,所以这个指数方位在10进制中应该是在 10^-38到10^38之间。
- printf 何时刷新输出缓冲区
第四章 字符串和格式化输入 / 输出
- printf()和scanf()的号修饰符,号帮助printf定义输出宽度,而帮助scanf跳过对应的输入项。
- printf("weight = %*.*f", width, precision, weight); //传入宽度,小数位数精度,值
- scanf("%*d %*d %d", &n); //跳过输入的2个数字,将第三个数字整数复制给n
第五章 运算、表达式和语句
- 表达式 由运算符和操作数组合构成的,每个表达式都有一个值。
- 语句 一个语句是一条完整的计算机指令,在C中,语句用结束处的一个分号标识。声明语句、赋值语句、函数语句、结构化语句、空语句、复合语句。
- 副作用 是对数据对象或文件的修改。
- 顺序点 一个顺序点是程序执行中的一点:在该点处,所有的副作用都在进入下一步之前被计算。在C中,语句里的分号标识了一个顺序点,一些运算符也有顺序点。任何一个完整的表达式的结束也是一个顺序点。
- 完整表达式 一个完整的表达式是这样一个表达式 ——它不是一个更大表达式的子表达式。
- sizeof运算符 返回操作数所占的空间,操作数可以是类型说明符或具体的变量,sizeof("22222")这样也是可以的
- 指派运算符 准确的类型转换 int val = (int)3.1415
- 自增运算符、自减运算符 优先级仅低于括号
- i++是返回原来的值,并将 i 加 1,++i 是将 i 加 1 以后返回 i 的值
第六章 C控制语句:循环
- for语句
for(initialize;test;update)statementinitialize 表达式只在循环语句执行前执行一次。然后对test表达式求值,如果该表达式为真,循环就被执行一次,然后计算update表达式,接着再次检查test表达式。
- 逗号运算符
首先,它保证被它被它分开的表达式按从左到右的次序进行计算,换句话说,逗号是个顺序点,逗号左边产生的所有副作用都在程序运行到逗号右边前生效。其次,整个逗号表达式的值时右边成员的值。price = 249,500;(合法的语句,price = 249,而整个表达式的值是500)逗号也被用作分割符printf("%d %d",m,m)逗号都是分隔符,而不是逗号运算符
- do...while语句
dostatementwhile(expression);
第七章 C控制语句:分支和跳转
- continue 语句导致循环体的剩余部分被跳过。
- 对于while和do while循环,continue语句之后发生的动作是求循环体判断表达式的值。
- 对于for循环,下一个动作是先求更新表达式的值,然后再求循环判断表达式的值。
第八章 字符输入/输出和输入确认
- 缓冲分为两类,完全缓冲I/O和行缓冲I/O。
- 对于完全缓冲来说,缓冲区满时被清空,这种类型的缓冲通常出现在文件输入中。缓冲区的大小取决于系统。
- 对于行缓冲来说,遇到一个换行字符时将被清空缓冲区。键盘输入是标准的行缓冲。
- 出入输出重定向 < >
- scanf会跳过空格 换行 和制表符,意思是scanf遇到空白之后会将其留在缓冲区中,等待下次读取, 而getchar 则不会。
第九章 函数
- void 函数隐式的 return 发生在函数的最后一个语句完成时。
- 递归的几个基本要点
- 每一级的函数调用都有自己的变量。
- 每一次的函数调用都会有一次返回。
- 递归函数中,位于递归调用前的语句和各级被调函数具有相同的执行顺序。
- 递归函数中,位于递归调用后的语句的执行顺序和各个别调函数的顺序相反。
- 虽然每一级递归都有自己的变量,但是函数代码并不会得到复制。
- 递归函数中必须包含可以终止递归调用的语句。
- 尾递归
- 递归的代价
第十章 数组和指针
- 未初始化的数组的元素数值是不确定的。
- 如果初始化部分数组,未初始化的元素则被设置为0。
- 一元运算符 和++ 具有相同的优先级,但它在结合时是从右往左进行的。ptr++
- 在C中,两个表达式ar[i]和*(ar+i)是等价的。而且不管ar是一个数组名还是一个指针变量,两个表达式都可以工作,然而只有ar是一个指针变量时,才可以使用ar++这样的表达式。
- 指向常量的指针(pointer to constant) 和 指针常量
int sum( int ar[][],int rows) //错误的声明,编译器会将数据符号转化为指针。这样转化的时候需要知道指针指向所指对象的数据大小。int sum( int ar[][4],int rows //合法声明, ar指向4个int值构成的数组。
- 一般地,声明N维数组的指针时,除了最左边的方括号可以留空之外,其他都需要填写数值。
- 变长数组的意思是数组的维度可以用变量来指定。
第十一章 字符串和字符串函数
- 字符串常量是指位于一对引号内的任意字符,字符串常量属于静态储存类。
- 字符串数组初始化是从静态存储区把一个字符串复制给数组,而指针初始化只是复制字符串的地址。数组形式中数组名是一个地址常量,所以不能应用++操作符。
gets:读取换行符之前的所有字符(不包括换行符),在这些字符后添加一个空字符(\0)。它将读取换行符并将丢弃,这样下一次读取就会在新的一行开始。不安全函数。fgets:需要制定大小n,最多会读取n-1个字符。 scanf:读取到下一个空白符(空格、换行、制表符),如果制定字符宽度n,则最多读取n个字符。scanf(%10s,name)。puts:自动添加一个换行符。fputs:不会自动添加换行符。strcat:strncat:strcmp:strncmp:strcpy: strcpy的高级属性 strcpy(copy+7,origin); 第一个参数不需要指向数组的开始,这样就只可以复制数组的一部分。strncpy:char * strchar(char * s ,int c):返回一个指向字符创s1中存放c的第一个位置的指针或空指针。char * strpbrk(const char * s1, const char * s2):返回字符串s1中存放s2字符串任何字符的第一个位置或空指针char * strrchar(const char * s,int c):返回字符串s中最后一次出现字符c的位置的指针或空指针。char * strstr(const char * s1,const char * s2):返回字符串s1中第一次出现s2的地方或空指针。
第十二章 存储类、链接和内存管理
C为变量提供了5种不同的存储模型,或称存储类。还有基于指针的第六种存储模型。存储时期就是变量在内存中保留的时间,变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量。
- 作用域:代码块作用域、函数原型作用域或者文件作用域。
- 函数原型作用域从变量定义处一直到原型声明的末尾。
- 链接:外部链接(external linkage)、内部链接(internal linkage)、空链接(no linkage).
- 具有代码块作用域或者函数原型作用域的变量有空链接。用static存储类说明符表明一个文件作用域变量具有内部链接。
- 存储时期:静态存储时期(static storage duration)和自动存储时期(automatic storage duration)。
- 具有文件作用域的变量具有静态存储时期,注意对于具有文件作用域的变量,关键词static表明链接类型,并非存储时期。
- 自动变量:默认情况下,在代码块或函数的头部定义的任意变量属于自动存储类。
- 关键字auto成为存储类说明符。
- 自动变量不会被自动初始化
- 内层定义覆盖
- 寄存器变量:关键字register声明一个寄存器变量。
- 无法获取寄存器变量的地址。
- 可以使用register声明的类型是有限的。
- 可以把一个形式参量请求为寄存器变量。只需要在函数头部使用register关键字。
void macho(register int n)
- 具有代码块作用域的静态变量:使用存储类说明符static在代码块内部声明创建。
- 静态变量和自动变量拥有相同的作用域,但当包含这些变量的函数消失时,他们并不消失。
- 静态变量和外部变量在程序调入内存时已经就位了。语句只是告诉函数可以使用该变量。
- 对函数参量不能使用static。
- 具有外部链接的静态变量具有文件作用域、外部链接和静态存储时期,这一类型有时候被称为外部存储类,这一类型的变量被称为外部变量。
- 如果变量是在别的文件中定义的,使用extern来声明该变量就是必须的。
- 外部变量会被代码块作用域的变量内层定义覆盖。
- 外部变量如果没有被初始化,将被自动初始化。
- 只可以用常量表达式来初始化外部变量。
- 定义声明和引用声明。定义声明为变量留出了存储空间,它构成了变量的定义,引用声明不会引起空间分配。
- 具有内部链接的静态变量,只可以被与它在同一个文件中的函数使用。
- 可以在函数中使用存储类说明符extern来再次声明任何具有文件作用域的变量。
- 存储类和函数
- 函数也有存储类型。函数可以是外部的(默认情况下)或者静态的(c99增加了第三种可能性,即内联函数)。
- 外部函数亦可被其他文件中的函数调用,而静态函数只可以在定义它的文件中使用。
- 使用static存储类型的原因之一就是创建为一个特定模块所私有的函数,从而避免可能的名字冲突。
- 通常使用extern关键字来声明在其他文件中定义的函数。
- 分配内存:malloc()和free()
- 函数malloc()它接受一个参数:所需内存的字节数,返回内存块的第一个字节的void类型指针或空指针(找不到所需空间时)。
- 一般,对应没个malloc()调用,应该调用一次free()。
- 设想malloc()和free()管理一个内存池。每次调用malloc()分配内存给程序使用,每次调用free()归还内存到池中,使内存可被再次使用。
- free()的参数应该是一个指针,指向由malloc()分配的内存块。
- 不能使用free()释放通过其他方式分配的内存。
- 内存分配还可以使用calloc(),函数calloc()将块中所有的位置都置为0。
- 存储类与动态内存分配
在编译时就已经知道了静态存储时期变量所需的内存数量,存储在这一部分的数据在整个程序这个运行期间都可用。这一个类型的没个变量在程序开始时就已存在,到程序终止时终止。 一个自动变量在程序进入包含该变量的代码块时产生,在推出这一代码块时终止。因此伴随着程序对函数的调用和终止,自动变量使用的内存数量也在增加和减少。典型地,将这一部分内存理解为一个堆栈。这意味着在内存中,新变量在创建时按顺序加入,在消亡时按相反顺序移除。 动态分配的内存在调用malloc()或相关函数时产生,在调用free()时释放。由程序员而不是一系列固定规则控制内存持续时间,因此内存块可在一个函数中创建,而在另一个函数中释放。由于这点,动态内存分配所需的内存部分可能变成碎片状,也就是说,在活动的内存块之间散布着未使用的字节片。不管怎样,使用动态内存往往导致进程比使用堆栈内存慢。
第十三章 文件输入/输出
- 一个文件通常是磁盘上的一段命名的储存区。C将文件看成是连续的字节序列,其中每一个字节都可以单独地读取。
- 文本视图和二进制视图
- exit()、EXIT_SUCCESS与EXIT_FAILURE
- fopen()返回FILE指针或NULL指针
"r": 读模式; "w": 写模式; "a": 追加模式; "r+": 更新模式,所有之前的数据都保留; "w+": 更新模式,所有之前的数据都删除; "a+": 追加更新模式,所有之前的数据都保留,只允许在文件尾部做写入。
- getc()与putc()
- 文件结尾EOF
- fclose()成功关闭返回0,否则返回EOF
- fprintf()与fscanf(),FILE指针是第二个参数
- fgets()与fputs()
- fseek()与ftell()
SEEK_SET:文件开始; SEEK_CUR:当前位置; SEEK_END:文件结尾。
- fread()与fwrite()
第十四章 结构及其他数据形式
- 结构声明是描述结构如何组合的主要方法。
- 结构的声明
使用关键字struct,它表示接下来是一个结构。后面是一个可选的标记,它是用来引用该结构的快速标记。接下来是用一对花括号括起来的结构成员列表。每个成员变量都用它自己的声明来描述,用一个分号来结束描述,每个成员可以是任何一种C的数据类型,甚至可以是其他结构。
- 结构的初始化
- 访问结构成员
- 指向结构的指针
- 和数组不同,一个结构的名字不是该结构的地址,必须使用&运算符。
- 使用 ->运算符获取指针所指结构的成员。
- 在一些系统上结构的大小有可能大于它的内部各成员大小之和,那是因为系统对数据的对齐存储需求会导致缝隙。
- 想函数传递结构信息
- 使用结构指针
- 把结构作为参数传递:函数的形参是实参的副本
- 现在的C允许吧一个结构赋值给另一个结构,不能对数组这样做。
- 现在的C也可以将结构作为函数的返回值。
- 如果需要一个结构来存储字符串,请使用字符数组成员。存储字符指针有他的用处,但也有被严重误用的可能。
- 结构、指针和malloc()
- 在结构中使用指针处理字符串的一个有意义的例子是使用malloc()分配内存,并用指针来存放地址。
- C99新增复合文字特性不仅适用于数组也适用于结构。可以适用复合文字创建一个被用来作为函数参数或被赋值给另一个结构的结构。
(struct book){"The Idiot","Fyodor",6.69}
- 伸缩型数组成员
- 声明一个伸缩型数组成员的规则
- 伸缩型数组成员必须是最后一个数组成员。
- 结构中必须至少有一个其他成员。
- 伸缩型数组就像普通数组一样被声明,除了它的方括号内是空的。
- 如果声明了一个带有伸缩型数组成员的结构变量,您不能使用该成员做任何事情,因为没有为它分配任何内存空间。
- 使用结构指针及malloc()来使用带有伸缩型成员的结构变量。
- 声明一个伸缩型数组成员的规则
- 使用结构数组的函数
- 把结构内容保存到文件中:使用fread()和fwrite()
- 联合简介
联合是一个能在同一个存储空间里(但不同时)存储不同类型数据的数据类型。
- 联合点运算符表示正在使用哪个成员数据。在同一时间只能存储一个值。
- 枚举类型
- 枚举常量
- 在一个特定作用域内的结构标记、联合标记以及枚举标记都共享同一个命名空间,并且这个命名空间与普通变量使用的命名空间是不同的。而在C++中标记和变量名在同一命名空间。
- typedef简介
- 使用typedef来命名一个结构类型时,可以省去结构的标记。如果两个结构的声明都未使用标记,但是使用相同的成员,那么C认为这两个结构具有同样的类型。
- 奇特的声明
当进行一个声明时,可以添加一个或多个修饰符来修饰名称。
*
表示一个指针()
表示一个函数[]
表示一个数组
函数和指针
提示:声明一个指向特定函数类型的指针,首先生个一个该类型的函数,然后用`(*pf)`形式的表达式代替函数名称;pf就成为了可指向那种类型函数的指针了。
第十五章 位运算
- C的位运算符
- 逻辑运算符: `~`:按位取反 `&`:位与(AND) `|`:位或(OR) `^`:位异或 用法:掩码、打开位、关闭位、转置位、查看一位的值 - 移位运算符: `<<`:左移 `>>`:右移
位字段
位字段有一个结构声明建立,该结构声明为每个字段提供标签,并决定字段的宽度。 可以使用未命名的字段宽度“填充”未命名的空洞,使用一个宽度为0的未命名的字段迫使下一个字段与下一个整数对齐。 一个重要的机器依赖性是将字段放置到一个int中的顺序,存在大小端的区别所有位字段往往难以移植。
第十六章 C预处理器和C库
- 从宏变成最终的替换文本的过程称为宏展开。
- 宏常量可以代替char * 的字符串常量(char * 声明的字符串在程序代码和数据区,是只读的,这样声明只是获取到了字符串的指针)。 - 宏常量可以用来指定标准数组的大小。(c99后可以用变量来使用变长数组)。 - 宏常量可以作为const值的初始化值。 - 宏函数
- 宏函数
- 宏参数字符串化:#运算符,可以把语言符号转化成相应的字符串。
#define PTX(X) printf("The value of "#X" is %d\n",X);
- 预处理器粘合剂:##运算符,这个运算符可以把两个语言符号组成一个单个语言符号
#define XNAME(n) x##n
- 可变宏:...和 VA_ARGS:宏定义中的参数列表的最后一个参数为省略号,这样,预定义宏__VA_ARGS__就可以被用在替换的部分中,以表明省略号代表什么。
#define PR(...) printf(__VA_ARGS__);
- 宏参数字符串化:#运算符,可以把语言符号转化成相应的字符串。
- 宏还是函数
- 宏与函数的选择实际上时间与空间的权衡。宏花费更多空间而函数花费更多时间。
- 宏使用注意事项:
- 宏名称中不能有空格。
- 用圆括号括住每一个参数,并括住宏整体的定义。
- 用大写字母表示宏函数名。
- 如果打算用宏代替函数,先确定用宏替换是否会引起重大速度差异,只是用一次的宏对程序的运行时间不会有太大的改善,在循环嵌套中使用宏更有助于程序的运行。
- #include指令
包含大型头文件并不一定显著增加程序的大小。很多情况下,头文件中的内容是编译器产生最终代码所需的信息,而不是加到最终代码里的具体语句。 头文件最常见的形式包括: - 明显的常量 - 宏函数 - 函数声明 - 结构模板定义 - 类型定义
- 其他指令
- #undef:取消定义一个给定的#define,即使宏没有定义也是合法的。
- #ifdef 和 #ifndef
- #if 和 #elif:后面跟整数常量表达式。
`#if defined(x)` 这里defined是一个预处理器运算符。
- #line 和#error
#line 用于重置__LINE__和__FILE__宏报告的行号及文件名;#error 指令使预处理器发出一条错误消息,该消息包含指令中的文本。
- #pragma
- 内联函数 - C库
- math.h库
- stdlib库:atexit()、qsort()
- string.h库:memcpy()、memmove()
- 可变参数stdarg.h:
可变长参数的函数的第一个参数起特殊作用,传给该参数的值将是省略号后面参数的个数。 - va_list ap:声明一个va_list类型的ap变量用于存放可变长参数 - va_start(ap):将ap初始化为参数列表 - va_arg(ap,int):按照类型迭代获取参数,实参的类型必须与传入va_arg的类型说明符一致,这里并不会做隐式类型转换。 - va_end(ap):释放动态分配的用于存放参数的内存。 - va_copy(apcopy,ap):复制一份参数列表。