第一章 C 穿越时空的迷雾
小即是美。事物发展都有个过程,由简入繁,不能一开始就想得太复杂,Multics, IBM的OS/360都是因此而失败。
C语言的许多特性是为了方便编译器设计者而建立的。
C编译器不曾实现的一些功能必须通过其他途径实现----标准I/O库和C预处理器。
C预处理器主要实现以下3个功能
l 字符串替换
l 头文件包含
l 通用代码模板的扩展
在宏扩展中,空格会对扩展的结果造成很大的影响。
宏后面不可加';',它不是C语句。
宏最好只用于命名常量,并为一些适当的结构提供简捷的记法。宏名应该大写这样便很容易与函数调用区分开来。
valve =! Open 与 valve != open
const int *p;是指不能够通过p来改变int的值,即:*p = 30和p[3] = 4都是错误,但p是可以改变。
const int *与int *是相容的,都是指向int的指针;const int **与int **不相容,前者是指向const int *的指针,int **是指向int *的指针。
l 整型升级
int array[] = {23,34,12,17,204,99,16}; #define TOTAL_ELEMENTS(sizeof(array)/sizeof(array[0])) void main() { int d= -1; if(d <= TOTAL_ELEMENTS -2) { cout<<"successful"<<endl; } else { cout<<"fail"<<endl; } }
上述代码输出”fail”
尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性。只有在使用位段和二进制掩码时,才可以使用无符号数。应该在表达式中使用强制类型转换,使操作数均为有符号数,或者无符号数,这样就不必由编译器来选择结果的类型。有个例子,在ANSI C中,-1 <(unsigned char)1 为真,而-1 < (unsigned int)1 为假。
第二章这不是Bug,而是语言特性
进步——是计算机软件工程和编程语言设计艺术逐步发展的重要动因。这也是为什么C++语言令人失望的原因:它对C语言中存在的一些最基本问题没有什么改进,而它对C语言最重要的扩展(类)却是建立在脆弱的C类型模型上。
多做之过:
l fall through作为switch的默认行为是个失误;
l 相邻的字符串自动合并成一个字符串;
l 太多的缺省可见性,全局可见,
一个大型函数一群“内部”函数不得不在该函数的外部进行定义。没有人会记得在它们之前加上static限定符,所以他们在缺省情况下是全局可见的。
误做之过:
l C语言中符号重载:static 在函数内部,表示该变量的值在各个调用间一直保持延续性;在函数这一极,表示该函数只对文本文件可见。
l extern用于函数定义表示全局可见(属于冗余),用于变量,表示它在其他地方定义。
运算符优先级存在的问题:.
l 优先级高于*, *p.f表示*(p.f);函数()高于*;
l ==和!=高于位运算符(val & mask != 0)表示val & (mask != 0);
l ==和!=高于赋值符,c = getchar() != EOF表示c = (getchar() != EOF);
l 算数运算符高于移位运算符 msb<<4 + lsb表示msb<<(4+lsb);
l 逗号最低。I = 1,2 为 (I =1),2
有些专家建议在C语言中记牢两个优先级就够了:乘除先于加减,在涉及其他的操作符时一律加括号
如果对于堆栈的每次访问之前都要检查其大小和访问权限,对于软件来说代价太大了,根本不可行。
gets(char *s),不检查缓冲区的空间,而fgets(char *s, int n, FILE *stream)可以对读入的字符数设置一个上限n。fgets对缓冲大小进行限制的方式,更为安全
少错之过,标准参数的处理以及把lint程序错误的从编译器中分离出来。
让充满Bug的代码快速通过编译实在是不划算。----我习惯于写过代码后用眼睛看一遍,确认无误后再编译调试,看来以后可以在中间加上一步用lint检查。
最大策略,如果下一个标记有超过一种的解释方案,编译器将选取组成最长字符序列的方案代码 z= y+++x 意思为 z= y+++x
如果程序员可以在同一代码块中同时进行malloc和free操作,内存管理是最轻松的。
深刻教训:即使可以保证你的编程语言100%可靠,你仍然可能成为算法中灾难的牺牲品。----确实如此,学好算法。
第三章 分析C语言的声明
声明器(declarator), 就是标识符以及与它组合与它组合在一起的任何指针,函数括号,数组下标等。
以下形式: 标识符
或 标识符[下标]
或 标识符(参数)
或 (声明器)
----注意括号不能乱加,就两个地方可以加括号
声明格式:类型说明符 声明器[,声明器];
类型说明符: int char void等
存储类型: extern static register auto
类型限定符: const volatile
理解C语言声明的优先级规则
A 声明从它的名字开始读取,然后按照优先级顺序依次读取。
B 优先级从高到底依次是:
B.1 声明中被括号括起来的那部分
B.2 后缀操作符:
括()表示一个函数,
[]表示这是一个数组。
B.3 前缀操作符:
*表示指向...的指针
C 如果const和(或)volatile关键字与类型说明符(如int,long等)相邻,它作用于类型说明符;其他情况下const和(或)volatile关键字作用于它左边紧邻的指针*号。
用优先级规则分析C语言声明:
char * const *(*next)();
char *(* c[10])(int **p);
如果需要频繁地对整个数组进行赋值操作,可以通过把它放入struct中。
在调用函数中,参数传递时首先尽可能地存放到寄存器中(追求速度)。
union也可以把同一个数据解释成两种不同的东西,不用强制类型转换。
第四章 令人震惊的事实:数组和指针并不相同
数组和指针并不是相同的
Test1.cpp
Int mango[100];
Test2..cpp
Extern int* mango;
编译会出错,说明指针和数组并不是一样的
在x=y 中,x代表一个地址,被称为左值。Y代表的事地址中的内容,被称为右值。左值在编译时可知,右值在运行时才可知。数组名代表一个地址,是一个左值,但它不能作为赋值对象,于是c语言引入了可修改的左值概念,数组名是一个不可修改的左值。
数组有个令人吃惊的性质:array == &array,数组这个性质可以让你使用array[i]来访问数组元素,而不是使用(&array)[i]来访问数组元素。
指针并不具有像数组那样的array== &array属性,对指针取下标会先取变量地址中的内容,把这个内容当作地址来进行偏移操作,int[5]和int* p= new int[5]内存布局上有区别
但是作为函数定义的形式参数,数组和指针是一样的
数组在经过函数参数传递后再取sizeof得到的会是指针大小
二维数组(数组的数组)和指针的指针(指针的数组, main 的 argv 参数)具有相同的访问方法(我想这应该是认为二维数组和指针的指针相同的主要原因):
void f1(char array[3][3])
{
…array[0][0]; 通过两个下标来访问
}
void main(int argc, char** argv) 也可写成char* argv[]
{
…argv[0][0]; 和二维数组的访问方法一样
}
}
第五章 对链接的思考
1. 编译器包括:预处理器,语法和语义检查器,代码生成器,汇编程序,优化器,链接器
2. 如果函数库的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态链接;
3. 如果可执行文件只包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,那么我们称之为动态链接
4. 动态连接的主要目的就是把程序与它们使用的特定函数库版本中分离开来
5. 动态链接提高了性能。
1, 可执行文件比功能相同的静态链接文件体积小。节省磁盘空间和虚拟内存
2, 所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝
6. 警惕Interpositioning
就是通过编写与库函数同名的函数来取代库函数的行为。他可以是库函数在特定的程序中被同名的用户函数所取代,通常是为了提高效率或者调试。但很容易发生自己代码中某个符号的定义取代函数中的相同符号的意外。不仅你自己的对库函数调用被自己版本的函数调用取代,所有调用该库函数调用也将用自己的函数替代。库函数被覆盖时,被编译器无视了。这也是遵循C语言的设计哲学:程序员所做的都是对的。
第六章 运动的诗章:运行时数据结构
1. Unix中的段和Inter x86架构中段的区别
l Unix中,段表示一个二进制文件相关的内存块
l Inter x86的内存模型中,段表示一种设计的结果。在这种设计中,地址空间并非一个整体,而是分成一些64大小的区域,称之为段
2. Unix中BSS段名字是“blockStarted by Symbol”BSS只保存那些没有值得变量,所以事实上他并不需要保存这些变量的映像,运行时所需要的BSS段的大小记录在目标文件中。
3. 堆栈段主要有3个用途
l 堆栈为函数内部声明的局部变量提供存储空间
l 进行函数调用时,堆栈存储与此有关的一些维护性信息
l 堆栈也可以被用作暂时存储区
4, 堆栈中的过程活动记录
5, Setjmp 和longjmp
n goto语句不能跳出c语言当前函数,但是longjmp可以调到其他文件的函数中
n 使用volatile声明setjmp变量
第七章 对内存的思考
1,Intel 80x86 内存模型,中内存地址的形成经过是:取段寄存器的值,左移4位,(相当于乘上16)
2,memcpy中source和destination的大小正好都是Cache容量的整数倍
填充于同一Cache行的主存地址恰好都是该Cache行大小的整数倍
3,堆中的所有东西都是匿名的--不能按名字访问,只能通过指针间接访问
第八章 为什么程序员无法分清万圣节和圣诞节
1, 复杂的类型转换可以按下面3个步骤编写
l 一个对象的声明,它的类型就是想要转换的结果类型
l 删去标识符(以及任何如extern之类的存储限定符),并把剩余的内存方正一对括号里。
l 把步骤2产生的内容放在需要进行类型转换的对象左边
2,函数的参数也是表达式,所以也会发生类型提升。不用函数原型,会先提升再自动剪裁。
如果使用了函数原型,缺省参数提升就不会发生,与实际类型相符合。----但数组到指针的提升仍会发生