C专家编程学习笔记

第1章 C:穿越时空的迷雾

C语言的起源

源于通用电气、麻省理工和贝尔实验室联合创立的庞大的项目,项目失败之后,其中两个成员对BCPL语言进行了简化,创建了B语言,后来为了解决B语言的一些问题,创建了“New B”语言,在此基础上开始了早期C语言的设计。

K&R C

1978年,C语言经典名著《The C Programming Language>出版,该书受到广泛赞誉,其作者Brian Kernighan和Dennis Ritchite名声大噪,这本书中介绍的C语言被称为”K&R C”。

ANSI C

“K&R C”之后,随着C语言的流行,C语言被业界广泛采用,但存在许多不同的实现和差别。1983年,美国国家标准化组织(ANSI)成立了C语言工作小组,开始了C语言的标准化工作。
1989年12月,C语言标准草案被ANSI委员会接纳,随后被国际标准化组织ISO接纳,但ISO删除了Rationale一节。1990初,ANSI重新采纳了ISO C的版本。因此,从原则上说,ANSI C就是ISO C。

不可移植的代码

  • 由编译器定义的行为,比如整型数右移时,要不要扩展符号位。
  • 参数求值顺序,比func(a++,a++,a++)

编译限制

  • 在函数定义中形参数量的上限至少可以达到31个
  • 在函数调用时实参数量的上限至少可以达到31个
  • 在一行源代码里至少可以有509个字符
  • 在表达式中至少可以支持32层嵌套的括号

第2章 这不是Bug,而是语言特性

strlen(str)不包含结尾的’\0’,这一点要特别注意。

switch case语句中,default语句可以出现在case列表的任何位置,通常出现在最后。
switch的另一个问题是它内部的任何语句都可以添加标签,并在执行时跳转到该标签。如果把default错写为defau0t,程序仍然可以编译通过,但switch语句没有默认处理函数。
switch语句需要注意的另外一个问题是,case语句结束时如果没有break语句,会顺序执行后面的语句。这是由于编译器实现时缺省采用”fall through”,在编程时要特别注意,不要忘记漏写break,否则,你会浪费很多时间在调试排除错误上。

C语言中,两个或多个相邻的字符串常量之间如果没有逗号等分隔符,它们会在编译时自动合并。除了最后一个字符串,其余每个字符串末尾的’\0’都会被自动删除。
比如:

printf("hello world"
        "123456"
        "hello world");
等同于:
printf("hello world123456hello world");

C函数在缺省情况下是全局可见的,最好在函数名前加上static,使其在文件范围内可见。

C语言中的符号重载

符号 意义
static 在函数内部,改变变量的生命周期,该变量的值在各个调用期间一直保持延续性;在函数这一级,改变函数的可见范围,表示本文件可见
extern 用于函数定义,表示全局可见;用于变量,表示变量在其它文件中定义
void 作为函数的返回类型,表示不返回任何值;在指针声明中,表示通用指针的类型;位于参数中,表示没有参数
* 乘法运算符;用于指针,表示间接引用;用于声明中,表示指针;
& 位的AND操作;取地址操作符
= 赋值符
== 比较运算符
<= 小于等于运算符
<<= 左移复合赋值运算符
< 小于运算符;#include指令的左定界符
() 在函数定义中,形式参数表的左右定界符;调用函数;改变表达式的运算顺序;强制类型转换;宏定义中用于定义带参数的宏;sizeof操作符中作为操作数的定界符

sizeof * p的含义是sizeof(*p)

运算符优先级存在的问题

表达式 实际含义 优先级
*p.f *(p.f) .的优先级高于*
int *ap[] int *(ap[]) []的优先级高于*
int *fp() int *(fp()) 函数()的优先级高于*
a<<4+b a<<(4+b) 算术运算符的优先级高于移位

C语言中应注意空格的使用,比如

ratio=*X/*Y  编译器会报错,把/*识别为注释的开始部分

C语言很容易出现内存操作的问题,局部变量不能作为返回值,内存管理的最好方式是在同一代码块中进行malloc和free操作。

第3章 分析C语言的声明

C语言声明的语法非常晦涩难懂,最大的问题是无法以一种人们所习惯的自然方式从左向右阅读一个声明。

const int * grape; 指针所指的对象是只读的
const * int grape;指针是只读的

合法的声明中存在限制条件:
函数的返回值不能是一个函数,所以像foo()()这样是非法的;
函数的返回值不能是一个数组,所以像foo()[]这样是非法的;
数组里面不能有函数,所以像foo这样是非法的;
函数的返回值允许是一个指针,可以是函数指针或者数组指针

结构体

结构体变量之间使用’=’赋值语句,会对结构体中每个域进行拷贝,所以可以在结构体中放置数组,这样就可以用赋值语句拷贝整个数组了。

枚举
C语言中,枚举可以用#define代替。由于枚举在其它大多数语言中都存在,所以C语言最终也实现了它。

理解C语言的优先级规则
A 声明从它的名字开始读取,然后按照优先级顺序依次读取
B 优先级由高到低依次是:
B.1 声明中被括号括起来的那部分
B.2 后缀操作符:括号表示这是一个函数,方括号表示这是一个数组
B.3 前缀操作符:星号表示“指向……的指针

typedef为数据类型创建别名,而不是创建新的数据类型。

最后,复习前面的知识:

char *(*c[10])(int         **p)含义:c是一个数组,它的元素类型是函数指针,其所指向的函数的返回值是一个指向char类型的指针。

第4章 令人震惊的事实:数组和指针并不相同

声明与定义的区别

定义是一种特殊的声明,它创建了一个变量,为变量分配内存,同一个变量的声明只能出现在一个地方;
声明是普通的声明,只是简单说明了在其它地方创建的变量的名字,它允许你使用这个名字,描述在其它地方创建的变量,同一变量的声明可以出现在多个地方。

左值与右值

左值的本质是一个地址,这个地址在编译时确定。

指针与数组并不完全相同

定义为指针,但以数组方式引用,是错误,典型写法如下:
char buf[100];//a.c中
extern char *buf;//b.c中

第5章 对链接的思考

  1. 编译器

绝大多数编译器并不是一个单一的庞大程序,通常由多个小程序所组成,这些小程序由编译驱动器的控制程序来调用,这些单独的小程序包括:
- 预处理器(preprocessor)
- 语法和语义检查器(syntactic and semantic checker)
- 代码生成器(code generator)
- 汇编程序(assembler)
- 优化器(optimizer)
- 链接器(linker)
- 驱动器程序(driver program): 调用上面的小程序并向各个程序传递正确选项
2. 警惕Interpositioning

Interpositioning:通过编写与库函数同名的函数来取代该库函数的行为。

不仅自己所进行的所有对该库函数的调用将被自己版本的库函数取代,而且所有调用该库函数的系统调用也将用自己版本的函数取而代之。

Interpositioning本身不是bug,是编译器明确要求支持的,但使用Interpositioning会存在隐患,可以用其它方法实现,应避免使用Interpositioning。

第6章 运动的诗章:运行时数据结构

1.段

在BSD Unix中,段表示一个二进制文件相关的内存块,如数据段,文本段,BSS段。
BSS段是Block Started By Symbol(由符号开始的块)的缩写,它是旧式IBM704汇编程序的一个伪指令,UNIX借用了这个名字,沿用至今。有人喜欢把它记作”Better Save Space(更有效节省空间)”。由于BSS段保存的是未初始化的变量,所以BSS段不保存在目标文件中(除了记录BSS段在运行时所需要的大小)。
由此可以看出,Windows下PE结构与unix下映像文件结构非常类似

2.堆栈

  • 运行时堆栈是一种特殊的内存空间,用于保存局部变量、临时数据、传递到函数的参数以及函数的返回地址。
  • 栈是一种后进先出的结构,如果想要修改栈中的数据,不受此限制。
  • 除了递归调用之外,栈并非必须。对于需要使用栈的地方,可以用BSS段代替。
  • C语言不允许函数的嵌套,所有函数在词法层次中都是位于最顶层。绝大多数的语言(比如Java)允许在函数内部定义函数和数据。

3.auto和static关键字

auto关键字用于修饰自动变量,函数内部声明的数据缺省就是这种方式。当函数结束后,自动变量将不复存在,它所占用的栈空间将被回收,可能在任何时候被覆盖,指向这样的自动变量的指针被称为dangling pointer(悬垂指针)。

static关键字可以改变变量的存储位置。

4.UNIX中的栈与Windows中的栈

  • Unix系统中堆栈会自动生长,可以把堆栈看成是无限大的空间
  • Windows系统中应用层线程堆栈大小缺省是1Mb,不嗯给你自动生长

第7章 对内存的思考

1.磁盘

磁盘的制造商都是使用十进制而不是二进制来表示磁盘的容量。所以2GB的磁盘容量可以存储2000000000个字节的数据而不是2147483648个字节。

2.虚拟内存

虚拟内存由MMU(内存管理单元)管理,每个进程都有独立的地址空间(通常是4GB)。
由操作系统通过分页机制实现,当访问的页面不在物理内存中,便会引发缺页中断。

3.Cache

Cache的特点是容量小、价格高、速度快。Cache的访问速度与系统的时钟周期相同,比普通内存的访问速度快很多,但是价格也贵得多,并且单位体积更大,消耗的能量也更多。

对编写应用程序的程序员而言,Cache和虚拟内存都是透明的。如果要编写内核程序,就需要考虑函数的中断级别,是否能够访问分页内存。

4.heap

heap位于数据段。heap用于动态分配的内存。动态分配的内存只能通过指针进行间接访问,不能按名字访问。

使用堆要特别注意内存泄漏问题,还有指针操作时,解引用一个包含非法值的指针,或者解引用一个空指针。

第8章 为什么程序员无法分清万圣节和圣诞节

1.类型提升

在K&R C中,表达式中的每个char、short和位段类型以及枚举类型都会被提升为int,由于函数的参数也是表达式,所以也会发生类型提升。提升的前提是int能够完整地容纳原先的数据,否则将被提升为unsigned int。
ANSI C标准延续了自动类型提升的概念,但在许多地方已经褪色。如果编译器能够保证运算结果一直,也可以省略类型提升(这通常出现在表达式中存在常量操作数的时候)。

2.原型之痛

类型提升的本质就是把操作数提升为更大的类型,然后又直接把结果裁剪为原来的大小。之所以要这么做,是为了简化编译器,所有的操作数都是同一长度。
K&R C和ANSI C采用不同的函数声明。ANSI C建立原型是为了消除形参和实参之间类型的不匹配。

K&R C函数声明和ANSI C函数定义 以及 K&R C函数定义和ANSI C 函数声明 可能会导致 参数传递过程中数据长度发生变化,所以应避免这种情况发生。

第9章 再论数组

1.数组与指针的不同

在声明时,数组与指针不能混用,数组的声明就是数组,指针的声明就是指针;
使用sizeof操作符时,数组与指针是不同的。

2.什么情况下数组与指针相同

表达式中的数组名(与声明不同)被编译器当作一个指向该数组第一个元素的指针;在表达式中,指针与数组是可以互换的,因为它们在编译器中的最终形式都是指针,并且都可以取下标进行操作,取下标操作的操作数是可以交换的,a[6]与6[a]结果是一样的。

在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针。

3.C语言把数组下标作为指针的偏移量

根本原因是指针和偏移量是底层硬件所使用的基本模型。
每个指针只能指向一种类型,因为编译器在对指针进行解引用时知道应该取几个字节,以及每个下标的步长应该取几个字节。

4.为什么C语言把形参数组当作指针

在C语言中,所有非数据形参的实参均以传值形式调用,所谓传值调用就是对实参进行一份拷贝并传递给被调用的函数,被调用的函数不能修改实参的值,而只能修改传递给它的那份拷贝。结构体形参也是传值调用。
如果数组采用传值调用,无论在时间上还是内存空间上的开销都可能是非常大的。而且在绝大多数情况下,程序员其实只对数组中某个特定的数据感兴趣。

5.数组和指针的可交换性总结

  1. 用a[i]这样的形式对数组进行访问总是被编译器改写或解释为像*(a+i)这样的指针访问。
  2. 指针始终就是指针。它绝对不可以改写成数组。可以用下标形式访问指针,一般都是指针作为函数参数,而且实际传递给函数的是一个数组。
  3. 作为函数参数的数组的声明可以看作是一个指针(也只有这种情况)。作为函数参数的数组始终会被编译器修改成为指向数组第一个元素的指针。
  4. 当把一个数组定义为函数参数时,可以选择把它定义为数组,也可以选择把它定义为指针。如果选择定义为数组,那就无法修改形参的值,因为数组名不能作为一个左值。如果选择定义为指针,可以修改形参的值。
  5. 在其它情况下,定义和声明必须匹配。如果定义了一个数组,在其它文件中对进行声明必须把它声明为数组,指针也是如此。

附录A 程序员工作面试的秘密

1.如何检测到链表中存在循环

答案有多种,经典答案是用两个指针同时指针链表头部,对链表进行遍历。每次遍历时两个指针的步长不同,如果存在循环链表,那么其中一个指针肯定能追上另外一个指针。

2.C语言中不同的增值语句的区别何在?

x=x+1;
++x;
x++;
x+=1;

回答这个问题应该在适当的上下文环境中,区别是编译器生成的汇编代码不同。
+ +x表示取x的地址,增加的它的内容,然后把内容放到寄存器中。
x+ +表示取x的地址,把它的内容放到寄存器中,然后增加内存中x的值。

3.库函数调用与系统调用有何区别

库函数调用需要付出函数调用的开销。但系统调用比库函数调用的开销还大。因为系统调用需要把上下文环境切换到内核模式。

4.编写一些代码,确定一个变量是有符号数还是无符号数

#define ISUNSIGNED(a) (a>=0 && ~a>=0)

经典语录

只有短命鬼才需要在幼儿园就学会一切。

逸闻趣事

1、Intel处理器发家史

Intel4004是一个4位微控制器,它是1970年Intel为满足一个单独的顾客——一家日本计算器公司的特殊需求而开发的。Intel的设计工程师的想法是生产一种通用目的的可编程芯片,而不是遵循当时为每个顾客量身定制的逻辑规则。Intel原先预计售出几百个这样的芯片,但通用目的的设计很快显示巨大的市场潜力。4位的字长实在太小了,所以在1972年4月,8位的8008芯片诞生了。两年后,8080芯片诞生了,这是第一款性能强大到可以称为微处理器的芯片。它包含完整的8008指令集,并增加了30条自己的指令,从而开创了一个沿用至今的传统。
8085处理器充分利用了芯片整合技术,它将三款芯片整合成一块。实质上是把8080处理器、8224时钟驱动器、8228控制器整合到一款芯片上。虽然它内部的数据总线宽度是8位的,但它使用了16位的地址总线,所以能够访问64KB的内存。
8086处理器于1978年诞生,它对8085作了改进,具有16位的数据总线和20位的地址总线,寻址空间为1MB(这在当时是一个非常惊人的数字)。
如果说8080是一块使Intel跻身豪门行列的芯片,那么8086就是一块使Intel保持豪门地位的芯片。我们可能永远不会知道IBM在1979年选择Intel的8088(一款与8086同代的8位芯片)作为它新开发的PC的CPU的确切原因。从技术上说,当时有许多公司可以提供更为出色的方案,如Motorala和National Semiconductor。由于选择了Intel的芯片,IBM帮助Intel在接下来的20多年里财源滚滚,就像IBM选择了Microsoft的MS——DOS作为PC的操作系统从而使Microsof飞黄腾达一样。具有讽刺意味的是,1993年8月,Intel的股票市值达到了266亿美元,超过了IBM的245亿美元。
Intel和Microsoft凭借其独家经营的产品,获得了远远超出其贡献的暴利。IBM在绝望中挣扎,试图恢复自己的地位。它推出了PowerPC,企图打破Intel在硬件上的垄断;同时推出了OS/2操作系统,试图动摇Microsoft在软件上的统治。OS/2无疑是失败了,PowerPC的命运则前途未卜。

2、IBM选择MS——DOS的原因

IBM在PC方面所做的部分决定(也许是大部分决定)显然是出于非技术的原因。在决定采用MS——DOS之前,IBM安排了一个会议,与Digital Research公司的Gary Kildall 商讨CP/M操作系统的事宜。就是会议举行的当天,出现了传说中的故事:由于天气非常好,Gary Kildall 决定改乘自己的私人飞机与会,结果误点。IBM的经理们可能对长时间的等待颇感恼火,便转而与Microsoft匆匆达成了协议。
Bill Gates当时刚从Seattle Computer Product公司购买了QDOS, 对它稍作修改, 更名为 MS-DOS。
不要为 Seattle Computer Product 公司感到太遗憾, 他们的QDOS 本身很大程度上 也是基于 Gary Kildall 的CP/M。

3、MS-DOS中640K的限制缘何而来

S-DOS中640K的内存限制源于8086芯片总共1MB的地址空间。1MB可以划分为16个64KB,MS-DOS把6个段留给自己使用,剩下的10个64KB归应用程序使用。正如Bill Gates在1981年 所说的那样,“640KB内存对于所有人来说都已足够了”。

1996年,盖茨在接受媒体采访时澄清了有关“640K内存”的传闻:“我虽说过一些蠢话,做过一些傻事,可这句话绝对不是我说的。业界从没有人说某种容量的内存已经足够了的话。但竟然有人将640K内存已经足够这样无聊的话安在我的头上,经常有人问起我这件事。”

“我从来没说过这样的话;但它却像谣传一样到处传播,以讹传讹。你知道IBM PC内存只有640K的时候,业界所经历的痛苦吗?它们的内存一度只有512K,我们还不断推动PC内存向更大的容量发展。我从来没有说过这番言论,倒是说过与之相反的话。”

你可能感兴趣的:(学习笔记,C专家编程学习笔记)