实验一 现代C++编程初体验
实验任务1 标准库反转与旋转
程序源代码 task1.cpp
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
template<typename T>
void output(const T &c) {
for (const auto &item : c) std::cout << item << ' ';
std::cout << '\n';
}
void test1() {
using namespace std;
string s0{"0123456789"};
cout << "s0 = " << s0 << '\n';
string s1(s0);
reverse(s1.begin(), s1.end());
cout << "s1 = " << s1 << '\n';
string s2(s0.size(), ' ');
reverse_copy(s0.begin(), s0.end(), s2.begin());
cout << "s2 = " << s2 << '\n';
}
void test2() {
using namespace std;
vector<int> v0{2, 0, 4, 9};
cout << "v0: "; output(v0);
vector<int> v1{v0};
reverse(v1.begin(), v1.end());
cout << "v1: "; output(v1);
vector<int> v2{v0};
reverse_copy(v0.begin(), v0.end(), v2.begin());
cout << "v2: "; output(v2);
}
void test3() {
using namespace std;
vector<int> v0{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
cout << "v0: "; output(v0);
vector<int> v1{v0};
rotate(v1.begin(), v1.begin() + 1, v1.end());
cout << "v1: "; output(v1);
vector<int> v2{v0};
rotate(v2.begin(), v2.begin() + 2, v2.end());
cout << "v2: "; output(v2);
vector<int> v3{v0};
rotate(v3.begin(), v3.end() - 1, v3.end());
cout << "v3: "; output(v3);
vector<int> v4{v0};
rotate(v4.begin(), v4.end() - 2, v4.end());
cout << "v4: "; output(v4);
}
int main() {
std::cout << "测试1:\n"; test1();
std::cout << "\n测试2:\n"; test2();
std::cout << "\n测试3:\n"; test3();
return 0;
}
实际运行记录

观察与思考
1. reverse 和 reverse_copy 有什么区别?
我的理解是,两个函数都能实现元素顺序的反转,但保存结果的位置不同。reverse 直接修改原来的字符串或数组;reverse_copy 则把反转后的内容放到另一个位置,原来的内容不变。
例如,原字符串是 "12345",使用 reverse 后,原字符串变成 "54321";使用 reverse_copy 后,原字符串还是 "12345",目标字符串变成 "54321"。使用后者时,要提前给目标字符串准备足够的空间。
2. rotate 算法是如何改变元素顺序的?它的三个参数分别代表什么?
rotate 实现的是循环移动。可以把序列从指定位置分成前后两部分,再把后半部分放到前面,前半部分放到后面,各部分内部的顺序不变。
三个参数中,第一个表示操作的起始位置,第二个指定哪个位置的元素移动后成为第一个元素,第三个表示操作区间最后一个元素的后面一个位置,这个位置本身不参与操作。
例如,对 0 1 2 3 4 使用 rotate(v.begin(), v.begin() + 2, v.end()),结果是 2 3 4 0 1,相当于循环左移两位。
实验任务2 随机数 排序与统计
程序源代码 task2.cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <iomanip>
#include <cstdlib>
#include <ctime>
template<typename T>
void output(const T &c) {
for (const auto &item : c) std::cout << item << ' ';
std::cout << '\n';
}
int generate_random_number() {
return std::rand() % 101;
}
void test1() {
using namespace std;
vector<int> v0(10);
generate(v0.begin(), v0.end(), generate_random_number);
cout << "v0: "; output(v0);
vector<int> v1{v0};
sort(v1.begin(), v1.end());
cout << "v1: "; output(v1);
vector<int> v2{v0};
sort(v2.begin() + 1, v2.end() - 1);
cout << "v2: "; output(v2);
}
void test2() {
using namespace std;
vector<int> v0(10);
generate(v0.begin(), v0.end(), generate_random_number);
cout << "v0: "; output(v0);
auto min_iter = min_element(v0.begin(), v0.end());
auto max_iter = max_element(v0.begin(), v0.end());
cout << "最小值: " << *min_iter << '\n';
cout << "最大值: " << *max_iter << '\n';
auto ans = minmax_element(v0.begin(), v0.end());
cout << "最小值: " << *ans.first << '\n';
cout << "最大值: " << *ans.second << '\n';
double avg1 = accumulate(v0.begin(), v0.end(), 0.0) / v0.size();
cout << "均值: " << fixed << setprecision(2) << avg1 << '\n';
sort(v0.begin(), v0.end());
double avg2 = accumulate(v0.begin() + 1, v0.end() - 1, 0.0)
/ (v0.size() - 2);
cout << "去掉最大值、最小值之后,均值: " << avg2 << '\n';
}
int main() {
std::srand(static_cast<unsigned>(std::time(nullptr)));
std::cout << "测试1:\n"; test1();
std::cout << "\n测试2:\n"; test2();
return 0;
}
实际运行记录

观察与思考
1. generate 算法的作用是什么?
generate 的作用是按照指定的生成规则,给一个区间内的元素依次赋值。它每处理一个位置,就调用一次生成函数,并把返回值放到这个位置。
本题中,vector 已经有10个元素,所以会调用10次随机数生成函数,给这10个元素分别赋值。它不会自动增加容器中的元素数量。
2. minmax_element 和分别调用 min_element、max_element 相比,有什么优势?
min_element 用来找最小值的位置,max_element 用来找最大值的位置,而 minmax_element 一次调用就能同时得到这两个位置,写起来更简洁,也能减少比较次数。
返回结果中的 first 表示最小值的位置,second 表示最大值的位置,前面加上 * 才能取出对应的数值。我理解它比较适合同时需要最大值和最小值的情况。
3. 改成 lambda 表达式后,效果是否等同?如何理解这种写法?
改成 [](){ return std::rand() % 101; } 后,功能是等同的,仍然是每次生成一个0~100范围内的整数。
我把 lambda 表达式理解成一个可以直接写在使用位置的“没有名字的小函数”。这里的 [] 表示不捕获外部局部变量,() 表示不接收参数,花括号里写的是函数执行的内容。
对于这种代码很短、只在这里使用的功能,lambda 可以省去单独声明和定义函数的步骤。如果函数比较复杂,或者需要在多个地方使用,我觉得单独定义函数会更清楚。另外,两次运行产生的随机数可能不同,这并不说明两种写法的功能不同。
实验任务3 字符串变换
程序源代码 task3.cpp
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
unsigned char func(unsigned char c) {
if (c == 'z') return 'a';
if (c == 'Z') return 'A';
if (std::isalpha(c)) return static_cast<unsigned char>(c + 1);
return c;
}
void test1() {
std::string s1{"Hello World 2049!"};
std::cout << "s1 = " << s1 << '\n';
std::string s2;
for (unsigned char c : s1)
s2 += static_cast<char>(std::tolower(c));
std::cout << "s2 = " << s2 << '\n';
std::string s3;
for (unsigned char c : s1)
s3 += static_cast<char>(std::toupper(c));
std::cout << "s3 = " << s3 << '\n';
}
void test2() {
std::string s1{"I love cosmos!"};
std::cout << "s1 = " << s1 << '\n';
std::string s2(s1.size(), ' ');
std::transform(s1.begin(), s1.end(), s2.begin(), func);
std::cout << "s2 = " << s2 << '\n';
}
int main() {
std::cout << "测试1: 字符串大小写转换\n"; test1();
std::cout << "\n测试2: 字符变换\n"; test2();
return 0;
}
实际运行记录

观察与思考
1. 自定义函数 func 的功能是什么?
func 的功能是把英文字母替换成字母表中的下一个字母,并保留原来的大小写。例如,a 变成 b,A 变成 B。
对于最后一个字母,需要单独处理:z 变成 a,Z 变成 A。数字、空格和标点符号保持不变。因此,"I love cosmos!" 经过处理后会变成 "J mpwf dptnpt!"。
2. tolower 和 toupper 的功能分别是什么?
tolower 用来把大写字母转换成对应的小写字母,toupper 用来把小写字母转换成对应的大写字母。数字、空格和标点符号等不需要进行大小写转换的字符保持不变。
这两个函数一次处理一个字符,所以本题通过循环,依次处理字符串中的每个字符,完成整个字符串的大小写转换。
3. transform 的4个参数分别表示什么?把第三个参数 s2.begin() 改成 s1.begin() 有什么区别?
结合本题,四个参数分别表示:
s1.begin():从原字符串的第一个字符开始处理。s1.end():处理到原字符串末尾,所指的尾后位置不参与处理。s2.begin():从目标字符串的第一个位置开始保存结果。func:规定每个字符按照什么规则进行转换。
第三个参数是 s2.begin() 时,转换结果保存在 s2 中,s1 保持不变。改成 s1.begin() 后,结果就直接写回 s1,原字符串本身会被修改。
这时 s2 仍然是初始化时的空格,所以如果想查看转换结果,也要把输出对象改成 s1。
实验任务4 回文串判断
程序源代码 task4.cpp
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
bool is_palindrome(const std::string &s);
bool is_palindrome_ignore_case(const std::string &s);
int main() {
std::string s;
// 将布尔值输出为 true 或 false。
std::cout << std::boolalpha;
// 判断5组字符串和1组单字母,共6组,处理完后自动结束。
for (int i = 0; i < 6; ++i) {
if (!(std::cin >> s)) {
return 0;
}
std::cout << "区分大小写: "
<< is_palindrome(s) << '\n';
std::cout << "不区分大小写: "
<< is_palindrome_ignore_case(s) << "\n\n";
}
return 0;
}
// 区分大小写:将副本反转,再与原字符串比较。
bool is_palindrome(const std::string &s) {
std::string temp = s;
std::reverse(temp.begin(), temp.end());
return temp == s;
}
// 不区分大小写:先统一转成小写,再判断回文。
bool is_palindrome_ignore_case(const std::string &s) {
std::string temp = s;
for (char &c : temp) {
c = static_cast<char>(
std::tolower(static_cast<unsigned char>(c))
);
}
return is_palindrome(temp);
}
实际运行记录

观察与思考
如果希望测试的字符串包含空格,代码应该如何调整?
原来的 cin >> s 遇到空格就会结束本次字符串读取。例如输入 "hello oop",第一次只能读到 "hello",不能把整行作为一个字符串处理。
可以把 while (std::cin >> s) 改成 while (std::getline(std::cin, s))。这样每次会读取完整的一行,字符串中间的空格也能保留下来,同时仍然支持多组输入。
需要注意,读取进来的空格也会参与回文判断,程序不会自动忽略它们。如果前面还使用过 cin >> 读取其他数据,需要先处理输入中残留的换行符,再使用 getline。
实验任务5 进制转换
程序源代码 task5.cpp
#include <iostream>
#include <string>
#include <algorithm>
std::string dec2n(int x, int n = 2);
int main() {
int x;
// 输入3个非负整数,分别输出进制转换结果。
for (int i = 0; i < 3; ++i) {
if (!(std::cin >> x)) {
return 0;
}
if (x < 0) {
std::cout << "请输入非负整数。\n";
--i;
continue;
}
std::cout << "十进制: " << x << '\n'
<< "二进制: " << dec2n(x) << '\n'
<< "八进制: " << dec2n(x, 8) << '\n'
<< "十二进制: " << dec2n(x, 12) << '\n'
<< "十六进制: " << dec2n(x, 16) << '\n'
<< "三十二进制: " << dec2n(x, 32) << "\n\n";
}
// 3组结果输出完后,输入一个字母(例如q)并回车结束。
char end;
std::cin >> end;
return 0;
}
std::string dec2n(int x, int n) {
if (x < 0 || n < 2 || n > 36) {
return "参数错误";
}
if (x == 0) {
return "0";
}
const std::string digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::string result;
// 每次取出的余数按从低位到高位的顺序保存。
while (x > 0) {
result += digits[x % n];
x /= n;
}
// 反转后得到正常的高位在前的表示。
std::reverse(result.begin(), result.end());
return result;
}
实际运行记录

实验任务6 字母密文对照表
程序源代码 task6.cpp
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
// 输出表头:小写字母 a~z
cout << " ";
for (int j = 0; j < 26; ++j) {
cout << char('a' + j) << ' ';
}
cout << '\n';
// 输出第 1~26 行
for (int i = 1; i <= 26; ++i) {
cout << setw(2) << i << ' ';
for (int j = 0; j < 26; ++j) {
cout << char('A' + (j + i) % 26) << ' ';
}
cout << '\n';
}
return 0;
}
实际运行记录

实验任务7 算术题生成与自动评测
程序源代码 task7.cpp
#include <iostream>
#include <iomanip>
#include <cstdlib>
#include <ctime>
#include <algorithm>
#include <limits>
int main() {
std::srand(static_cast<unsigned>(std::time(nullptr)));
const int total = 10;
const char symbols[] = {'+', '-', '*', '/'};
int correct = 0;
for (int i = 1; i <= total; ++i) {
int a = std::rand() % 10 + 1;
int b = std::rand() % 10 + 1;
int op = std::rand() % 4;
int expected = 0;
if (op == 0) {
expected = a + b;
} else if (op == 1) {
if (a < b) std::swap(a, b);
expected = a - b;
} else if (op == 2) {
expected = a * b;
} else {
// 两个数始终在[1, 10]内,重新抽取直到能够整除。
while (a % b != 0) {
a = std::rand() % 10 + 1;
b = std::rand() % 10 + 1;
}
expected = a / b;
}
std::cout << "第" << i << "题: " << a << ' '
<< symbols[op] << ' ' << b << " = ";
int answer;
while (!(std::cin >> answer)) {
if (std::cin.eof()) return 0;
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "请输入整数答案: ";
}
if (answer == expected) ++correct;
}
std::cout << "答对 " << correct << " 道,共 " << total << " 道。\n"
<< "正确率: " << std::fixed << std::setprecision(2)
<< 100.0 * correct / total << "%\n";
return 0;
}
实际运行记录

实验总结
本实验的几个算法都通过迭代器区间操作数据,区间通常采用左闭右开形式 [first, last)。把数据存储交给 string 和 vector,把反转、排序、变换等操作交给标准库,可以让程序更直接地表达目的。使用前仍应明确算法是否修改原数据、目标空间是否足够,以及输入是否满足前提。
回文判断可以通过“复制、变换、比较”组合实现;进制转换需要注意余数的产生顺序和零值情况;随机算术题则需要分别处理减法和除法的约束。平均值和正确率应确保使用浮点运算,多组输入应明确结束方式。
浙公网安备 33010602011771号