大卫信奥 · J2知识点总结 · 直播课

J2 知识点 · 十二单元合集

从数组与字符串,到函数、结构体、文件与位运算。
十二个单元,学会组织与处理数据。

← 返回级别目录
J2 封面插图
课程目录
大卫信奥 · J2 知识点 · 第 1 单元
01

一维数组

把一整组同类型的数据放在一起,学会用下标定位、用循环处理。

一组数据下标与初始化循环遍历常见错误
一维数组封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
一组数据
从很多变量,走到一个数组。
B
下标与初始化
先分清容量、有效数据和合法下标。
C
循环遍历
循环遍历,完成读入、输出与统计。
D
常见错误
用边界例子检查自己的程序。
PART 01

一组数据,一个名字

成绩有 5 个、50 个、500 个时,程序不应该靠增加变量名来扩容。

同类型连续存储按下标访问
知识讲解

数据一多,单个变量就不方便了

原来的写法

int score1 = 82, score2 = 95;
int score3 = 76, score4 = 88;
int score5 = 91;

求和还能写出来;如果要读取第 k 个分数呢?

给一组数据一个名字

score
[0]82
[1]95
[2]76
[3]88
[4]91
!

数组把同类型的数据组织在一起。score[2] 就能定位到第三个元素。

知识讲解

定义数组:类型、名字和容量

int score[5];
double price[10];
char letter[26];
写法
含义
int
每个元素的类型
score
这组数据的名字
[5]
一共 5 个元素,不是最大下标
知识讲解

第几个与下标,相差 1

score[5]
[0]82
[1]95
[2]76
[3]88
[4]91
第 1 个 → score[0]
→
第 k 个 → score[k-1]
→
第 5 个 → score[4]
!

容量是 5,合法下标是 0~4。score[5] 已经在数组外面。

知识讲解

用下标读,用下标改

每条语句都落在一个格子上

int a[5] = {10, 20, 30, 40, 50};
a[2] = 99;
cout << a[0] << " " << a[2];

只改第三个元素

修改后的 a
[0]10
[1]20
[2]99
[3]40
[4]50
● 运行结果
10 99
知识讲解

停一下:下标表达式也要先算出值

先独立判断

下面的输出是什么?

int a[4] = {4, 7, 2, 9};
int i = 1;
cout << a[i + 1];

选择并说明理由

A 4
B 7
C 2
D 9
选 C:输出 2。
!

i+1 为 2,访问 a[2],对应第三个格子。

PART 02

先给初值,再参与计算

“有空间”与“里面有可用的数据”是两件事。

列出元素不足补零全零初始化
知识讲解

四种常用的初始化写法

代码
得到什么
int a[4] = {1,2,3,4};
逐个给初值
int b[] = {5,6,7};
由初值推导容量为 3
int c[5] = {8,9};
后面三个元素自动补 0
int d[5] = {};
五个元素都为 0
!

初始化发生在定义时;数组定义后,不能用 a={1,2,3} 给普通数组整体赋值。

知识讲解

部分初始化:省掉的元素补 0

int a[6] = {4, 2};
a
[0]4
[1]2
[2]0
[3]0
[4]0
[5]0
!

这不是“只有前两个格子存在”:容量仍为 6,每个格子都有确定初值。

知识讲解

局部数组没有初始化,不能当作全 0

建议的起步习惯

int count[10] = {};
// 然后再做 count[x]++

计数、累加等操作,需要先给出可靠初值。

需要避开的写法

int count[10];
// 不要直接 count[3]++

未初始化的局部整型元素不能直接读取;不能指望得到某个固定“随机数”。

知识讲解

容量与实际使用数量,分别记录

a[8],当前 n=3
[0]7
[1]2
[2]9
[3]—
[4]—
[5]—
[6]—
[7]—

容量

定义时准备了 8 个格子。

当前有效数据

只读入了 3 个数,后续只处理 a[0] 到 a[2]。

!

即使未用格子被初始化为 0,也不表示它们自动成为题目数据。

PART 03

用同一个循环,访问每个元素

先确定“走哪些下标”,再决定“到每个格子做什么”。

0 到 n-1循环体换动作边界先想清
知识讲解

循环读入:每个数进入一个格子

本例 0≤n≤100,数值在 ±1000 内

int n;
cin >> n;
int a[100] = {};
for (int i=0; i<n; i++)
    cin >> a[i];

输入顺序对应下标

● 运行结果
输入:4 8 3 6 1
a[0..3]
[0]8
[1]3
[2]6
[3]1
逐步演示 · 正序

正序遍历:下标从 0 走到 n−1

i
访问的元素
输出
0
a[0]
8
1
a[1]
3
2
a[2]
6
3
a[3]
1
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 开始,不执行循环。

数组本身没有被改动

存储顺序保持不变
[0]8
[1]3
[2]6
[3]1
● 运行结果
1 6 3 8
知识讲解

累加求和:只换循环体

每经过一格,加入总和

long long sum = 0;
for (int i=0; i<n; i++)
    sum += a[i];
cout << sum;

用 8、3、6、1 手推

加入的数
sum
初始
0
8
8
3
11
6
17
1
18
知识讲解

找最大值:先有一个真实候选

本例保证至少有一个数

int best = a[0];
for (int i=1; i<n; i++) {
    if (a[i] > best)
        best = a[i];
}

为什么不直接从 0 开始?

全是负数的一组数据
[0]-8
[1]-3
[2]-6
[3]-1
!

用 0 当最大值会得到一个根本不在数组里的答案;先用 a[0],需要 n≥1。

知识讲解

找第一个目标:位置与值要分清

线性查找的核心

int pos = -1;
for (int i=0; i<n; i++) {
    if (a[i] == target) {
        pos = i;
        break;
    }
}

本例 target=6

a
[0]8
[1]3
[2]6
[3]1

找到的值是 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];
读 a[i]
→
写 b[i]
→
继续下一个 i
!

b=a 不能给普通数组整体赋值;相同类型的单个元素可以正常赋值。

知识讲解

边界判断:循环到底访问了几格?

先独立判断

a 的容量为 5,n=5。下面的循环有什么问题?

for (int i=0; i<=n; i++)
    cout << a[i];

选择并说明理由

A 少访问一次
B 访问 a[5] 越界
C 最后一个下标是 5
D 没有问题
选 B:一共试图访问 6 格。
!

合法下标 0~4;改成 i<n。越界是未定义行为,不保证立刻报错。

知识讲解

写数组程序前,先回答四个问题

容量够吗?

题目最多多少个元素?数组是否装得下?

初值对吗?

被读取的格子,是否已经读入或初始化?

下标在哪?

0 开始还是 1 开始?循环端点是否对应?

答案表示什么?

求的是值、下标、出现次数,还是整组数据?

小结

今天你学会了:

存储
同类型的一组元素容量与有效数量分开
访问
第 k 个对应下标 k-1先保证下标在范围内
处理
正序、倒序、累加、查找先明确边界再写循环
大卫信奥 · J2 知识点 · 第 2 单元
02

字符数组

把字符装进数组,理解 C 风格字符串为什么需要一个结束标记。

字符与文本输入与长度常用函数边界与安全
字符数组封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
字符与文本
单个字符和一串字符分别怎么保存。
B
输入与长度
分清缓冲区容量和字符串长度。
C
常用函数
长度、复制、比较与追加。
D
边界与安全
给结束符留空间,写之前先算容量。
PART 01

字符数组,也是一维数组

格子里存放 char;当作 C 风格字符串使用时,还需要约定在哪里结束。

char字符数组零字符终止
知识讲解

单引号是字符,双引号是字符串

一个字符

char c = 'A';
cout << c;
c
A

一串字符

char s[] = "Cat";
cout << s;
s
[0]C
[1]a
[2]t
[3]\0
知识讲解

\0:结束标记也占一个格子

char s[6] = "Cat";
[0]C
[1]a
[2]t
[3]\0
[4]\0
[5]\0
项目
本例结果
数组容量
6 个 char
字符串长度
3 个字符
结束符位置
s[3],本身不计入字符串长度
!

空字符 \0 的值是 0;它与数字字符 '0' 不是同一个字符。

知识讲解

保存同一个词,可以有不同的初始化写法

char a[4] = "Cat";
char b[] = "Cat";
char c[4] = {'C', 'a', 't', '\0'};
3 个可见字符
→
+ 1 个结束符
→
至少 4 个 char
!

char d[3]={'C','a','t'} 是合法字符数组,但不能直接把它当作以零终止的 C 字符串输出。

知识讲解

零字符和字符 0,读起来相似,用途不同

写法
本课 ASCII 兼容环境中的值
作用
'\0'
0
C 字符串结束标记
'0'
48
显示出来的数字字符 0
' '
32
空格
'\n'
10
换行
知识讲解

容量与长度:你会分别回答吗?

先独立判断

下面两项分别是多少?

char s[8] = "code";
// sizeof(s) 和 strlen(s)

选择并说明理由

A 8 和 8
B 4 和 4
C 8 和 4
D 4 和 8
选 C:容量 8,字符串长度 4。
!

strlen 扫描到 \0 为止;sizeof(s) 此处得到整个数组的字节数。

PART 02

输入一串字符,要考虑空格和容量

读取单个词与读取整行,不是同一项需求。

读词读行容量含结束符
知识讲解

cin >> s:读取一个不含空白的词

为字符数组限制输入长度

char s[20];
cin >> setw(20) >> s;
cout << s;

需要 <iomanip>;最多保存 19 个输入字符,再留一个 \0。

遇到空格停止

● 运行结果
输入:hello world
hello
!

后面的 world 仍留在输入流中;不会自动接进同一个字符串。

知识讲解

cin.getline:空格也属于这一行

本例一行至多 19 个字符

char s[20];
if (cin.getline(s, 20))
    cout << s;
else
    cout << "too long";

换行是边界,不是内容

● 运行结果
输入:hello world
hello world
!

读取到换行后结束,换行被取走。容量不足会设置失败状态,不能当作完整读入。

知识讲解

想保存 10 个可见字符,最少开 11 格

至少 char s[11]
[0]a
[1]b
[2]c
[3]d
[4]e
[5]f
[6]g
[7]h
[8]i
[9]j
[10]\0
!

结束符是实际占用空间的字符,不能只按屏幕上看得到的字符来开数组。

知识讲解

遍历 C 字符串:遇到零字符结束

本例已保证 s 正确终止

for (int i=0; s[i]!='\0'; i++)
    cout << s[i] << '\n';

末尾的 \0 不输出

s
[0]C
[1]a
[2]t
[3]\0
● 运行结果
C
a
t
知识讲解

统计某个字符:把循环动作改为计数

int count = 0;
for (int i=0; s[i]!='\0'; i++) {
    if (s[i] == 'a') count++;
}
cout << count;
s="banana"
[0]b
[1]a
[2]n
[3]a
[4]n
[5]a
[6]\0

a 出现 3 次;只统计内容,不把结束符计入。

PART 03

常用字符串函数,都遵守结束符约定

先保证字符串有效和容量足够,再调用 <cstring> 中的函数。

strlenstrcpystrcmpstrcat
知识讲解

strlen:数到结束符之前

需要 <cstring>

char s[20] = "hello";
cout << strlen(s);

返回的是长度

扫描过程
[0]h
[1]e
[2]l
[3]l
[4]o
[5]\0
● 运行结果
5
知识讲解

strcpy:复制内容,也复制结束符

char src[] = "cat";
char dst[10];
strcpy(dst, src);

从这里读

src
[0]c
[1]a
[2]t
[3]\0

写到这里

dst 前 6 格示意
[0]c
[1]a
[2]t
[3]\0
[4]—
[5]—
!

参数顺序是 目标、来源;目标容量至少为 strlen(src)+1,来源与目标不能重叠。

知识讲解

比较内容用函数,不能把数组名当内容

需求
写法
原因
复制字符串
strcpy(b,a);
不能用 b=a 给普通数组整体赋值
判断内容相同
strcmp(a,b)==0
a==b 比较的是转换后的指针值
计算有效长度
strlen(a)
不是整个数组容量
知识讲解

strcmp:看结果的正负,不猜具体差值

结果
表示什么
示例
小于 0
第一个字符串字典序更小
strcmp("ant","cat")<0
等于 0
内容相同
strcmp("cat","cat")==0
大于 0
第一个字符串字典序更大
strcmp("dog","cat")>0
!

标准保证的是负数、零、正数,不保证不同平台都返回 -1 或 1。

知识讲解

字典序:从左到右找第一个不同的位置

比较
决定位置
结论
cat 与 car
第三个字符:t 与 r
cat 更大
cat 与 cats
前三个字符相同,短串先结束
cat 更小
"10" 与 "2"
第一个字符:'1' 与 '2'
"10" 更小,不按数值比
!

本课按 ASCII 兼容字符集理解大小写和数字字符;这不是中文拼音排序规则。

知识讲解

strcat:覆盖旧结束符,再补新结束符

追加前

char s[12] = "red";
strcat(s, "blue");
s 原内容
[0]r
[1]e
[2]d
[3]\0

追加后

s 新内容
[0]r
[1]e
[2]d
[3]b
[4]l
[5]u
[6]e
[7]\0
● 运行结果
redblue
知识讲解

交换两个词:临时区不能省

a → temp
→
b → a
→
temp → b
char a[20]="red", b[20]="blue";
char temp[20];
strcpy(temp,a);
strcpy(a,b);
strcpy(b,temp);
● 运行结果
blue red
知识讲解

倒序输出字符:长度先转成可用下标

输入长度在 0~19 之间

int len = static_cast<int>(strlen(s));
for (int i=len-1; i>=0; i--)
    cout << s[i];

边界分别想一遍

  • 空串:len=0,i=-1,不进入循环。
  • 一个字符:只访问 s[0]。
  • 倒序访问不会自动改变原字符串。
知识讲解

追加能不能成功,先算需要几格

先独立判断

这段程序的目标容量是否足够?

char s[6] = "cat";
// 想追加 "dog"

选择并说明理由

A 够,3+3=6
B 不够,还需要结束符
C 能自动扩容
D 只会追加一个字符
选 B:至少需要 7 格。
!

不能调用 strcat 去试越界;应先增加容量,或改用下一课的 string。

知识讲解

只改小写字母:先判断范围,再计算偏移

本例按 ASCII 英文字母处理

for(int i=0;s[i]!='\0';i++) {
    if(s[i]>='a' && s[i]<='z')
        s[i]=s[i]-'a'+'A';
}

数字、空格和标点保持原样

● 运行结果
输入:Hello, J2!
HELLO, J2!
!

减去 'a' 得到字母序号,再加上 'A' 得到对应大写字母;不要不加判断就修改所有字符。

知识讲解

字符数组处理的是字节,不等于任意语言文字

本课主要处理

ASCII 兼容的英文字母、数字、标点和空格。

遇到 UTF-8 中文时

一个汉字通常占多个字节;逐字节大小写转换或倒序,不能直接当作“逐汉字”处理。

!

先明确输入字符范围,再选择处理方法。

小结

今天你学会了:

字符串约定
内容后必须有 \0容量至少为长度+1
输入与访问
读词与读行分开选有效位置与结束符分开想
常用函数
strlen / strcpystrcmp / strcat,先验容量
大卫信奥 · J2 知识点 · 第 3 单元
03

字符串

用 std::string 管理文本,把“要做什么”写得更直接。

创建与输入访问与修改组合与比较查找与边界
字符串封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
创建与输入
定义字符串,读一个词或一整行。
B
访问与修改
长度、下标、遍历和逐字符修改。
C
组合与比较
赋值、拼接和字典序。
D
查找与边界
正确使用 find 与 string::npos。
PART 01

管理一段文字,先认识 string

字符数组是固定容量的底层工具;string 提供更直接的文本操作。

<string>自动管理存储内容操作
知识讲解

创建一个 string,就有一个字符串对象

string a;              // 空串
string b = "hello";
string c = b;
string d(4, '*');      // "****"
对象
内容
size()
a
空串
0
b
hello
5
c
hello
5
d
****
4
知识讲解

字符数组和 string,不用混着记接口

操作
char 数组中的 C 字符串
std::string
求长度
strlen(s)
s.size() / s.length()
复制内容
strcpy(dst,src)
dst=src
拼接
strcat(dst,src)
dst+=src
比较内容
strcmp(a,b) 看正负
a==b、a<b
读一行
cin.getline(s,容量)
getline(cin,s)
知识讲解

cin >> s:一次读一个词

输入输出都能直接用

string s;
cin >> s;
cout << s;

空白分开两个词

● 运行结果
输入:hello world
hello

第二个词仍在输入流里。

知识讲解

getline(cin,s):保留一行里的空格

也能读到空行

string s;
getline(cin, s);
cout << s.size() << "\n" << s;

看清字符个数

● 运行结果
输入:red blue
8
red blue
!

换行被取走但不保存;中间和开头的空格都属于这一行。

知识讲解

混用 >> 与 getline:先处理上一行的尾巴

n 后面的换行还没读走

int n;
cin >> n;
cin.ignore(
    numeric_limits<streamsize>::max(),
    '\n');
string line;
getline(cin, line);

明确“下一行”这个约定

● 运行结果
输入:3 hello world
  hello world
!

需要 <limits>。ignore 丢弃当前行剩余部分;std::ws 还会吃掉后面的前导空白,含义不同。

PART 02

读写单个字符,仍然靠下标

string 能改变长度,不代表下标访问可以省略边界判断。

size()s[i]遍历
知识讲解

有效字符下标:0≤i<size()

string s="hello"
[0]h
[1]e
[2]l
[3]l
[4]o
cout << s[0] << " " << s[4];
s[0] = 'H';
cout << "\n" << s;
● 运行结果
h o
Hello
!

本课只读写范围内的实际字符;不要用 s[s.size()] 当作最后一个字符。

知识讲解

size 与 length:返回相同的字符存储长度

两个接口效果一致

string s = "a b";
cout << s.size() << " "
     << s.length();

空格也算一个字符

s 的 3 个字符
[0]a
[1]␣
[2]b
● 运行结果
3 3
!

对本课 ASCII 文本可直接按字符数理解;UTF-8 中文的 size() 通常是字节数。

知识讲解

逐字符处理:循环范围由字符串决定

for (size_t i=0; i<s.size(); i++) {
    cout << s[i] << " ";
}
检查 i<s.size()
→
处理 s[i]
→
i++
!

size_t 是 size() 常用的无符号长度类型。正序边界直接写 i<s.size()。

知识讲解

修改字符,不需要重新创建整串

只把空格换成下划线

for (size_t i=0; i<s.size(); i++) {
    if (s[i] == ' ') s[i] = '_';
}

每次只动一个下标

原内容
[0]a
[1]
[2]b
[3]
[4]c
● 运行结果
a_b_c
知识讲解

空串没有第一个字符

先判断,再访问

if (!s.empty()) {
    cout << s[0];
} else {
    cout << "empty";
}

两种等价的判空思路

  • s.empty():是否没有字符。
  • s.size()==0:长度是否为 0。
  • 读入可能为空时,不要无条件使用 s[0]。
PART 03

赋值、拼接与比较

同样是 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,再用 + 拼接

写法
判断
说明
string("ab")+"cd"
正确
左侧是 string 对象
s+"cd"
正确
s 已声明为 string
"ab"+"cd"
错误
两个字符串字面量不能这样相加
"ab" "cd"
合法的字面量连接
编译期相邻字面量,含义不同
知识讲解

string 的 == 比内容,< 按字典序

cout << (string("cat") == "cat");
cout << (string("cat") < "cats");
cout << (string("10") < "2");
● 运行结果
111
!

比较从左到右看第一个不同字符;不是先比长度,也不是把文本自动转为整数。

知识讲解

停一下:字典序不等于数值大小

先独立判断

下面哪个关系成立?

string a="9", b="10";

选择并说明理由

A a<b
B a==b
C a>b
D 无法比较
选 C:a>b。
!

第一个字符 '9' 大于 '1';数字字符串若按数值比较,需要先明确数值转换。

PART 04

查找先得到位置,再判断是否存在

找不到时有专门的标记,不能把它当作合法下标。

find从 0 计位置string::npos
知识讲解

find 返回第一次匹配的起点

s="banana"
[0]b
[1]a
[2]n
[3]a
[4]n
[5]a
size_t pos = s.find("ana");
if (pos != string::npos)
    cout << pos;
else
    cout << "not found";
● 运行结果
1
知识讲解

find 不能直接放进 if 当作是否存在

容易误解的判断

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
}
字符串
第一次起点
继续找的起点
下一次匹配
banana
1
2
3
!

从 first+1 开始允许重叠匹配;跳过整个已匹配片段是另一种统计约定。

知识讲解

处理文本之前,明确这三个约定

按词还是按行?

输入里有没有空格?空行是否有效?

按内容还是按位置?

比较字符串,用内容运算;访问字符,先验证位置。

按什么字符范围?

本课示例按 ASCII 文本;不要把字节操作直接套到任意中文。

小结

今天你学会了:

输入与访问
cin 读词,getline 读行empty / size / 合法下标
内容操作
= 复制,+ / += 拼接== 比内容,< 比字典序
查找
find 给第一次起点与 string::npos 明确比较
大卫信奥 · J2 知识点 · 第 4 单元
04

函数基础

把重复动作封装起来,用参数表达变化,让程序按清楚的步骤工作。

定义与调用调用过程参数传递拆分程序
函数基础封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
定义与调用
看懂函数的组成和调用位置。
B
调用过程
从调用处出发,执行完再回来。
C
参数传递
分清形参、实参和复制的值。
D
拆分程序
把重复动作变成可以复用的函数。
PART 01

一个动作,给它一个名字

重复的代码容易漏改;把同一个动作写在一处,需要时再调用。

封装动作减少重复表达意图
知识讲解

先找出程序里反复出现的动作

每个标题前后都要分隔线

cout << "======\n";
cout << "START\n";
cout << "======\n";

希望程序读起来像这样

line();
cout << "START\n";
line();
!

line 是动作的名字;line() 是执行这个动作。

知识讲解

我们已经调用过不少函数

函数 / 写法
完成的事
所属头文件
sqrt(9.0)
求非负数的平方根
<cmath>
strlen(s)
求 C 字符串长度
<cstring>
getline(cin,s)
读取一整行
<string>
!

标准库已经提供一些工具;自己的重复动作,也可以定义为函数。

知识讲解

函数定义:说明动作如何完成

void line() {
    cout << "======\n";
}
组成
本例含义
void
不向调用处返回一个结果值
line
函数名
()
本例不需要参数
{ ... }
函数体,真正执行的语句
知识讲解

函数名要合法,也要看得懂

遵守标识符规则

  • 字母、数字和下划线。
  • 不能以数字开头。
  • 不能使用 int、for 等关键字。

名字表达动作

printLine
showTitle
countLetters

大小写不同就是不同的名字;避免用含义不明的 f1、f2。

知识讲解

定义不会自动执行,调用才会执行

函数放在 main 前面

void line() {
    cout << "======\n";
}
int main() {
    line();
    line();
    return 0;
}

两次调用,两次执行

● 运行结果
======
======
!

调用语句要有括号和分号:line();。函数体末尾的 } 后不需要分号。

逐步演示 · 调用过程

调用时,执行位置会来回移动

main 中的 line()
→
进入 line 的函数体
→
执行 cout
→
回到调用后的语句
时刻
正在执行哪里
①
main:输出 A
②
line:输出 ======
③
main:继续输出 B
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() { ... }
}

函数体里可以调用已经声明的函数。

PART 02

同一个动作,接收不同的数据

输出 3 个星号和输出 8 个星号,变化的是数量,不需要写两套函数。

形参实参值传递
知识讲解

参数:把变化的数据交给函数

void stars(int count) {
    for (int i=0; i<count; i++)
        cout << "*";
    cout << "\n";
}
stars(3)
→
count 得到 3
→
输出 ***
!

count 是函数执行这一次动作时使用的数量;下次调用可以传另一个值。

知识讲解

形参写在定义里,实参写在调用处

位置
写法
名称
函数定义
void stars(int count)
count 是形参
一次调用
stars(3)
3 是实参
另一次调用
stars(n)
表达式 n 的值作为实参
!

参数类型写在定义或声明里;调用时传具体的值或表达式,不再写 int。

知识讲解

实参既可以是常量,也可以是表达式

每次调用,都重新求实参的值

int n = 2;
stars(3);
stars(n);
stars(n+2);

依次输出

● 运行结果
***
**
****
知识讲解

普通变量按值传递:复制一个值

函数接收到自己的 x

void change(int x) {
    x = 99;
    cout << x << " ";
}
// main 中:
int a = 7;
change(a);
cout << a;

两份变量,不是同一个格子

main 中 a
7
复制 7
→
形参 x 改为 99
● 运行结果
99 7
知识讲解

停一下:函数里变了,外面呢?

先独立判断

调用结束后,main 中的 n 是多少?

void addOne(int x) { x++; }
// main 中:
int n=5;
addOne(n);
cout << n;

选择并说明理由

A 4
B 5
C 6
D 无法确定
选 B:仍然输出 5。
!

本课的 int 形参按值传递。x 是副本;修改 x 不会自动修改 n。

知识讲解

多个参数:数量、顺序和类型要对应

void repeat(char c, int count) {
    for (int i=0; i<count; i++) cout << c;
    cout << "\n";
}
// main 中:
repeat('#', 4);
形参
实参
本次含义
char c
'#'
要输出的字符
int count
4
重复次数
● 运行结果
####
知识讲解

调用接口时,不靠隐式转换猜意思

一眼能看懂的调用

repeat('*', 5);
repeat('A', 3);

先字符,后次数;与函数定义一致。

应该主动避免

// repeat(5, '*');
// repeat('*');
// repeat('*', 3, 2);

顺序颠倒可能发生类型转换而编译通过;参数缺少或多余通常会编译报错。

PART 03

先描述动作,再填函数体

主程序负责安排步骤,函数负责完成一件清楚的事。

接口清楚一个函数一个动作短代码便于检查
知识讲解

把“输出一行字符”做成可复用动作

函数只负责这一行

void repeat(char c, int count) {
    for(int i=0;i<count;i++)
        cout<<c;
    cout<<'\n';
}

main 安排调用顺序

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";
}
约定 0≤n≤1000
→
printSum(4)
→
输出 10
!

这里的函数直接输出。下一课再把“算出结果”和“怎么使用结果”分开。

知识讲解

文本处理:数据交进来,动作包起来

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;
}
printCount("banana", 'a')
→
输出 3
!

本课按值传递 string,函数有自己的字符串对象;本例字符串长度不超过 1000。

知识讲解

写函数前,先用一句话描述接口

完成什么?

例如:输出 count 个字符 c,然后换行。

需要什么?

字符和数量由参数交进来,不藏在函数体的固定数字里。

边界是什么?

count=0 是否只输出换行?调用者是否保证范围合法?

小结

今天你学会了:

定义与调用
函数体描述动作调用时跳入,结束后返回
参数
形参在定义,实参在调用本课普通值形参接收副本
组织程序
声明应先于调用名称、接口、边界写清楚
大卫信奥 · J2 知识点 · 第 5 单元
05

函数进阶

让函数把结果交回来,再用作用域管理每一份变量。

返回结果return局部作用域全局数据
函数进阶封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
返回结果
用返回值连接计算与使用。
B
return
看清函数何时结束。
C
局部作用域
分清每个变量可以在哪里使用。
D
全局数据
合理使用共享常量和固定容量数组。
PART 01

函数除了做事,还可以交回结果

先计算一个值,再决定把它输出、保存,还是用于下一次计算。

返回类型return 表达式调用表达式
知识讲解

输出到屏幕,与返回给调用处,是两件事

负责显示

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);
实参 3、5
→
形参 a、b
→
return 8
→
answer 得到 8
!

可以把 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); 会丢弃结果,不会自动输出。

知识讲解

返回类型,应当表达结果是什么

返回类型
适合的结果
示意
int
范围合适的整数
两个小整数的和
long long
可能较大的整数结果
一组数的累计值
double
小数结果
return (a+b)/2.0;
bool
是否满足条件
return x%2==0;
void
不返回结果值
只输出一行字符
知识讲解

返回 double,也要注意中间的运算

先用整数相除,可能已经截断

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
知识讲解

函数调用可以组成一个表达式

int add(int a, int b) { return a+b; }
cout << add(add(1,2),3);
步骤
得到什么
先计算内层 add(1,2)
3
再计算外层 add(3,3)
6
交给 cout
输出 6
!

本例各次调用只计算结果,没有依赖调用先后顺序的副作用。

PART 02

return 一执行,这次调用就结束

不论 return 写在函数中间还是末尾,执行到它都会返回调用处。

结束当前函数每条路径void 也可提前返回
知识讲解

return 后面的语句,不再接着执行

顺着执行路径追踪

int twice(int x) {
    return x*2;
    cout << "after";
}
cout << twice(4);

真正发生的事

x=4
→
return 8
→
回到 cout
● 运行结果
8

不会输出 after。实际代码应删除永远到不了的语句。

知识讲解

多个 return,可以对应不同分支

每条路径都有结果

int absolute(int x) {
    if (x<0) return -x;
    return x;
}

本例 x 在 ±1000 内

调用
执行路径
返回值
absolute(-7)
进入 if
7
absolute(0)
跳过 if
0
absolute(5)
跳过 if
5
知识讲解

非 void 函数,不能漏掉返回路径

有问题的路径

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 函数可以用 return; 提前结束

没有结果值,也能结束这次调用

void show(int n) {
    if (n<0) {
        cout << "invalid";
        return;
    }
    cout << n;
}

注意写法

  • void 函数用 return;。
  • int 函数用 return 一个 int 结果。
  • return 结束函数;break 只退出所在的那层循环。
知识讲解

停一下:两个 return 会都执行吗?

先独立判断

调用 classify(0) 的返回值是多少?

int classify(int x) {
    if (x>=0) return 1;
    return -1;
}

选择并说明理由

A 0
B 1
C -1
D 先 1 后 -1
选 B:返回 1。
!

条件为真,执行第一个 return 后就结束本次调用。

知识讲解

main 是程序入口,也有标准写法

int main() {
    // 从这里开始组织程序
    return 0;
}

本课使用 int main()

不写 void main()。返回 0 通常表示正常结束。

main 有特殊规则

走到 main 末尾等价于返回 0;这不是普通 int 函数的通用规则。

!

在标准 C++ 程序中,不把 main 当普通函数主动调用。

PART 03

变量在哪一层,就先看哪一层

同一个名字可能代表不同变量;出了可见范围,就不能继续使用。

代码块函数局部形参
知识讲解

花括号,为局部变量划出范围

在代码块内声明

int a=3;
{
    int b=5;
    cout << a+b;
}
// 这里不能使用 b

从外向内可见,反向不成立

外层:a

内层:a 可见,b 在这里声明

知识讲解

for 初始化语句里的 i 有自己的范围

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;
}

这三份 x 互相独立

first 中的 x
3
second 中的 x
8
main 中的 x
5
知识讲解

局部同名会遮住外层名字

最近的一层先找到

int x=10;
{
    int x=3;
    cout << x << " ";
}
cout << x;

内层结束,外层重新可见

● 运行结果
3 10
!

这里创建了两个不同变量。为了易读,实际编程尽量避免不必要的同名遮蔽。

PART 04

确实需要共享,再使用全局数据

共享常量很直接;共享可变数据越多,函数之间越容易互相影响。

全局常量固定数组初始化
知识讲解

全局变量:写在函数外面

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 不能在程序中重新赋值;使用前仍要保证实际数量不超过它。

全局数组零初始化

与未初始化的普通局部数组不同;是否全局,也不会改变合法下标范围。

知识讲解

局部能解决的事,优先放在局部

数据用途
建议位置
原因
一次计算的中间值
函数局部变量
依赖更清楚
传给函数的输入
形参
接口直接说明所需数据
全程固定的容量
全局 const 常量
统一约定
确实需要共享的大数组
可考虑全局数组
集中管理并说明使用范围
小结

今天你学会了:

返回值
计算与输出分开每条返回路径都检查
return
立即结束本次调用void 可提前 return;
变量范围
局部、形参、全局分清同名遮蔽尽量少用
大卫信奥 · J2 知识点 · 第 6 单元
06

循环嵌套

让一个循环管理行,另一个循环管理列,再用条件筛选组合。

执行顺序行列关系组合枚举边界与规模
循环嵌套封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
执行顺序
理解内层每次都重新开始。
B
行列关系
从矩形走到三角形。
C
组合枚举
遍历每一种可能再筛选。
D
边界与规模
看清次数、换行和跳出范围。
PART 01

一行做几次,整行再做几遍

先会输出一行,再把“输出一行”这个动作重复。

外层管行内层管列每行末尾换行
知识讲解

先完成一行:连续输出 4 个星号

for (int j=1; j<=4; j++)
    cout << "*";
cout << "\n";
● 运行结果
****
!

星号之间不换行;整行输出完成后,再输出一个换行。

知识讲解

把这一行重复 3 次,就得到矩形

外层把一整行包起来

for (int i=1; i<=3; i++) {
    for (int j=1; j<=4; j++)
        cout << "*";
    cout << "\n";
}

3 行,4 列

● 运行结果
****
****
****

换行在内层循环结束后,在外层循环内部。

知识讲解

每进入新的一行,j 都重新从 1 开始

外层 i
内层 j 的变化
完成的动作
1
1 → 2 → 3 → 4
输出第 1 行
2
1 → 2 → 3 → 4
输出第 2 行
3
1 → 2 → 3 → 4
输出第 3 行
!

不是 i、j 同时每次各加 1;是内层完整走完一遍,外层才前进一步。

知识讲解

把循环展开,看见每一次 (i,j)

2 行、3 列的执行顺序

行 / 列
0
1
2
0
①
②
③
1
④
⑤
⑥

这里的行列坐标从 0 开始

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
知识讲解

执行次数:独立范围,乘起来

外层 3 次
→
每次内层 4 次
→
核心语句 12 次
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";  // 一行的结束位置
}
知识讲解

行数与列数,都可以由输入决定

约定 1≤rows,cols≤20

int rows, cols;
cin >> rows >> cols;
for(int i=1;i<=rows;i++) {
    for(int j=1;j<=cols;j++)
        cout<<"*";
    cout<<"\n";
}

两个方向分别控制

● 运行结果
输入:2 5
*****
*****

只改行数,不会改变每行的星号数。

PART 02

列数跟着行号改变,就有了形状

写内层边界之前,先列出“第几行,需要几个字符”。

行号 i列号 j随行变化的上界
知识讲解

第 i 行,输出 i 个星号

先列关系表

行号 i
星号数量
内层 j
1
1
1
2
2
1~2
3
3
1~3
4
4
1~4

由表得到边界

for(int i=1;i<=4;i++) {
    for(int j=1;j<=i;j++)
        cout<<"*";
    cout<<"\n";
}
● 运行结果
*
**
***
****
知识讲解

三角形的总次数,是每行次数相加

i
内层执行次数
累计次数
1
1
1
2
2
3
3
3
6
4
4
10
1+2+3+4
→
10 次输出
!

内层上界写 i;误写成固定 n,就重新变回矩形。

知识讲解

倒三角:每行比上一行少一个

n=4 时的关系

行号 i
数量 n-i+1
1
4
2
3
3
2
4
1

从数量推出循环边界

for(int i=1;i<=n;i++) {
    for(int j=1;j<=n-i+1;j++)
        cout<<"*";
    cout<<"\n";
}
● 运行结果
****
***
**
*
知识讲解

空心矩形:位置决定输出什么

四条边上输出星号

*
*
*
*
*
␣
␣
*
*
*
*
*

坐标从 1 开始

bool edge = i==1 || i==rows
         || j==1 || j==cols;
if (edge) cout << "*";
else cout << " ";
!

外层、内层照常遍历整个矩形;循环体用条件选星号或空格。

知识讲解

乘法表:范围与输出内容分别考虑

下三角,只保留 j≤i

for(int i=1;i<=3;i++) {
    for(int j=1;j<=i;j++)
        cout<<j<<"x"<<i<<"="
            <<i*j<<" ";
    cout<<"\n";
}

先看 3 行版本

● 运行结果
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<<"*";

选择并说明理由

A 3
B 6
C 9
D 12
选 B:1+2+3=6 个。
!

范围随 i 变化。本例没有换行,星号会连在一行,但数量仍然是 6。

PART 03

枚举组合,再筛选条件

一个循环选第一个量,另一个循环选第二个量;条件负责判断。

完整范围不漏不重先算规模
知识讲解

两个独立选择,用两层循环组合

a、b 都从 1、2、3 中选

for(int a=1;a<=3;a++)
    for(int b=1;b<=3;b++)
        cout<<a<<","<<b<<" ";

每个格子代表一组选择

行 / 列
0
1
2
0
1,1
1,2
1,3
1
2,1
2,2
2,3
2
3,1
3,2
3,3

有序对 (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<<" ";

n=4 时

● 运行结果
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;
3 种 a
→
每种 3 种 b
→
每种再有 3 种 c
→
27 组
!

嵌套层数增加,组合数可能增长很快;写完循环前先估算次数。

PART 04

看清 break 与 continue 的范围

读嵌套循环时,要把每个控制语句归到最近的一层循环。

breakcontinue不要改错变量
知识讲解

break:退出当前所在的内层循环

每一行都在 j=3 前停下

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 开始。

知识讲解

continue:跳过内层这一次剩余动作

j=2 时不执行 cout

for(int i=1;i<=2;i++) {
    for(int j=1;j<=3;j++) {
        if(j==2) continue;
        cout<<j;
    }
    cout<<"\n";
}

for 随后仍会执行 j++

● 运行结果
13
13
!

不要把 continue 误解成“结束这一整行”。

知识讲解

规模要先算:每层 1000 次意味着什么?

结构
核心语句次数
直觉
一层 1000 次
1000
约一千次
两层,各 1000 次
1,000,000
约一百万次
三层,各 1000 次
1,000,000,000
约十亿次
!

实际耗时还与每次做什么、机器和时限有关。先算次数,再判断枚举范围是否合理。

知识讲解

写完嵌套循环,沿四个方向检查

变量

内层条件和自增都在操作 j,是否误写成 i?

初始化

每轮外层,内层是否重新从起点开始?

边界

独立上界、i、n-i+1,哪一个符合关系表?

输出

空格和换行的位置,是否对应每列与每行?

小结

今天你学会了:

执行过程
内层走完,外层再变每次重置内层变量
行列关系
矩形:独立上界三角:上界随行号变化
枚举与控制
范围完整,条件筛选break / continue 只管本层
大卫信奥 · J2 知识点 · 第 7 单元
07

二维数组

用行、列两个坐标组织数据,把嵌套循环用到真正的表格上。

二维模型定义与初值两种遍历表格处理
二维数组封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
二维模型
看清行数、列数与两个下标。
B
定义与初值
固定容量、分行初始化和补零。
C
两种遍历
按行、按列,用不同顺序访问。
D
表格处理
求和、交换与转置。
PART 01

用行与列,定位一个格子

班级成绩表、棋盘、地图,都可以先抽象为行和列。

行 i列 ja[i][j]
知识讲解

把一维的一排格子,扩展成多行

2 行 × 3 列

行 / 列
0
1
2
0
82
95
76
1
88
91
84

先找行,再找列

int a[2][3];
// a[1][2]:第 2 行第 3 列

两个下标都从 0 开始。第一对方括号选行,第二对选列。

知识讲解

定义:行容量与列容量都要写清

int a[3][4];
double score[5][3];
char board[8][8];
a 有 3 行
→
每行有 4 个 int
→
一共 12 个元素
!

本课使用固定容量数组。int a[n][m] 不是标准 C++ 的普通固定数组写法。

知识讲解

两个方向,都必须满足各自的边界

数组 int a[3][4]
合法范围
最后一个下标
行下标 i
0≤i<3
2
列下标 j
0≤j<4
3
a[2][3] = 7;   // 合法
// a[3][0]     // 行越界
// a[0][4]     // 列越界
!

不能因为总元素数为 12,就把 a[0][4] 当作合法访问下一行的写法。

知识讲解

按行给初值,最容易看清结构

每一对内层大括号是一行

int a[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

把代码对应到格子

行 / 列
0
1
2
0
1
2
3
1
4
5
6
知识讲解

部分初始化:每行剩余部分也补 0

每行按自己的初值填充

int a[3][4] = {
    {1, 2},
    {3}
};

没有给初值的格子全部为 0

行 / 列
0
1
2
3
0
1
2
0
0
1
3
0
0
0
2
0
0
0
0
知识讲解

需要全零时,直接使用空初始化列表

int a[3][4] = {};
行 / 列
0
1
2
3
0
0
0
0
0
1
0
0
0
0
2
0
0
0
0
!

普通局部数组如果只写 int a[3][4];,元素没有可靠初值;全局数组则会零初始化。

知识讲解

有初值时,可以省行数,不能省列数

列数仍然给定

int a[][3] = {
    {1,2,3},
    {4,5,6}
};

编译器推导出 2 行,每行 3 个元素。

为什么列数很重要?

每行多宽,决定了这一行如何分组,也决定两个坐标如何定位元素。

!

初学时优先明确写行列数,或用分行大括号清楚表达。

知识讲解

读取与修改,都用 a[i][j]

先读,再改

int a[2][3]={{1,2,3},{4,5,6}};
cout << a[1][0] << "\n";
a[0][2] = 9;
cout << a[0][2];

只修改第一行第三列

行 / 列
0
1
2
0
1
2
9
1
4
5
6
● 运行结果
4
9
知识讲解

停一下:下标对应哪一个数?

先独立判断

这句 cout 输出什么?

int a[2][3]={{2,4,6},{8,10,12}};
cout << a[1][1];

选择并说明理由

A 4
B 8
C 10
D 12
选 C:输出 10。
!

行下标 1 是第 2 行,列下标 1 是这一行的第 2 列。

PART 02

嵌套循环,恰好管理行与列

外层选定一个方向,内层把另一个方向走完。

先行后列先列后行存储不变
知识讲解

按行遍历:一行走完,再去下一行

外层 i,内层 j

for(int i=0;i<2;i++)
    for(int j=0;j<3;j++)
        cout<<a[i][j]<<" ";

访问顺序 1 → 2 → 3 → 4 → 5 → 6

行 / 列
0
1
2
0
1
2
3
1
4
5
6
● 运行结果
1 2 3 4 5 6
知识讲解

按列遍历:一列走完,再去下一列

外层 j,内层 i

for(int j=0;j<3;j++)
    for(int i=0;i<2;i++)
        cout<<a[i][j]<<" ";

访问顺序 1 → 4 → 2 → 5 → 3 → 6

行 / 列
0
1
2
0
1
2
3
1
4
5
6
● 运行结果
1 4 2 5 3 6
知识讲解

换的是循环顺序,a[i][j] 的含义不变

方向
外层
内层
访问表达式
按行
行 i
列 j
a[i][j]
按列
列 j
行 i
a[i][j]
!

只是想按列输出时,不要随手把访问表达式也换成 a[j][i];长方形表格很容易因此越界。

知识讲解

按行输入:数据约定要与循环一致

本例 1≤n,m≤10,元素在 ±1000 内

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 个整数

● 运行结果
输入:2 3 1 2 3 4 5 6

实际使用左上 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
PART 03

表格计算,先决定统计范围

全表、单行、单列,区别主要在循环和累加器放在哪里。

总和逐行逐列整行交换转置
知识讲解

全表求和:每个格子只加一次

累加器写在双层循环外

long long sum=0;
for(int i=0;i<n;i++)
    for(int j=0;j<m;j++)
        sum += a[i][j];
cout << sum;

1 到 6 的总和

行 / 列
0
1
2
0
1
2
3
1
4
5
6
● 运行结果
21
知识讲解

逐行求和:每换一行,sum 重新归零

累加器在外层里面初始化

for(int i=0;i<n;i++) {
    int sum=0;
    for(int j=0;j<m;j++)
        sum += a[i][j];
    cout << sum << "\n";
}

每行有自己的结果

行 / 列
0
1
2
0
1
2
3
1
4
5
6
● 运行结果
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 << " ";
}
行 / 列
0
1
2
0
1
2
3
1
4
5
6
● 运行结果
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] 确实存在。全负数表格也可以正确处理。

知识讲解

交换两行:每一列的对应元素都交换

p、q 是合法的行下标

for(int j=0;j<m;j++) {
    int temp=a[p][j];
    a[p][j]=a[q][j];
    a[q][j]=temp;
}

p=0、q=1

行 / 列
0
1
2
0
4
5
6
1
1
2
3

列的位置不变,整行数据一起移动。

知识讲解

交换两列:每一行都要处理

for(int i=0;i<n;i++) {
    int temp=a[i][p];
    a[i][p]=a[i][q];
    a[i][q]=temp;
}
固定 p、q 两列
→
遍历行 i
→
交换每行的两格
!

交换行要遍历列;交换列要遍历行。先说清“固定谁、变化谁”。

知识讲解

转置:原来的行与列互换角色

原表 a:2×3

行 / 列
0
1
2
0
1
2
3
1
4
5
6

新表 b:3×2

行 / 列
0
1
0
1
4
1
2
5
2
3
6
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] 完成转置。

小结

今天你学会了:

定位
两个下标:先行后列两个方向分别检查边界
遍历
按行:外 i 内 j按列:外 j 内 i
处理
累加器位置决定统计范围交换与转置先确定形状
大卫信奥 · J2 知识点 · 第 8 单元
08

结构体

把属于同一对象的信息装在一起,让姓名、成绩和编号始终对应。

自定义类型成员访问记录数组排序规则
结构体封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
自定义类型
理解结构体类型与结构体变量。
B
成员访问
使用点运算符读写字段。
C
记录数组
用数组存放一组完整记录。
D
排序规则
整条记录移动,多关键字逐层比较。
PART 01

一条记录,包含多种信息

数组适合同类型的一组数据;结构体适合同一个对象的多项属性。

类型对象成员
知识讲解

三组平行数组,很容易对不上人

分散保存

string name[3];
int score[3];
int id[3];

如果只交换 score,不交换 name 和 id,信息就错配了。

把同一人的字段打包

编号
姓名
成绩
1
Ada
92
2
Bob
85
3
Coco
92
!

一条记录整体移动,就能保留字段之间的对应关系。

知识讲解

struct 定义一种新的类型

struct Student {
    string name;
    int score;
    int id;
};
部分
含义
Student
结构体类型名,不是某个学生
name / score / id
每个 Student 对象都有这些成员
末尾的 ;
结构体定义必须有分号
知识讲解

类型像表单格式,变量是填好的一张表

struct Student { ... };
→
Student s;
→
s 是一个对象

同一种类型,可以创建多个对象

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 前面,让后面的函数都能使用这个类型。

知识讲解

按成员声明顺序,给对象一个初值

依次对应 name、score、id

Student a{"Ada", 92, 1};
Student b{};

初值列表不是按字段名字猜

对象
name
score
id
a
Ada
92
1
b
空串
0
0
!

使用 {} 更稳妥。普通局部 Student s; 的 string 成员会构造为空串,但 int 成员没有可靠初值。

知识讲解

点运算符:先指定对象,再指定成员

Student s{"Ada",92,1};
cout << s.name << " " << s.score;
s.score += 3;
表达式
含义
s
整条学生记录
s.name
这一条记录的 string 姓名
s.score
这一条记录的 int 成绩
!

修改后 s.score 为 95,姓名和编号保持原值。

知识讲解

输入成员,与输入普通变量一样

本例姓名没有空格

Student s{};
cin >> s.name >> s.score >> s.id;
cout << s.name << " " << s.score;

读入之后,对象完整保存三项数据

● 运行结果
输入:Ada 92 1
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;

选择并说明理由

A Ada
B Bob
C AdaBob
D 不能整体复制
选 A:输出 Ada。
!

b 是复制得到的另一个对象;修改它的 string 成员,不会改变 a.name。

PART 02

结构体数组,保存一组记录

第 i 个学生就是 students[i],再用点运算符访问这个学生的成员。

Student students[100]students[i].score整条记录
知识讲解

结构体数组:每个格子是一条记录

Student students[3] = {
    {"Ada", 92, 1},
    {"Bob", 85, 2},
    {"Coco",92, 3}
};
下标
name
score
id
0
Ada
92
1
1
Bob
85
2
2
Coco
92
3
知识讲解

先用 [i] 找对象,再用 . 找成员

students
→
students[1]
→
students[1].score
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 开始。

知识讲解

遍历记录时,也能使用普通条件

只显示成绩不少于 90 的记录

for(int i=0;i<n;i++) {
    if(students[i].score>=90)
        cout<<students[i].name<<"\n";
}

当前三条记录

name
score
Ada
92
Bob
85
Coco
92
● 运行结果
Ada
Coco
知识讲解

交换名次时,整条记录一起移动

Student temp = students[0];
students[0] = students[1];
students[1] = temp;
新下标
name
score
id
0
Bob
85
2
1
Ada
92
1
!

只交换 score 会把成绩送给另一个人。姓名、编号等属于同一对象的字段必须一起移动。

PART 03

先说明谁在前,再交给排序工具

结构体没有自动知道“成绩优先还是编号优先”;规则需要我们写出来。

比较函数严格在前多关键字
知识讲解

比较函数,回答“a 是否应该排在 b 前面”

bool before(Student a, Student b) {
    return a.score > b.score;
}
a.score
b.score
before(a,b)
92
85
true:a 在前
85
92
false:a 不在前
92
92
false:单看成绩不区分
!

这是成绩降序规则。比较函数不负责交换对象,也不直接输出。

知识讲解

预览 sort:按规则重排一个区间

#include <algorithm>
// 已定义 Student 和 before
sort(students, students+n, before);
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;
}

本例编号互不相同

排序后
name
score
id
①
Ada
92
1
②
Coco
92
3
③
Bob
85
2
知识讲解

多关键字:前一项相同,才看下一项

总分不同 → 总分高在前
→
总分相同 → 单科高在前
→
还相同 → 编号小在前
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 要求的严格弱序关系,不能保证正确行为。

知识讲解

一份完整的小程序,检验记录对应关系

输入 3 条:姓名、成绩;编号自动生成

● 运行结果
输入:3 Bob 85 Coco 92 Ada 92

成绩降序,编号升序

● 运行结果
Coco 92 2
Ada 92 3
Bob 85 1
!

相同成绩按原编号排序;并不是把姓名按字母顺序排。

知识讲解

如果规则完全相同,sort 不保证原顺序

希望得到什么
应该怎么表达
成绩相同按编号升序
比较器明确补 id
成绩相同按姓名字典序
比较器明确补 name
所有关键字都相同
本规则不区分这两条记录
!

不要依赖 sort 恰好保留同分记录的原来顺序;需要的顺序,应当写进规则。

小结

今天你学会了:

定义与成员
struct 定义类型,末尾有 ;对象.成员,字段各有类型
一组记录
数组下标找对象赋值、交换都移动整条记录
排序规则
从最高优先级逐层比较严格比较,不用 >= / <=
大卫信奥 · J2 知识点 · 第 9 单元
09

文件与简单 STL

让程序读写文件,再用标准库完成取极值、交换和区间排序。

文件流重定向常用工具区间排序
文件与简单 STL封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
文件流
打开、读写、检查、关闭。
B
重定向
了解 freopen 如何改变输入输出。
C
常用工具
max、min、swap。
D
区间排序
sort 的范围与比较规则。
PART 01

用文件保存输入输出

键盘与屏幕是输入输出的一种方式;文件也能保存完整数据。

<fstream>输入文件输出文件
知识讲解

文件操作,先把数据通路画清楚

sum.in 的文本
→
ifstream fin
→
程序计算
→
ofstream fout
→
sum.out 的文本

输入文件示例

● sum.in 内容
12 30

输出文件示例

● sum.out 内容
42
!

文件扩展名只是一种约定;具体名称与读写方式,以题目或项目要求为准。

知识讲解

ifstream 读,ofstream 写

#include <fstream>
ifstream fin("sum.in");
ofstream fout("sum.out");
int a,b;
fin >> a >> b;
fout << a+b << "\n";
对象
方向
使用方式
fin
从文件到程序
与 cin 类似,用 >>
fout
从程序到文件
与 cout 类似,用 <<
知识讲解

相对路径,从进程的工作目录开始找

运行工作目录是 /course/run

ifstream fin("sum.in");
工作目录 /course/run
→
查找 /course/run/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() 后再次检查。

知识讲解

把文件输入、计算、文件输出接起来

完整程序按这条路线组织

打开并检查 fin
→
读入并检查
→
打开并检查 fout
→
计算、写入并检查

本页示例的约定

  • 工作目录有 sum.in。
  • 输入两个 ±1000 内的整数。
  • 输出 sum.out;重新运行会覆盖它。
  • 成功后可打开输出文件核对。
PART 02

freopen:把原来的通路接到文件

cin、cout 的写法可以保留;标准输入和标准输出的来源已经改变。

<cstdio>stdinstdout
知识讲解

先重定向,再进行任何输入输出

#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,也不在读写之后再切换通路。

知识讲解

两种方式,都要服从输入输出要求

方面
文件流
freopen 重定向
读写对象
fin / fout 等自定义对象
沿用 cin / cout 或标准 C I/O
影响范围
对应的文件流对象
标准输入 / 输出通路
便于同时处理多份文件
分别创建多个流
通常用于替换标准通路
共同前提
文件名、路径和格式明确
检查打开是否成功
知识讲解

评测怎么要求,就使用哪种输入输出

要求标准输入输出

使用 cin / cout,不保留无条件的本地文件重定向。

明确要求文件读写

严格按题目规定的名称、格式和大小写读写。

本地调试

可用文件保存样例;提交前再次核对目标环境的约定。

!

不存在所有评测都通用的固定文件名;也不能把“本地跑通”直接等同于提交成功。

PART 03

简单的标准库工具,减少重复代码

使用函数之前,先说清参数类型、作用对象,以及它会不会修改原数据。

max / minswapsort
知识讲解

本课认识四个常用工具

工具
作用
本课头文件
max(a,b)
较大值
<algorithm>
min(a,b)
较小值
<algorithm>
swap(a,b)
交换两个对象
<utility>
sort(first,last)
排序指定区间
<algorithm>
!

这些是标准库工具。本课只使用这些简单接口,容器和迭代器体系留待后续课程。

知识讲解

max 和 min 返回结果,不修改原变量

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;
max(4,9) 得到 9
→
max(9,2) 得到 9
● 运行结果
9 2
知识讲解

swap 直接交换两个对象的内容

调用前:a=3,b=8

int a=3,b=8;
swap(a,b);
cout<<a<<" "<<b;
● 运行结果
8 3

适用对象要有相应交换操作

  • 两个 int 变量。
  • 两个 string 对象。
  • 本课的两个同类型结构体对象。
!

swap(a,b) 会修改对象;max(a,b) 只取得结果。

知识讲解

结构体排序时,也可以整条 swap

swap(students[i], students[j]);
姓名
→
成绩
→
编号
→
一起交换
!

若只写 swap(students[i].score,students[j].score),交换的就只有成绩,记录对应关系会被破坏。

PART 04

sort:左端包含,右端不含

先确定哪一段数据有效,再把那一段交给排序函数。

左闭右开默认升序比较器
知识讲解

sort(a,a+n):把前 n 个元素排成升序

排序前
[0]8
[1]3
[2]6
[3]1
[4]5
int a[5]={8,3,6,1,5};
sort(a,a+5);
排序后
[0]1
[1]3
[2]5
[3]6
[4]8
!

sort 会修改数组本身;这里默认从小到大,不是只产生一个新数组。

知识讲解

右端 a+n,是最后一个元素后面的边界

选中区间 [a, a+5)
[0]8
[1]3
[2]6
[3]1
[4]5
端点
对应位置
是否被排序
a
下标 0
包含
a+5
下标 4 后面的边界
不包含,不解引用
!

a+5 可以表示这个合法区间的尾后边界;不能据此读取 a[5]。

知识讲解

只排下标 1~3:右端要写到 4

int a[5]={8,3,6,1,5};
sort(a+1,a+4);
只选择中间 3 格
[0]8
[1]3
[2]6
[3]1
[4]5
两边的 8、5 保持原位
[0]8
[1]1
[2]3
[3]6
[4]5
知识讲解

从 0 还是从 1 存,区间跟着变化

有效数据下标
sort 写法
不包含的右端
0~n-1
sort(a,a+n);
a+n
1~n
sort(a+1,a+n+1);
a+n+1
l~r,包含 r
sort(a+l,a+r+1);
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);

选择并说明理由

A 2、3、4
B 2、3、4、5
C 3、4、5
D 整个数组
选 A:下标 2、3、4。
!

左端包含,右端不包含。元素个数为 5-2=3。

知识讲解

边界检查:有效数量与容量不能混用

n=0 或 n=1

合法的空区间、单元素区间无需改变;仍要保证区间端点有效。

n 小于容量

只排实际读入的前 n 个数据;不要把未用格子混进去。

排序后还要原顺序?

提前复制原数据,或在结构体里保存原编号。

小结

今天你学会了:

文件
按工作目录解释相对路径检查打开、读取和输出状态
工具
max / min 取结果swap 修改两个对象
排序
sort 修改指定区间左闭右开,比较器严格
大卫信奥 · J2 知识点 · 第 10 单元
10

进制转换

同一个整数,可以用不同进制表示;转换时抓住位权和整除取余。

数位与位权双向转换八与十六代码表示
进制转换封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
数位与位权
理解逢几进一和位置的价值。
B
双向转换
按位累积、除基取余。
C
八与十六
用 3 位、4 位二进制分组。
D
代码表示
分清字面量、输入和输出格式。
PART 01

数的值,与写出来的形式,要分开

十进制 13 与二进制 1101,可能表示同一个数量。

基数数码位权
知识讲解

r 进制:每一位只能放 0~r−1

进制
基数 r
可用数码
进位规则
二进制
2
0、1
逢二进一
八进制
8
0~7
逢八进一
十进制
10
0~9
逢十进一
十六进制
16
0~9、A~F
逢十六进一
!

十六进制 A、B、C、D、E、F 分别表示十进制 10、11、12、13、14、15。

知识讲解

从右向左,位权是基数的 0、1、2…次幂

十进制 347
[0]3
[1]4
[2]7
数位
百位
十位
个位
位权
10²=100
10¹=10
10⁰=1
贡献
3×100
4×10
7×1

347 = 3×100 + 4×10 + 7

知识讲解

指数是“重复乘几次”,不是 C++ 的 ^

数学写法
含义
结果
2⁰
约定为 1
1
2¹
2
2
2³
2×2×2
8
2⁴
2×2×2×2
16
!

C++ 的 ^ 是按位异或,不是乘方。小整数的 2 的幂可逐次乘 2;下一课再学习位移。

知识讲解

二进制 1101:把为 1 的位权加起来

从右起,位编号为 0、1、2、3
[0]1
[1]1
[2]0
[3]1
数码
1
1
0
1
位权
8
4
2
1
贡献
8
4
0
1

1101₂ = 8+4+1 = 13₁₀

知识讲解

二进制加法,仍然按位进位

单列规则

相加
写下
进位
0+0
0
0
0+1
1
0
1+1
0
1
1+1+1
1
1

例如 3+1

   11₂
+  01₂
------
  100₂

个位 1+1 写 0 进 1,下一位再处理进位。

知识讲解

二进制减法:向高一位借来的量是 2

100₂−1₂

  100₂
- 001₂
------
  011₂

用十进制核对

100₂ 是 4
→
减去 1
→
011₂ 是 3
!

借位的规则与十进制相同;差别在于每一位的基数是 2。

PART 02

两个转换方向,两种方法

两个方向的方法不同,写代码前先确定转换方向。

value×base+digit除基取余余数逆序
知识讲解

从左到右:旧值乘基数,再加新数码

value = value*2 + digit;
读入的数码
更新过程
value
初始
—
0
1
0×2+1
1
1
1×2+1
3
0
3×2+0
6
1
6×2+1
13
!

读完整串 1101,就得到 13。每一步都把之前的数整体左移一位,再加入新的一位。

知识讲解

字符转数码:用字符与 0 的差值

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 保存。

知识讲解

13 转成二进制:反复除以 2

当前数
除以 2 的商
余数
13
6
1
6
3
0
3
1
1
1
0
1
产生余数 1、0、1、1
→
从后向前读
→
1101₂
!

第一次得到的是最低位。直接按产生顺序输出,会把位序反过来。

知识讲解

把余数存起来,再倒序输出

本例 0≤n≤10⁹

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];

0 要单独处理

if(n==0) {
    cout<<0;
    return 0;
}
!

上面的 while 对 0 一次也不执行;完整程序先处理零。

知识讲解

把基数 2 换成 r,转换方法仍然成立

方向
基本动作
r 进制 → 十进制
value=value*r+digit
十进制 → r 进制
取 n%r,再令 n/=r
余数超过 9
10~15 映射为 A~F
!

基数与数码都必须合法,累计结果也不能超出所选整数类型。

PART 03

8=2³,16=2⁴,所以可以按位分组

每个八进制位对应 3 个二进制位;每个十六进制位对应 4 个。

八进制十六进制从右向左分组
知识讲解

八进制 25:2×8+5,也等于 21

25₈
→
2×8+5
→
21₁₀
八进制一位
0
1
2
3
4
5
6
7
对应 3 位二进制
000
001
010
011
100
101
110
111
!

25₈ → 010 101₂,去掉最前面的多余零后为 10101₂。分组内部的零不能随便删除。

知识讲解

十六进制 2D:2×16+13,也等于 45

2D₁₆
→
2×16+13
→
45₁₀
十六进制数码
A
B
C
D
E
F
数码的十进制值
10
11
12
13
14
15
对应 4 位二进制
1010
1011
1100
1101
1110
1111
!

2D₁₆ → 0010 1101₂ → 101101₂。字母大小写通常都可接受,但自写转换程序要明确支持。

知识讲解

二进制转八或十六:从右边开始分组

按 3 位一组,转八进制

1101011₂
→ 001 | 101 | 011
→   1     5     3
→ 153₈

按 4 位一组,转十六进制

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 保存。

PART 04

代码中的数,与输出格式

整数存储的是值;前缀和流操纵符负责说明或选择表示形式。

字面量前缀hex / oct / dec格式状态
知识讲解

同一个数值,可以有不同整数字面量

cout << 13 << " "
     << 0b1101 << " "
     << 015 << " "
     << 0xD;
● 运行结果
13 13 13 13
!

0b 是二进制前缀,前导 0 表示八进制,0x 表示十六进制。不要给普通十进制整数随手加前导零。

知识讲解

cout 的 hex / oct / dec 会保留状态

int n=26;
cout<<hex<<n<<" "<<n<<"\n";
cout<<oct<<n<<"\n";
cout<<dec<<n;
● 运行结果
1a 1a
32
26
!

设置 hex 后,后面的整数继续按十六进制输出,直到再次切换。标准流没有同样的 bin 操纵符。

知识讲解

cin 也可以按指定进制读取整数

unsigned int n=0;
cin >> hex >> n;
cout << dec << n;
● 运行结果
输入:2D
45
!

这里的输入是十六进制文本;cin 的格式状态也会保留。cout 使用自己的输出格式状态。

知识讲解

C 风格格式:类型和格式说明符要匹配

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 个二进制位

选择并说明理由

A 101 | 100
B 010 | 110
C 101 | 10 后补 0
D 位数不同不能转换
选 B:010|110,得到 26₈。
!

从右边分组;最左组在左侧补零。26₈=2×8+6=22₁₀。

小结

今天你学会了:

位权
第 k 位的权是 rᵏ数码必须小于基数
转换
转十进制:乘基数再加数码转出去:除基取余逆序
代码
前缀说明字面量进制hex / oct / dec 状态会保留
大卫信奥 · J2 知识点 · 第 11 单元
11

位运算

逐位理解整数运算,用清楚的位图和边界规则,避免只背口诀。

逐位运算移位简单应用类型与优先级
位运算封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
逐位运算
与、或、异或各有自己的真值规则。
B
移位
看见高位移出与低位补零。
C
简单应用
掩码、奇偶和成对抵消。
D
类型与优先级
明确无符号类型、位宽和括号。
PART 01

把一个数展开成一排二进制位

本课按常见 32 位 unsigned int 环境演示非负整数;位图只展示低 8 位。

bit低位在右unsigned
知识讲解

位编号从右边的 0 开始

13 的低 8 位
0
0
0
0
1
1
0
1
位编号
7
6
5
4
3
2
1
0
位权
128
64
32
16
8
4
2
1
!

最低位权为 1;位编号与“从左数第几个”不同。补在左边的零不改变非负整数的值。

知识讲解

先统一类型约定,再讨论逐位运算

unsigned int x=13u;
unsigned int y=10u;

本课环境

unsigned int 为 32 位。示例值与移位次数都按这个环境给定。

实际位宽可检查

#include <limits>
numeric_limits<unsigned int>::digits
!

不把有符号负数、符号位和溢出的规则混进初学口诀;换类型时要重新检查规则。

知识讲解

逐位运算与逻辑判断,处理对象不同

类别
运算符
作用
按位
&、|、^、~
处理整数的各个二进制位
逻辑
&&、||、!
按真假进行逻辑判断
移位
<<、>>
移动整数中的位
!

同一个 << 还用于 cout 输出;应根据左右操作数和表达式位置识别含义。

PART 02

先按一位计算,再把每一位拼起来

每一列独立使用同一条规则,不像加法那样进位。

& 都为 1| 至少一个 1^ 不同为 1
知识讲解

一张表,比较与、或、异或

a
b
a & b
a | b
a ^ b
0
0
0
0
0
0
1
0
1
1
1
0
0
1
1
1
1
1
1
0
!

与:同时为 1;或:至少一个 1;异或:这一位不同才为 1。

知识讲解

13 & 10:共同为 1 的位保留下来

13:00001101
0
0
0
0
1
1
0
1
10:00001010
0
0
0
0
1
0
1
0
& 结果:00001000 = 8
0
0
0
0
1
0
0
0
cout << (13u & 10u);
知识讲解

13 | 10:任意一边为 1,这一位就是 1

13:00001101
0
0
0
0
1
1
0
1
10:00001010
0
0
0
0
1
0
1
0
| 结果:00001111 = 15
0
0
0
0
1
1
1
1
cout << (13u | 10u);
知识讲解

13 ^ 10:相同变 0,不同变 1

13:00001101
0
0
0
0
1
1
0
1
10:00001010
0
0
0
0
1
0
1
0
^ 结果:00000111 = 7
0
0
0
0
0
1
1
1
cout << (13u ^ 10u);
知识讲解

补充:~ 翻转整个整数类型中的位

低 8 位翻转示意

原低 8 位:00001101
0
0
0
0
1
1
0
1
翻转后的低 8 位:11110010
1
1
1
1
0
0
1
0

只想观察低 8 位,要再加掩码

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

选择并说明理由

A 8
B 6
C 7
D 1
选 B:00000110,也就是 6。
!

逐位不同为 1。异或本身不产生进位,不能直接当作整数加法。

PART 03

移位:改变每一位所在的位置

使用无符号整数,并保证移位次数在有效范围内。

左移右移0≤次数<位宽
知识讲解

左移 1 位:低位补 0,高位可能移出

x=13:00001101
0
0
0
0
1
1
0
1
x << 1
→
低位补 0
结果的低 8 位:00011010
0
0
0
1
1
0
1
0
cout << (13u << 1);
!

13×2=26 在本例类型范围内,所以结果与乘 2 一致。

知识讲解

无符号右移:高位补 0,低位被丢弃

x=13:00001101
0
0
0
0
1
1
0
1
x >> 1
→
最低位 1 被移出
结果:00000110 = 6
0
0
0
0
0
1
1
0
cout << (13u >> 1);
!

对无符号整数,右移 k 位等于除以 2ᵏ 后向下取整。这里 13/2 得到 6。

知识讲解

左移不保证无限变大,高位可能丢掉

unsigned int x=0x80000000u;
cout << (x<<1);
32 位中的最高位为 1
→
左移一位,移出类型宽度
→
结果为 0
!

在本课 32 位无符号环境中,左移按模 2³² 的结果保存。不能对所有数都只套“乘 2”而忽略类型范围。

知识讲解

移位次数不能为负,也不能达到位宽

32 位 unsigned int 的写法
判断
x<<0、x>>0
次数有效,值不变
x<<1 到 x<<31
次数有效,仍要理解高位移出
x<<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。

PART 04

小应用:看清哪几位在起作用

掩码只选择某些位;异或可让成对出现的数据互相抵消。

低位掩码异或抵消
知识讲解

判断奇偶,只看最低位

unsigned int x;
cin >> x;
if ((x & 1u) == 0u) cout << "even";
else cout << "odd";
12 的最低位为 0
0
0
0
0
1
1
0
0
13 的最低位为 1
0
0
0
0
1
1
0
1
!

结果比较要加括号:(x&1u)==0u。最低位的其他高位在 &1 后都会被清零。

知识讲解

掩码:只保留低 4 位

x=45:00101101
0
0
1
0
1
1
0
1
mask=15:00001111
0
0
0
0
1
1
1
1
x & mask:00001101 = 13
0
0
0
0
1
1
0
1
cout << (45u & 15u);
知识讲解

异或的三条性质,来自逐位规则

性质
如何理解
x^x == 0
每一位相同,结果全部为 0
x^0 == x
每一位与 0 异或,保持原值
顺序、分组可调整
相同无符号类型下可交换、可结合
7 ^ 3 ^ 7
→
7 ^ 7 ^ 3
→
0 ^ 3
→
3
知识讲解

成对抵消:先确认出现次数的约定

unsigned int answer=0;
for(int i=0;i<n;i++) {
    unsigned int x; cin>>x;
    answer ^= x;
}
cout << answer;
!

本例保证:一个数只出现一次,其余每个数恰好出现两次。满足这个约定,异或结果才是要找的那个数。

知识讲解

没有出现次数保证,就不能这样解释结果

满足约定

● 运行结果
输入:7 3 7 9 9
异或为 3

7 和 9 各一对,剩下单独的 3。

不满足约定

● 运行结果
输入:1 2 3
异或为 0
!

三个数都出现一次,0 甚至没在输入中出现。异或只给出位运算结果,不自动理解题意。

PART 05

把想表达的关系,用括号写出来

能背优先级还不够;读者应该直接看出你先计算哪一部分。

移位与加法比较与按位输出表达式
知识讲解

常见优先级,从高到低看

顺序
本课相关运算符
更高
一元 ~
↓
乘除取余 → 加减
↓
移位 << >>
↓
大小比较 → 相等比较
↓
按位 & → ^ → |
更低
逻辑 && → ||
!

这里只列常用关系,并非完整 C++ 运算符表。混用多类运算时,用括号明确分组。

知识讲解

x & 1u == 0u,会先做相等比较

没有括号,含义走偏

x & 1u == 0u
// 等价分组:
x & (1u == 0u)

先取最低位,再比较

(x & 1u) == 0u
!

不要指望按位与比 == 先算。给需要先做的位运算加括号。

知识讲解

cout << 位运算表达式,也要加括号

cout << (x & y);
cout << (x ^ y);
cout << (x << 1);
!

没有括号时,输出用的 << 可能先与 x 结合,后续运算就变成错误的表达式。

知识讲解

停一下:加法和移位,谁先算?

先独立判断

下面表达式的值是多少?

1u << 2 + 1

选择并说明理由

A 5
B 6
C 8
D 9
选 C:先算 2+1,再左移 3 位,得到 8。
!

实际代码建议明确写 1u<<(2+1);想先移位再加一,就写 (1u<<2)+1。

小结

今天你学会了:

逐位规则
& 同时为 1,| 至少一边为 1^ 不同为 1,成对可抵消
移位边界
无符号类型、有效次数高位移出,结果受位宽约束
应用习惯
先说明数据出现次数约定混合表达式主动加括号
大卫信奥 · J2 知识点 · 第 12 单元
12

课程总结

把十一课连接成一张知识地图,用模型、过程和边界检查自己的程序。

数据怎么放过程怎么写结果怎么查
课程总结封面插图
学习目标概览(按内容分组)

今天要走过的路线

A
数据怎么放
数组、文本、二维表、结构体。
B
过程怎么写
循环、函数、库工具与输入输出。
C
结果怎么查
类型、边界、顺序和数据约定。
PART 01

先认出数据,再选择工具

写代码之前,先说清对象是什么、它们是什么关系。

同类型的一组一段文本行列关系完整记录
知识讲解

J2 知识地图:三条线连接十一课

数据组织

  • 第 1 课:一维数组
  • 第 2、3 课:字符数组与 string
  • 第 7 课:二维数组
  • 第 8 课:结构体和记录数组

程序组织

  • 第 4、5 课:函数与作用域
  • 第 6 课:循环嵌套
  • 第 9 课:文件与简单 STL

整数表示

  • 第 10 课:进制与转换
  • 第 11 课:位运算
  • 同一个值,不同表示;
  • 同一种运算,先检查类型。
知识讲解

看到数据,先问它属于哪一种模型

数据特征
优先想到
需要确认
同类型的一组数
一维数组
容量、有效数量、下标
有空格的一段文本
string + getline
空行、前导空格是否保留
固定字符缓冲区
char 数组
结束符与容量
多行多列的同类型数据
二维数组
行列意义与边界
每个对象有不同类型的属性
结构体数组
字段关系与排序规则
知识讲解

数组:容量、有效数量、合法下标

int a[6];当前 n=3
[0]8
[1]3
[2]6
[3]—
[4]—
[5]—
容量 6
→
使用 3 格
→
有效下标 0、1、2
!

求和从 0 开始;最大值从真实元素开始;查找失败要保留专用标记。

知识讲解

两套文本接口,分别记清楚

任务
C 风格字符串
std::string
长度
strlen(s)
s.size()
复制
strcpy,目标容量先验
赋值 =
拼接
strcat,给结束符留位
+ 或 +=
比较
strcmp 的正负与零
==、< 等内容比较
整行输入
cin.getline(s,容量)
getline(cin,s)
知识讲解

输入格式,决定读词还是读行

以空白分开的字段

cin >> name >> score;

适合没有空格的姓名等字段。

整行内容,包括中间的空格

getline(cin, line);

之前使用 >> 时,要按输入约定处理剩余换行。

PART 02

输入、处理、输出,分工清楚

函数让动作有名字,循环让动作有规律,标准库减少重复实现。

参数返回值循环边界区间
知识讲解

一个清楚的函数,回答三个问题

输入是什么?

int add(int a,int b)

参数表达依赖,范围应有约定。

完成什么计算?

return a+b;

一件清楚的小任务,便于验证。

交回什么?

int answer=add(3,5);

返回值交给调用者,输出由使用者决定。

知识讲解

变量追踪:先确定是哪一份变量

全局变量
→
main 的局部变量
→
函数的形参副本
→
内层代码块变量
!

不能仅凭名字一样就认作同一个变量。对普通值形参的修改,不会自动改掉外面的实参。

知识讲解

双层循环:固定什么,变化什么?

任务
外层固定
内层变化
按行访问二维表
行 i
列 j
按列访问二维表
列 j
行 i
输出三角形
当前行 i
1~i 的位置
枚举两个选择
第一个选择
第二个选择的完整范围
知识讲解

结构体与 sort:规则和对象要一起想

字段属于同一条记录
→
比较器描述谁先谁后
→
sort 移动整条记录
if(a.score!=b.score) return a.score>b.score;
return a.id<b.id;
!

先主关键字,再次关键字;相等时比较器应返回 false。区间写成左闭右开。

知识讲解

选工具时,先看它是否修改数据

工具
得到结果
修改原对象吗?
max / min
较大值 / 较小值
本身不修改参数
swap
交换后的内容
修改两个对象
sort
有序区间
修改选定区间
string.find
匹配起点 / npos
不修改字符串
知识讲解

文件与屏幕,是两条不同的输出通路

标准输入输出

cin
→
计算
→
cout

题目要求标准输入输出时,不保留无条件的文件重定向。

明确要求文件时

检查打开
→
检查读取
→
写入并检查

按规定的名称和格式读写,确认实际工作目录。

PART 03

检查整数的值、表示与运算

进制改写数字的样子;位运算直接处理选定整数类型的二进制位。

表示形式位权类型宽度
知识讲解

进制转换,两种方向各记一个动作

r 进制 → 十进制

value=value*r+digit;

数码合法;从左到右累积;结果不溢出。

十进制 → r 进制

digit=n%r;
n/=r;

余数从低位产生,最后逆序;0 要有明确输出。

知识讲解

位运算:规则之外,还要检查类型与分组

问题
检查点
&、|、^ 算什么?
逐位计算,不与 &&、|| 混用
移位几次?
次数非负,并且小于实际位宽
左移会一直变大吗?
高位可能移出,结果受类型约束
表达式先算哪部分?
按位运算、比较、输出混用时加括号
异或能找唯一值吗?
必须先满足“其余成对”的数据约定
PART 04

程序写完,用小数据检查你的假设

不要只找一个能运行的样例;要挑能区分正确与错误的边界。

空与最小重复与负数首尾位置极限容量
知识讲解

为每种结构,准备有针对性的边界

结构 / 操作
优先检查的数据
数组
n=0 是否允许;n=1;恰好满容量;全负数
字符串
空行;一个字符;前后有空格;查找在开头或不存在
二维数组
1 行、多列;多行、1 列;非正方形
记录排序
同分;多关键字仍相同;原编号与下标变化
进制与位运算
0;最大约定值;非法数码;有效移位边界
知识讲解

检查顺序:先定位问题属于哪一层

编译报错 → 语法、声明和类型
→
运行异常 → 越界、未初始化和无效操作
→
答案错误 → 数据约定、边界和执行顺序
!

用很小的数据手推变量和下标,再与实际输出逐步比较;避免一次改动多个不相关位置。

知识讲解

把知识连接起来:一份成绩表的数据通路

读入姓名和成绩
→
结构体保存记录
→
函数计算派生结果
→
比较器描述规则
→
sort 排序
→
遍历输出

先说明数据约定

人数上限、姓名是否有空格、成绩范围、同分顺序。

再决定实现工具

固定容量结构体数组、清楚的函数接口、合法的排序区间。

!

同样的程序组织,也适用于商品、比赛记录和坐标点等数据。

知识讲解

能说清楚原因,才算把规则用起来

存储

  • 为什么第 k 个常对应下标 k-1?
  • 为什么字符数组要给结束符留一格?

过程

  • 为什么改形参,外面的 int 不一定变?
  • 为什么每换一行,行和要重新归零?

规则

  • 为什么 sort 的右端要多写一格?
  • 为什么异或结果不总是“唯一的数”?
小结

今天你学会了:

认识数据
一维、文本、二维、记录先建立模型再选工具
组织程序
循环描述重复,函数封装动作输入输出和计算各有职责
检查假设
类型、容量、边界、执行顺序用能区分对错的小数据验证
—

完整程序 · C++17