把一整组同类型的数据放在一起,学会用下标定位、用循环处理。
成绩有 5 个、50 个、500 个时,程序不应该靠增加变量名来扩容。
int score1 = 82, score2 = 95; int score3 = 76, score4 = 88; int score5 = 91;
求和还能写出来;如果要读取第 k 个分数呢?
数组把同类型的数据组织在一起。score[2] 就能定位到第三个元素。
int score[5]; double price[10]; char letter[26];
intscore[5]本课固定长度数组用编译期确定的容量;不依赖 int a[n] 这种编译器扩展。
容量是 5,合法下标是 0~4。score[5] 已经在数组外面。
int a[5] = {10, 20, 30, 40, 50}; a[2] = 99; cout << a[0] << " " << a[2];
10 99
下面的输出是什么?
int a[4] = {4, 7, 2, 9}; int i = 1; cout << a[i + 1];
i+1 为 2,访问 a[2],对应第三个格子。
“有空间”与“里面有可用的数据”是两件事。
int a[4] = {1,2,3,4};int b[] = {5,6,7};int c[5] = {8,9};int d[5] = {};初始化发生在定义时;数组定义后,不能用 a={1,2,3} 给普通数组整体赋值。
int a[6] = {4, 2};这不是“只有前两个格子存在”:容量仍为 6,每个格子都有确定初值。
int count[10] = {}; // 然后再做 count[x]++
计数、累加等操作,需要先给出可靠初值。
int count[10]; // 不要直接 count[3]++
未初始化的局部整型元素不能直接读取;不能指望得到某个固定“随机数”。
定义时准备了 8 个格子。
只读入了 3 个数,后续只处理 a[0] 到 a[2]。
即使未用格子被初始化为 0,也不表示它们自动成为题目数据。
先确定“走哪些下标”,再决定“到每个格子做什么”。
int n; cin >> n; int a[100] = {}; for (int i=0; i<n; i++) cin >> a[i];
先读 n,再读 n 个元素。循环条件 i<n,不能写成 i<=n。
a[0]a[1]a[2]a[3]for (int i=0; i<n; i++) cout << a[i] << " ";
for (int i=n-1; i>=0; i--) cout << a[i] << " ";
本例 i 用有符号 int;n=0 时从 -1 开始,不执行循环。
1 6 3 8
long long sum = 0; for (int i=0; i<n; i++) sum += a[i]; cout << sum;
int best = a[0]; for (int i=1; i<n; i++) { if (a[i] > best) best = a[i]; }
用 0 当最大值会得到一个根本不在数组里的答案;先用 a[0],需要 n≥1。
int pos = -1; for (int i=0; i<n; i++) { if (a[i] == target) { pos = i; break; } }
找到的值是 6,记录的下标是 2。
没找到时 pos 保持 -1;不能拿 -1 去访问数组。
int a[4] = {3, 1, 4, 2}; int b[4] = {}; for (int i=0; i<4; i++) b[i] = a[i];
b=a 不能给普通数组整体赋值;相同类型的单个元素可以正常赋值。
a 的容量为 5,n=5。下面的循环有什么问题?
for (int i=0; i<=n; i++) cout << a[i];
合法下标 0~4;改成 i<n。越界是未定义行为,不保证立刻报错。
题目最多多少个元素?数组是否装得下?
被读取的格子,是否已经读入或初始化?
0 开始还是 1 开始?循环端点是否对应?
求的是值、下标、出现次数,还是整组数据?
把字符装进数组,理解 C 风格字符串为什么需要一个结束标记。
格子里存放 char;当作 C 风格字符串使用时,还需要约定在哪里结束。
char c = 'A'; cout << c;
char s[] = "Cat"; cout << s;
空字符 \0 的值是 0;它与数字字符 '0' 不是同一个字符。
char a[4] = "Cat"; char b[] = "Cat"; char c[4] = {'C', 'a', 't', '\0'};
char d[3]={'C','a','t'} 是合法字符数组,但不能直接把它当作以零终止的 C 字符串输出。
'\0''0'' ''\n'下面两项分别是多少?
char s[8] = "code"; // sizeof(s) 和 strlen(s)
strlen 扫描到 \0 为止;sizeof(s) 此处得到整个数组的字节数。
读取单个词与读取整行,不是同一项需求。
char s[20];
cin >> setw(20) >> s;
cout << s;需要 <iomanip>;最多保存 19 个输入字符,再留一个 \0。
hello
后面的 world 仍留在输入流中;不会自动接进同一个字符串。
char s[20]; if (cin.getline(s, 20)) cout << s; else cout << "too long";
hello world
读取到换行后结束,换行被取走。容量不足会设置失败状态,不能当作完整读入。
结束符是实际占用空间的字符,不能只按屏幕上看得到的字符来开数组。
for (int i=0; s[i]!='\0'; i++) cout << s[i] << '\n';
C a t
int count = 0; for (int i=0; s[i]!='\0'; i++) { if (s[i] == 'a') count++; } cout << count;
a 出现 3 次;只统计内容,不把结束符计入。
先保证字符串有效和容量足够,再调用 <cstring> 中的函数。
char s[20] = "hello"; cout << strlen(s);
5
char src[] = "cat"; char dst[10]; strcpy(dst, src);
参数顺序是 目标、来源;目标容量至少为 strlen(src)+1,来源与目标不能重叠。
strcpy(b,a);strcmp(a,b)==0strlen(a)strcmp("ant","cat")<0strcmp("cat","cat")==0strcmp("dog","cat")>0标准保证的是负数、零、正数,不保证不同平台都返回 -1 或 1。
本课按 ASCII 兼容字符集理解大小写和数字字符;这不是中文拼音排序规则。
char s[12] = "red"; strcat(s, "blue");
redblue
容量条件:strlen(s)+strlen(追加内容)+1≤目标容量;两个区域不重叠。
char a[20]="red", b[20]="blue"; char temp[20]; strcpy(temp,a); strcpy(a,b); strcpy(b,temp);
blue red
int len = static_cast<int>(strlen(s)); for (int i=len-1; i>=0; i--) cout << s[i];
这段程序的目标容量是否足够?
char s[6] = "cat"; // 想追加 "dog"
不能调用 strcat 去试越界;应先增加容量,或改用下一课的 string。
for(int i=0;s[i]!='\0';i++) { if(s[i]>='a' && s[i]<='z') s[i]=s[i]-'a'+'A'; }
HELLO, J2!
减去 'a' 得到字母序号,再加上 'A' 得到对应大写字母;不要不加判断就修改所有字符。
ASCII 兼容的英文字母、数字、标点和空格。
一个汉字通常占多个字节;逐字节大小写转换或倒序,不能直接当作“逐汉字”处理。
先明确输入字符范围,再选择处理方法。
用 std::string 管理文本,把“要做什么”写得更直接。
字符数组是固定容量的底层工具;string 提供更直接的文本操作。
string a; // 空串 string b = "hello"; string c = b; string d(4, '*'); // "****"
需要 #include <string>。string 管理自己的存储,增长仍受可用内存等资源限制。
strlen(s)s.size() / s.length()strcpy(dst,src)dst=srcstrcat(dst,src)dst+=srcstrcmp(a,b) 看正负a==b、a<bcin.getline(s,容量)getline(cin,s)string s; cin >> s; cout << s;
hello
第二个词仍在输入流里。
string s;
getline(cin, s);
cout << s.size() << "\n" << s;8 red blue
换行被取走但不保存;中间和开头的空格都属于这一行。
int n; cin >> n; cin.ignore( numeric_limits<streamsize>::max(), '\n'); string line; getline(cin, line);
hello world
需要 <limits>。ignore 丢弃当前行剩余部分;std::ws 还会吃掉后面的前导空白,含义不同。
string 能改变长度,不代表下标访问可以省略边界判断。
cout << s[0] << " " << s[4]; s[0] = 'H'; cout << "\n" << s;
h o Hello
本课只读写范围内的实际字符;不要用 s[s.size()] 当作最后一个字符。
string s = "a b"; cout << s.size() << " " << s.length();
3 3
对本课 ASCII 文本可直接按字符数理解;UTF-8 中文的 size() 通常是字节数。
for (size_t i=0; i<s.size(); i++) { cout << s[i] << " "; }
size_t 是 size() 常用的无符号长度类型。正序边界直接写 i<s.size()。
for (size_t i=0; i<s.size(); i++) { if (s[i] == ' ') s[i] = '_'; }
a_b_c
if (!s.empty()) { cout << s[0]; } else { cout << "empty"; }
同样是 a=b,string 对象与普通字符数组的规则不同。
string a = "red"; string b = a; a = "blue"; cout << a << " " << b;
blue red
b 保存了复制得到的内容;之后改 a,不会把 b 一起改成 blue。
string a="red", b="blue"; string c = a + "-" + b; cout << a << " " << c;
red red-blue
string a="red"; a += "blue"; a += '!'; cout << a;
redblue!
string("ab")+"cd"s+"cd""ab"+"cd""ab" "cd"cout << (string("cat") == "cat"); cout << (string("cat") < "cats"); cout << (string("10") < "2");
111
比较从左到右看第一个不同字符;不是先比长度,也不是把文本自动转为整数。
下面哪个关系成立?
string a="9", b="10";
第一个字符 '9' 大于 '1';数字字符串若按数值比较,需要先明确数值转换。
找不到时有专门的标记,不能把它当作合法下标。
size_t pos = s.find("ana"); if (pos != string::npos) cout << pos; else cout << "not found";
1
if (s.find("cat")) { // 不能这样判断是否找到 }
位置 0 转成 false,反而漏掉开头匹配。
if (s.find("cat") != string::npos) { cout << "found"; }
保留 size_t / auto 返回位置,避免随意转成 int 再用 -1 猜结果。
size_t first = s.find("ana"); if (first != string::npos) { size_t next = s.find("ana", first+1); // 再比较 next 与 string::npos }
从 first+1 开始允许重叠匹配;跳过整个已匹配片段是另一种统计约定。
输入里有没有空格?空行是否有效?
比较字符串,用内容运算;访问字符,先验证位置。
本课示例按 ASCII 文本;不要把字节操作直接套到任意中文。
把重复动作封装起来,用参数表达变化,让程序按清楚的步骤工作。
重复的代码容易漏改;把同一个动作写在一处,需要时再调用。
cout << "======\n"; cout << "START\n"; cout << "======\n";
line();
cout << "START\n";
line();line 是动作的名字;line() 是执行这个动作。
sqrt(9.0)<cmath>strlen(s)<cstring>getline(cin,s)<string>标准库已经提供一些工具;自己的重复动作,也可以定义为函数。
void line() { cout << "======\n"; }
voidline(){ ... }printLine showTitle countLetters
大小写不同就是不同的名字;避免用含义不明的 f1、f2。
void line() { cout << "======\n"; } int main() { line(); line(); return 0; }
====== ======
调用语句要有括号和分号:line();。函数体末尾的 } 后不需要分号。
cout << "A\n"; line(); cout << "B\n";
void line() { cout << "======\n"; } // 后面写 main,调用 line()
void line(); // 声明有分号 int main() { line(); } void line() { cout << "======\n"; }
声明告诉编译器接口;函数体通常只定义一次。初学时优先使用“定义写在 main 前”。
void line() { /* ... */ } int main() { line(); return 0; }
int main() { // 此处不能再定义 line 函数 // void line() { ... } }
函数体里可以调用已经声明的函数。
输出 3 个星号和输出 8 个星号,变化的是数量,不需要写两套函数。
void stars(int count) { for (int i=0; i<count; i++) cout << "*"; cout << "\n"; }
count 是函数执行这一次动作时使用的数量;下次调用可以传另一个值。
void stars(int count)stars(3)stars(n)参数类型写在定义或声明里;调用时传具体的值或表达式,不再写 int。
int n = 2;
stars(3);
stars(n);
stars(n+2);*** ** ****
void change(int x) { x = 99; cout << x << " "; } // main 中: int a = 7; change(a); cout << a;
99 7
调用结束后,main 中的 n 是多少?
void addOne(int x) { x++; } // main 中: int n=5; addOne(n); cout << n;
本课的 int 形参按值传递。x 是副本;修改 x 不会自动修改 n。
void repeat(char c, int count) { for (int i=0; i<count; i++) cout << c; cout << "\n"; } // main 中: repeat('#', 4);
####
repeat('*', 5); repeat('A', 3);
先字符,后次数;与函数定义一致。
// repeat(5, '*'); // repeat('*'); // repeat('*', 3, 2);
顺序颠倒可能发生类型转换而编译通过;参数缺少或多余通常会编译报错。
主程序负责安排步骤,函数负责完成一件清楚的事。
void repeat(char c, int count) { for(int i=0;i<count;i++) cout<<c; cout<<'\n'; }
repeat('=', 8); cout << "J2\n"; repeat('-', 8);
======== J2 --------
void printSum(int n) { int sum = 0; for (int i=1; i<=n; i++) sum += i; cout << sum << "\n"; }
这里的函数直接输出。下一课再把“算出结果”和“怎么使用结果”分开。
void printCount(string s, char target) { int count = 0; for (size_t i=0; i<s.size(); i++) if (s[i] == target) count++; cout << count; }
本课按值传递 string,函数有自己的字符串对象;本例字符串长度不超过 1000。
例如:输出 count 个字符 c,然后换行。
字符和数量由参数交进来,不藏在函数体的固定数字里。
count=0 是否只输出换行?调用者是否保证范围合法?
让函数把结果交回来,再用作用域管理每一份变量。
先计算一个值,再决定把它输出、保存,还是用于下一次计算。
void showSum(int a, int b) { cout << a+b; }
调用后屏幕出现文字;调用表达式没有结果值。
int add(int a, int b) { return a+b; }
返回一个 int;调用者决定怎么使用它。
int add(int a, int b) { return a+b; } // main 中: int answer = add(3, 5);
可以把 add(3,5) 当成一个值为 8 的表达式。
cout << add(3,5);int s=add(3,5);int twice=2*add(3,5);if (add(3,5)>6) { ... }有返回值的函数可以多次复用;只写 add(3,5); 会丢弃结果,不会自动输出。
return (a+b)/2.0;return x%2==0;double average(int a, int b) { return (a+b)/2; }
a=3、b=4 时,先算整数 7/2,结果是 3。
double average(int a, int b) { return (a+b)/2.0; }
3.5
本例 a、b 在 ±1000 内,a+b 不会溢出。
int add(int a, int b) { return a+b; } cout << add(add(1,2),3);
本例各次调用只计算结果,没有依赖调用先后顺序的副作用。
不论 return 写在函数中间还是末尾,执行到它都会返回调用处。
int twice(int x) { return x*2; cout << "after"; } cout << twice(4);
8
不会输出 after。实际代码应删除永远到不了的语句。
int absolute(int x) { if (x<0) return -x; return x; }
int sign(int x) { if (x>0) return 1; // x<=0 时走到了末尾 }
一般非 void 函数到达末尾却不返回结果,会导致未定义行为。
int sign(int x) { if (x>0) return 1; if (x<0) return -1; return 0; }
void show(int n) { if (n<0) { cout << "invalid"; return; } cout << n; }
调用 classify(0) 的返回值是多少?
int classify(int x) { if (x>=0) return 1; return -1; }
条件为真,执行第一个 return 后就结束本次调用。
int main() { // 从这里开始组织程序 return 0; }
不写 void main()。返回 0 通常表示正常结束。
走到 main 末尾等价于返回 0;这不是普通 int 函数的通用规则。
在标准 C++ 程序中,不把 main 当普通函数主动调用。
同一个名字可能代表不同变量;出了可见范围,就不能继续使用。
int a=3; { int b=5; cout << a+b; } // 这里不能使用 b
外层:a
内层:a 可见,b 在这里声明
for (int i=0; i<3; i++) { cout << i << " "; } // 循环结束后,这个 i 不再可用 for (int i=0; i<2; i++) cout << i;
后一个 for 可以重新声明自己的 i。若循环结束后还需要下标,应当提前设计变量的声明位置。
void first() { int x=3; } void second() { int x=8; } int main() { int x=5; first(); cout << x; }
函数形参也属于该函数的局部名字;其他函数不能直接使用它。
int x=10; { int x=3; cout << x << " "; } cout << x;
3 10
这里创建了两个不同变量。为了易读,实际编程尽量避免不必要的同名遮蔽。
共享常量很直接;共享可变数据越多,函数之间越容易互相影响。
int count = 0; void addOne() { count++; } int main() { addOne(); addOne(); cout << count; return 0; }
2
本例在函数外、调用前声明,两个函数使用同一个 count。
const int CAP = 1000; int a[CAP]; int main() { // 全局 a 的元素开始时都为 0 return 0; }
CAP 不能在程序中重新赋值;使用前仍要保证实际数量不超过它。
与未初始化的普通局部数组不同;是否全局,也不会改变合法下标范围。
让一个循环管理行,另一个循环管理列,再用条件筛选组合。
先会输出一行,再把“输出一行”这个动作重复。
for (int j=1; j<=4; j++) cout << "*"; cout << "\n";
****
星号之间不换行;整行输出完成后,再输出一个换行。
for (int i=1; i<=3; i++) { for (int j=1; j<=4; j++) cout << "*"; cout << "\n"; }
**** **** ****
换行在内层循环结束后,在外层循环内部。
不是 i、j 同时每次各加 1;是内层完整走完一遍,外层才前进一步。
for(int i=0;i<2;i++) for(int j=0;j<3;j++) cout<<i<<","<<j<<" ";
0,0 0,1 0,2 1,0 1,1 1,2
内层走完一行后,再进入下一行;每次都会重新执行 j=0。
for(int i=0;i<3;i++) for(int j=0;j<4;j++) count++;
count 从 0 开始。这里每次内层次数相同;三角形那样随 i 改变的范围,不能直接按 3×4 算。
for (int i=1; i<=rows; i++) { for (int j=1; j<=cols; j++) cout << "*"; cout << "\n"; // 一行的结束位置 }
int rows, cols; cin >> rows >> cols; for(int i=1;i<=rows;i++) { for(int j=1;j<=cols;j++) cout<<"*"; cout<<"\n"; }
***** *****
只改行数,不会改变每行的星号数。
写内层边界之前,先列出“第几行,需要几个字符”。
for(int i=1;i<=4;i++) { for(int j=1;j<=i;j++) cout<<"*"; cout<<"\n"; }
* ** *** ****
内层上界写 i;误写成固定 n,就重新变回矩形。
for(int i=1;i<=n;i++) { for(int j=1;j<=n-i+1;j++) cout<<"*"; cout<<"\n"; }
**** *** ** *
bool edge = i==1 || i==rows || j==1 || j==cols; if (edge) cout << "*"; else cout << " ";
外层、内层照常遍历整个矩形;循环体用条件选星号或空格。
图中的 ␣ 只用于表示空格,不是程序实际输出的符号。
for(int i=1;i<=3;i++) { for(int j=1;j<=i;j++) cout<<j<<"x"<<i<<"=" <<i*j<<" "; cout<<"\n"; }
1x1=1 1x2=2 2x2=4 1x3=3 2x3=6 3x3=9
循环决定有哪些位置;表达式决定每个位置打印什么。
按代码逐行数一数。
for(int i=1;i<=3;i++) for(int j=1;j<=i;j++) cout<<"*";
范围随 i 变化。本例没有换行,星号会连在一行,但数量仍然是 6。
一个循环选第一个量,另一个循环选第二个量;条件负责判断。
for(int a=1;a<=3;a++) for(int b=1;b<=3;b++) cout<<a<<","<<b<<" ";
有序对 (1,2) 与 (2,1) 是不同选择。
for(int a=1;a<=3;a++) { for(int b=1;b<=3;b++) { if(a+b==4) cout<<a<<","<<b<<" "; } }
1,3 2,2 3,1
如果约定 a、b 必须不同,还应补上 a!=b;条件来自数据约定,不能凭感觉删组合。
for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) cout<<i<<","<<j<<" ";
0,1 0,2 0,3 1,2 1,3 2,3
j>i 自动排除自己与自己,也避免把同一对下标正反各算一次。
int count=0; for(int a=1;a<=3;a++) for(int b=1;b<=3;b++) for(int c=1;c<=3;c++) count++; cout<<count;
嵌套层数增加,组合数可能增长很快;写完循环前先估算次数。
读嵌套循环时,要把每个控制语句归到最近的一层循环。
for(int i=1;i<=2;i++) { for(int j=1;j<=4;j++) { if(j==3) break; cout<<j; } cout<<"\n"; }
12 12
break 不会一次跳出所有循环。下一行的 j 重新从 1 开始。
for(int i=1;i<=2;i++) { for(int j=1;j<=3;j++) { if(j==2) continue; cout<<j; } cout<<"\n"; }
13 13
不要把 continue 误解成“结束这一整行”。
实际耗时还与每次做什么、机器和时限有关。先算次数,再判断枚举范围是否合理。
内层条件和自增都在操作 j,是否误写成 i?
每轮外层,内层是否重新从起点开始?
独立上界、i、n-i+1,哪一个符合关系表?
空格和换行的位置,是否对应每列与每行?
用行、列两个坐标组织数据,把嵌套循环用到真正的表格上。
班级成绩表、棋盘、地图,都可以先抽象为行和列。
int a[2][3]; // a[1][2]:第 2 行第 3 列
两个下标都从 0 开始。第一对方括号选行,第二对选列。
int a[3][4]; double score[5][3]; char board[8][8];
本课使用固定容量数组。int a[n][m] 不是标准 C++ 的普通固定数组写法。
a[2][3] = 7; // 合法 // a[3][0] // 行越界 // a[0][4] // 列越界
不能因为总元素数为 12,就把 a[0][4] 当作合法访问下一行的写法。
int a[2][3] = {
{1, 2, 3},
{4, 5, 6}
};int a[3][4] = {
{1, 2},
{3}
};int a[3][4] = {};普通局部数组如果只写 int a[3][4];,元素没有可靠初值;全局数组则会零初始化。
int a[][3] = {
{1,2,3},
{4,5,6}
};编译器推导出 2 行,每行 3 个元素。
每行多宽,决定了这一行如何分组,也决定两个坐标如何定位元素。
初学时优先明确写行列数,或用分行大括号清楚表达。
int a[2][3]={{1,2,3},{4,5,6}}; cout << a[1][0] << "\n"; a[0][2] = 9; cout << a[0][2];
4 9
这句 cout 输出什么?
int a[2][3]={{2,4,6},{8,10,12}};
cout << a[1][1];行下标 1 是第 2 行,列下标 1 是这一行的第 2 列。
外层选定一个方向,内层把另一个方向走完。
for(int i=0;i<2;i++) for(int j=0;j<3;j++) cout<<a[i][j]<<" ";
1 2 3 4 5 6
for(int j=0;j<3;j++) for(int i=0;i<2;i++) cout<<a[i][j]<<" ";
1 4 2 5 3 6
a[i][j]a[i][j]只是想按列输出时,不要随手把访问表达式也换成 a[j][i];长方形表格很容易因此越界。
int n,m; cin>>n>>m; int a[10][10] = {}; for(int i=0;i<n;i++) for(int j=0;j<m;j++) cin>>a[i][j];
实际使用左上 n×m 个格子;不能拿整块容量当作有效数据。
for(int i=0;i<n;i++) { for(int j=0;j<m;j++) { if(j) cout << " "; cout << a[i][j]; } cout << "\n"; }
1 2 3 4 5 6
if(j) 让相邻数字之间有一个空格,行首和行尾都没有多余空格。
全表、单行、单列,区别主要在循环和累加器放在哪里。
long long sum=0; for(int i=0;i<n;i++) for(int j=0;j<m;j++) sum += a[i][j]; cout << sum;
21
for(int i=0;i<n;i++) { int sum=0; for(int j=0;j<m;j++) sum += a[i][j]; cout << sum << "\n"; }
6 15
for(int j=0;j<m;j++) { int sum=0; for(int i=0;i<n;i++) sum += a[i][j]; cout << sum << " "; }
5 7 9
此处每列最多 10 个、每个在 ±1000 内,用 int 足够;范围变大时应重新选累加类型。
int best=a[0][0]; for(int i=0;i<n;i++) for(int j=0;j<m;j++) if(a[i][j]>best) best=a[i][j];
本例 n≥1 且 m≥1,a[0][0] 确实存在。全负数表格也可以正确处理。
for(int j=0;j<m;j++) { int temp=a[p][j]; a[p][j]=a[q][j]; a[q][j]=temp; }
列的位置不变,整行数据一起移动。
for(int i=0;i<n;i++) { int temp=a[i][p]; a[i][p]=a[i][q]; a[i][q]=temp; }
交换行要遍历列;交换列要遍历行。先说清“固定谁、变化谁”。
for(int i=0;i<2;i++) for(int j=0;j<3;j++) b[j][i]=a[i][j];
本例另开 int b[3][2]。长方形数组不能靠原表内随意交换 a[i][j] 与 a[j][i] 完成转置。
把属于同一对象的信息装在一起,让姓名、成绩和编号始终对应。
数组适合同类型的一组数据;结构体适合同一个对象的多项属性。
string name[3]; int score[3]; int id[3];
如果只交换 score,不交换 name 和 id,信息就错配了。
一条记录整体移动,就能保留字段之间的对应关系。
struct Student { string name; int score; int id; };
Student a; Student b;
a.name 与 b.name 是不同对象的姓名字段;修改其中一个,不会自动修改另一个。
#include <iostream> #include <string> using namespace std; struct Student { string name; int score; }; int main() { Student s{}; return 0; }
本课将结构体定义放在全局、main 前面,让后面的函数都能使用这个类型。
Student a{"Ada", 92, 1};
Student b{};使用 {} 更稳妥。普通局部 Student s; 的 string 成员会构造为空串,但 int 成员没有可靠初值。
Student s{"Ada",92,1};
cout << s.name << " " << s.score;
s.score += 3;ss.names.score修改后 s.score 为 95,姓名和编号保持原值。
Student s{};
cin >> s.name >> s.score >> s.id;
cout << s.name << " " << s.score;Ada 92
姓名若允许空格,应重新设计按行输入的格式;不要用 >> 假装读到了整行。
Student a{"Ada",92,1};
Student b{};
b = a;
b.score = 80;
cout << a.score << " " << b.score;92 80
本例结构体只含 string 和 int,成员随赋值一起复制。它与普通数组不能整体赋值的规则不同。
下面输出什么?
Student a{"Ada",92,1};
Student b=a;
b.name="Bob";
cout<<a.name;b 是复制得到的另一个对象;修改它的 string 成员,不会改变 a.name。
第 i 个学生就是 students[i],再用点运算符访问这个学生的成员。
Student students[3] = {
{"Ada", 92, 1},
{"Bob", 85, 2},
{"Coco",92, 3}
};cout << students[1].name; // Bob students[1].score += 5; // 85 → 90
Student 是类型名,不能写 Student.score 来表示所有学生的成绩。
int n; cin >> n; Student students[100]{}; for(int i=0;i<n;i++) { cin >> students[i].name >> students[i].score; students[i].id=i+1; }
本例 0≤n≤100,姓名为一个词,成绩 0~100;id 记录原始读入顺序,数组下标则从 0 开始。
for(int i=0;i<n;i++) { if(students[i].score>=90) cout<<students[i].name<<"\n"; }
Ada Coco
Student temp = students[0]; students[0] = students[1]; students[1] = temp;
只交换 score 会把成绩送给另一个人。姓名、编号等属于同一对象的字段必须一起移动。
结构体没有自动知道“成绩优先还是编号优先”;规则需要我们写出来。
bool before(Student a, Student b) { return a.score > b.score; }
这是成绩降序规则。比较函数不负责交换对象,也不直接输出。
#include <algorithm>
// 已定义 Student 和 before
sort(students, students+n, before);第 9 课会系统讲 sort 和左闭右开区间。本课先用这个模板理解“数据记录 + 比较规则”。
bool before(Student a, Student b) { if(a.score != b.score) return a.score > b.score; return a.id < b.id; }
if(a.total!=b.total) return a.total>b.total; if(a.math !=b.math ) return a.math >b.math; return a.id<b.id;
规则应当有清楚的优先级;不要把三条条件简单用 || 连起来。
return a.score > b.score;成绩相同就返回 false;反过来比较也应为 false。
// 错误: // return a.score >= b.score;
相等时会得到 true,破坏 sort 要求的严格弱序关系,不能保证正确行为。
Coco 92 2 Ada 92 3 Bob 85 1
相同成绩按原编号排序;并不是把姓名按字母顺序排。
不要依赖 sort 恰好保留同分记录的原来顺序;需要的顺序,应当写进规则。
让程序读写文件,再用标准库完成取极值、交换和区间排序。
键盘与屏幕是输入输出的一种方式;文件也能保存完整数据。
12 30
42
文件扩展名只是一种约定;具体名称与读写方式,以题目或项目要求为准。
#include <fstream> ifstream fin("sum.in"); ofstream fout("sum.out"); int a,b; fin >> a >> b; fout << a+b << "\n";
ifstream fin("sum.in");IDE、终端、评测系统,可能设置不同的工作目录。文件明明存在却打不开时,先检查实际运行位置。
文件名大小写和后缀都应准确;不要依赖系统隐藏扩展名后的显示。
ifstream fin("sum.in"); if (!fin) { cerr << "cannot open input\n"; return 1; }
文件名拼错、路径不对、工作目录不同。
权限、路径类型等,也可能导致失败。
int a,b; if (!(fin >> a >> b)) { cerr << "invalid input\n"; return 1; }
本例约定文件里有两个 ±1000 以内的整数。遇到空文件、非数字或缺少数据,不能继续拿未成功读入的值计算。
ofstream fout("sum.out"); if (!fout) return 1; fout << answer << "\n"; fout.flush(); if (!fout) return 1;
局部文件流离开作用域时自动关闭;也可以在需要时主动 close()。
本例 flush 后检查写入状态。若需要在关闭时也确认结果,可以显式 close() 后再次检查。
cin、cout 的写法可以保留;标准输入和标准输出的来源已经改变。
#include <cstdio> if (!freopen("sum.in", "r", stdin)) return 1; if (!freopen("sum.out", "w", stdout)) return 1; int a,b; cin >> a >> b; cout << a+b << "\n";
"r" 表示读,"w" 表示写并覆盖。此模板不额外关闭 stdin/stdout,也不在读写之后再切换通路。
使用 cin / cout,不保留无条件的本地文件重定向。
严格按题目规定的名称、格式和大小写读写。
可用文件保存样例;提交前再次核对目标环境的约定。
不存在所有评测都通用的固定文件名;也不能把“本地跑通”直接等同于提交成功。
使用函数之前,先说清参数类型、作用对象,以及它会不会修改原数据。
这些是标准库工具。本课只使用这些简单接口,容器和迭代器体系留待后续课程。
int a=8, b=3; cout << max(a,b) << " " << min(a,b); cout << "\n" << a << " " << b;
8 3 8 3
两个参数应使用匹配的类型;max(1,2.5) 这样的混合类型调用不能直接照搬。
int a=4, b=9, c=2; int high=max(max(a,b),c); int low =min(min(a,b),c); cout << high << " " << low;
9 2
int a=3,b=8; swap(a,b); cout<<a<<" "<<b;
8 3
swap(a,b) 会修改对象;max(a,b) 只取得结果。
swap(students[i], students[j]);
若只写 swap(students[i].score,students[j].score),交换的就只有成绩,记录对应关系会被破坏。
先确定哪一段数据有效,再把那一段交给排序函数。
int a[5]={8,3,6,1,5};
sort(a,a+5);sort 会修改数组本身;这里默认从小到大,不是只产生一个新数组。
a+5 可以表示这个合法区间的尾后边界;不能据此读取 a[5]。
int a[5]={8,3,6,1,5};
sort(a+1,a+4);sort(a,a+n);sort(a+1,a+n+1);sort(a+l,a+r+1);第三行前提:0≤l≤r<容量。不要把不同下标约定的模板混在一起。
bool bigger(int a,int b) { return a>b; } sort(a,a+n,bigger);
#include <functional>
sort(a,a+n,greater<int>());两种写法都表示从大到小;比较时相等要返回 false。
a 容量至少为 6,下面调用会排序哪些下标?
sort(a+2,a+5);
左端包含,右端不包含。元素个数为 5-2=3。
合法的空区间、单元素区间无需改变;仍要保证区间端点有效。
只排实际读入的前 n 个数据;不要把未用格子混进去。
提前复制原数据,或在结构体里保存原编号。
同一个整数,可以用不同进制表示;转换时抓住位权和整除取余。
十进制 13 与二进制 1101,可能表示同一个数量。
十六进制 A、B、C、D、E、F 分别表示十进制 10、11、12、13、14、15。
347 = 3×100 + 4×10 + 7
C++ 的 ^ 是按位异或,不是乘方。小整数的 2 的幂可逐次乘 2;下一课再学习位移。
1101₂ = 8+4+1 = 13₁₀
11₂ + 01₂ ------ 100₂
个位 1+1 写 0 进 1,下一位再处理进位。
100₂ - 001₂ ------ 011₂
借位的规则与十进制相同;差别在于每一位的基数是 2。
两个方向的方法不同,写代码前先确定转换方向。
value = value*2 + digit;
读完整串 1101,就得到 13。每一步都把之前的数整体左移一位,再加入新的一位。
char c = '7'; int digit = c - '0'; cout << digit + 1;
8
只在 c 确实是 '0'~'9' 时这样转换;读到 'A' 不能继续套同一个差值。
long long value=0; for(size_t i=0;i<s.size();i++) { char c=s[i]; if(c!='0' && c!='1') { cout<<"invalid"; return 0; } value=value*2+(c-'0'); }
本页是核心片段,完整程序会在无效字符处结束。本例非空字符串长度不超过 30,结果可由 long long 保存。
第一次得到的是最低位。直接按产生顺序输出,会把位序反过来。
int digit[32]={}, count=0; while(n>0) { digit[count++]=n%2; n/=2; } for(int i=count-1;i>=0;i--) cout<<digit[i];
if(n==0) { cout<<0; return 0; }
上面的 while 对 0 一次也不执行;完整程序先处理零。
value=value*r+digitn%r,再令 n/=r基数与数码都必须合法,累计结果也不能超出所选整数类型。
每个八进制位对应 3 个二进制位;每个十六进制位对应 4 个。
25₈ → 010 101₂,去掉最前面的多余零后为 10101₂。分组内部的零不能随便删除。
2D₁₆ → 0010 1101₂ → 101101₂。字母大小写通常都可接受,但自写转换程序要明确支持。
1101011₂ → 001 | 101 | 011 → 1 5 3 → 153₈
1101011₂ → 0110 | 1011 → 6 B → 6B₁₆
最左组不足位数时在左边补零,不在右边补;补在右边会改变数值。
int digitValue(char c) { if(c>='0' && c<='9') return c-'0'; if(c>='A' && c<='F') return c-'A'+10; if(c>='a' && c<='f') return c-'a'+10; return -1; }
得到数码 d 后,继续检查 0≤d<base。例如字符 8 是十进制数码,但不是合法八进制数码。
int d=digitValue(c); if(d<0 || d>=base) { cout<<"invalid"; return 0; } value=value*base+d;
完整程序约定 2≤base≤16,文本为 1~8 个数码,不含前缀和符号。最多为 FFFFFFFF₁₆,可用 unsigned long long 保存。
整数存储的是值;前缀和流操纵符负责说明或选择表示形式。
cout << 13 << " " << 0b1101 << " " << 015 << " " << 0xD;
13 13 13 13
0b 是二进制前缀,前导 0 表示八进制,0x 表示十六进制。不要给普通十进制整数随手加前导零。
int n=26; cout<<hex<<n<<" "<<n<<"\n"; cout<<oct<<n<<"\n"; cout<<dec<<n;
1a 1a 32 26
设置 hex 后,后面的整数继续按十六进制输出,直到再次切换。标准流没有同样的 bin 操纵符。
unsigned int n=0; cin >> hex >> n; cout << dec << n;
45
这里的输入是十六进制文本;cin 的格式状态也会保留。cout 使用自己的输出格式状态。
unsigned int n=26u; printf("%u %o %x\n", n,n,n); // 读取八进制:scanf("%o", &n); // 读取十六进制:scanf("%x", &n);
26 32 1a
需要 <cstdio>。本页 %u、%o、%x 对应 unsigned int;不要把任意整数类型与格式串混用。
10110₂ 转八进制,哪种分组正确?
// 每个八进制位,对应 3 个二进制位从右边分组;最左组在左侧补零。26₈=2×8+6=22₁₀。
逐位理解整数运算,用清楚的位图和边界规则,避免只背口诀。
本课按常见 32 位 unsigned int 环境演示非负整数;位图只展示低 8 位。
最低位权为 1;位编号与“从左数第几个”不同。补在左边的零不改变非负整数的值。
unsigned int x=13u; unsigned int y=10u;
unsigned int 为 32 位。示例值与移位次数都按这个环境给定。
#include <limits> numeric_limits<unsigned int>::digits
不把有符号负数、符号位和溢出的规则混进初学口诀;换类型时要重新检查规则。
同一个 << 还用于 cout 输出;应根据左右操作数和表达式位置识别含义。
每一列独立使用同一条规则,不像加法那样进位。
与:同时为 1;或:至少一个 1;异或:这一位不同才为 1。
cout << (13u & 10u);
cout << (13u | 10u);
cout << (13u ^ 10u);
unsigned int x=13u; cout << ((~x) & 255u);
242
~x 翻转全部 32 位;不能把 ~13u 直接说成 242。
cout << (6u & 3u) << " "
<< (6u && 3u);2 1
6&3 逐位计算得到 2;6&&3 把两边当作真假,得到 true,默认输出为 1。&& 还具有短路求值。
下面表达式的结果是什么?
5u ^ 3u // 5 = 00000101 // 3 = 00000011
逐位不同为 1。异或本身不产生进位,不能直接当作整数加法。
使用无符号整数,并保证移位次数在有效范围内。
cout << (13u << 1);
13×2=26 在本例类型范围内,所以结果与乘 2 一致。
cout << (13u >> 1);
对无符号整数,右移 k 位等于除以 2ᵏ 后向下取整。这里 13/2 得到 6。
unsigned int x=0x80000000u; cout << (x<<1);
在本课 32 位无符号环境中,左移按模 2³² 的结果保存。不能对所有数都只套“乘 2”而忽略类型范围。
x<<0、x>>0x<<1 到 x<<31x<<32 或 x>>32不要通过运行一次“碰巧得到某个值”来推断无效移位的规则。
x &= mask;x = x & mask;x |= mask;x = x | mask;x ^= mask;x = x ^ mask;x <<= k;x = x << k;x >>= k;x = x >> k;x&mask 是一个结果值;x&=mask 则会修改 x。
掩码只选择某些位;异或可让成对出现的数据互相抵消。
unsigned int x; cin >> x; if ((x & 1u) == 0u) cout << "even"; else cout << "odd";
结果比较要加括号:(x&1u)==0u。最低位的其他高位在 &1 后都会被清零。
cout << (45u & 15u);
x^x == 0x^0 == xunsigned int answer=0; for(int i=0;i<n;i++) { unsigned int x; cin>>x; answer ^= x; } cout << answer;
本例保证:一个数只出现一次,其余每个数恰好出现两次。满足这个约定,异或结果才是要找的那个数。
异或为 3
7 和 9 各一对,剩下单独的 3。
异或为 0
三个数都出现一次,0 甚至没在输入中出现。异或只给出位运算结果,不自动理解题意。
能背优先级还不够;读者应该直接看出你先计算哪一部分。
这里只列常用关系,并非完整 C++ 运算符表。混用多类运算时,用括号明确分组。
x & 1u == 0u
// 等价分组:
x & (1u == 0u)(x & 1u) == 0u
不要指望按位与比 == 先算。给需要先做的位运算加括号。
cout << (x & y); cout << (x ^ y); cout << (x << 1);
没有括号时,输出用的 << 可能先与 x 结合,后续运算就变成错误的表达式。
下面表达式的值是多少?
1u << 2 + 1
实际代码建议明确写 1u<<(2+1);想先移位再加一,就写 (1u<<2)+1。
把十一课连接成一张知识地图,用模型、过程和边界检查自己的程序。
写代码之前,先说清对象是什么、它们是什么关系。
求和从 0 开始;最大值从真实元素开始;查找失败要保留专用标记。
cin >> name >> score;
适合没有空格的姓名等字段。
getline(cin, line);
之前使用 >> 时,要按输入约定处理剩余换行。
函数让动作有名字,循环让动作有规律,标准库减少重复实现。
int add(int a,int b)
参数表达依赖,范围应有约定。
return a+b;一件清楚的小任务,便于验证。
int answer=add(3,5);返回值交给调用者,输出由使用者决定。
不能仅凭名字一样就认作同一个变量。对普通值形参的修改,不会自动改掉外面的实参。
if(a.score!=b.score) return a.score>b.score; return a.id<b.id;
先主关键字,再次关键字;相等时比较器应返回 false。区间写成左闭右开。
题目要求标准输入输出时,不保留无条件的文件重定向。
按规定的名称和格式读写,确认实际工作目录。
进制改写数字的样子;位运算直接处理选定整数类型的二进制位。
value=value*r+digit;
数码合法;从左到右累积;结果不溢出。
digit=n%r; n/=r;
余数从低位产生,最后逆序;0 要有明确输出。
不要只找一个能运行的样例;要挑能区分正确与错误的边界。
用很小的数据手推变量和下标,再与实际输出逐步比较;避免一次改动多个不相关位置。
人数上限、姓名是否有空格、成绩范围、同分顺序。
固定容量结构体数组、清楚的函数接口、合法的排序区间。
同样的程序组织,也适用于商品、比赛记录和坐标点等数据。