《The C Programming Language》读书笔记 说明

《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.csdn.net/owl2008/,但是

要首先声明,也许csdn不能拒绝一些自以为是的评论家。但我是绝对不欢迎的,要读我的东西,你先得给我最起

码的尊重。

 

注:这是在第二书店的系列的结束篇。对第七八两掌的技术心得,我会在本blog站后续笔记中写出


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值