《The C Programming Language》读书笔记 说明
作为笔记而言,完全是一种自写自看的行为,本来是没有必要写这篇东西的。但是作为一个生活在网络时代的学
生来说,想学好一样东西最好的办法把自己理解的东西放出去,让人讨论,从而,错误得到及时的更正,正确的
思想得到肯定,进一步激发深入学习的激情,另外,还可以避免因为成天面对机器而忘记人话是咋说的(还真的差
点忘了),恩,好处多多,何乐不为?
为什么要学习c语言?对于今天这个惟利是图的世界来说,恐怕初学者第一要问的就这个问题,他们中的很多人都
会说在拥有c++,java,c#这些高级语言的今天,c能做什么呢?在网络中,得到的回答往往是:c无所不能,然
后是一些语重心长的说教,呵呵,对于一个对编程知道不多的人来说这样的回答没有任何意义,因此我对这样的
问题的回答是:那些高级语言的出现并不能结束c三十年的长盛不衰,c语言没有被任何一种语言所代替,而和c同
时代的那些东西。恐怕今天的人连名字都忘了,而在这个世界的每个角落都有无数的编程爱好者和从业人士对c有
着无以伦比的狂热。这是事实,这是真理,它证明了一切。无须多言了。
毫无疑问没有人能比K&R对c更有发言权了,用他们亲笔所写的《The C Programming Language》来入门是再合适
不过了,尽管现在市场上关于c的教材到处都是,但是大半都是以这本书为根基的,严格来说关于c的一切疑问都
可以在这本书中得到解释,我的笔记也将以此为本,另外会引入另一本巨作《c专家编程》的观点(强烈向各位有
一定c基础的人。推荐此书),为了灵活,这里不作任何约定,在每篇笔记开头会标明笔记内容。
另外,必须声明:本人绝对拥有此笔记的版权,任何不经本人同意,就自行修改盗窃,我保留起诉的权利,
注:本笔记以《The C Programming Language》第二版中译本为准。
笔记范围:《The C Programming Language》第一章
应该说,算上这次,我应该是第四次读这本书了,每次重读都收获不少。都引起我新的思考,很难想象这本一本
技术小书给人留下的感觉是如此深刻,如此令人回味,本章的内容是很容易理解的概念,对于编程学习者是最起
码的知识,但是有些东西还是被初学者忽视。以至于在各个技术社区的初学者问题中层出不断。其实这些东西,
在第一章就得到了比较完整的解释,下面只是把他们提出来强调一下,
第一.关于循环终止。在很多书籍中都存在这样的循环语句:
while ( getchar() != EOF ) {….}
很多人不明白这个EOF为何物?具体如何操作?以至于让上面的语句变成了无限循环,呵呵,其实EOF。是文件结
束符(end of file),在第七章有说明。其为系统常量。值为-1,当然你在终端输入-1,循环并未结束,why?
how to do?恩,你在《c专家编程》里会了解到,c的第一批使用者都是系统设计者和编译器设计者。在他们的理
念里,信息往往以文件为单位的。这个标志只是文件结束的状态,一般不由用户提供。而键盘等输入端在os中是
个极其特殊的文件。需要用户显式标志文件结束。说是系统常量当然有着系统依赖性,因此不同的系统标志方式
就完全不同。Windows下是ctrl+z。linux下是ctrl+D.另外EOF其实不用显式说明。上面的代码与此完全等价:
while( getchar() ) {……}
第二.声明和定义。尽管这个问题地球人都知道,但是真正说的清楚的人并不多。有人认为变量的声明和定义没
有什么区别。有人认为这个与初始化有关系(我曾经就这样白痴,汗~~~),对于后者那就是根本就不明白这
两个概念,声明只是给编译器一个提示,有这么个名字存在于程序中,和运行环境毫无关系。可以重复出现,定
义是具体分配内存空间和指定了变量的位子(左值),在同一域中只能出现一次;对于前者的观点。在单文件程
序中。几乎找不到错误。但如果你把这样的句子放人头文件,int a;一旦这个头文件被重复包涵。必然出现链接
错误。其实这样:
extern int a;//声明
int a;//定义
第三.字符数组和字符串。有人认为这两个是同一概念,是这样吗?不,完全不是,前者为容器(数据结构),后
者为数据。这样说也许太理论化,好,我们来改写下那个hello world
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf( "/0hello world " );
system("pause");
return 0;
}
呵呵,什么也没有?是的。还记得字符串是怎么结束的吗?/0 "/0hello world "是个常量数组。但是字符
串却是“”。字符数组和字符长度是不一样的,
这章尽管非常简单,但是每个例子都经典之作,你能从代码中学到文字中不能学到的东西。建议你每个都抄一遍
,
《The C Programming Language》读书笔记2
第二章:(本文首载于第二书店本人的暑假系列笔记)
本章的内容是学习编程中最基础东西,任何一门语言都会告诉你他支持那些数据类型、那些运算、有那些特点、
以及有那些不完善的东西。学习这些东西相对来说是单调了点,麻烦了点,但是只有通过了这座迷宫,你才能就
进入c这个神奇的领域。因此初学者的成功至少有一半来自“耐心”。呵呵,准备好了吗?
本章的内容还是非常简单的,但是作者的字里行间隐藏了很多重要的信息,不加注意就会从我们的眼皮低下溜了
去,下面将一一列出以示强调。
第一.变量和常量。很多人对于他们的区别很模糊,个人认为他们的主要区别在于是否分配内存空间,换句话说
,就是是否存在左值。左值是什么?在第二章的从头到尾好像没找到这个名词,呵呵,你可以在附录中关于变量
的条目中找到他,其实就是变量的地址,变量一旦被定义,左值就被确定了,一直到他的生存期结束。我们通常
说的变量的值是指变量的右值。这才是我们能操作的对象。根据这个理论,那么就不难知道其实被const修饰的对
象不是常量,他有左值,但是这里有个小麻烦,在本章的开头写明了被const修饰的是常量(本章第二段有个()
说明),我查看了原版,并没有这个补充说明,看来应该是译者的理解,在《c专家编程》中的一个例子证明了我
的想法是正确的,例子如下:
#include “stdio.h”
#include “stdlib.h”
#define one 1
const int two = 2;
int main()
{
int ix = 1;
switch( ix )
{
case one: printf( "this is 1" );/*ok*/
break;
case two: printf( "this is 2" );/*error*/
}
system( "pause" );
return 0;
}
大家都知道,case后面只能跟常量表达式,因此被const修饰的变量不是常量,只是变量的右值一般不能改变罢了
。另外你也可以从上面感觉出#define和const的区别。
第二.关于换码序列。这个更多地方叫转义字符,他们大多数是有一些特殊的功能的字符,在上篇笔记中你已经
看到了他的一点威力,下面我们再看一段代码:
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
int main()
{
int ix;
ix = strlen( "/0abc" );
printf( "this is %d/n", ix );
ix = strlen( "/007abc" );
printf( "this is %d/n", ix );
system( "pause" );
return 0;
}
你会发现,两个差不多的字符串长度完全不一样,什么回事呢?第一个我们可以理解:/0是字符串结束符,因此
其后的任何东西都不能算字符串的内容,因此长度为0。但是第二个呢?我们查了换码序列表就知道‘/007’这个
为一个字符,因此长度为4。这个时候问题来了,编译器为什么没把‘/007’理解为‘/0’‘0’‘7’呢?如果这
样的话长度也将为0,我们又没人为的加分割符号,呵呵,显然这个和编译器的具体实现相关,凭我们现有知识无
法弄明白这点,姑且留着,等待“悟“的一天吧,相信我,这绝对是一种享受。
第三,关于++运算符,在很多教材上都有个看起来很经典的题目,其代码如下:
#include "stdio.h"
#include "stdlib.h"
int main()
{
int ix, iy;
iy = 1;
ix = ( iy++ ) + ( iy++ ) + ( iy++ );
printf( "this is %d/n", ix );
iy = 1;
ix = ( ++iy ) + ( iy++ ) + ( iy++ );
printf( "this is %d/n", ix );
iy = 1;
ix = ( ++iy ) + ( ++iy ) + ( iy++ ) ;
printf( "this is %d/n", ix );
iy = 1;
ix = ( ++iy ) + ( ++iy ) + ( ++iy ) ;
printf( "this is %d/n", ix );
system( "pause" );
return 0;
}
呵呵,是不是很晕?这个本来无非为了说明先加后加的问题,这个地球人都知道,这里不加说明了,但是这样的
程序本身就有很大的问题,编译器的运算并非一定是从左到右的(有些是按树的遍历来算的),因此你会发现不
同的编译器结果会不一样,关于这个本章的结尾有很完整的解释,我就不再多说了,总之,这个测试本身就违背
了语言的特性。
《The C Programming Language》读书笔记3
第三章:当好机器的老板
无论什么时候我们都不该忘记我们是在学一门语言,而学语言的基本要求是:准确无误的用它来表示自己的意
图,不仅要让机器读懂,也要让别人(只要他会c语言)读懂你的意思。记住,语言是用来交流的,不论是编程语
言还是自然语言。现在让我们对这两个交流的对象分别作个分析,如何才能让他们明白你想干什么,打算怎么干
。
对于机器来说,我们要做的相对要简单点,编程语言的语法比自然语言要简单的多了,一切都由顺序、选择、
循环三种结构复合而成,初学者要做的只是走一个“抄写-改写-模仿-习惯”的过程而已。等这些语句成了你
的习惯那就太好了,就像你说汉语的时候不会去考虑你用的是陈述句还是感叹句,呵呵,(这个让我想起了我糟
糕的英语,汗~~~)。当然我们对机器要做的远远不止这些,让机器读懂这只是第一步而已,如何让机器按照
我们的意思运行的更好、更快才是我们要追求的境界,当然,这个境界没有止境。得在经验中慢慢积累,下面只
是提出几个个人的建议而已:
第一. 尽量使用局部变量。因为c语言有个特点,在同个域中的变量必须定义在所有处理语句之前(分程序
[o1] 除外),这意味着在程序开始的时候就必须分配好所有的静态空间,而很多数据在程序中用很少,因此我们
需要减少这些不必要的开销,灵活运用分程序可以将这些对象进一步局部化,比较下面两段代码:
Code1:
#include "stdio.h"
#include "stdlib.h"
int main()
{
int ix;
char c;
scanf( "%c", &c );
if( c == 'y')
{
ix = 100;
printf( "this is %d! /n", ix );
}
system( "pause" );
return 0;
}
Code2
#include "stdio.h"
#include "stdlib.h"
int main()
{
char c;
scanf( "%c", &c );
if( c == 'y')
{
int ix = 100;
printf( "this is %d! /n", ix );
}
system( "pause" );
return 0;
}
你会发现如果我们不输入‘y’系统就没有必要为ix分配空间。
第二,注意和正视一些看起来像bug的语言特性,比如switch语句,可以说从c语言建立的那天起对他的争论就没
有停止过,它的向下穿越给我们带来了不少麻烦。以至于在《c专家编程》的第二章中把它说成是“多做之过“,
但是我们发现有时候它的功能还是不可代替的,比如判断一个数是否属于某个离散集合:
#include "stdio.h"
#include "stdlib.h"
int main()
{
int i;
while( scanf( "%d", &i ) != EOF )
{
switch( i )
{
case 1: case 2:
case 3: case 5:
case 8: case 13:
printf( "yes!/n" );
break;
default :
printf( "no!/n" );
break;
}
}
system( "pause" );
return 0;
}
呵呵,这个数列大家都熟悉,但是除了switch语句你能找到比他更简洁的表示方法吗?但这正是运用了语句的向
下穿越性啊。goto语句也有类似的情况,只要我们仔细研究,这些看起来很麻烦的东西都会变得非常美好。
好了,对机器的交流我们就说到这儿吧。在下次笔记中我们将谈谈和人的交流-程序的风格问题。
--------------------------------------------------------------------------------
[o1]也叫复合语句
(本文首载于第二是书店)
《The C Programming Language》读书笔记4
程序设计初步
到现在为止,我们已经对语言的基本元素有了个比较完整的了解了,但是总是停留在表达式等细节方面,我们
很难写出程序来,在今天任何一个程序都是个工程,如何组织我们已经掌握的这些基本元素,使得他们变成有一
点功能的有机整体,这个就需要一个整体观念的设计思想,对于c来说第一步该是过程化程序设计思想,换而言之
,就是函数的设计,在上篇文字中我们已经看到了,其核心问题是如何分解要解决的问题,写出各个有独立功能
的函数,然后由进入接口函数(在控制台环境下,通常是main函数)组成完整的程序。但是光是这样,我们能解
决的问题相当有限,因为在实际应用中,我们要处理的不是那么简单的内置类型(int,char等),而是比这些复
杂的多的数据类型,因此第二步该是如何针对具体问题写出抽象模型,即ADT(抽象数据类型),进而实现基于对
象的设计思想,而学习指针和结构就需要带着这样的思想去探索,下面将通过一个简单list(链表)的设计来简
要的说明一下该如何建立一个完整的程序。
第一步,建立一个空项目,最好不要选择“控制台程序”模板,这样能使得你的设计思路清楚明白,记住你现
在在学习,方便快速不是你该追求的东西。
第二步,静下心来好好想一想。你的链表要提供那些接口、那些可以给用户修改的部分(如具体的数据类型)
,这些放在用户可见的list.h文件中。在本文中假设我们提供初始化、销毁、增加节点、删除节点、 插入节点、
查找、和打印输出几项功能。那么在上面的工程里加入一个叫llst.h的文件,输入代码如下:
#ifndef LIST_H
#define LIST_H
/*定义函数状态*/
#ifndef ERR
#define ERR -1
#define OK 1
#endif
typedef int status;
typedef void type; /*用户可以根据具体需要更改此类型*/
typedef struct listitem {
type date; /*节点数据*/
struct listitem *next; /*指向下个节点*/
} list_node;//链表节点
typedef struct {
struct listitem *ptr; /*链表头指针*/
int size; /*链表长度*/
} list;//链表
list* list_init ( void ); /*初始化*/
status list_destroy( list* ); /*销毁*/
status add_node( list*, const type ); /*加入一个节点*/
status delete_all( list* );//清空
status delete_node( list*, list_node* ); /*删除一个节点*/
status insert_node( list*, const type ); /*插入一个节点*/
list_node* find_node( const list*, const type ); /*查找*/
status list_print( const list* ); /*打印*/
#endif
第三步,在工程中加入list.c文件。Include了上面刚刚建立的头文件,并实现每个极口,由于在通常情况下此文
件并不是用户可见(这里把维护等问题除外),所以笔者没加什么注释。当然这个不是什么好习惯,这里过于简
单,注释就显得有些多余。
首先是include需要的头文件:
#include "stdio.h"
#include "stdlib.h"
/*严格来说上面该用尖括号,由于网页显示不得已为之*/
#include "list.h"
接下来是初始化和销毁的实现
list* list_init ( void )
{
list *p = ( list* )malloc( sizeof( list ) );
if( p == 0 )
return 0;
p->ptr = 0;
p->size = 0;
return p;
}
status list_destroy( list *pev )
{
if( pev == 0 )
return ERR;
delete_all( pev );
free( pev );
return OK;
}
按理说,函数不能返回指针,呵呵,这里有个很多初学者都误会的问题,返回局部对象的左值和局部对象的引用(
后者是c++中的说法)被返回的确不可以,因为局部对象在函数的活动记录(即函数调用栈中)分配,函数一旦结
束局部对象被回收,返回的将是无效地址。因此象下面这样的函数是错误的,
int* f()
{
int *p, a;
p = &a;
return p;
}
但是由malloc分配的是堆上分配的,他不会随着函数的结束而被回收。但是这样用要相当小心,必须防止内存泄
漏。程序结束前必须free掉该空间。
下面就是完整的list.c
#include "stdio.h"
#include "stdlib.h"
/*严格来说此处处该用尖括号,由于网页显示不得已为之*/
#include "list.h"
list* list_init ( void )
{
list *p = ( list* )malloc( sizeof( list ) );
if( p == 0 )
return 0;
p->ptr = 0;
p->size = 0;
return p;
}
status list_destroy( list *pev )
{
if( pev == 0 )
return ERR;
delete_all( pev );
free( pev );
return OK;
}
status add_node( list *p, const type date )
{
list_node *pev =
( list_node* )malloc( sizeof( list_node ) );
if( pev == 0 )
return ERR;
pev->date = date;
pev->next = p->ptr;
p->ptr = pev;
p->size++;
return OK;
}
status delete_node( list *p, list_node *pev )
{
list_node *temp = pev;
if( pev == 0 )
return ERR;
pev = temp->next;
free( temp );
p->size--;
return OK;
}
status delete_all( list *pev )
{
int ix;
if( pev == 0 )
return ERR;
if( pev->size = 0 )
return ERR;
for( ix = 0; ix < pev->size; ++ix, ++pev->ptr )
delete_node( pev, pev->ptr );
return OK;
}
status insert_node( list *p, const type date )
{
list_node *pev = p->ptr; ;
if( p == 0 )
return ERR;
pev = find_node( p, date );
if( pev == 0 )
{
type ia;
printf( "输入要插入的数/n" );
scanf( "%d", &ia );
add_node( p, ia );
}
else
{
type ia;
list_node *pv =
( list_node* )malloc( sizeof( list_node ) );
if( pev == 0 )
return ERR;
printf( "输入要插入的数/n" );
scanf( "%d", &ia );
pv->date = ia;
pv->next = pev->next;
pev->next = pv;
p->size++;
}
return OK;
}
list_node* find_node( const list *pev , const type date )
{
int ix;
list_node *p = pev->ptr;
for( ix = 0; ix < pev->size; ++ix )
if( p->date == date )
return p;
else
p = p->next;
return 0;
}
status list_print( const list *pev )
{
int ix;
list_node *p = pev->ptr;
if( pev == 0 )
return ERR;
if( pev->size == 0 )
return OK;
for( ix = 0; ix < pev->size; ++ix )
{
printf( "%d/t", p->date );
p = p->next;
}
printf( "/n" );
return OK;
}
第四步,自己写个main函数,由于个人的调试方式不同,这里不给出代码。只要确保每个函数都能正常工作就行
了。
好了,到现在为止我们把一个数据结构的实现走了一遍。当然,为了简单文字。笔者减少了很多list该有的功能
。很多人认为我写太烂,现在再次说明,本文和初学者交流的文字,高手们就不必在这篇文字浪费你的时间了
《The C Programming language》读书笔记5
尽量利用能利用的资源
在上篇文字中,我们设计了一个非常简单的list,在设计的过程运用了在本书第五、六章的知识,这些东西
是c语言中最难的部分,学术方面的讨论随处可见,指针的用法和特性多得让人无法记住,个人认为最好的方法是
多实践,在实践遇到的问题往往就是最常见的、最重要的知识点,至于那些特别的特性,等熟悉了那些常见的后
也就不难理解他们了。
本书的第七。八两章所述的内容严格来说是不属于语言本身的东西,是的,我认为该这么说,这个关系到对”
库”的理解,库是什么?是别人已经写好的东西(类型、函数、常量等等),我们的程序可以根据他们提供的接
口调用就可,以节省我们开发的时间和精力,但是必须明白,不是没有库,我们就不能写东西了。第八章的内容
就是告诉我们如何根据具体的os写出类似标准的io库,是的,库必须是系统相关的,当然你可以最大限度的保证
他的可移植性(这正是标准库的成功之处)。
当然,对于大多数程序开发而言,库的运用可能是程序设计水平高低的最重要的指标之一,没有人会笨到放着
的东西不用,而化费大量的时间去自己写一个(当然作为学习研究则正好相反)。有一次,一个网友跟我说标准
c++和c怎么也干不了,当我提出反对意见时,此人气势汹汹的质问:“你不用WIN API写个窗体给我看看!?”。
我无言了,因为要说的太多了, 比如:难道API是凭空出现的吗?难道我写不出API就可以说c++和c无此能力吗?
我无此荣幸,就算有,也不是片刻就可以拿出来给他证明的。因此我选择沉默,人类的任何成功都是建立在前人
的基础上的,这样才有我们引以为傲的效率。
回到正题,标准库非常庞大(别的也小不到那去),os的系统调用也很多,对于这些我的建议是:记住常用的,
别的用到的时候查手册之类的东西即可,比如,以c标准库为例,在本书附录B中提到的大多都需要记住。至于别
的,大家可以去看看《c语言参考手册》。
好了。暑假结束了,本笔记也可以结束了(是的,我听到有人在说:”这个家伙终于闭嘴了”,有自知之明?
呵呵,或许是“他”知之明吧。),写笔记是笔者的一种学习习惯,csdn的编辑也只是为了向大家推荐几本好书
并引起良好的学习讨论,才要求笔者修改一些针对性用词,比如“暑假学习笔记“等等,并转载此处。对于学生
而言,有个交流的平台是件好事,自己的错误可以得到及时的更正。不管作者的水平如何,他写出了自己的想法
,整理了自己的知识,提出了自己善意的建议,这就不是件容易的事情。可是我们看到什么呢?无思考的指错。
没看清楚文字就拿代码发难。无意义的漫骂,大概是为了显示自己的水平吧,更有些人说是某位作者是为D币而来
(我在本站另一书评中所见的评论)。其实稍对本站有些理解的人都知道,得到和他买书化的钱根本就不能相提
并论,这些现象不能不说是个遗憾。
最后的最后,要是有朋友对我还有什么建议的话,可以访问我的blog:http://blog.youkuaiyun.com/owl2008/,但是
要首先声明,也许csdn不能拒绝一些自以为是的评论家。但我是绝对不欢迎的,要读我的东西,你先得给我最起
码的尊重。
注:这是在第二书店的系列的结束篇。对第七八两掌的技术心得,我会在本blog站后续笔记中写出