第二次编程作业

1、李宇晴 3224004230 Github链接:https://github.com/zgyh218/3224004230
2、PSP表格:
| PSP2.1 | 预估时间(分钟) | 实际耗时(分钟 |
| Planning | 15 | 20 |
| Estimate | 15 | 20 |
| Development | 340 | 520 |
| Analysis | 60 | 90 |
| Design Spec | 30 | 40 |
| Design Review | 15 | 20 |
| Coding Standard | 10 | 10 |
| Design | 45 | 60 |
| Coding | 120 | 180 |
| Code Review | 20 | 30 |
| Test | 40 | 90 |
| Reporting | 50 | 75 |
| Test Report | 20 | 30 |
| Size Measurement | 10 | 15 |
| Postmortem &Process
Improvement Plan | 20 | 30 |
| 合计 | 405 | 615 |
3、效能分析:
本次使用 Visual Studio 性能探查器,采用 CPU 采样分析,在 Release 模式下传入参数-n 10000 -r 50,一次性生成 10000 道四则运算题目,采集程序 CPU 占用情况。
性能分析结果如图所示,程序消耗 CPU 最多的函数为generateOne(),CPU 占比达到 27.07%。
【性能改进思路】

  1. 初始版本generateOne使用递归重试,当生成负数结果时递归调用自身重新生成表达式。当连续多次生成负数表达式时,递归深度持续增加,存在栈溢出崩溃风险。优化方案:将递归重试改为while(true)循环,循环内部生成表达式并校验结果,只有结果非负时才返回表达式,消除栈溢出隐患。
  2. 随机数生成器优化:最初代码每次调用generateOne都重新创建随机数种子,会造成随机序列重复。优化:将随机数生成器mt19937只初始化一次,减少重复创建随机对象带来的性能开销。
  3. 分数约分优化:分数结构体仅在构造函数执行一次最大公约数 gcd 约分,避免每次运算反复约分,减少不必要的计算。
    优化前后对比:优化前递归方案在大量生成题目时存在程序崩溃风险;优化后的循环方案稳定性大幅提升,同时减少了随机对象创建开销,大批量生成题目时运行更稳定。
    4.性能分析图:
    性能分析图
    ·图片内容:CPU 占用函数调用树,generateOne占用最高。
    4、设计实现过程:
    程序采用结构体 + 函数模块化开发,无自定义类,一共分为 5 大模块:
  4. Fraction 分数结构体模块:存储分子、分母;重载加减乘除运算符;gcd最大公约数实现分数自动约分;fracToString()分数转字符串,支持带分数格式输出。
  5. 表达式求值模块
    • tokenize():表达式字符串分词,分割数字、运算符、括号
    • infixToPostfix():中缀表达式转为后缀逆波兰表达式
    • parseFrac():字符串解析为 Fraction 分数对象
    • calcPostfix():后缀表达式栈计算,返回分数结果
    • calcExprStr():入口函数,输入表达式字符串,直接得到计算结果
  6. 题目随机生成模块 generateOne():随机生成操作数(整数 / 分数)、运算符,随机添加括号;循环过滤负数结果,返回表达式字符串和对应答案。generate():循环批量生成 N 道题目,写入Exercises.txt、Answers.txt。
  7. 自动批改模块 grade():读取题目文件、标准答案文件,逐行读取、重新计算题目真值,比对答案,输出对错信息到Grade.txt。
  8. 主函数 main:解析命令行参数,区分生成模式与批改模式;输出帮助信息。
    ·主函数以及generateOne()函数流程图:

流程图1

流程图2
5、代码说明:

include

include

include

include

include

include

include

include

include

include

using namespace std;

//分数结构体,封装分子分母,重载四则运算
struct Fraction {
long long numerator; //分子
long long denominator; //分母

Fraction() : numerator(0), denominator(1) {}
Fraction(long long n, long long d) {
if (d == 0) {
numerator = 0;
denominator = 1;
} else {
long long g = gcd(abs(n), abs(d));
numerator = n / g;
denominator = d / g;
if (denominator < 0) {
numerator *= -1;
denominator = -1;
}
}
}
//欧几里得算法求最大公约数,用于分数约分
long long gcd(long long a, long long b) {
while (b != 0) {
long long tmp = b;
b = a % b;
a = tmp;
}
return a;
}
//重载四则运算符
Fraction operator+(const Fraction& other) const {
return Fraction(numerator * other.denominator + other.numerator * denominator, denominator * other.denominator);
}
Fraction operator-(const Fraction& other) const {
return Fraction(numerator * other.denominator - other.numerator * denominator, denominator * other.denominator);
}
Fraction operator
(const Fraction& other) const {
return Fraction(numerator * other.numerator, denominator * other.denominator);
}
Fraction operator/(const Fraction& other) const {
return Fraction(numerator * other.denominator, denominator * other.numerator);
}
bool operator==(const Fraction& other) const {
return numerator == other.numerator && denominator == other.denominator;
}
};

//分数转为字符串,支持带分数形式输出
string fracToString(const Fraction& f) {
if (f.numerator == 0) return "0";
long long n = f.numerator;
long long d = f.denominator;
if (d == 1) {
return to_string(n);
}
if (abs(n) > d) {
long long integer = n / d;
long long rem = abs(n) % d;
if (rem == 0) return to_string(integer);
if (n < 0) {
return "-" + to_string(abs(integer)) + "'" + to_string(rem) + "/" + to_string(d);
} else {
return to_string(integer) + "'" + to_string(rem) + "/" + to_string(d);
}
} else {
return to_string(n) + "/" + to_string(d);
}
}

//表达式分词:拆分表达式字符串,分割成数字、运算符、括号
vector tokenize(const string& expr) {
vector tokens;
string num;
for (char ch : expr) {
if (ch == ' ') continue;
if (isdigit(ch) || ch == '/') {
num += ch;
} else {
if (!num.empty()) {
tokens.push_back(num);
num.clear();
}
tokens.push_back(string(1, ch));
}
}
if (!num.empty()) tokens.push_back(num);
return tokens;
}

//中缀表达式转后缀逆波兰表达式
vector infixToPostfix(const vector& tokens) {
vector post;
stack st;
for (auto& t : tokens) {
if (t == "+" || t == "-" || t == "*" || t == "/") {
while (!st.empty() && getPriority(st.top()) >= getPriority(t)) {
post.push_back(st.top());
st.pop();
}
st.push(t);
} else if (t == "(") {
st.push(t);
} else if (t == ")") {
while (!st.empty() && st.top() != "(") {
post.push_back(st.top());
st.pop();
}
st.pop();
} else {
post.push_back(t);
}
}
while (!st.empty()) {
post.push_back(st.top());
st.pop();
}
return post;
}

// 获取运算符优先级
int getPriority(const string& op) {
if (op == "+" || op == "-") return 1;
if (op == "*" || op == "/") return 2;
return 0;
}

//字符串解析为分数对象
Fraction parseFrac(const string& s) {
size_t slash = s.find('/');
if (slash == string::npos) {
return Fraction(stoll(s), 1);
} else {
string nStr = s.substr(0, slash);
string dStr = s.substr(slash + 1);
return Fraction(stoll(nStr), stoll(dStr));
}
}

//后缀表达式求值
Fraction calcPostfix(const vector& post) {
stack st;
for (auto& t : post) {
if (t == "+" || t == "-" || t == "" || t == "/") {
Fraction b = st.top(); st.pop();
Fraction a = st.top(); st.pop();
Fraction res;
if (t == "+") res = a + b;
else if (t == "-") res = a - b;
else if (t == "
") res = a * b;
else if (t == "/") res = a / b;
st.push(res);
} else {
st.push(parseFrac(t));
}
}
return st.top();
}

//对外接口:输入表达式字符串,直接计算结果
Fraction calcExprStr(const string& expr) {
auto tokens = tokenize(expr);
auto post = infixToPostfix(tokens);
return calcPostfix(post);
}

//生成单条合法表达式,循环生成直到结果非负,避免递归栈溢出
pair<string, Fraction> generateOne(int r) {
vector ops = {"+", "-", "*", "/"};
random_device rd;
mt19937 gen(rd());

while(true)
{
uniform_int_distribution<> opDist(0, 3);
uniform_int_distribution<> numDist(1, r - 1);
int opCnt = uniform_int_distribution<>(1,3)(gen);
vector exprParts;
for(int i=0;i<=opCnt;i++){
int n = numDist(gen);
int d = numDist(gen);
if(d == 0) d = 1;
if(uniform_int_distribution<>(0,1)(gen)){
exprParts.push_back(to_string(n));
}else{
exprParts.push_back(to_string(n)+"/"+to_string(d));
}
}
string expr;
for(int i=0;i<exprParts.size();i++){
expr += exprParts[i];
if(i != exprParts.size()-1){
string op = ops[opDist(gen)];
expr += op;
}
}
if(opCnt >=2 && uniform_int_distribution<>(0,1)(gen)){
expr = "(" + expr + ")";
}
Fraction ans = calcExprStr(expr);
//过滤负数结果题目
if(ans.numerator >= 0)
{
return make_pair(expr, ans);
}
}
}

//批量生成题目,写入Exercises.txt、Answers.txt
void generate(int n, int r) {
ofstream fExe("Exercises.txt");
ofstream fAns("Answers.txt");
if(!fExe.is_open() || !fAns.is_open()){
cout << "[错误] 文件打开失败!" << endl;
return;
}
cout << "开始生成 " << n << " 道题目..." << endl;
for(int i=1;i<=n;i++){
pair<string, Fraction> ret = generateOne(r);
string expr = ret.first;
Fraction ans = ret.second;
fExe << i << ". " << expr << " =" << endl;
fAns << i << ". " << fracToString(ans) << endl;
}
fExe.close();
fAns.close();
cout << "生成完成!Exercises.txt 和 Answers.txt 已输出" << endl;
}

//批改模块:读取题目文件和答案文件,比对输出Grade.txt
void grade(const string& exePath, const string& ansPath) {
ifstream fExe(exePath);
ifstream fAns(ansPath);
ofstream fGrade("Grade.txt");
if(!fExe.is_open() || !fAns.is_open() || !fGrade.is_open()){
cout << "[错误]批改文件打开失败!" << endl;
return;
}
string lineExe, lineAns;
int correct = 0, total = 0;
while(getline(fExe, lineExe) && getline(fAns, lineAns)){
total++;
size_t dotPos = lineExe.find(". ");
string expr = lineExe.substr(dotPos+2);
expr.pop_back();
Fraction stdAns = calcExprStr(expr);

size_t dotAns = lineAns.find(". ");
string ansStr = lineAns.substr(dotAns+2);
Fraction userAns = parseFrac(ansStr);

if(stdAns == userAns){
correct++;
fGrade << total << ". Correct" << endl;
}else{
fGrade << total << ". Wrong. Right answer: " << fracToString(stdAns) << endl;
}
}
fGrade << "Total: " << total << ", Correct: " << correct << endl;
fExe.close();
fAns.close();
fGrade.close();
cout << "批改完成!Grade.txt 已输出" << endl;
}

int main(int argc, char* argv[]) {
if (argc >=5 && string(argv[1]) == "-n" && string(argv[3]) == "-r") {
int n = stoi(argv[2]);
int r = stoi(argv[4]);
generate(n, r);
} else if (argc >=5 && string(argv[1]) == "-e" && string(argv[3]) == "-a") {
grade(argv[2], argv[4]);
} else {
cout << "==== 使用说明 ====" << endl;
cout << "生成题目: 四则计算器.exe -n 10 -r 10" << endl;
cout << "批改答案: 四则计算器.exe -e Exercises.txt -a Answers.txt" << endl;
cout << "-n:题目数量,-r:数值上限" << endl;
}
cout << "\n按回车键退出...";
cin.get();
return 0;
}
6、测试运行:
·测试环境
Windows10,Visual Studio 2022,C++11 标准。
·运行方式:CMD 命令行调用 exe,传入命令参数。

表格
·正确性说明

  1. 分数使用分子分母精确存储,没有浮点数精度丢失问题,不会出现 0.1+0.2!=0.3 这类浮点错误。
  2. 中缀转后缀算法保证运算符优先级、括号逻辑正确。
  3. 题目生成阶段循环过滤结果为负数的表达式,符合小学四则运算需求。
  4. 文件读写增加文件打开判断,遇到路径错误时给出提示。
  5. 批改模块重新对原题表达式求值,和标准答案比对,实现自动判分。
    ·CMD运行结果:

CMD运行结果

7、项目小结:
本次结对编程完成 C++ 四则运算自动生成与批改程序。开发过程遇到了多个典型问题:最开始argc参数判断错误导致程序无法进入生成逻辑;递归生成表达式造成栈溢出;VS 调试工作目录问题,txt 文件找不到;分数约分、表达式优先级等细节 bug。通过 PSP 预估时间,发现预估时间和实际耗时差距较大,大量时间消耗在调试 bug 上,尤其是测试环节。本次项目掌握了逆波兰表达式求值、分数精确运算、命令行参数解析、文件 IO、性能调优的知识。

posted @ 2026-09-22 20:12  lazyyyyyy  阅读(7)  评论(0)    收藏  举报