65. C Prime Plus 笔记-2017.12.16

1. C语言在提供更多自由的同时,也让使用者承担了更大的责任。
2. 想拥有自由就必须时刻保持警惕。C语言紧凑简洁,结合了大量的运算载体。正因如此,我们也可以编写出让人极其费解的代码。虽然没必要强迫自己编写晦涩的代码,但是有兴趣写写也无妨。
3. 编译器把源代码转换成中间代码,链接器把中间代码和其它代码合并,生成可执行文件。C使用这种分而治之的方法方便对程序进行模块化,可以独立编译单独的模块,稍后再用链接器合并已编译的模块。通过这种方法,如果只更改某个模块,不必因此重新编译其他模块。另外,链接器还将你编写的程序和预编译的库代码合并。
4. 链接器的作用是,把你编写的目标代码、系统的标准启动代码和库代码3部分合并成一个文件,即可执行文件。对于库代码,链接器只会把程序中要用到的库函数代码提取出来。
5. LLVM项目成为cc的另一个替代品。该项目是与编译器相关的开源软件集合,始于伊利诺伊大学的2000份研究项目。它的Clang编译器处理C代码,可以通过clang调用。有多种版本供不同的平台使用,包括Linux。2012年,Clang成为FreeBSD的默认C编译器。Clang也对最新的C标准支持得很好。

Mac系统默认就是该编译器。

zhgxun-pro:~ zhgxun$ cc -v
Apple LLVM version 9.0.0 (clang-900.0.37)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
zhgxun-pro:~ zhgxun$ gcc -v
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.0.0 (clang-900.0.37)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
zhgxun-pro:~ zhgxun$
6. 在大多数情况下,头文件包含了编译器创建最终可执行程序要用到的信息。头文件帮助编译器把你的程序正确地组合在一起。
7. C程序一定是从main()函数开始执行。
8. C语言中,所有变量都必须先声明才能使用。这意味着必须列出程序中用到的所有变量名及其类型。
9. 如果要声明变量,应该声明在何处?前面提到过,C99之前的标准要求把声明都置于块的顶部,这样规定的好处是:把声明放在一起更容易理解程序的用途。C99允许在需要时才声明变量,这样做的好处是:在给变量赋值之前声明变量,就不会忘记给变量赋值。但是实际上,许多编译器都还不支持C99。
10. 换行符是一个转义序列(escape sequence)。转义序列用于代表难以表示或无法输入的字符。
11. 编程是一件富有挑战性的事情。程序员要具备抽象和逻辑的思维,并谨慎地处理细节问题(编译器会强迫你注意细节问题)。平时和朋友交流时,可能用错几个单词,犯一两个语法错误,或者说几句不完整的句子,但是对方能明白你想说什么。而编译器不允许这样,对它而言,几乎正确仍然是错误。
12. C语言是通过赋值运算符而不是赋值语句完成赋值操作。根据C标准,C语言并没有所谓的“赋值语句”,本书及一些其他书籍中提到的“赋值语句”实际上是表达式语句(C语言的6种基本语句之一)。
13. 市面上许多书籍(包括本书)都把这种语句叫作“函数调用语句”,但是历年的C标准中从来没有函数调用语句!值得一提的是,函数调用本身是一个表达式,圆括号是运算符,圆括号左边的函数名是运算对象。在C11标准中,这样的表达式是一种后缀表达式。在表达式末尾加上分号,就成了表达式语句。请初学者注意,这样的“函数调用语句”实质是表达式语句。本书的错误之处已在翻译过程中更正。
14. 位、字节和字是描述计算机数据单元或存储单元的术语。这里主要指存储单元。最小的存储单元是位(bit),可以储存0或1(或者说,位用于设置“开”或“关”)。虽然1位储存的信息有限,但是计算机中位的数量十分庞大。位是计算机内存的基本构建块。字节(byte)是常用的计算机存储单位。对于几乎所有的机器,1字节均为8位。这是字节的标准定义,至少在衡量存储单位时是这样。计算机的字长越大,其数据转移越快,允许的内存访问也更多。
15. 对我们而言,整数和浮点数的区别是它们的书写方式不同。对计算机而言,它们的区别是储存方式不同。
16. 浮点数通常只是实际值的近似值。
17. 以下示例的最后一行,只初始化了cats,并未初始化dogs。这种写法很容易让人误认为dogs也被初始化为94,所以最好不要把初始化的变量和未初始化的变量放在同一条声明中。
// 有效,但是这种格式很糟糕
int dogs, cats = 94;
18. 可以使用printf()函数打印int类型的值。第2章中介绍过,%d指明了在一行中打印整数的位置。%d称为转换说明,它指定了printf()应使用什么格式来显示一个值。格式化字符串中的每个%d都与待打印变量列表中相应的int值匹配。这个值可以是int类型的变量、int类型的常量或其他任何值为int类型的表达式。作为程序员,要确保转换说明的数量与待打印值的数量相同,编译器不会捕获这类型的错误。
19. short int类型(或者简写为short)占用的存储空间可能比int类型少,常用于较小数值的场合以节省空间。与int类似,short是有符号类型。long int或long占用的存储空间可能比int多,适用于较大数值的场合。与int类似,long是有符号类型。long long int或long long(C99标准加入)占用的储存空间可能比long多,适用于更大数值的场合。该类型至少占64位。与int类似,long long是有符号类型。unsigned int或unsigned只用于非负值的场合。这种类型与有符号类型表示的范围不同。
20. 为什么说short类型“可能”比int类型占用的空间少,long类型“可能”比int类型占用的空间多?因为C语言只规定了short占用的存储空间不能多于int,long占用的存储空间不能少于int。这样规定是为了适应不同的机器。例如,过去的一台运行Windows 3的机器上,int类型和short类型都占16位,long类型占32位。后来,Windows和苹果系统都使用16位储存short类型,32位储存int类型和long类型(使用32位可以表示的整数数值超过20亿)。现在,计算机普遍使用64位处理器,为了储存64位的整数,才引入了long long类型。现在,个人计算机上最常见的设置是,long long占64位,long占32位,short占16位,int占16位或32位(依计算机的自然字长而定)。原则上,这4种类型代表4种不同的大小,但是在实际使用中,有些类型之间通常有重叠。
21. int类型那么多,应该如何选择?首先,考虑unsigned类型。这种类型的数常用于计数,因为计数不用负数。而且,unsigned类型可以表示更大的正数。如果一个数超出了int类型的取值范围,且在long类型的取值范围内时,使用long类型。然而,对于那些long占用的空间比int大的系统,使用long类型会减慢运算速度。因此,如非必要,请不要使用long类型。另外要注意一点:如果在long类型和int类型占用空间相同的机器上编写代码,当确实需要32位的整数时,应使用long类型而不是int类型,以便把程序移植到16位机后仍然可以正常工作。类似地,如果确实需要64位的整数,应使用long long类型。如果在int设置为32位的系统中要使用16位的值,应使用short类型以节省存储空间。通常,只有当程序使用相对于系统可用内存较大的整型数组时,才需要重点考虑节省空间的问题。使用short类型的另一个原因是,计算机中某些组件使用的硬件寄存器是16位。
22. 打印unsigned int类型的值,使用%u转换说明;打印long类型的值,使用%ld转换说明。如果系统中int和long的大小相同,使用%d就行。但是,这样的程序被移植到其他系统(int和long类型的大小不同)中会无法正常工作。在x和o前面可以使用l前缀,%lx表示以十六进制格式打印long类型整数,%lo表示以八进制格式打印long类型整数。
23. char类型用于储存字符(如,字母或标点符号),但是从技术层面看,char是整数类型。因为char类型实际上储存的是整数而不是字符。计算机使用数字编码来处理字符,即用特定的整数表示特定的字符。美国最常用的编码是ASCII编码,本书也使用此编码。例如,在ASCII码中,整数65代表大写字母A。因此,储存字母A实际上储存的是整数65(许多IBM的大型主机使用使用另一种编码——EBCDIC,其原理相同。另外,其他国家的计算机系统可能使用完全不同的编码)。
24. 标准ASCII码的范围是0~127,只需7位二进制数即可表示。通常,char类型被定义为8位的存储单元,因此容纳标准ASCII码绰绰有余。许多其他系统(如IMB PC和苹果Macs)还提供扩展ASCII码,也在8位的表示范围之内。一般而言,C语言会保证char类型足够大,以储存系统(实现C语言的系统)的基本字符集。
25. 许多字符集都超过了127,甚至多于255。例如,日本汉字(kanji)字符集。商用的统一码(Unicode)创建了一个能表示世界范围内多种字符集的系统,目前包含的字符已超过110000个。国际标准化组织(ISO)和国际电工技术委员会(IEC)为字符集开发了ISO/IEC10646标准。统一码标准也与ISO/IEC10646标准兼容。
26. C语言把1字节定义为char类型占用的位(bit)数,因此无论是16位还是32位系统,都可以使用char类型。
27. 在C语言中,用单引号括起来的单个字符被称为字符常量(character constant)。编译器一发现’A’,就会将其转换成相应的代码值。单引号必不可少。其实,用’A’代替65才是较为妥当的做法,这样在任何系统中都不会出问题。因此,最好使用字符常量,而不是数字代码值。C语言将字符常量视为int类型而非char类型。
28. 单引号只适用于字符、数字和标点符号,浏览ASCII表会发现,有些ASCII字符打印不出来。C语言提供了3种方法表示这些字符。第1种方法前面介绍过——使用ASCII码。第2种方法是,使用特殊的符号序列表示一些特殊的字符。这些符号序列叫作转义序列(escape sequence)。从C90开始,不仅可以用十进制、八进制形式表示字符常量,C语言还提供了第3种选择——用十六进制形式表示字符常量,即反斜杠后面跟一个x或X,再加上1~3位十六进制数字。
29. C标准规定警报字符不得改变活跃位置。标准中的活跃位置(active position)指的是显示设备(屏幕、电传打字机、打印机等)中下一个字符将出现的位置。简而言之,平时常说的屏幕光标位置就是活跃位置。在程序中把警报字符输出在屏幕上的效果是,发出一声蜂鸣,但不会移动屏幕光标。
30. C99标准添加了_Bool类型,用于表示布尔值,即逻辑值true和false。因为C语言用值1表示true,值0表示false,所以_Bool类型实际上也是一种整数类型。但原则上它仅占用1位存储空间,因为对0和1而言,1位的存储空间足够了。
31. C语言提供的另一种浮点类型是double(意为双精度)。double类型和float类型的最小取值范围相同,但至少必须能表示10位有效数字。一般情况下,double占用64位而不是32位。一些系统将多出的32位全部用来表示非指数部分,这不仅增加了有效数字的位数(即提高了精度),而且还减少了舍入误差。另一些系统把其中的一些位分配给指数部分,以容纳更大的指数,从而增加了可表示数的范围。无论哪种方法,double类型的值至少有13位有效数字,超过了标准的最低位数规定。
32. 计算机缺少足够的小数位来完成正确的运算。
33. printf()何时把输出发送到屏幕上?最初,printf()语句把输出发送到一个叫作缓冲区(buffer)的中间存储区域,然后缓冲区中的内容再不断被发送到屏幕上。C标准明确规定了何时把缓冲区中的内容发送到屏幕:当缓冲区满、遇到换行字符或需要输入的时候(从缓冲区把数据发送到屏幕或文件被称为刷新缓冲区)。
34. 用数组(array)储存字符串(character string)。在该程序中,用户输入的名被储存在数组中,该数组占用内存中40个连续的字节,每个字节储存一个字符值。使用%s转换说明来处理字符串的输入和输出。
35. C语言没有专门用于储存字符串的变量类型,字符串都被储存在char类型的数组中。数组由连续的存储单元组成,字符串中的字符被储存在相邻的存储单元中,每个单元储存一个字符。
36. 数组末尾位置的字符\0。这是空字符(null character),C语言用它标记字符串的结束。空字符不是数字0,它是非打印字符,其ASCII码值是(或等价于)0。C中的字符串一定以空字符结束,这意味着数组的容量必须至少比待存储字符串中的字符数多1。
37. 什么是数组?可以把数组看作是一行连续的多个存储单元。用更正式的说法是,数组是同类型数据元素的有序序列。
38. 字符串常量”x”和字符常量’x’不同。区别之一在于’x’是基本类型(char),而”x”是派生类型(char数组);区别之二是”x”实际上由两个字符组成:’x’和空字符\0。
39. sizeof运算符,它以字节为单位给出对象的大小。strlen()函数给出字符串中的字符长度。因为1字节储存一个字符,读者可能认为把两种方法应用于字符串得到的结果相同,但事实并非如此。
#include <stdio.h>
#include <string.h>
#define S "My name is test"

int main(void)
{
    printf("strlen(s)=%zd, sizeof(s)=%zd\n", strlen(S), sizeof(S));
}

输出

zhgxun-pro:c2 zhgxun$ gcc test.c 
zhgxun-pro:c2 zhgxun$ ./a.out 
strlen(s)=15, sizeof(s)=16
zhgxun-pro:c2 zhgxun$

然而,sizeof运算符给出的数更大,因为它把字符串末尾不可见的空字符也计算在内。

40. string.h头文件包含多个与字符串相关的函数原型,包括strlen()。
41. 一般而言,C把函数库中相关的函数归为一类,并为每类函数提供一个头文件。例如,printf()和scanf()都隶属标准输入和输出函数,使用stdio.h头文件。string.h头文件中包含了strlen()函数和其他一些与字符串相关的函数(如拷贝字符串的函数和字符串查找函数)。
42. 圆括号的使用时机否取决于运算对象是类型还是特定量?运算对象是类型时,圆括号必不可少,但是对于特定量,可有可无。也就是说,对于类型,应写成sizeof(char)或sizeof(float);对于特定量,可写成sizeof name或sizeof 6.28。尽管如此,还是建议所有情况下都使用圆括号,如sizeof(6.28)。

在实际应用中,strlen()和sizeof是非常重要的编程工具。

43. 编译程序时,程序中所有的TAXRATE都会被替换成0.015。这一过程被称为编译时替换(compile-time substitution)。在运行程序时,程序中所有的替换均已完成。通常,这样定义的常量也称为明示常量(manifest constant)。
#define TAXRATE 0.015

用大写表示符号常量是C语言一贯的传统。这样,在程序中看到全大写的名称就立刻明白这是一个符号常量,而非变量。大写常量只是为了提高程序的可读性,即使全用小写来表示符号常量,程序也能照常运行。

44. C90标准新增了const关键字,用于限定一个变量为只读。这使得MONTHS成为一个只读值。也就是说,可以在计算中使用MONTHS,可以打印MONTHS,但是不能更改MONTHS的值。const用起来比#define更灵活。
// MONTHS在程序中不可更改,值为12
const int MONTHS = 12;
45. C头文件limits.h和float.h分别提供了与整数类型和浮点类型大小限制相关的详细信息。每个头文件都定义了一系列供实现使用的明示常量。例如,limits.h头文件包含以下类似的代码:
#define INT_MAX +32767
#define INT_MIN -32768

直接打印系统中存储的最大整数:

#include <stdio.h>
#include <limits.h>

int main(void)
{
    printf("%d\n", INT_MIN);
    printf("%d\n", INT_MAX);
}
zhgxun-pro:c2 zhgxun$ ./a.out 
-2147483648
2147483647
zhgxun-pro:c2 zhgxun$

当然/usr/include/i386/limits.h还有很多定义的整数,比如:

#define    SCHAR_MAX    127        /* min value for a signed char */
#define    SCHAR_MIN    (-128)        /* max value for a signed char */

#define    UCHAR_MAX    255        /* max value for an unsigned char */
#define    CHAR_MAX    127        /* max value for a char */
#define    CHAR_MIN    (-128)        /* min value for a char */

#define    USHRT_MAX    65535        /* max value for an unsigned short */
#define    SHRT_MAX    32767        /* max value for a short */
#define    SHRT_MIN    (-32768)    /* min value for a short */

#define    UINT_MAX    0xffffffff    /* max value for an unsigned int */
#define    INT_MAX        2147483647    /* max value for an int */
#define    INT_MIN        (-2147483647-1)    /* min value for an int */

这些明示常量代表int类型可表示的最大值和最小值。如果系统使用32位的int,该头文件会为这些明示常量提供不同的值。

46. printf()函数也有一个返回值,它返回打印字符的个数。如果有输出错误,printf()则返回一个负值(printf()的旧版本会返回不同的值)。
47. scanf()函数返回成功读取的项数。如果没有读取任何项,且需要读取一个数字而用户却输入一个非数值字符串,scanf()便返回0。当scanf()检测到“文件结尾”时,会返回EOF(EOF是stdio.h中定义的特殊值,通常用#define指令把EOF定义为-1)。
48. 赋值表达式语句的目的是把值储存到内存位置上。用于储存值的数据存储区域统称为数据对象(data object)。C标准只有在提到这个概念时才会用到对象这个术语。使用变量名是标识对象的一种方法。
49. 左值(lvalue)是C语言的术语,用于标识特定数据对象的名称或表达式。因此,对象指的是实际的数据存储,而左值是用于标识或定位存储位置的标签。右值(rvalue)指的是能赋值给可修改左值的量,且本身不是左值。
50. C99做了进一步调整,新增了%zd转换说明用于printf()显示size_t类型的值。如果系统不支持%zd,可使用%u或%lu代替%zd。
51. C有一系列专门处理字符的函数,ctype.h头文件包含了这些函数的原型。这些函数接受一个字符作为参数,如果该字符属于某特殊的类别,就返回一个非零值(真);否则,返回0(假)。例如,如果isalpha()函数的参数是一个字母,则返回一个非零值。
52. C是在美国用标准美式键盘开发的语言。但是在世界各地,并非所有的键盘都有和美式键盘一样的符号。因此,C99标准新增了可代替逻辑运算符的拼写,它们被定义在ios646.h头文件中。如果在程序中包含该头文件,便可用and代替&&、or代替||、not代替!。
53. 输入/输出函数不是C定义的一部分,C把开发这些函数的任务留给编译器的实现者来完成。在实际应用中,UNIX系统中的C实现为这些函数提供了一个模型。ANSI C库吸取成功的经验,把大量的UNIX I/O函数囊括其中,包括一些我们曾经用过的。由于必须保证这些标准函数在不同的计算机环境中能正常工作,所以它们很少使用某些特殊系统才有的特性。因此,许多C供应商会利用硬件的特性,额外提供一些I/O函数。其他函数或函数系列需要特殊的操作系统支持,如Winsows或Macintosh OS提供的特殊图形界面。这些有针对性、非标准的函数让程序员能更有效地使用特定计算机编写程序。
54. 为什么要有缓冲区?首先,把若干字符作为一个块进行传输比逐个发送这些字符节约时间。其次,如果用户打错字符,可以直接通过键盘修正错误。当最后按下Enter键时,传输的是正确的输入。

虽然缓冲输入好处很多,但是某些交互式程序也需要无缓冲输入。例如,在游戏中,你希望按下一个键就执行相应的指令。因此,缓冲输入和无缓冲输入都有用武之地。

缓冲分为两类:完全缓冲I/O和行缓冲I/O。完全缓冲输入指的是当缓冲区被填满时才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。缓冲区的大小取决于系统,常见的大小是512字节和4096字节。行缓冲I/O指的是在出现换行符时刷新缓冲区。键盘输入通常是行缓冲输入,所以在按下Enter键后才刷新缓冲区。

ANSI C决定把缓冲输入作为标准的原因是:一些计算机不允许无缓冲输入。如果你的计算机允许无缓冲输入,那么你所用的C编译器很可能会提供一个无缓冲输入的选项。

55. 从概念上看,C程序处理的是流而不是直接处理文件。流(stream)是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。
56. 通常,EOF定义在stdio.h文件中:#define EOF (-1)为什么是-1?因为getchar()函数的返回值通常都介于0~127,这些值对应标准字符集。但是,如果系统能识别扩展字符集,该函数的返回值可能在0~255之间。无论哪种情况,-1都不对应任何字符,所以,该值可用于标记文件结尾。
57. 这种被设计用于测试函数的程序有时被称为驱动程序(driver),该驱动程序调用一个函数。如果函数成功通过了测试,就可以安装在一个更重要的程序中使用。
58. 函数原型是C语言的一个强有力的工具,它让编译器捕获在使用函数时可能出现的许多错误或疏漏。如果编译器没有发现这些问题,就很难觉察出来。是否必须使用函数原型?不一定。你也可以使用旧式的函数声明(即不用声明任何形参),但是这样做的弊大于利。

有一种方法可以省略函数原型却保留函数原型的优点。首先要明白,之所以使用函数原型,是为了让编译器在第1次执行到该函数之前就知道如何使用它。因此,把整个函数定义放在第1次调用该函数之前,也有相同的效果。此时,函数定义也相当于函数原型。

59. C允许函数调用它自己,这种调用过程称为递归(recursion)。递归有时难以捉摸,有时却很方便实用。结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。可以使用循环的地方通常都可以使用递归。有时用循环解决问题比较好,但有时用递归更好。递归方案更简洁,但效率却没有循环高。
60. char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。在C语言中,指针有许多用法。
61. 地址运算符:&一般注解:后跟一个变量名时,&给出该变量的地址。地址运算符:*一般注解:后跟一个指针名或地址时,*给出储存在指针指向地址上的值。
62. pointer ptr;不能这样声明指针,为什么不能这样声明?因为声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。另外,程序必须知道储存在指定地址上的数据类型。long和float可能占用相同的存储空间,但是它们储存数字却大相径庭。
63. *和指针名之间的空格可有可无。通常,程序员在声明时使用空格,在解引用变量时省略空格。
64. 编写程序时,可以认为变量有两个属性:名称和值(还有其他性质,如类型,暂不讨论)。计算机编译和加载程序后,认为变量也有两个属性:地址和值。地址就是变量在计算机内部的名称。在许多语言中,地址都归计算机管,对程序员隐藏。然而在C中,可以通过&运算符访问地址,通过*运算符获得地址上的值。
65. 虽然打印地址可以满足读者好奇心,但是这并不是&运算符的主要用途。更重要的是使用&、*和指针可以操纵地址和地址上的内容。
66. 函数的返回类型和形参列表构成了函数签名。因此,函数签名指定了传入函数的值的类型和函数返回值的类型。

交换变量的值。

#include <stdio.h>

void swap(int *, int *);

int main(void)
{
    int a = 10, b = 20;
    swap(&a, &b);
    printf("a=%d, b=%d\n", a, b);
}

void swap(int * a, int * b)
{
    int t;
    t = *a;
    *a = *b;
    *b = t;
}

这其实还有一个小插曲,上次面试时,还有人问我,在PHP中,如何做到不使用第三个变量,交换两个变量的值?当时也没多想,就说不会。这个记得培训时,也说过很多次。当然这些在工作上是几乎不会这样的,纯属扯淡而已。不过面试是人家问你,也没啥好说的。不想回答就糊弄一下,谦让一下没必要浪费彼此的时间。

zhgxun-pro:~ zhgxun$ php -a
Interactive shell

php > $a = 10;
php > $b= 20;
php > list($a, $b) = [$b, $a];
php > echo $a;
20
php > echo $b;
10
php >

看吧,PHP中交换两个变量的值老简单了,这在python和go中,更是直接层面的说明,比如python

zhgxun-pro:~ zhgxun$ python
Python 2.7.13 (default, Apr  4 2017, 08:47:57)
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 30
>>> b = 50;
>>> a, b = b, a
>>> a
50
>>> b
30
>>>

更是清晰明了到吐血,go中一模一样。所以说,问这种问题,真的当时想起来就说一声,要是忘记了就说不会,无所谓,不要跟傻逼较真。