《The C Programming language》 -- Brian W. Kernighan / Dennis M. Ritchie
读《TCPL》I
《TCPL》(纸质-中文)没有再出新的版本来与C99相匹配,最新的C标准为C11(CSDN视C99和C11为同一文件,所以没有上传)。所以摘抄(根据个人情况)的文字都只是遵从C89的。
若有空闲时间,猜其中例子验证和习题将在读完HB后再安装个Linux来练习。练习不限于Cxx且比摘录重要得多。
EOF定义在头文件<stdio.h>,是一个整型数。其具体数值是什么并不重要,只要它与任何char类型的值都不相同即可。这里使用符号常量,可以确保程序不需要依赖于其对应的任何特定的数值。
格式规范%s规定,对应的参数必须是以'\0'结束的字符序列(字符串)。
字符常量'\0'表示值为0的字符,也就是空字符(null)。我们通常用'\0'的形式替代0,以强调某些表达式的字符属性。
对象的类型决定该对象可取值的集合以及可以对该对象执行的操作。
一般来说,自动转换就是把“比较窄的”操作数转换为“比较宽的”操作数。
C语言中,很多情况会进行隐式的算术类型转换。一般来说,如果二元运算符的两个操作数具有不同的类型,那么在进行运算之前先要把“较低”的类型提升为“较高”类型。运算结果为较高类型。(表达式中的float类型的操作数不会自动转换为double类型,双精度运算耗时)
在没有函数原型的情况下,char和short类型都将被转换为int类型,float类型将被转换为double类型。因此,即使调用函数的参数为char或float类型,程序也把函数参数默认声明为int或double类型。在有函数声明时,声明将对参数进行自动强制转换。(1. 有函数原型按照函数原型定义进行参数传递。2. int/double为默认转换类型。)
理解强制类型转换“(类型名)表达式”,表达式首先被赋值给类型名指定的类型的某个变量,然后再用该变量替换上述整条语句。表达式的值不发生改变。
自增与自减运算符只能作用于变量。
&&与||在知道结果为真或假后立即停止运算。
按位运算符只能作用于整型操作数。
条件表达式实际上就是一种表达式,它可以用在其他表达式可以使用的任何地方。
非自动变量只能进行一次初始化,初始化表达式必须为常量表达式。每次进入函数或程序模块时,显示初始化的自动变量都将被初始化一次,其初始化表达式可以使任何表达式。默认情况下,外部变量与静态变量将被初始化为0。未经显示初始化的自动变量的值为未定义值。【区分初始化和赋值】
在不显示初始化的情况下,外部变量和静态变量都被初始化为0,而自动变量和寄存器变量的初值则没有定义。自动变量的初始化等效于其赋值语句。
常量表达式在编译时求值,而不在运行时求值。
编译器对枚举变量赋值的检查
枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于宏语句来说,它的优势在于常量值可以自动生成。尽管可以声明enum类型的变量,但编译器不检查这种类型的变量中存储的值是否为该枚举的有效值。不过,枚举变量提供这种检查,因此枚举变量宏更具有优势。此外,调试程序可以以有符号形式打印出枚举变量的值。
网解:K&R的意思,是指编译器会check变量的赋值是否是该enum类型的,但不会check赋值本身是否合法。 |
有关这些类型长度定义的符号常量以及其他与机器和编译器有关的属性可以在标准头文件<limits.h>与<float.h>中找到。
对于内部名而言,至少前31个字符是有效的。函数名与外部变量名包含的字符数目可能小于31,这是因为汇编程序和加载程序可能会使用这些外部名,而语言本身是无法控制加载和汇编程序的。对于外部名,ANSI标准仅保证前6个字符的唯一性,并且不区分大小写。
也没读懂这段话,从网上看到对这段话理解:
A N S I标准规定,标识符可以为任意长度,但外部名必须至少能由前6个字符唯一地区分,并且不区分大小写。这里外部名指的是在链接过程中所涉及的标识符,其中包括文件间共享的函数名和全局变量名。因此外部名abcdefgh和abcdef将被当作同一个标识符处理。 A N S I标准还规定内部名必须至少能由前31个字符唯一地区分。内部名指的是仅出现于定义该标识符的文件中的那些标识符。C语言中的字母是有大小写区别的,因此count Count COUNT是三个不同的标识符。标识符不能和C语言的关键字相同,也不能和用户已编制的函数或C语言库函数同名。
C语言标识符命名规则 所谓标识符,是指我们为变量(variable)、宏(macro),或者函数(function)等等取的名字。( 在C语言中,标识符是对变量、函数标号和其它各种用户定义对象的命名。)例如 int num; 这个语句中的 num 就是一个标识符。
1. 长度限制 C89 规定,编译器至少应该能够处理 31 个字符(包括 31)以内的内部标识符(internal identifier);而对于外部标识符(external identifier),编译器至少应该能够处理 6 个字符(包括 6)以内的外部标识符。 最新的 C99 标准规定,编译器至少应该能够处理 63 个字符(包括 63)以内的内部标识符;编译器至少应该能够处理 31 个字符(包括 31)以内的外部标识符。 事实上,我们可以使用超出最大数目限制的字符来命名标识符,不过编译器会忽略超出的那部分字符。也就是说,如果我们用 35 个字符来命名变量,而那个编译器最多只能处理 31 个字符的变量名的话,那么多出的那 4 个字符就会被编译器忽略,只有前面的 31 个字符有效。有些古老的编译器只能处理 8 个字符以内的标识符,对于这样的编译器来说,标识符 kamehameha 和 kamehameko 是等价的,因为它们前面 8 个字符相等。 2. 可用字符和组合规则 标准规定,标识符只能由大小写英文字母,下划线(_),以及阿拉伯数字组成。标识符的第一个字符必须是大小写英文字母或者下划线,而不能是数字。 操作系统和 C 语言标准库里的标识符一般以下划线开头,这是约定俗成的。因此,我们应该避免使用下划线作为我们自己定义的标识符的开头。 |
C语言在程序设计中考虑了函数的高效性和易用性。C程序一般都由许多小的函数组成,而不是由少量较大的函数组成。一个程序可以保存在一个或者多个源文件中。
#include
形如#include "filename”或#include "filename"的行都被替换由"filename"指定的文件的内容。如果"filename"用引号引起来,则在源文件所在位置查找该文件;如果在该位置没有找到该文件,或者如果是"filename"是用尖括号<与>括起来的,则将根据相应的规则查找该文件,这个规则同具体的实现有关。被包含的文件本身也可包含#include指令。
当某个包含文件的内容发生了变化,那么所有依赖于该包含文件的源文件都必须重新编译。
#define
#define指令定义的名字的作用域从其定义开始,到被编译的源文件的末尾处结束。
#
在替换文本中,参数名以#作为前缀则结果将被扩展为由实际参数替换该参数的带引号的字符串。如#define dprint(expr) printf(#expr " =%g\n", expr)
在程序中使用语句dprint(x/y);调用该宏时则该宏被替换为printf("x/y" " =%g\n", expr);即printf("x/y =%g\n", expr);
##
预处理运算符##为宏扩展提供了一种连接实际参数的手段。如果替换文本中的参数与##相邻,则该参数将被实际参数替换且##前后的空白符将被删除,并对替换后的结果重新扫描。如#define paste(front, back) front ## back
宏调用paste(name,1)的结果将建立记号name1.
条件包含
#if语句对其中的常量整型表达式(不能包含sizeof、类型转换运算符或enum常量)进行求值,若该表达式的值不等于0,则包含其后的各行,直到遇到#endif、#elif或#else语句为止。在#if语句中可以使用表达式#defined(name),该表达式遵循以下规则:当name已经定义时,其值为1;否则为0.
用static限定符定义的函数,除了对该函数声明所在的文件可见外,其它文件都无法访问。
register声明只适用于自动变量以及函数的形式参数。编译器可以忽略过量的或不支持的register变量声明。不管register变量实际上是不是存放在寄存器中,它的地址都是不能访问的。
函数隐式声明:如果先前没有声明过的一个名字出现在某个表达式中,并且其后紧跟一个左圆括号,那么上下文就会认为改名字就是一个函数,该函数的返回值被假定为int,但上下文并对其参数作任何假设。
如果函数声明中不包含参数,则编译程序不会对函数参数作任何假设,会关闭所有的参数检查。
函数的声明和定义必须一致。如果函数与调用它的主函数main放在同一源文件中且类型不一致,编译器就会检测到错误。但,如果函数在另一个文件中且是单独编译的,这种函数的声明与定义不匹配就无法检测出来(程序会按照隐式声明方式去调用声明中提到的那个函数(名),返回int)。
return 语句后面也不一定需要表达式。当return语句的后面没有跟表达式时,函数将不向调用者返回值。当被掉函数执行到最后的右花括号而结束执行时,控制同样也会返回给调用者(不返回值)。
在C语言中,指针的使用非常广泛,原因之一是,指针常常是表达某个计算的唯一途径,另一个是,同其他方法比较起来,使用指针通常可以生成更高效、更紧凑的代码。
计算数组a[i]时,C实际现将其转换为*(a + i)的形式,然后再对其进行求值。当把数组名传递给一个函数时,实际 上传递的是该数组的第一个元素的地址。引用数组边界之外的元素是非法的。
void *类型的指针可以存放指向任何类型的指针,但它不能间接引用自身。
如果结构声明的后面不带变量表,则不需要为它分配存储空间,它仅仅描述了一个结构的模板或轮廓。
结构的合法操作只有几个:作为一个整体复制和赋值,通过&运算符取地址,访问其成员。
联合只能用第一个成员类型的值对其进行初始化。
typedef并没有创建一个新类型,它只是为某个已存在的类型增加了一个新的名称而已。typedef声明也没有增加任何新的语义:通过这种方式声明的变量与通过普通声明方式声明的变量具有完全相同的属性。typedef类似于#define,但typedef是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。
typedef可以使程序参数化,提高程序的可移植性。如果typedef声明的数据类型同机器有关,则当程序移植到其他机器上时,只需要改变typedef类型定义就可以了。typedef为程序提供更好的说明性 ---- Treeptr类型比一个声明为指向复杂结构的指针更容易让人理解。
一元运算符sizeof是一个编译时(compile-time)的运算符。不能将其置于预处理模块中。
(UNIX)操作系统通过一些列的系统调用提供服务,这些系统调用实际上时操作系统内的函数,它们可以被用户程序调用。我们经常需要使用一些重要的系统调用用以获得最高的效率,或者访问标准库中没有的某些功能。ANSI C标准库是以UNIX系统为基础建立起来的。
试图修改const限定符限定的值,其结果取决于具体的实现。
取模运算符%在有负操作数的情况下,整数除法截取方向以及取模运算结果的符号取决于具体机器的实现。
当把double类型转换为float类型时,是进行四舍五入还是截取取决于具体的实现。
除了char类型的长度为一个字节外,其余的类型的长度同具体的实现有关。
联合的读取类型必须是最近一次存入的类型。如果最后一次保存的类型与读取类型不一致,则结果取决于具体的实现。
字段的所有属性几乎都同具体的实现有关。字段是否能覆盖子边界有具体的实现定义。字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用,特殊宽度0可以用来强制在下一个边界上对齐。必须仔细考虑字段哪端优先的问题。字段没有地址,不能对其使用&运算符。
C语言没有指定char类型的变量是无符号还是有符号的变量。
带符号值与无符号值之间的比较运算时与机器相关,因为它们取决于机器中不同整数类型的大小。【C99在一个表达式中,凡是可以使用int或unsigned int类型做右值的地方也都可以使用有符号或无符号的char型、short型和Bit-field。如果原始类型的取值范围都能用int型表示,则其类型被提升为int,如果原始类型的取值范围用int型表示不了,则提升为unsigned int型】
C没有指定同一运算符中多个操作数的计算顺序(&&, ||, ?:和,运算符除外)。如在x = f(x) + g(x)的语句中,f()可以在g()之前计算,也可以在g()之后计算。因此,如果函数f或g改变了另一个函数所使用的变量,那么x的结果可能会依赖于这两个函数的计算顺序。为了保证特定的计算顺序,可以把中间结果保存在临时变量中。
C没有指定函数各参数的求值顺序,如printf("%d %d \n", ++n, power(2, n));在不同的编译器中可能会产生不同的结果,这取决于n的自增运算在power调用之前还是之后执行。
试图通过指针修改字符串常量的内容,其结果是没有定义的。