开篇的闲话
学C++的文件流输入输出的那一周,老师指了书上的两道题作为作业,并说“总算可以有现成的题可以直接布置给你们了”,还说这两道题要亲自批改,算作期末成绩。
我起初觉得,这可能只是由于老师之前总是发互评作业,成绩参考性低,所以才准备自己评分。但着手做了第一道题,觉得还是有点难度,尤其对于我这种脑袋转得慢的人来说。
于是赞叹老师的别有用心。(不过我觉得老师可能并没有想这么多,只是随手圈了两道题,而是我想多了而已哈哈)
目前只完成了这两题中的第一道题:(第二题貌似没那么难了哈哈)
题目
编程将一个C++程序文件中的注释以及函数体内部的语句全部去掉后输出到一个新的文件。假设“/* */”形式的注释没有嵌套。若程序名为proccpp,则以命令行方式运行之:
proccpp test1.cpp result2.cpp
能够将test1.cpp
处理后的结果生成为result2.cpp
文件。
输入样例:
# include < iostream >
# include < iomanip>
using namespace std;
/* test sample
*/
void func(int n)
{ if(n) {
cout << "in func" << endl;
}
}
int main() //this is main
{
double f;
cin>> f;
return 0;
}
输出样例:
# include < iostream >
# include < iomanip>
using namespace std;
void func()
{
}
int main()
{
}
其实写到这里,其实我发现我又审错题了;一开始是只看到把注释删掉,没看到要把函数定义删掉;这会儿是又发现,原来要把函数的参数也删掉;但其实我觉得要是连函数的参数也删掉就没啥意义了,因为本来删掉注释和定义这样的操作,貌似是可以用在和别人分享自己写的代码时的场景的,比如自己没有单独写头文件,而又不想分享具体的实现时。所以,我不准备改了(可能是因为太懒 )。
(写完程序之后)在某度上搜索了一下,发现好像没有考虑到转义字符的情况的文章(暗自狂喜)(具体情况下面会介绍)
虽说是C++,可是我把它写成了C程序的样子,(一点儿也不面向对象!哪天有空了改改!)。如果读者把ifstream
、ofstream
、get()
、put()
等等换成C语言的方法,应该也是能够正常使用的!
删掉CPP文件里的注释
那我们就来说正事儿吧,首先是去掉注释。
我们C/C++的注释有两种,一种是C风格,即/* ... */
样式的注释,可以跨行;另一种是C++风格,以//
开头,直到行尾。
我一般喜欢采用逐字符读取的形式,以免自己程序的存储空间不够导致错误,比如遇到下面的这种代码,把整个函数写在一行:
int main() {
for(int i=0;i<=100;i++){
cout<<i<<' ';} return 0;}
或者是更长的代码……我就不做示范了。
所以逐字符读取可能会好一些吧。然后笔者计划根据模式处理读入的字符:如果不处于注释模式,则在检查读取到的字符后将其输出;如果处于注释模式,则只检查,不输出;
如果我们遇到/
这个字符,就说明可能遇到了一个注释;然后再判断下一个字符,如果还是/
,则说明遇到了//
风格的注释,进入注释模式A;如果遇到*
,则说明遇到了/* ... */
风格的注释,进入注释模式B;
退出“模式”也要分而治之:
如果是C++风格,则遇到\n
即可认为注释结束;如果是C风格,则遇到*
后再读一个字符,如果是/
,则可以认为注释结束,反之不然。
这样一想很简单,但其实还有个问题,如果代码的中包含了char *str = "//abc"
或者类似这样的语句,会导致程序错误的认为进入了注释模式,从而出错;所以,字符串中的东西我们是应该照抄下来的;
所以又有了一个字符串模式。在字符串模式下,照抄读取到的字符,直到退出字符串模式。而识别到"
就可以认为进入字符串模式,再次识别到,就可以退出;
然后写出来了,运行,用这个程序处理这个程序的源文件(套娃?),结果貌似是不行的……
因为忘了考虑转义字符,比如如果有一个这样的字符串"'\"' is a quotation mark"
,那么程序可能会提早结束字符串模式,然而这是不对的,所以要考虑;
那么再写。
虽然转义字符有很多,但是我们只需要考虑一种 ,也就是\
后面跟了"
一个字符的情况,其他的直接照抄也不会影响我们退出模式的判断。
然后又写好了,发现还是不行……为什么呢?其实是因为自己的代码里会有诸如if (ch == '\"')
这样的字段,所以字符常量也要考虑;
所以又多了一种字符常量模式。自然,这里要考虑\
后面跟了'
的情况。
综上,就可以优雅的删除CPP代码里面的注释了;
哦不,其实有个小遗憾,就是C++风格的注释如果在单独的一行,就会留下一段空白,这个暂时还没想到好的解决办法,因为我没有采用一次读一整行再处理的方法,所以发现一个整行注释的时候,可能已经写了不少行前空格了,如果要删除,可能会涉及到输出文件流“倒带”的操作,而我还不是很会这个。
也许可以设一个小小的行前缓冲区?因为我不信一行代码前面能有几千个空格不成?(哈!问题貌似得到了解决,但我暂时还不想解决,嘿嘿)
rmcomment.cpp:只有除掉注释的功能
#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;
int main(int argc, char* argv[]) //main函数的参数,此处暂不赘述
{
//可以理解成我们在控制台输入的指令被视作几个字符串,
//指向他们的指针,存在(char*) argv[]里面
//argc就是 字符串 的个数,本例中就是有3个
if (argc < 3) {
cout<<"Too few arguments!\n";
cout<<"Try 'proccpp [src filename] [dst filename]'"<<endl;
return 0;
}
else if (argc > 3) {
cout<<"Too many arguments!\n";
cout<<"Try 'proccpp [src filename] [dst filename]'"<<endl;
return 0;
}
ifstream inFile;
inFile.open(argv[1],ios::in); //argv[1]就是指向输入文件名的指针
if(!inFile) {
cout<<"Failed to open file '"<<argv[0]<<"'."<<endl;
return 0;
}
ofstream outFile;
outFile.open(argv[2],ios::out); //同理,argv[2]就是指向输出文件名的指针
if (!outFile)
{
cout<<"Failed to open file '"<<argv[1]<<"'."<<endl;
inFile.close(); //如果打开文件失败,记得关闭之前打开的文件
return 0;
}
char ch,chch;
bool str_mode = false; //字符串模式
bool char_mode = false; //字符常量模式
bool comment_mode_1 = false; // C++风格的注释
bool comment_mode_2 = false; // C风格的注释
while (inFile.get(ch)) //读取一个字符
{
if (str_mode) {
//如果处于字符串模式
outFile.put(ch); //读取到直接输出
if(ch=='\\') {
//如果读取到了疑似转义字符
inFile.get(ch); //那么再读一个,并写出
outFile.put(ch); //其实这里误打误撞好像解决了\符号在行末意义的问题
}
else if(ch=='\"') str_mode = false; //退出字符串模式
continue; //继续下一个循环;也可以删掉,因为有else if
}
else if (char_mode) {
outFile.put(ch); //读取到直接输出
if(ch=='\\') {
inFile.get(ch);
outFile.put(ch);
}
else if (ch == '\'') char_mode = false;
continue; //可以不要
}
else if (comment_mode_1)
{
if(ch == '\n') {
//到了行末该结束注释模式了
//outFile.put(ch);
outFile<<'\n'; //补一个换行上来,
//其实这里最好设置一下根据不同系统的处理
//Windows的行末是\r\n,Linux/Unix下只是\n
//可以用预编译指令啥的做一下判断,不判断问题也不大,
//比如你看我就没有判断
comment_mode_1 = false;
}
continue; //还是可以不要
}
else if(comment_mode_2)
{
if(ch=='*')
{
inFile.get(chch); //还要再读一个判断是不是该退出注释模式
if(chch == '/') comment_mode_2 = false;
}
continue; //不然就直接跳过ignore
}
else
{
if(ch == '\"') {
//读到一个双引号就可以认为进入字符串了
outFile.put(ch);
str_mode = true;
}
else if(ch == '\'') {
//同理
outFile.put(ch);
char_mode = true;
}
else if (ch == '/') {
//疑似注释模式
inFile.get(chch);
if (chch == '/') {
comment_mode_1 = true;
}
else if(chch == '*') {
comment_mode_2 = true;
}
else {
//不是注释模式,照常输出
outFile.put(ch);
outFile.put(chch);
}
}
else outFile.put(ch); //啥都不是,照常输出
//cout.put(ch); //可以把结果输出到控制台,但没啥用
}
}
inFile.close();
outFile.close();
return 0;
}
删掉CPP文件里的函数定义
好吧,因为自己没看清楚题,又得重新写/改代码;但其实也没亏多少。
这个看起来很简单,只需要识别左右大括号就好了,识别到一个左括号就进入“函数定义模式”,里面内容全部ignore,然后直到识别到右括号;
咳咳,可是函数体里还是会有大括号的啊……!
那我们用栈的思想,记录下遇到的左括号/括号对的数目,遇到左括号就++
,遇到右括号就--
,当遇到左括号且数目减到0时,就可以退出函数定义模式了。
理想很丰满,显示很骨感 ↑ (事后的自嘲)
好,写!
可是写完之后发现不行,盯着看不知道哪儿错了,以为自己程序结构或者条件判断哪儿错了,改来改去也没用;后知后觉才发现,如果代码(的注释、字符串)里有不成对儿的大括号,这种偷懒的方式还是会gg。
所以程序定义这里也不能偷懒,还是得逐字符的读,还是得判断各种模式……(哭了)
甚至,还不能简单的删除最外层大括号的里面东西,因为类的声明我们是不想删掉的;这里我采取的思路是,判断左大括号是否在右圆括号后面:如果在圆括号后面,恰好又有一个{
的时候,则可以认为进入了“函数体”(之所以加引号是因为还有可能是循环啊啥的,也符合这种情况)。
所以又多出一个“在右圆括号后面模式”。
要考虑这么多,不知道怎么改,又不想重写……想了许久后,还是想出了在原来代码上修改的方法:也就是不为“函数定义模式”设计一个单独的分支,而是将其作为输出前需要判断的一个状态
(对,我其实有个为未来的考虑,也就是想给这个程序指定可选的option,可以指定只删除注释或是怎么的;毕竟咱写代码不是为了作业,而是为了自己,嘿嘿)
下面直接请proc()
出场吧:
void proc(const char* srcName, const char * dstName)
{
ifstream inFile;
inFile.open(srcName,ios::in);
if(!inFile)
{
cout<<"Failed to open file '"<<srcName<<"'."<<endl;
return ;
}
ofstream outFile;
outFile.open(dstName,ios::out);
if (!outFile)
{
cout<<"Failed to open file '"<<dstName<<"'."<<endl;
inFile.close();
return ;
}
char ch,chch;
bool str_mode = false;
bool char_mode = false;
bool in_function = false;
bool comment_mode_1 = false; // // mode
bool comment_mode_2 = false; // /**/ mode
bool after_right_bracket = false;
int brace_num = 0;
while (inFile.get(ch))
{
if (str_mode)
{
if(!in_function) outFile.put(ch);
if(ch=='\\') {
inFile.get(ch);
if(!in_function) outFile.put(ch);
}
else if(ch=='\"') str_mode = false;
continue;
}
else if (char_mode)
{
if(!in_function) outFile.put(ch);
if(ch=='\\') {
inFile.get(ch);
if(!in_function) outFile.put(ch);
}
else if (ch == '\'') char_mode = false;
continue;
}
else if (comment_mode_1)
{
if(ch == '\n') {
if(!in_function) outFile<<'\n';
comment_mode_1 = false;
}
else continue;
}
else if (comment_mode_2)
{
if(ch=='*')
{
inFile.get(chch);
if(chch == '/') comment_mode_2 = false;
}
else continue;
}
else //正常读取模式
{
if(ch == '\"') // " means a string begins
{
if(!in_function) outFile.put(ch);
str_mode = true;
}
else if(ch == '\'') // ' means a char begins
{
if(!in_function) outFile.put(ch);
char_mode = true;
}
else if(ch == ')') //means after a bracket
{
if(!in_function) outFile.put(ch);
after_right_bracket = true;
}
else if (ch == '/') //means a possible comment
{
inFile.get(chch);
if(chch == '/'){
comment_mode_1 = true;
}
else if(chch == '*'){
comment_mode_2 = true;
}
else {
//注意除了下边的那个else if分支,
//这里也是一个正常读取的情况
//所以也要进行 函数定义模式 相关的判断和处理
if(!in_function) outFile.put(ch);
if(!in_function) outFile.put(chch);
if(chch == '{') {
brace_num++;
}
else if(chch == '}' )
{
brace_num--;
if(brace_num == 0) {
in_function = false;
after_right_bracket = false;
outFile.put(ch);
}
}
}
}
else if (in_function)
{
if(ch == '{') {
brace_num++;
}
else if(ch == '}' )
{
brace_num--;
if(brace_num == 0) {
in_function = false;
after_right_bracket = false;
outFile.put(ch);
}
}
}
else if(after_right_bracket && !in_function && !comment_mode_1 && !comment_mode_2 && !str_mode && !char_mode)
{
outFile.put(ch);
if (ch == ';') after_right_bracket = false;
else if (ch == '{') {
in_function = true;
brace_num++;
}
else continue;
}
else outFile.put(ch);
}
}
inFile.close();
outFile.close();
}
剩下的部分:
如果读者想要将这个程序拿去实验,有劳您自行将上边的代码插入到下边这个里边了,这里为了阅读方便我就强行断开成两截了
这里也顺便插播一下使用说明:
将proccpp.cpp编译成可执行程序proccpp后,请使用命令提示符切换到可执行文件所在目录,
使用’proccpp 输入文件名 输出文件名’ 来调用程序处理CPP文件;
使用’proccpp --help’查看帮助信息;
使用’proccpp --version’查看版本信息;
自己读取命令行参数的方法可能不是很规范,还请多指教。
#include <iostream>
#include <iomanip>
#include <fstream>
#include <cstring>
using namespace std;
void proc(const char* srcName, const char * dstName);
int main(int argc, char *argv[])
{
const char *option_help = "--help";
const char *option_version = "--version";
int i = 1;
while (i < argc && argv[i][0] == '-')
{
if (strcmp(option_help, argv[i]) == 0) //show 'help'
{
cout << "Usage: proccpp [options] srcfile dstfile" << endl;
cout << "Options:" << left << endl;
cout << " " << setw(12) << option_help << "Display help information" << endl;
cout << " " << setw(12) << option_version << "Display program version" << endl;
return 0;
}
else if (strcmp(option_version, argv[i]) == 0)
{
cout<<"proccpp 1.0\n";
cout<<"a program that helps you to proc your source cpp file"<<endl;
return 0;
}
else {
cout<<"Illegal option parameter! Try'proccpp --help'."<<endl;
return 0;
}
i++;
}
char *srcFilename = NULL;
char *outFilename = NULL;
if (argc - i > 2)
{
cout << "Too many arguments! Try 'proccpp --help'." << endl;
return 0;
}
else if (argc - i < 2)
{
cout << "Too few arguments! Try 'proccpp --help'." << endl;
return 0;
}
srcFilename = argv[argc-2];
outFilename = argv[argc-1];
proc(srcFilename,outFilename);
return 0;
}
虽然这次的代码只有不到200行,但笔者写了两天……(而且快要写哭了 )比较惭愧,明白了自己还是有很多地方亟待进步。
希望这个程序能帮到大家。
(我应该把想说的都写下来了吧?算了,想到再来补好了……