第一章
第一节
1,register关键字简化了编译器,却将包袱丢给了程序员。
第三节
1,宏处理器只应该适量使用。最好只用于命名常量,并为一些适当的结构提供简捷的记法。
2,不要使用C预处理器修改语言的基础结构。
第八节
1,K&R C中,函数声明的形参类型检测在链接时或者不作检查,函数定义头以分号结尾。
2,虽然编译器忽略形参名,但函数声明的形参名对程序员有好处。
3,除非需要使用类型提升,否则不要使用K&R C的写法定义函数。
4,从const char *到const char **
第九节
1,const char * 和 char * const区别
const char *的const修饰的对象是其指针所指的char对象。
char * const的const修饰的对象是该指针本身的值。
2,从char **赋值到const char **
如果从char *复制到const char *,如很多的字符串处理函数,都是可以的。
但是从char **赋值到const char **,就不行了。
因为char**的含义是:指向,一个指向char类型的指针,的指针。
而const char **的含义是:指向,一个指向char类型的,带const限定的指针,的指针。
即两个指针的类型并不相同。
但是将char **赋值给char * const *是没有问题的。因为const修饰的是指针的对象,不再是指针本身。
第十节
1,c中的浮点数据类型:
float, double, long double三种,其长度分别为4,8, 12个字节。不可在其前加unsigned修饰。
2,寻常算术转换:
如果一个操作数是long double,则另一个操作数被转换为long double;如果一个操作数是double,
则另一个操作数被转为double,再如果一个操作数是float,则另一个操作数被转为float。
结果也一样跟着被转换。
3,c中的字符及整型
char , short int, int , long int , long long int
修饰short, long , long long其后可不接int,默认为int(问题,不为int还能是其他么?)
4,ansi c的值保留原则:
多种不同类型的整型混合运算的时候,其转换是依照类型的大小进行。
5,整型提升:
c专家编程中1.10节的内容看得懂,但梳理不了。
第十一节
#pragma指示器的作用是由编译器定义的。
第二章
第一节
1,分析编程语言缺陷的一种方法就是将所有缺陷分成三种:多做之过,少做之过,和误做之过。
第二节
1,switch的default语句可以出现在case列表的任意位置,并在所有case均无法匹配时被执行。
2,switch在花括号之后的任意语句都可以进行变量声明,但在case之前的语句不会被执行。
3,switch内部的任何语句都可以包含标签。
4,switch缺省采用"fall througth",在97%的情况下都是错误的。
5,拖尾逗号没有存在的必要(字符串数组的最后一个逗号)
6,c中对信息可见性的选择非常少。
第三节
1,apple = sizeof(int) * p; 不懂。
2,当按常规方式使用时,可能会引起误会的任何运算符,都是c运算符存在错误优先级的表现。
3,“有些运算符是的优先级是错误的”,忘记这些错误,就是对了。
4,结合性,在几个操作符具有相同优先级时决定先执行哪个。
5,在函数调用中,各个参数的计算顺序是不确定的。
第四节
1,unix和ansi c都不能区分传递给程序的选项和参数。
2,空格在c中有时是必不可少的,例如:z=x+++++y;z=*x/*y;
3,c++的行注释可能会破坏原来正确的c代码,如:
a //*
//*/ b
4,避免函数返回值被覆盖的最好方法也许就是要求调用者提供自行分配内存。
5,将lint程序作为一个独立的工具通常意味着将lint程序束之高阁。
第五节
1,即使可以保证你的编程语言100%可靠,你仍然可能成为算法中灾难性bug的牺牲品。
第三章
第一节
1,K&R 承认C语言声明的语法有时会带来严重的问题。
2,指向数组的指针
char (*j)[20];
j = (char (*)[20])malloc(20);
3,telnet程序的一行代码:
char * const *(*next)();
第二节
1,列出的两个表看不懂,不知道有什么用。
第三节,第四节
1,理解c语言声明的优先级规则和图标分析c语言声明,感觉作用不大。感觉记住了运算符的优先级和结合性才是王道。
第五节
1,signal函数的ANSI C声明:
void (*signal(int sig, void(*func)(int)))(int);
分析:signal跟其后的括号结合,则signal是一个函数;signal函数的返回值跟signal前的星号结合,即返回一个指针。
到此,最左边括号的全部内容分析完。返回的指针右边又是一个括号(int),则返回的指针是个函数指针,该指针指向
的函数返回void类型。
2,typedef的混乱语法:
可以将多个声明器塞到一个声明中;
typedef可以不放在声明的开始处。
如:
typedef int *ptr, (*fun)(), arr[5];
/*
*ptr是int指针类型,fun是返回值为int类型的函数指针类型,arr是长度为5的int数组类型。
*/
unsigned const long typedef int volatile *kumquat;
第六节
1,可以用其他类型说明符对宏类型名进行扩展,但对typedef所定义的类型名不能这么做,如:
#define peach int
unsigned peech i;
/*
ok
*/
typedef int banana;
unsigned banana i;
/*
error
*/
2,在连续几个变量声明中,用tpyedef定义的类型能够保证声明中所有的变量均为同一类型,而用#define定义的类型则无法保证。如:
#define int_ptr int *;
int_ptr chalk, cheese;
/*
cheese为整型
*/
typedef int * int_ptr;
int_ptr a, b;
/*
a,b均为整型指针
*/
第七节
1,不要使用typedef来省略struct关键字,因为struct关键字可以提示一些信息。
2,typedef应该用在:
数组,结构,指针及函数的组合类型。
可移植类型。
3,应该始终在结构的定义中使用结构标签,即使它并非必须。
4,typedef struct my_tag {int i;} my_type;这个typedef声明引入了my_type这个名字作为"struct my_tag{int i;}"的简写。
第八节
1,编写一个能够分析c语言的声明并将它们翻译成通俗语言的程序。
2,该节有伪代码,第九节有详细c代码。
第四章
第三节
1,代码:
/*file1*/
int s[3] = {1, 2, 3};
/*file2*/
extern int *s;
在file2中不能使用下标进行数组访问,如s[0]。
解释:当在file2使用s[0]的时候,其内存访问方式为,提取s本身的地址,得到一个指针,再提取指针指向的地址,该地址加上偏移,取该偏移地址的值。
而在file1使用使用s[0]的时候,其内存访问方位为,提取s本身的地址,加上偏移,取偏移地址的值。
而在file1,file2中变量s本身的值是相同的,但在file2中的s作为指针,其指向并未赋值。
可以使用以下代码验证:
/*file1*/
int p[10] = {1, 2, 3};
int *s = p;
/*file2*/
extern int *p;
extern int *s;
int
main(int argc, char *argv[])
{
printf("%p\n", p);
printf("%p\n", &p);
printf("%p\n", s);
printf("%d\n", (int)(&p)[1]);
return 0;
}
2,似乎书上最后(4.3节)对:“声明为extern char *p,定义为char p[10],使用指针的p[i]进行访问”的解释不正确。
书上的意思是,对p[i]可以得到一个字符,而编译器理解成了指针,从而错误,
但实际错误应该是对p指向地址不确定。
3,对于多维数组的声明,需要提供除左边一维之外其他维的长度。
4,数组名在符号表中被映射为一个地址,不可修改。因此数组名是个左值,但不是可修改的左值。
第五章
第一节
1,绝大多数编译器通常由多达六七个稍小的程序锁组成,包括:预处理器,语法语义检查器,代码生成器,汇编程序,优化器,链接器和调用这些程序的驱动器。
2,PC的链接器一般只提供几个基本的I/O服务,称作BIOS。存在于内存中固定的地点,并不是每个可执行程序的一部分。
3,静态链接:库函数的代码被加入到可执行文件。
4,动态链接收集模块准备执行的三个阶段:link-editing, loading, runtime linking。
5,在main函数执行前,运行时载入器将共享数据对象载入进程地址空间,但直到被实际调用,才解析它们。
第二节
1,动态链接库必须保证的四个特定函数库:
libc(c运行时函数库),libsys(其他系统函数),libX(X windowing)和libnsl(网络服务)。
2,链接器通过将库文件名或路径名植入可执行程序来保证程序在运行时能找到所需函数库。
3,有些函数库只能使用动态链接,如果使用了其中一个,程序就必须使用动态链接。
4,位置无关代码?
第三节
1,头文件名字并不与它对于的函数库名相似;函数定义在一个函数库中,却声明在多个头文件中。
2,cpp, gcc, as, ld
3,在动态链接库中,所有的库符号进入输出文件的虚拟地址空间中,所有的符号对于链接到一起的所有文件都是可见的。相反,对于静态链接,在处理archive时,它只是在archive中查找载入器当前所知道的未定义符号。
第四节
1,Interpositioning就是通过编写与库函数同名的函数来覆盖该库函数的行为。
2,使用Interpositioning,不仅自己调用的该库函数会被自己的版本覆盖,另一些调用该库函数的库函数都会调用你自己的版本。
第六章
1,编程语言理论的经典对立之一就是代码和数据的区别。
第一节
1,a.out - 汇编语言程序和链接编辑输出格式
“汇编语言程序输出”这个名字的产生纯属历史原因。
第二节
1,目标文件和可执行文件可以有几种不同的格式,linux采用ELF(可扩展链接器格式)。
2,section是ELF文件中的最小组织单位。一个段(segments)一般包含几个section。
3,BSS段不保存在目标文件中(除了记录BSS段在运行时所需要的大小)
第三节
1,从本质上说,段在正在执行的程序中是一块内存区域,每个区域都有特定的目的。
第四节
1,堆栈段有三个主要用途:
为局部变量提供存储空间
保存函数调用栈帧
临时存储区,如算术表达式计算,alloca函数
2,除了递归调用之外,堆栈并非必须的。因为在编译时可以知道局部变量的大小,参数和返回地址所需的空间固定大小。
第五节
1,c语言自动提供的服务之一就是跟踪调用链。
2,linux栈帧结构:参数,返回地址,局部变量。
第六节
1,对于程序员来说,auto关键字几乎没有什么用处,因为它只能用于函数内部。但是在函数内部声明的数据缺省就是这种分配。唯一能用到auto的地方就是使你的声明更加清楚整齐。
2,过程活动记录并不一定要存在于堆栈中。事实上,尽可能地将过程活动记录的内容放到寄存器中会使函数调用的速度更快,效果更好。
第七节
1,保证局部变量在longjmp过程中一直保持它的值的唯一可靠方法是将它声明为volatile(这使用于那些值在setjmp执行和longjmp返回之间会改变的变量)。
2,setjmp和longjmp在c++中变异为更普通的异常处理机制catch和throw。
3,想goto一样,setjmp和longjmp使得程序难以理解和调试。如果不是处于特殊需要,最好避免使用它们。
第十一节
1,用grep来调试操作系统内核,一个非比寻常的概念。有时候甚至连源代码工具都可以帮助解决运行时问题!
第七章
第三节
1,所有现代的计算机系统,从最大的超级计算机到最小的工作站,都使用了虚拟内存。
第八章
第三节
1,类型提升的出现并不局限于涉及操作符和混合类型操作数的表达式。
2,ANSI C表示如果编译器能够保证运算结果一致,也可以省略类型提升,这通常出现在表达式中存在常量操作数的时候。
3,在ANSI C中,如果使用了适当的函数原型,类型提升就不会发生,否则也会发生。在被调用的函数内部,提升后的参数被裁剪为原先声明的大小。
4,这就是为什么单个的printf格式字符串%d能适应几个不同类型,short,char,int,而不论实际传递的参数是上述类型的哪一个。
第四节
1,ANSI C函数原型的目的是使C语言成为一种更加可靠的语言。建立原型就是为了消除一种普通(但很难发现)的错误,就是形参和实参之间类型不匹配。
第五节
1,缺省的函数声明(函数在另外的文件定义)是K&R C的声明?
2,K&R C函数声明和K&R C函数定义,会发生类型提升。
3,ANSI C函数声明和ANSI C函数定义,不会发声类型提升。
4,ANSI C和K&R C的声明和定义混合的时候,注意K&R C的声明会提升后传递参数,而K&R C的定义会按提升之后接受参数。
5,由于不懂汇编,书上那些代码不太明白。
第六节
1,curses函数库提供了基于字符的屏幕控制函数。
第七节
1,在c语言中,有好几种方法来表达FSM(有限自动机),但它们绝大多数都是基于函数指针数组,如:
extern int a(), b(), c(), d();
int (*state[])() = {a, b, c, d};
2,调用函数和通过指针调用函数(或任意层次的指针间接引用)可以使用同一种语法。至于数组,也有一个对应的方法。这种做法进一步恶化了本来就有缺陷的“声明与使用相似”的设计哲学。
第八节
1,如果你拥有十分复杂的数据结构时,你可以编写并编译一个函数,用于遍历整个数据结构并打印出来。这个函数不会在代码的任何地方调用,但它却是可执行文件的一部分。它就是debugging hooks。
2,有时候,花点时间把编程问题分解成几个部分往往是解决它的最快方法。
第九节
1,调用qsort时,可以在调用时对第四个参数(函数指针)进行强制类型转换,而无须在函数内进行。
第九章
第一节
1,声明可以分三种情况:外部声明;定义;函数参数声明。
2,对编译器而言,一个数组就是一个地址,一个指针就是一个地址的地址。
第二节
1,表达式中的数组名(与声明不同)被编译器当作一个指向该数组第一个元素的指针。
2,下标总是与指针的偏移量相同。
3,在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针。
4,在下列情况中,对数组的引用不能用指向该数组第一个元素的指针来替代:
(1)数组作为sizeof()的操作数。
(2)使用&操作符取数组的地址。
(3)数组是一个字符串(或宽字符串)常量初始值(不懂)
5,因为编译器需要知道指针进行解引用操作时应该取几个字节,以及每个下标的步长应该取几个字节,所以指针总有类型限制。
6,c语言把数组下标改写成指针偏移量的根本原因是指针和偏移量是底层硬件所使用的基本模型。
第三节
1,之所以要把传递给函数的数组参数转换为指针是出于效率的考虑,这个理由常常也是对违反软件工程做法的辩解。
2,在函数调用中,数组和函数参数是传址调用(函数参数都是传址?有什么表现?)
3,在函数内部对数组参数的任何引用都将产生一个对指针的引用。
4,有一样操作只能在指针里进行而无法在数组中进行,那就是修改它的值。
第六节
1,尽管术语上称作“多维数组”,但C语言实际上只支持“数组的数组”。
2,由于“行/列主序”这个术语只适用于恰好是二维的多维数组,所以更确切的术语是“最右的下标先变化”。
3,只有字符串常量才可以初始化指针数组。
第十章
第二节
1,用于实现多维数组的指针数组有多种名字,如“Iliffe向量”,“display”,或“dope向量”。
第三节
1,用字符串指针填充Iliffe向量来创建一个“锯齿状数组”。
2,Iliffe向量可能会使字符串分配于内存中不同的页面中,导致更加频繁的页面交换。
3,“数组名被写成一个指针参数”,规则并是递归定义的。数组的数组会被改写为“数组的指针”,而不是“指针的指针”。
第六节
1,对多维数组作为参数传递的支持缺乏是C语言存在的一个内在限制。
2,函数可以返回一个结构体。
3,向printf函数传递一个NULL指针会导致程序的崩溃。
第七节
1,全局数组变量,不能使用变量来确定长度。
2,程序信息具有启发性,而非煽动性,并且要避免使用诸如带有亵渎性,口语化,幽默或者夸张的非专业用语。尤其是,如果你规规距距地这样做,就可以避免在凌晨3点钟被叫醒。
4,在实践中,不要把realloc()函数的返回值直接赋给字符指针。如果realloc()函数失败,它会使该指针的值编程NULL,这样就无法对现有的表进行访问。
备注:
这个月里诸多不顺,但还是一点一点翻完了这本书。自己做的笔记极少会看,算做个纪念咯。
2012-05-28