一个小程序的调试过程——源程序注释过滤器

 BS的《The C++ programming language》上有一道习题:写一个程序,使它能剥掉C++程序里的所有注释。题目要求从cin读进写入cout,但我还是觉得用文件来读写更有用一点。于是就开始构思这个小东西。

C++的注释分两种。/* */和//,两种注释各有特点。第一种虽然有些像“括号”,但不能嵌套,所以就不考虑堆栈的问题了。//注释符作用到本行结尾,如果在注释最后加一个/,就可以把下一行也注释掉。另外,字符串里的注释符号是不起任何作用的。

看起来情况很多、很复杂,但如果引入一个状态量,根据当前字符所处的状态来判断该如何处理,这个问题还是可以解决的。搞一个枚举出来:
enum Flag{ NORMAL, INSTRING, INOLDCOMMENT, INNEWCOMMENT };
我划分了四种情况,也就是:正常情况(初始就是这种情况,须进一步检测注释符以及引号),在字符串里的情况(只检测是否到了“后引号”处,而不用检测注释符号),在老注释符里的情况(只管检测*/,其余一概不管),在新注释符里的情况(直接挖掉一行,并检测这行末尾是不是/,如果是,就再挖掉一行)。

开始的想法是,一个一个字符读进,然后就写一个char NextValidChar( void )函数,用来返回下一个“合法”的字符。也就是说,把ifstream和ofstream都搞成全局的,这个函数如果遇到“合法的字符”(注释以外的)就直接返回这个字符,如果是注释内的字符就递归调用本身——也就是说一直调用它本身直到遇到“合法”字符为止。后来才发现,这样效率太低了——过一会再说这个问题。毕竟,要先make it work, make it right,才能make it efficient。

这里先埋一个伏笔:我使用了标准库istream对象的一个成员函数:putback( char ),用这个函数的作用在于当我检测/*和//时候,如果/后面跟的不是*或者/,我就要把这个字符放回输入流并返回前面的那个/,以免把不是注释的这个字符给漏掉了。按理说,在检测*/的时候也应该这样做,但我开始武断地认为就算漏掉了也没关系——它反正是要被“丢弃”的字符。这就是后来困扰我的问题的真正根源。

以下是我第一个版本的程序,以上的分析过程花了我半个小时,下面的coding也是半个小时:

//LiuKai @ HUST
//2006-7-26

/**********************************************************************************
KillComment4.cpp用来剥掉C++源程序中的所有注释——exec6.6.22
本程序读入一个cpp文件,并将剥去注释的程序写入另一个文件
***********************************************************************************/

#include <iostream>
#include <fstream>
#include <string>

using namespace std;

ifstream is;
ofstream os;
string inFileName = "comment.cpp";
string outFileName = "comment.txt";

enum Flag{ NORMAL, INSTRING, INOLDCOMMENT, INNEWCOMMENT };
Flag status = NORMAL;

char NextValidChar( void )
{
 char next = 0;
 is.get(next);
 if( next == 0 )
  return 0;
 switch( status )
 {
 case NORMAL:
  {
   if( next == '/' )
   {
    char after;
    is.get(after);
    if( after == '*' )
    {
     status = INOLDCOMMENT;
     return NextValidChar();
    }
    else if( after == '/' )
    {
     status = INNEWCOMMENT;
     return NextValidChar();
    }
    else
    {
     is.putback(after);
     return next;
    }

   }//end condition /
   else if( next == '/"' )
   {
    status = INSTRING;
    return next;

   }//end condition "
   else
   {
    status = NORMAL;
    return next;

   }//other conditions
  }
  break;

 case INSTRING:
  {
   if( next == '/"' )
   {
    status = NORMAL;
   }
   return next;
  }
  break;

 case INOLDCOMMENT:
  {
   if( next == '*' )
   {
    char after;
    is.get(after);
    if( after == '/' )
    {
     status = NORMAL;
     return NextValidChar();
    }
    
   }//end condition *

   return NextValidChar();
  }
  break;

 case INNEWCOMMENT:
  {
   string rubbish;
   getline(is,rubbish);
   if( *(rubbish.end()-1)=='//' )
   {
    getline(is,rubbish);
   }
   status = NORMAL;
   return '/n';
  }
  break;

 default:
  cerr << "Impossible!" << endl;
  return 0;
 }
}

int main( void )
{
 is.open( inFileName.c_str() );
 os.open( outFileName.c_str() );
 char ch;
 ch = NextValidChar();

 while( ch )
 {
  os << ch;
  ch = NextValidChar();
 }

 is.close();
 os.close();

 return 0;
}
客观的说,这个程序写得是很丑的——有一个超长的函数,就是那个NextValidChar(),coding过程中没有碰到任何语法上的问题(那是当然),第一次测试找了一个不久之前写的小程序,把它读入以后又生成了一个txt文件。很幸运(或者说很不幸)我的程序这次工作得很好,把那个源程序里的注释剥得干干净净,任何情况都符合我的预期,而且代码的格式还保证得很漂亮。我当时的感觉超high,因为我几乎没有遇到任何错误就解决了这个问题——高高兴兴吃饭去了。

晚饭回来还又“欣赏”了一遍这个程序,觉得应该多测试几个文件——我是对的。把读入文件名改成另一个源文件,我立马傻了眼:生成的文件是空的!难道是没读进去?果然,文件名打错了。我松了一口气,改正文件名,再运行一次。怪了,怎么生成的文件里面还是什么都没有?仔细察看生成文件的属性,发现不是空的了,而是六个字节。这就说明文件是读入了,但是剥得有问题。
按照我的注释风格,文件一开头就是这样的:
//LiuKai @ HUST
//2006-7-26

/**********************************************************************************
KillComment.cpp用来剥掉C++源程序中的所有注释——exec6.6.22
本程序读入一个cpp文件,并将剥去注释的程序写入另一个文件
***********************************************************************************/
我用跟踪调试,发现程序在按照我的意思把前两个新式注释替换成回车以后,在读下面那个老式注释的时候返回了一个0,当然是递归返回的,我只能判断问题出在那个老式注释之后。为了证实这个想法,我在第三行添加了一些文字,再次运行后那些文字被原样写到了目标文件,后面的依然是空白。
我测试的两个文件的注释风格完全一样。为什么第一个可以工作得很好但第二个就会出问题呢??我比较了这两个测试文件的注释,突然想到:是不是汉字的问题?我的程序是一个字节一个字节的读进的,碰到0就结束。而汉字占两个字节,会不会有一个字节是0呢?至于第一个文件可以工作也很好解释,那就是没有碰到这样的汉字。

怎么解决这个问题呢?也就是说,怎样区分文件结束标志的那个0和汉字里可能有的那个0呢?呵呵,难不倒我。汉字里即时有字节0,也不可能连着两个字节都是零,当然极端一点可能前一个汉字的低字节和后一个汉字的高字节都是0,那么,总不至于有三个零连在一起的情况吧!而文件结束不一样,文件结束以后,无论对ifstream调用多少次get函数得到的结果一定都是0。这样的话,我就可以在函数判断到该返回0的时候加一个判断,向后在读两个字符,设置一个叫chinese的bool标志。如果都为零,则说明是文件结束,不是汉字的原因,chinese就为false,反之,chinese为true。然后在main函数的循环条件里加上这个东西,也就是说返回的字符是0而且chinese是false时才能退出循环(才是真正的文件结尾)。

改过以后我又测试了一遍,结果和第一个版本的结果完全一样——清者自清,浊者自浊。似乎看不到任何区别。是我的汉字判断有问题,还是问题出在别处?为了弄清到底是不是这个问题,我把测试失败的那个源文件拷贝了一个副本,并把副本里的注释符号手工去掉,只剩汉字。再次测试,发现我两个版本的程序对于副本里的汉字都能顺利地读出来。这时候,我就确定我的错误和汉字无关。

和汉字无关,那就一定和老式注释符/**/有关。回到第一个版本。我耐心察看函数的调用过程,有了一点发现:我的函数在遇到注释里的字符的时候,在跳过这个字符之外还要递归调用自身,当找到一个“合法”字符时再逐级传上来——这是很危险的:注释长度动辄几百个字符,我这样岂不是让函数递归几百层么?且不说效率极低,还有堆栈溢出的可能。会不会就是因为堆栈溢出而导致的程序结束,而只能把溢出之前的东西写进文件呢?很有可能。调用方式必须改。把函数从递归改为非递归,看起来是一件伤筋动骨的事情。可这个函数比较特殊,我所用的“递归”只是为了写程序方便,这个过程从逻辑上来说是不递归的。我只要先设置一个bool量skip,用来描述当前字符是否需要写入目标文件(即前面所说的“合法”),然后让NextValidChar()函数中所有递归调用(即return NextValidChar())的地方都把这个布尔量设为true(即需要skip),然后简单的返回当前值。在向目标文件写字符式需要检查这个量。这样,我就成功地把函数的调用方式改了过来。

这是第三个版本了。但令我沮丧的是,它竟然还和第一个版本得到完全一样的结果。当然我也不后悔我的改动,毕竟它能和前两个版本工作得一样好(对有些源文件结果是正确的),而且效率肯定很高,又算是歪打正着解决了一个潜在的问题吧。

那么真正的问题到底在哪里呢?当我翻动程序看到前面那个putback函数时,终于意识到问题的所在。我在扫描老式注释的结束符*/的时候,没有用skip。我最初是这样想的,*后面如果是/的话,那么它就是结束符,如果不是的话,也用不着把提前读到的*放回输入流,因为这些都是注释里面的字符,迟早是要被丢掉的。这种想法在通常的“*/”情况下是没有问题的,在我经常使用的“***********/”情况下当“*”为奇数个的时候也是没有问题的。问题就出在当星号为偶数个的时候。这时,函数会“正常地”读到倒数第二个星号处,然后向后探索看是否遇到“/”,结果不是/而是最后的那个星号,也就是读到了“**”。由于我没有把多读的那个星号放回输入流,下次读到的就是那个单独的/,而且在以后的字符里,再也没有“*/”出现(这种注释我一般只放在最前面),于是,整个文件都被当成了注释的内容,因而全部被skip了。

真正的解决问题只需要加一条语句,但解决这个问题我却走了这么多弯路。但这些弯路都没白走,它们的确促使我更深入地思考这个程序并且改进其中不合理之处。以下是我最后版本的程序:
//LiuKai @ HUST
//2006-7-26

/**********************************************************************************
KillComment.cpp用来剥掉C++源程序中的所有注释——exec6.6.22
本程序读入一个cpp文件,并将剥去注释的程序写入另一个文件
***********************************************************************************/

#include <iostream>
#include <fstream>
#include <string>

using namespace std;

ifstream is;
ofstream os;
string inFileName = "A_IConvert.cpp";
string outFileName = "comment.cpp";

enum Flag{ NORMAL, INSTRING, INOLDCOMMENT, INNEWCOMMENT };
Flag status = NORMAL;
bool skip = false;
bool chinese = false;

char NextValidChar( void )
{
 char next = 0;
 is.get(next);
 if( next == 0 )
 {
  //处理中文字符
  char testc1,testc2;
  is.get(testc1);
  is.get(testc2);
  if( testc1!=0 || testc2!= 0 )
  {
   chinese = true;
   is.putback(testc2);
   is.putback(testc1);
  }
  return 0;
 }
 
 switch( status )
 {
 case NORMAL:
  {
   if( next == '/' )
   {
    char after;
    is.get(after);
    if( after == '*' )
    {
     status = INOLDCOMMENT;
     skip = true;
     return next;
    }
    else if( after == '/' )
    {
     status = INNEWCOMMENT;
     skip = true;
     return next;
    }
    else
    {
     is.putback(after);
     skip = false;
     return next;
    }

   }//end condition /
   else if( next == '/"' )
   {
    status = INSTRING;
    skip = false;
    return next;

   }//end condition "
   else
   {
    status = NORMAL;
    skip = false;
    return next;

   }//other conditions
  }
  break;

 case INSTRING:
  {
   if( next == '/"' )
   {
    status = NORMAL;
   }
   skip = false;
   return next;
  }
  break;

 case INOLDCOMMENT:
  {
   if( next == '*' )
   {
    char after;
    is.get(after);
    if( after == '/' )
    {
     status = NORMAL;
     skip = true;
     return next;
    }
    else
    {
     is.putback(after);
    }
    
   }//end condition *
   skip = true;
   return next;
  }
  break;

 case INNEWCOMMENT:
  {
   string rubbish;
   getline(is,rubbish);
   if( *(rubbish.end()-1)=='//' )
   {
    getline(is,rubbish);
   }
   status = NORMAL;
   skip = false;
   return '/n';
  }
  break;

 default:
  cerr << "Impossible!" << endl;
  return 0;
 }
}

int main( void )
{
 is.open( inFileName.c_str() );
 os.open( outFileName.c_str() );
 char ch;
 ch = NextValidChar();

 while( ch && !chinese )
 {
  if(!skip)
   os << ch;
  ch = NextValidChar();
 }

 is.close();
 os.close();

 return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值