c++课程设计

原题

要求:设计一个保留字的统计程序
建立保留字文件;
从源数据文件(C或C++语言程序)中,读取字符或字符串,与保留字文件中的保留字进行匹配比较,并统计计数。输出两张表文件:保留字计数,扫描程序的次数,非保留字计数。

想法

我先说说我是第一天是怎么弄的,第一天。。。什么也没写出来,我连 保留字 是什么都不知道,后来百度后才知道 保留字就是c/c++关键字比如 int short while for,然后再看看题目,要求已经很明确了,建一个保留字文件以及一个c/cpp的源代码(任意的),通过程序筛选出保留字与非保留字,并且计数,最后分别用两个文件输出。所以,这个程序可以分成3部分:

  1. 文件读取
  2. 筛选以及计数
  3. 文件输出

先说文件输入输出,这个我是有点头疼的,之前c里面是用fread,fwrite两个函数,但这里显然不合适,你无法预测文档的大小。无奈,拿起上学期的c++书,翻到了文件输入输出流,书上的太简略,没有合适的代码,于是就先放在了这里。再来想一想第二个步骤,第二个步骤也是最难的部分,首先看筛选,如何将保留字与非保留字从源码里区分开来,和周围同学讨论后,他们是这样想的:通过文件操作将源码读到一个大的字符数组里,然后再把保留表再读到一个字符数组里,然后通过模式匹配(比如KMP)完成计数。可是这只是保留字啊,剩下的非保留字怎么办呢?
继续讨论后决定通过字符串分割,将源码按 空格 分割下来,这样除保留字以外的所有字符都是非保留字。可是我又有了疑问:如何保证分割的精确性呢,要知道源码里还有大量的 括号,引号……

而且,我觉得这个肯定不是上佳的方案,先不考虑可行性,这个算法的时间成本太高了,保留字差不多快50个,光统计这个就得把源码扫50遍???
所以,要想尽量时间成本的话,就得边读边统计。

不过,关于计数我倒有个想法,我想用stl里的map,据说map内部里是由 红黑树维护的(一种非严格意义上的平衡二叉树),用了map就非常方便,这样就可以直接将 字符串映射成 整型来计数了。

最后来重新总结一下需要解决的问题:

  1. 找到合适的方式使得将源码边读边计数
  2. 确保分割的精确性
  3. 文件输出

好了,第一天就这样结束了,虽然代码一行没写,但是有了目标。
下面简单介绍一下map。

使用map首先要加头文件

#include <map>

map就是从键(key)到值(value)的映射。因为重载了 []运算符,map像数组的“高级版”。例如我们可以用一个 map< string,int>month_name来表示“月份名字到月份编号”的映射,然后用 month_name[“July”]=7这样的方式来赋值。
本程序的具体操作下面再讲。

第二天,是取得重大突破的一天,因为我想到了以前做的一道Acm竞赛题,完整题目如下:安迪的第一个字典 UVa 10815大意就是输入一个文本,找出所有不同的单词,按字典序从小到大输出,单词不区分大小写。
代码如下:

#include <iostream>
#include <cstdio>
#include <string>
#include <set>
#include <sstream>
using namespace std;

set<string> dict;

int main(){
//  freopen("input.txt","r",stdin);
    string s,buf;
    while(cin >> s){
        for(int i = 0; i < s.length(); i++){
            if(isalpha(s[i])) s[i] = tolower(s[i]);
            else s[i] = ' ';
        }
            //注意下面两行
            stringstream ss(s);
            while(ss >> buf) dict.insert(buf);

    }
    for(set<string>::iterator it = dict.begin(); it != dict.end(); it++){
        cout << *it <<"\n"; 
    }
    return 0;
}

其中用到了 stringstream(需要头文件 #include< sstream>),这个stringstream是字符串输入输出流,我先给这个流取了个名字叫 ss然后用这个流读一篇文章,这个流式读取有个好处,就是自动按空格给你读,也就是说能以空格来一个一个的读单词。
这样的话,我联想到本次任务, 我可以先用文件流读取源码,然后再将所有非字母以及‘_’(保留字仅有的类型)用空格覆盖,再用这个字符串流读取这样,就实现了边读边筛选的功能了。
而且,切割相对精确。想到这里,我的思路已经很明确了,再去百度了一下文件流的操作,算法就出来了。

算法

用两个map< string,int>,分别存储保留字与非保留字。
将保留字的表保存至一个txt文档,再保存一个源代码文档,用文件流先读保留字文档,读一个string,就放到第一个map里,并且赋值为0,然后再用文件输入流读源码,每读一次将其他干扰字符用空格覆盖,再用字符串流再读一次,若在保留的map里找到了匹配的,该字符串对应值就自加一次,若没有找到,再放到非保留字的map里匹配,若匹配成功,该字符串对应的值自加一次,否则将该字符串当作新的键值加入到非保留字的map里并赋值为0。

代码程序

#include "stdafx.h"
#include <iostream>
#include <map>
#include <string>
#include <fstream>
#include <sstream>
using namespace std;

map<string, int> reserved_word_dict;  //用map保存保留字和非保留字最终统计结果
map<string, int> unreserved_word_dict;

//将数字转化成字符
string ChangeIntIntoString(int num) {
    if (num == 0) return "0";
    string str;
    while (num != 0) {
        int t = num % 10;
        str += (t + '0');
        num /= 10;
    }
    for (int i = 0; i < str.length() / 2; i++)
    {
        char c = str[i];
        str[i] = str[str.length() - i - 1];
        str[str.length() - i - 1] = c;
    }
    return str;
}

int main(){
    string reserved_word;
    ifstream Reserved_word_dict_file("保留字表.txt"); //文件流读入保留字表并记录到map中
    while (Reserved_word_dict_file >> reserved_word) {
        reserved_word_dict[reserved_word] = 0;        //读一个保留字就赋值为0
    }
    Reserved_word_dict_file.close();


    ifstream source_code("测试源程序代码.cpp");
    string temp, buf;
    while (source_code >> temp) {
        int i;
        for ( i = 0; temp[i] != '\0'; i++)
            //将干扰的字符全变为空格
            if (temp[i] == '/' && temp[i + 1] == '/') {
                for (; temp[i] != '\0'; i++)
                temp[i] = ' ';
                break;
            }
            else if (temp[i] != '_' && !isalpha(temp[i]))
                temp[i] = ' ';

        stringstream ss(temp);     //字符串流读写
        while (ss >> buf) {
            cout << buf << endl;
            if (reserved_word_dict.count(buf)) reserved_word_dict[buf]++;
            else {
                //非保留字的计数
                if (!unreserved_word_dict.count(buf)) unreserved_word_dict[buf] = 0;
                unreserved_word_dict[buf]++;
            }
        }
    }
    source_code.close();

    //文件写操作
    ofstream reserved_word_calculation("保留字统计表.txt");
    //遍历map
    for (map<string, int>::reverse_iterator it = reserved_word_dict.rbegin(); it != reserved_word_dict.rend(); it++) {
        string t, p;
        t = it->first;
        t += ':';
        p = ChangeIntIntoString(it->second);
        t += p;
        reserved_word_calculation << t << endl;
    }
    reserved_word_calculation.close();


    ofstream unreserved_word_calculation("非保留字统计表.txt");
    for (map<string, int>::reverse_iterator it = unreserved_word_dict.rbegin(); it != unreserved_word_dict.rend(); it++) {
        string t, p;
        t = it->first;
        t += ':';
        p = ChangeIntIntoString(it->second);
        t += p;
        unreserved_word_calculation << t << endl;
    }
    unreserved_word_calculation.close();

    return 0;
}

补充说明

  1. string为stl库里的一个类,非常方便,需要头文件 #include< string>,string类里重载了‘+’,可以实现字符串的拼接,同时也重载了‘[]’,可以以下标访问,string里的length()用来求字符串的长度。

  2. 文件输入输出的流文件需要加头文件 #include< fstream>,具体使用时要注意输入流类型是 ifstream,输出流类型是 ofstream,在这里ifstream/ostream等同于 int,定义了变量名后可以用 “变量名>>”这种方式读文件,写文件也是同样的道理,如:

    string s;
    ifstream  myfile("input.cpp");
    while(myfile >> s){……} 
    
  3. 我在map里用了count()函数,这个函数可以用来找键(key),如果找到了就返回1,否则返回0。
    另外看一下这段代码:

for (map< string, int>::reverse_iterator it = unreserved_word_dict.rbegin(); it != unreserved_word_dict.rend(); it++)

这里的 iterator是迭代器,这句话的意思就是遍历map里的值,如果取key值的话就用 it->first 取value值的话就用 it->second,若要输出要在前面加上“*”。

好了,这就是这个程序的设计过程了,如果大家有意见或者问题欢迎留言。