正文

给使用TC的初学者的一篇文章2007-10-31 16:58:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/yzfy/30651.html

分享到:

给使用TC的初学者的一篇文章 —— 作者:雨中飞燕

本文源自雨中飞燕之家C/C++学习论坛
http://yzfy.org,转载请注明出处。

前言:

“TC过时了”,我说。接着就一堆人问,“为什么过时啊?”,“TC不是很好用吗”,
“教材上也是用这个啊”,“二级考试不也是规定用的TC吗?”。
于是乎我已经无语了。


/----------------------------------------------------------------------/
一。头文件的问题


好了,来看个经典的TC2下在老潭的书的《C语言程序设计(第二版)》,
第4页的一段代码:
main( )
{
  printf("This is a C program.\n");
}

好了,当时的TC2的确允许使用printf和scanf可以不加头文件,
也的确允许main()这样的声明。允许是允许了,可是你写成这样对你有好处吗?

打个比方,Basic语言知道吗?这种语言可以不声明变量就使用。
的确同样也是允许了,可是给我的话,我会要求编译器强制检查变量是不是声明了再使用。
为什么?如果你需要用名字是nlen这个变量,当中有一个不小心写成n1en,
不强制检查的话,编译器不告诉你错,但运行结果是错误的。但这种错误要是在很多行
代码里你怎么找?特别是'l'和'1'如此相似。

编译器虽然允许,但你别以为那样写就是好事。说回刚刚的C代码,TC2允许不加头文件,
于是不少初学的人习惯了从不写头文件,以为C语言不需要头文件似的。
好了,等到看到那本书的138页,那个字符输入输出,用了gets,
就突然多了一个#include <stdio.h>,仔细一看,那一页中间有100来字强调
了要用库函数,可是没有讲怎么用,下面多了的那一行初学者也可能会觉得奇怪,
要是不理解的人就跳过了,要是没留意的更不当一回事了,谁让之前书里一直都
没有那句呢。然后,当有的初学者用TC3.0打代码的时候,编译提示说scanf没有
声明(或者是用TC2的打了gets函数说gets没有声明),他们就郁闷了,
都是按照书的代码啊,都一模一样还会错?于是跑来论坛问。有跑来问的还好,
最怕的就是用TC2的,看了数组觉得难还跳了过去的,于是一直都不会写头文件,
成了习惯。要是随便换个环境,结果没写头文件的,错了,结果还说自己写C代码
写那么多,这里怎么就错了,然后就怀疑编译器是不是坏的。然后就借口说这个
用不习惯要换TC2。结果就是恶性循环,跳不出TC2,学不到新领域的东西。
论坛上这种代码绝不少见,随便翻一些提问帖子肯定找得到。其实早在TC3.0已经
不再允许scanf和printf不加头文件了,随后的所有C编译器也不再支持TC2的
这个特性了。
[color=black]本文源自雨中飞燕之家yzfy.org,作者雨中飞燕[/color]

/----------------------------------------------------------------------/
二。main的声明


其实如果只是算潭书的第二版,这个无类型的main声明没有问题,只是直到C99标准
的出现,这个声明才彻底废除。其实也是同样一个问题,允许你不写,但这不是说你
这样用就是好事。不过,这个的后果有点不一样的就是,你忽略不写,很可能以为是
不需要返回的,或者是以为返回一个无类型的(void),接着就出现了main函数里不
写return 0;的问题。那本书就是从头到尾所有main函数一律没有return 0; 。好了,
等到学了函数那章,书上说,没有返回值的函数应该要用void来声明,好了,
void main就这样诞生了。不信就看看谭书的第三版,变成从头到尾的void main()。
不过话说回头,这样的问题对于初学者学习基本语法和结构方面和写写小程序的都不会
出现任何影响。在编译器上来说,这只是一个标准的问题,但对于你来说别小看了这么
一点点的变化。因为你习惯了main这种写法的话,必然会把这个习惯带到编写其它函数里
(这里暂且不讲main本身返回值的问题)。
举个简单例子,写以下代码:
#include <stdio.h>
factorial(int n)
{
    if(n==1)return 1;
    else if(n>1)return n * factorial(n-1);
}
int main(void)
{
    int n;
    while(scanf("%d", &n),n>=0)
    {
        printf("%d\n", factorial(n));
    }
    return 0;
}

看得出来是计算阶乘的(主要看那个子函数,main函数用回标准写法),
只不过少了int声明和最后一个必然的return而已,

看看输入一个0进去会发生什么事吧:
TC2: 1           (结果居然是碰对了)
VC6: -858993460  (不确定的随机结果)
GCC: 0           (C99标准)
如果你用TC,那你还可能以为这份代码是正确的,
于是你就以为递归算阶乘就是这么写的。
于是还反过来问我:“喂,雨中飞燕,这个结果不就是正确的吗,还错什么啊?”
于是我无语了。。。然后你也就留下了一个你可能以后都不打算去检查的Bug。。。

现在我们再来看一个:
#include <stdio.h>
long Factorial(int n)
{
    if(n==1)return 1;
    else if(n>1)return n * Factorial(n-1);
    return 1;
}
FactorialSum(int n)
{
    float f = 0;
    for(;n>0;--n)
    {
        f += 1.0/Factorial(n);
    }
    return f;
}
int main(void)
{
    int n;
    while(scanf("%d", &n),n>=0)
    {
        printf("%f\n", FactorialSum(n));
    }
    return 0;
}

这个是计算1/1! + 1/2! + 1/3! +...+ 1/n!的代码,Factorial的改好了,
然后加一个FactorialSum函数计算和。里面的变量f就是用来累加(喜欢用float来
保存浮点也是书上的一大不良特色),1写成1.0保证结果不是整数。似乎没错吧?
运行一下不就知道了嘛,输入1,输出0;输入2,输出0;输入5,输出0,
结果是输出全部是0。原因就是你省略类型埋的祸根,只需要在那个函数前面补上
一个float那结果就正确了。
[color=black]本文源自雨中飞燕之家yzfy.org,作者雨中飞燕[/color]
这个危害有多严重吗?在这里不算严重,因为在这里代码很短,相当容易看得出来。
要是换成大程序呢?上千代码甚至上万的代码,要是最后都懒得写一下return,
或者一个返回类型,那么错误还怎么找?你要是这个也省略那个也省略,
写了N多行代码的时候,一运行,这个结果也错那个输入也不行的时候,
你再看看你是不是被你自己的“习惯”给难住你自己吧。

其实说实在的,WIN-TC自带的一个tcsearch.exe文件,那个可以查函数用法和示例,
上面的示例代码全部都是一样的风格: int main(void)
非常规范标准的写法,可是有多少人看了这个了呢?


/----------------------------------------------------------------------/
三。函数声明与返回值


经典的老代码:
int max(x,y)
int x,y;
{
    return x>y ? x : y;
}

现在早已不是pascal时代了,这样的函数声明不但难读,并且现在C99标准已经不再
支持这种写法了。在老潭的书也仅仅用了半版不到的篇幅提到了一下,
其它地方并没有使用这种声明,这点做得还不错。不过很奇怪的是论坛上还时不时
能看见这种声明,这个到底是拜谁所赐呢?这我就不知道了,这里也不展开来讲了。


/----------------------------------------------------------------------/
四。代码风格


这个问题就非常严重了。现在的情况是,从代码风格,就可以知道你的大概水平了,
至少能知道你是不是菜鸟。那些代码缩进弄得乱七八糟的,不用看详细代码都知道
水平肯定高不到哪里去(当然不排除你可以故意弄乱)。因为要是作为一个新手,
要是写的代码乱,调试的时候或者自己看自己的代码的时候,要是你自己都觉得乱,
当代码有Bug,你要找出来的话,看你还头痛不头痛了。本来代码就有问题,再加上
格式乱,要是你自己看着都不舒服,那你还怎么去调试代码呢?自己不会看着头晕?
代码少的时候你可能不觉得,等你写了上百行代码的那再尝尝这种滋味吧。

更严重的问题是,DOS原版TC对缩进支持不好,
格式的控制相对其它的编辑器来说都要弱。
对于初学者,很容易弄出参差不齐的代码,
对于学习方面来说这是一个很不利的因素。
[color=black]本文源自雨中飞燕之家yzfy.org,作者雨中飞燕[/color]
再者,大括号的位置及变量声明位置的问题。看以下几种风格:
1.
int main(void) //潭氏风格
{int n,s;
    while(scanf("%d", &n),n>=0)
    {
        s = factorial(n);
        printf("%d\n", s);
    }
    return 0;
}

2.
int main(void) //视频教学风格?
{   int n,s;
    while(scanf("%d", &n),n>=0)
    {   s = factorial(n);
        printf("%d\n", s);
    }
    return 0;
}

3.
int main(void) //不知道这风格的来源
    {
    int n,s;
    while(scanf("%d", &n),n>=0)
        {
        s = factorial(n);
        printf("%d\n", s);
        }
    return 0;
    }

4.
int main(void){ //这种风格也有不少高手使用的
    int n,s;
    while(scanf("%d", &n),n>=0){
        s = factorial(n);
        printf("%d\n", s);
    }
    return 0;
}

5.
int main(void) //C Primer Plus 上的风格
{
    int n;
    while(scanf("%d", &n),n>=0)
    {
        int s = factorial(n);
        printf("%d\n", s);
    }
    return 0;
}

我推荐的写法是第5种。第三种写法貌似较少见,雨中飞燕本人不好作出评论。
但对于1,2,4三种写法,都是有原因的。原因也很简单,减少占用的行数。
特别是纸版书,节省这点行数累积起来可以节约不少纸张,降低书的成本。
对于powerpoint演示,如果不节约行数,代码根本没办法显示完。
但你要注意,它这样写可能不是为了告诉你要这样写出这样紧密的代码。
但同样的问题出现在TC上。DOS窗口标准大小80*25,高度只能显示25行,
再加上TC菜单和最下面的输出窗口,你能同时看到最多20行,要是按第5种风格
来写代码,会看得很辛苦(因为TC2用不了鼠标,TC3能用不过也麻烦),
于是造成N多紧缩型代码。如果你换成现在新的编辑软件,根本用不着这样。
Windows上的集成编辑软件一页下来就是三四十行,用鼠标滚轮滚一下就能上下拉,
一个代码块要是不想看还可以折叠起来,这些特性都是TC所无法比拟的,
都比TC方便得多。你还有什么理由抱着TC不放呢?还何必写紧缩型代码来为难
自己的那双眼睛呢?


/----------------------------------------------------------------------/
五。TC图形库


TC图形库这个可以说是TC一大特色,用TC2写出来的代码一般可以一眼看出,因为TC2
编译运行的时候,屏幕上原有的东西并不会清除,所以用TC的人一般会习惯在程序开头
写上clrscr()。当然,要是用Win-TC就不会发生这种情况。如果使用者本人知道这个
库是TC专有(好比是VC的MFC)的话,这倒问题不大。问题是如果使用者不知道的话,
屏幕输出用习惯了gotoxy,变得理所当然地认为C就应该支持这类函数的话,
那就糟糕了。论坛上跑来问VC6可不可以输出图形,或者问有没有gotoxy函数的人
不是一个两个人的问题了。
[color=black]本文源自雨中飞燕之家yzfy.org,作者雨中飞燕[/color]

/----------------------------------------------------------------------/
六。越界检查

由于在DOS下,DOS系统根本不会去检查程序的访问越界问题,无论你要对内存的哪里
进行读写,都是允许的(只要不把自己程序的代码区改写了就没事)。但在Windows下,
尤其是WinXP,内存是分块的,对只读块写数据或者对不可读写块进行读数据都会引发
异常,如果程序不能够处理这个异常,那么这个程序就会被强制关闭。有了这个异常
机制,当然会使你更容易查出程序的错误。特别地,在VC6的Debug模式下,
堆里未被初始化的内存被0xCD字节模式填充,堆里释放的内存被0xDD字节模式填充。
于是一但发生越界的时候,很容易通过程序运行结果得知当中有错,为调试带来了方便。
然而TC则不然,不管你越界了多少,你都收不到任何的警告或者错误。
而且在多数情况下,TC下运行正常的越界代码换VC6上就会结果出错。
有初学者以为这是VC6编译器有问题,其实不然,而是TC给你掩盖了这个错误。
#include <stdio.h>
int main(void)
{
    int n;
    int num[8];
    int sum[8] = {0};
    for(n=1;n<=8;++n)num[n] = 1;
    for(n=1;n<8;++n)sum[n] = num[n]+num[n+1];
    for(n=1;n<8;++n)
    printf("%d\n", sum[n]);
    getchar();
    return 0;
}

这个程序的目的是给num数组全部给1,然后sum数组计算num相邻两个数的和。
有的初学者可能会说,这个程序没问题,如果他以为int num[8];的下标是从1至8的话。
然后,照样运行一下看看。TC的结果是7个2,结果正常。
VC6的结果是,什么都没有显示,然后你打开资源管理器查看一下进程,
你就会发现这个程序占用CPU高达90%以上。你不要以为这是编译器发生了问题,
原因正是因为这里的越界导致了一个死循环!如果是GCC或更高版本的VC编译器,
把for里的8改成11也会发生死循环。但即使改成11,在TC上运行的结果也非常正常,
TC里完全没有任何错误的征兆。这里不分析这个死循环产生的原因,这里只是想告诉你,
你在TC上运行结果无误的程序不一定就逻辑上正确了(包括VC和GCC)。特别是TC几乎
没有任何的越界检查,所以更要求程序员在TC上编写代码时候要十分谨慎和细心。
但现在问题是现在使用TC的大多是初学者,在没有什么经验的情况下,很大可能
会写出类似这样的越界访问代码。这种代码也许在TC上没有问题,可是只要换一个
编译环境,问题就马上暴露出来了。不信就看看下面的经典TC错误代码吧:
#include <stdio.h>
int main(void)
{
    char *pstr;
    gets(pstr);
    puts(pstr);
    getchar();
    return 0;
}

(别告诉我说你不知道这代码错在哪里,你不知道错哪里的话那你平时一定是使用TC的)
[color=black]本文源自雨中飞燕之家yzfy.org,作者雨中飞燕[/color]

/----------------------------------------------------------------------/
七。结束语


现在再来回答文章一开头的内容:“为什么说TC过时”?
主要原因其实不是过时不过时的问题,主要是不适合现在初学C语言的学生们使用。
很容易因为TC过于宽松的一些语法,或者一些与C99标准不一样的语法,
让初学者养成不良习惯或者产生特定环境下的依赖性。这些都是对学习上不利的因素。
所以我一般给别人推荐DevC++或者VC2005就是这个原因,借助编译器的强制能力,
迫使你使用较为规范的方式去写代码。新加坡的环境为什么好?不是因为公民素质高,
是因为有法律规定,随便扔垃圾或者破坏环境的都会有严重的法律后果。

归根究底,不让你用TC,就是不希望你有不良习惯,更何况有很多比TC优秀的编译器呢?
何必因为教材或者考试是TC,你就一定用TC呢?

知识是属于自己的,用来真正充实你自己的
考试是给别人看的,用来得到一时之虚荣的
你希望得到充实,还是一时之虚荣?

本文源自雨中飞燕之家C/C++学习论坛http://yzfy.org,转载请注明出处。

补充:老潭新版(2006年版)C/C++书中就提及到代码规范的问题,
建议大家看书购书请购买最新版,否则如被旧书误导,所产生的问题请自行负责

阅读(4857) | 评论(14)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

loading...
您需要登录后才能评论,请 登录 或者 注册