2025年杭州市中小学生科技界·计算机编程活动 C++ Python 编程代码模块挑战赛 高中组 题目 1.分数(回忆版)

作者回忆,大多数可能不准确

题目描述

N个分数,需要以p/q的格式输出N个分数之和的最简格式(如果结果为整数你需要输出p/1)
解释:假设n=3, 将会输入3个p,q,例如p=1, 2, 3 q=3, 2, 1(按顺序对应p/q) 计算的结果输出应当为13/3

输入格式

第一行为整数n
接下来n行,每行两个整数pq表示分数p/q(以空格间隔)

输出格式

1行, 为p/q代表计算结果

输入输出样例

样例#1

输入

1
2
3
4
3
1 3
2 2
3 1

输出

1
13/3

说明/提示

对于100%的数据, $2\le n\le10$, $1\le p,q\le10^{9}$


作者提供的题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#define MAX_N 10
#define int long long

using namespace std;

int n, nums[MAX_N][2];

int inline gcd(int a, int b) {
while (b) swap(a %= b, b);
return a;
}

int inline lcm(int a, int b) {
return a / gcd(a, b) * b;
}
signed main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for(int i = 0; i < n; i++)
cin >> nums[i][0] >> nums[i][1];
int q = nums[0][1];
for (int i = 0; i < n; i++) {
q = lcm(q, nums[i][1]);
}
int p = 0;
for (int i = 0; i < n; i++) {
p += nums[i][0] * (q / nums[i][1]);
}
if (p == q) {
cout << "1/1\n";
return 0;
}
int factory = gcd(p, q);
p /= factory;
q /= factory;
cout << p << "/" << q << "\n";
}

C++中的"volatile"关键字

1.”volatile”的语义

1
volatile int flag = 0;
  • 表示flag可能被硬件、中断、多线程或其他外部因素修改
  • 编译器不能假设其值在两次访问之间保持不变
  • 禁止优化:如删除“看似无用”的读取、合并多次读取、将变量放入寄存器等

2.从汇编角度看

样例代码Code1: (无volatile)
1
2
3
4
5
int flag = 1;
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += flag; // 编译器可能优化为 sum += 1
}

可能生成的汇编代码

1
movl   $1000, %eax    ; sum = 1000 * 1

完全跳过循环和内存访问,因为编译器认为flag不会变。

样例代码Code2: (有volatile)
1
2
3
4
5
volatile int flag = 1;
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += flag; // 必须每次从内存读取 flag
}

可能生成的汇编代码

1
2
3
4
5
6
7
8
9
10
movl   $0, %ebx          ; sum = 0
movl $0, %ecx ; i = 0
.L1:
cmpl $1000, %ecx
jge .L2
movl flag(%rip), %eax ; ← 每次都从内存读取 flag!
addl %eax, %ebx ; sum += flag
incl %ecx
jmp .L1
.L2:
  • 每次循环都执行movl flag(%rip), %eax从内存加载flag
  • 即使flag在循环中未被本程序修改,编译器也不敢假设其值不变

本文章中的汇编不确保可以运行,是”伪代码”

3.样例代码实际输出的汇编码

Compile on Archlinux WSL

样例代码CodeA: (无volatile)
1
2
3
4
5
6
7
8
int main(){
int flag = 1;
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += flag;
}
return 0;
}

汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	.file	"codeA.cpp"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1, -4(%rbp)
movl $0, -12(%rbp)
movl $0, -8(%rbp)
jmp .L2
.L3:
movl -4(%rbp), %eax
addl %eax, -12(%rbp)
addl $1, -8(%rbp)
.L2:
cmpl $999, -8(%rbp)
jle .L3
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 15.2.1 20250813"
.section .note.GNU-stack,"",@progbits
样例代码CodeB: (有volatile)
1
2
3
4
5
6
7
8
int main(){
volatile int flag = 1;
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += flag;
}
return 0;
}

汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	.file	"codeB.cpp"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $1, -12(%rbp)
movl $0, -8(%rbp)
movl $0, -4(%rbp)
jmp .L2
.L3:
movl -12(%rbp), %eax
addl %eax, -8(%rbp)
addl $1, -4(%rbp)
.L2:
cmpl $999, -4(%rbp)
jle .L3
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 15.2.1 20250813"
.section .note.GNU-stack,"",@progbits

为什么没有区别?

因为没有使用编译器优化,O0模式

使用O2

CodeA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	.file	"codeA.cpp"
.text
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 15.2.1 20250813"
.section .note.GNU-stack,"",@progbits

CodeB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	.file	"codeB.cpp"
.text
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
movl $1, -4(%rsp)
movl $1000, %eax
.p2align 4
.p2align 4
.p2align 3
.L2:
movl -4(%rsp), %edx
movl -4(%rsp), %edx
subl $2, %eax
jne .L2
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 15.2.1 20250813"
.section .note.GNU-stack,"",@progbits

4.从汇编看

从开启O2的汇编里看,CodeA由于没有使用”volatile”关键字,for循环直接被优化了

1
2
3
main:
xorl %eax, %eax
ret
为什么?
  • flag是编译期常量(值为 1)
  • sum未被使用(无return sum或输出)
  • 编译器判定循环无副作用 -> Dead Code Elimination
    而CodeB中
1
2
3
4
5
6
7
8
9
10
main:
movl $1, -4(%rsp) # flag = 1 存入栈
movl $1000, %eax # 计数器初始化
.L2:
movl -4(%rsp), %edx # <- 强制从内存读取 flag
movl -4(%rsp), %edx # <- 再次读取(可能因对齐或语义保留)
subl $2, %eax
jne .L2
xorl %eax, %eax
ret
  • 循环被保留,尽管flag值不变
  • 每次循环都执行movl -4(%rsp), %edx
  • 即使%edx未被使用,编译器也不敢删除
  • 因为volatile表示“每次访问都有潜在副作用”
  • 循环步长为 2(subl $2, %eax)可能是编译器尝试合并两次加法(sum += flag + flag),但因”volatile”仍需两次独立读取

5.写在最后

  • “volatile”不是运行时特性,而是对编译器优化器的约束。
    -O0(无优化)下,普通变量与”volatile”行为相似(都访问内存),差异不明显
  • -O2等优化级别下,”volatile” 强制保留所有内存访问,确保程序行为符合预期