26Spring CS100 Midterm Review
正文by @AlexWei
笔误与细节修正by Gemini3.1 Pro
CE&UB汇总与历年考试总结部分全部由LLM生成,供参考
CE&UB汇总
该部分Generated by Google NotebookLM,Revised by Gemini3.1 Pro
1. 编译错误 (Compile Error, CE)
编译错误是指违反了语言的语法或类型系统规范,导致编译器无法生成可执行文件。
1.1 类型系统违规
静态类型不匹配:
C/C++ 是静态类型语言,不能将不兼容的类型直接赋值。
1
2int a = 42;
a = "hello"; // CE: 类型不匹配auto 缺乏初始化:
使用
auto推导类型时必须提供初始值。1
auto m; // CE: 无法推导类型
1.2 指针与引用限制
取地址限制:
不能对字面值或临时表达式取地址。
1
int *p = &42; // CE: 42 不是左值
引用初始化规则:
左值引用必须绑定到左值,且必须初始化。
1
2int &r; // CE: 必须初始化
int &r2 = 42; // CE: 不能绑定到字面值不存在的引用类型:
不允许定义指向引用的指针或引用的引用。
1
2int & &rr = r; // CE: 没有引用的引用
int &*pr = &r; // CE: 没有指向引用的指针
1.3 类与对象控制
访问权限违规:
在类外部访问
private成员。1
2class A { int x; };
A a; a.x = 1; // CE: x 是私有的const 成员函数限制:
在
const成员函数中修改数据成员(除非是mutable)。1
void Student::foo() const { name += "a"; } // CE: const函数不能修改成员
构造函数缺失:
调用不存在的默认构造函数。
1
2class A { public: A(int x) {} };
A obj; // CE: 已定义了带参构造,不再合成默认构造
1.4 其他语法限制
switch 标签要求:
case标签后必须是整型常量表达式。1
2int n = 10;
switch(x) { case n: ... } // CE: n 不是常量表达式数组长度:
非 VLA 数组长度必须是编译时常量。
1
2int n; scanf("%d", &n);
int arr[n]; // 在 C++ 或某些 C 标准下若不支持 VLA 则 CE数组初始化与赋值:
初始化器数量不能超过数组长度;数组成员不能直接复制赋值。1
2int b[2] = {2, 3, 5}; // CE:提供3个但预期2个
int a[10]; int c[10] = a; // CE:数组不可直接赋值嵌套数组维度不匹配:
向期望固定维度多维数组的函数传递其他维度数组。1
2
3void fun(int (*a)[5]);
int b[3][10];
fun(b); // CE(或编译器警告/UB):维度不匹配C 风格字符串相加:
直接使用+连接两个 C 风格字符串(const char*)。1
"hello" + "world"; // CE:禁止直接用 + 拼接指针
const未初始化或被修改:
必须给出初值,后续不可更改。1
2const int n; // 错误:C++中未初始化
n = 42; // 错误:修改只读变量
2. 未定义行为 (Undefined Behavior, UB)
未定义行为是指标准未规定后果的代码,程序可能产生随机结果、崩溃或被编译器优化掉。
2.1 算术与赋值
有符号整数溢出:
有符号数超出范围是 UB。
1
2int x = 2147483647; // 假设 int 是 32 位
x = x + 1; // UB: 有符号溢出求值顺序冲突:
在同一个表达式中未指定顺序地多次修改变量。
1
2i = i++ + 1; // UB: 修改顺序未指定
printf("%d %d", i, i++); // UB
2.2 指针与内存安全
解引用非法指针:
解引用空指针、野指针或悬空指针。
1
2int *p = NULL;
*p = 10; // UB: 解引用空指针非法内存释放:
Double free 或释放非动态分配的地址。不能逐个释放数组元素、释放偏移地址或部分释放。
1
2
3int x = 10; free(&x); // UB: 释放非动态内存
free(p); free(p); // UB: Double free
free(p + 50); // UB: 释放偏移地址内存配对错误:
new对应delete,new[]对应delete[]。1
2int *a = new int[10];
delete a; // UB: 应该是 delete[],因为分配的是数组返回局部变量地址:
访问已销毁的栈内存。
1
2int *foo() { int x = 42; return &x; }
int val = *foo(); // UB: x 已销毁指针运算超出数组范围:
即使不解引用,指针加减超出数组及其尾后(past-the-end)位置也是 UB。1
2
3int a[10];
int *p = &a[0];
p - 2; // UB: 超出数组起始严格别名违规 (Type-punned pointer dereference):
通过实际底层类型不匹配的指针解引用(例如用float*操作int变量)。1
2
3int i = 42;
float *fp = (float *)&i;
++*fp; // UB: 类型不匹配解引用解引用动态分配失败的空指针:
如果malloc返回 NULL,解引用会导致未定义行为。1
2int *arr = malloc(sizeof(int) * n);
arr[0] = 42; // 若 malloc 失败,此时等于解引用 NULL (UB)
2.3 数组与容器
下标越界:
访问数组索引
i < 0或i >= N。1
2int a[5];
a[10] = 1; // UB: 越界访问字符串缺失终止符:
处理没有
\0的 C 风格字符串。1
2char s[5] = "abcde"; // 数组大小为5,没空间存 '\0'
puts(s); // UB: 会一直往后读直到崩溃迭代器失效:
在
vector扩容后使用旧迭代器。1
2
3auto it = v.begin();
v.push_back(42); // 可能导致扩容
*it = 1; // UB: 迭代器可能已失效
2.4 修改只读数据
修改字符串字面值:
尝试修改存储在只读区的数据。
1
2char *p = "hello";
p[0] = 'H'; // UB: 尝试修改只读内存强行去除 const 修改:
通过转换去除
const后修改原为 const 的对象。1
2
3const int ci = 42;
int *p = (int *)&ci;
*p = 43; // UB
2.5 对象生命周期
使用未初始化变量:
使用局部非静态变量的随机值。
1
2int n;
int m = n + 1; // UB: 使用不确定值非 void 函数缺少返回:
函数路径未执行
return语句且使用了返回值。1
2int fun() { /* 缺少 return */ }
int x = fun(); // UB
C Part
Signed and unsigned integers 整型
一些前置概念:
width: 这种integer type用几位二进制数表示值
signed: 取值范围为
$$
[-2^{n-1},2^{n-1}-1]
$$unsigned: 取值范围为
$$
[0,2^{n}-1]
$$
| type | width (at least) | width (usually) |
|---|---|---|
| short | 16 bits | 16 bits |
| int | 16 bits | 32 bits |
| long | 32 bits | 32 or 64 bits |
| long long | 64 bits | 64 bits |
对应格式化字符串(in scanf&printf)
| type | format specifier |
|---|---|
short |
%hd |
int |
%d |
long |
%ld |
long long |
%lld |
unsigned short |
%hu |
unsigned |
%u |
unsigned long |
%lu |
unsigned long long |
%llu |
关于overflow
只有signed integers要考虑overflow, unsigned超限会”wrap-around”
在赋值语句中, 既需考虑临时右值的生成过程, 也需考虑赋值对象的类型
1 | int ival = 100000; long long llval = ival; |
自增++与自减--
- Postfix
a++,a--先返回原来的值, 再对a做自增/减 - Prefix
++a,--a先对a做自增/减, 再返回新的a值
Floating types 浮点数类型
一般用double即可
float单精度double双精度long double扩展精度
boolean type
定义在<stdbool.h>在if语句调用时1等价于true, 0等价于false
do-while循环
与while有什么区别? 在每次迭代中, 条件判断在函数体内something()执行后再检查
1 | do{ |
等价于
1 | while (1){ |
switch-case条件选择
expression被计算- 寻找使
expression == label为真的label, 执行对应case的body - ⚠️注意 从匹配的case起, 所有接下来的case都将不再匹配直接执行, 直到
break;出现或到switch末尾
1 | switch (expression) { |
Conditional operator ?!
动机: 简化表达的if-else控制流
condition ? expressionT : expressionF
等价于
1 | if (condition) |
Implicit initialization
只定义一个变量而不显式初始化? 会发生什么?
- 局部非静态变量, 未初始化(值不确定 e.g. wild pointers)
- 对全局变量和局部静态变量, 零初始化(空指针,
0,0.0, …)
数组和指针 Array&Pointer
数组的初始化
花括号初始化 单层array
1 | int a[10] = {2, 3, 5, 7}; // Correct: Initializes a[0], a[1], a[2], a[3] |
指定位置初始化
1 | int e[10] = {[0] = 2, 3, 5, [7] = 7, 11, [4] = 13}; //{2, 3, 5, 0, 13, 0, 0, 7, 11, 0} |
嵌套(nested) array初始化
1 | int a[4][3] = { // array of 4 arrays of 3 ints each (4x3 matrix) |
数组的类型
我们声明ElemType arr[N];, 数组arr的类型是ElemType [N]
Pointer arithmetic
a是一个T [N]类型的array时, 令p = &a[0], 此时p+i等价于&p[i],*(p+i)等价于a[i]
p + ireturns the address(char *)p + i * sizeof(T)i.e.
i * sizeof(T)bytes away from p.
Array-to-pointer隐式转换
a会被隐式转换为&a[0], T[N]可被隐式转换为T *因此:
p = &a[0]可以直接写成p = a*a等价于a[0]
应用: 直接用指针遍历array
1 | int a[10]; |
向函数传递数组
BIG IDEA: 向函数传递数组的唯一方式是传递它的首元素地址
1 | void fun(int *a); |
Pointer to array
动机: 将一维数组传入函数时, Type [N]会被隐式转换为Type *, 那二维数组Type [N][M]会被转换成什么呢?
类比一维数组, 所谓的”2d-array”Type [N][M]本质就是N个元素的一维数组, 只是每个元素类型是Type [M], 那作为函数参数, 就理应被转换为指向Type [M]类型的指针, 所以这里N是什么根本不要紧(回忆上述一维数组时的等效写法), 这就是所谓指向数组的指针(Pointer to array), 我们记作Type (*)[M]
⚠️要注意, 这里不要被一维时从
array到pointer误导, 把Type[M]默认理解为等价于Type *In other words:
Type (*)[M]和Type **是不一样的, 前者是指向array(成员类型为Type)的指针, 后者是指向Type *的指针
1 | void fun(int (*a)[M]); |
语法辨析速查
在下列表达式中, a的类型是什么? 它接受int[N][M]类型的参数吗?
void fun(int a[N][M]): A pointer toint[M]. 接受✅void fun(int (*a)[M]): A pointer toint[M]. 接受✅ Same as 1.void fun(int (*a)[N]): A pointer to int[N]. 如果N == M则接受✅, 否则拒绝❌void fun(int **a): A pointer toint *. 拒绝❌void fun(int *a[]): A pointer toint *. 拒绝❌void fun(int *a[N]): A pointer toint *. 拒绝❌void fun(int a[100][M]): A pointer toint[M]. 接受✅void fun(int a[N][100]): A pointer to int[100] . 若M == 100则接受✅, 否则拒绝❌
(重要)和Array of Pointers怎么区分呢?
一个不规范的个人记法:
为了避免混淆
我们要强调
pointer_name是个指针, 就要打个小括号把*和指针名pointer_name放在一起若不打小括号? 那
*就默认和前面的int跟在一块了, 它们一起代表数组的存储数据类型是int *
- Pointer to array
一个指针, 指向一个存N个int的数组
1 | int (*pointer_name)[N]; |
- Array of Pointers
一个数组, 存N个指向int的指针
1 | int *array_name[N]; |
void *
一种特殊的指针类型, 具有以下性质
任何指针都可隐式转换换为
void *void *类型的指针可隐式转换为任何指针类型printf("%p", ptr)中格式化占位符%p要求的是void *, 要传其他类型指针的话要先转换为void *printf("%p", (void *)ptr);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
- 动态内存分配函数(`malloc`)返回`void *`, 即所分配内存的地址
## 关于`const`
| 种类 | 别名 | 语法 | 定义/性质 |
| ------------------ | --------------------- | ----------------------------------- | ------------------------------------------------------------ |
| `const` variables | \ | `const T var_name = xxx` | 不允许直接修改 |
| pointer to `const` | Low-level `const`ness | `const T *pointer_name = &var_name` | 让编译器*thinks*指向了`const`变量,加`lock`<br>指向`const` 变量必须用, 指向non-`const`变量可以用 |
| `const` pointer | Top-level `const`ness | `T *const pointer_name = &var_name` | 指针本身是`const`的,<br>初始化后不能改指向到其他变量 |
- 补充 Low-Level和Top-Level const是可以共存的, 例如`const int *const pointer_name = &var_name`
## 动态内存
前置基础知识 Stack vs heap(dynamic) memory
- Stack通常更小, 用于存放局部和临时对象, 操作速度快, 自动分配内存
- Heap 通常存大/生命周期更长的对象, 操作速度较慢, 需手动管理分配
下列函数定义在`<stdlib.h>`
### `malloc`
定义
```C
void *malloc(size_t size);
用法
1 | T *ptr = malloc(n * sizeof(T)); // sizeof(T) * n bytes |
calloc
定义
1 | void *calloc(size_t num, size_t each_size); |
用法
1 | T *ptr = calloc(n, sizeof(T)); // sizeof(T) * n bytes |
free
动态管理的内存直到我们free它才会被销毁
请注意: 即使是malloc(0), calloc(0, N), calloc(N, 0)也必须free
它们的行为是:
- may or may not allocate memory
- 没有allocate内存就return空指针
- 如果allocate了内存, 会返回对应地址
- 你始终不可以解引用返回的指针
1 | free(ptr) |
C-style strings
本质上是 存char的array
1 | char s[10] = "abcde"; //s = {'a','b','c','d','e','\0'} |
I/O
1 | char str[100] = "abcdef"; |
注意scanf内存不安全, 最好的alternative是fgets
1 | char str[10]; |
1 | char str[100]; |
<string.h>
strlen(str): 返回字符串长度(不含\0)strcpy(dest, src): 深拷贝字符串Copies the string src to dest.strcat(dest, src): 字符串拼接 Appends a copy of src to the end of dest .strcmp(s1, s2): 字典序比较 Compares two strings in lexicographical order.strchr(str, ch): Finds the first occurrence of ch in str , 并返回对应位置指针
易错Q&A速查(generated by ChatGPT)
Q1: strcpy 会在末尾添加空字符 '\0' 吗?strncpy 呢?
A1:
strcpy(dest, src)会复制src,包括最后的'\0'。strncpy(dest, src, n)不一定会添加'\0'。- 如果
src长度小于n,会补零直到长度为n。 - 如果
src长度大于等于n,则不会自动添加'\0'。
- 如果
Q2: 对于 strcpy(dest, src),如果 dest 和 src 指向同一块内存,会发生什么?如果它们部分重叠呢?strcat 又如何?
A2:
- 如果
dest和src完全相同(同一个地址),通常结果等价于“不做任何事”。 - 如果它们发生部分重叠(overlap),行为是未定义行为(undefined behavior)。
strcat也是一样:如果源和目标区域重叠,行为未定义。
Q3: strcmp 的返回值是什么?它返回的是布尔值(true / false)吗?
A3:
不是。
strcmp(a, b) 返回的是一个整数:
< 0:a < b= 0:a == b> 0:a > b
比较依据是字典序(lexicographical order)。
例如:
1 | strcmp("abc", "abd") < 0 |
它不是返回 true / false 的函数。
字符串转换为数值类型
#include <stdlib.h>
strtol , strtoll , strtoul, strtoull , strtof , strtod , strtold
字符串字面值
string literals是只读量, 应当用low-level const保护, 防止意外修改的危险行为(UD/RE)
1 | const char *str ="abcde"; |
但可以拷贝到数组里
1 | char arr[] = "abcde" |
Array of Strings
本质是array of pointers, 每个pointer指向一个字符串字面值, 不是二维数组!
1 | const char *an_array_of_strings[] = { |
Command line arguments
1 | int main(int argc, char **argv){ |
argc是传入参数数量, 注意含程序名argv指向一个数组首元素, 这个数组有argc+1个指针, 前argc个元素(从argv[0]到argv[argc-1])是各参数(含程序名), 最后一个元素argv[argc]是NULL
一个读取任意长度字符串的函数例
1 |
|
struct
定义一个struct
1 | struct Student { |
实例化(注意与C++不同, 这里一定要写关键字struct)
1 | struct Student stu; |
动态分配
1 | struct Student *pStu = malloc(sizeof(struct Student)); |
将struct实例作为参数传入函数, 或被函数所return, 语义上都是member-wise拷贝
Recursion 递归
计算n的阶乘(n!)
1 | int factorial(int n) { |
选择排序 Selection-sort
1 | void sort_impl(int *a, int k, int n) { |
C++ Part
Lec11-13 C++ Basics
Standard library
- Standard Library中的names都被placed in
std这个namespace, e.g.std::cin - 对C standard library的兼容: 改
#include <xxx.h>为#include <cxxx>
<iostream>
std::cin, std::cout
e.g. A+B Problem
1 |
|
Question: std::cin >> s会忽略leading whitespaces吗?
Answer: 会. 例如用户输入” abc”, 实际读到s == "abc"
Question: std::cin >> s会读取整行吗? 什么时候停止读取?
Answer: 不会, 一旦遇到空白即停止. 例如用户输入”Hello World”, 实际读到s == "Hello"
Question: 如果我既不想忽略leading whitespaces, 又不想读取整行, 怎么做?
Answer: 使用std::getline(std::cin, s) 它会一直读到换行符\n再停(consume it) , 但并不会存储\n
<string>
定义与初始化
1 | std::string str0 = "aaa"; //operator= overloading |
可以用+做拼接(concatenation), 至少一边是std::string, +是左结合
用a += b, 语义同a = a+b, 但省去了建构临时objecta+b, 拷贝a的资源浪费
直接用<,<=,>,>=,==,!=等符号即可做字典序(lexicographical)比较, 不用像C一样用strcmp
Member functions
.size().empty()
<vector>
动机: 一个”better dynamic array”, 自动管理内存, 提供一系列内置方法, 自动拷贝, 有默认空初始化
- 存储不同
<类型>成员的std::vector不是同一类型
Create a std::vector
回忆: constructor相关知识
1 | std::vector<int> v{2, 3, 5, 7}; // A vector of `int`s, |
Copy a std::vector
1 | std::vector<int> v2 = v; // `v2`` is a copy of `v` |
内置方法(类成员函数)
.size()
.empty()
.push_back()(加到末尾 回忆python list.append())
.pop_back() (删除末尾 回忆python list.pop())
.front()和.back() 返回首/尾位置的引用
.at(index) 比[index]更好的下标访问, 有边界检查, 越界会throw exception: std::out_of_range
References (左值)引用
概念理解: 把一个别名(alias)绑定到一个对象(object)上
syntax: ReferredType &reference_name = some_referred_to_object
行内声明多个同类型references时, &要写在每一个reference_name前
错误示范:
1 | int& x = ival, y = ival, z = ival; |
一个引用只能被一次性绑定到一个对象, 不可重复绑定, 但一个对象可以有多个引用
补充: * and &各种用途大汇总
Both symbols have many identities!
In a declaration like Type
*x = expr,*is a part of the pointer typeType *.*声明指针In a declaration like Type
&r = expr,&is a part of the reference typeType &.&声明引用In an expression like
*opndwhere there is only one operand,*is the dereference operator.*解引用In an expression like
&opndwhere there is only one operand,&is the address-of operator.&取地址In an expression like
a * bwhere there are two operands,*is the multiplication operator.*做乘法In an expression like
a & bwhere there are two operands, & is the bitwise-and operator.&二进制与
reference-to-const
动机: 三大优点如下
避免拷贝 Avoids copy.
可绑右值(注意这点与一般reference不同) Accepts temporaries and literals (rvalues).
避免错误修改应该只读的量 The const qualification prevents accidental modifications to it.
e.g.
1 | int count_lowercase(const std::string &str) { // `str` is a reference-to-`const` |
Range-based for loops
e.g. for (char c : s)
联想: 类似python中for c in s?
- 若要对
s进行修改, 需声明循环变量c为引用char &c :s
类型转换
字符串 and 数字(arithmetic numbers)
function(此处省略std::, 定义在<string>) |
explanation |
|---|---|
stoi()(string to int), stol()(string to long), stoll(string to long long) |
字符串–>有符号整数 |
stoul()(string to unsigned long), stoull()(string to unsigned long long) |
字符串–>无符号整数 |
stof()(string to float), stod()string to double, stold()(string to long double) |
字符串–>浮点数 |
to_string() |
整数/浮点数–>字符串 |
Explicit Casts
const_cast
去掉low-level constness(DANGEROUS)
注意⚠️: 从const_castform的non-const access path修改const变量(top-level-const)会UB, 非必要不用const_cast 这样的危险行为
1 | int ival = 42; |
reinterpret_cast
转换指针类型(DANGEROUS)
注意⚠️: 勿忘实际指向的对象还是原类型, 不能当成一般转换后类型的指针用, 非必要不用reinterpret_cast
static_cast
详见Lec19 类型转换部分
auto
动机: 声明变量时, 让编译器推断类型, 简化某些very long types, 或者我们不知道但编译器知道的特殊类型, 如对lambda expression
一些例子:
1 | auto x = 42; // `int` |
补充:decltype(expr)函数, 可以不执行expr(它可以是对另一个函数的调用)而推断返回类型
Function overloading
总结: 共用函数名, 具有不同参数类型, 调用时分别match
a simple example
1 | void fun(int); |
nullptr
在C++中建议用nullptr代替NULL作为空指针
nullptrhas a unique typestd::nullptr_t(defined in<cstddef>), which isneithervoid *nor an integer.
Lec14-16 Class Basics
成员变量与成员函数
this指针
member function作为object的方法, 隐式接受对应objectclassName *类型的指针this, 并将在函数体内对成员变量xxx的访问, 隐式转换为this->xxx //等价于(*this).xxx
access control
private成员,只能通过成员函数或friend函数访问public成员, 在程序任意处可访问
const member functions
位置: 参数列表(T param)后 function body {前
e.g.bool graduated(int year) const {...}
用于只访问不修改 成员变量 的 成员函数
函数内的this指针也自动带上了const成为const X *
Constructors 构造函数
名字同类名
构造函数没有返回值 没有返回类型
初始化列表 initializer list
The initializer list starts with
:, and contains initializers for each data member, separated by,. The initializers must be of the form(...)or{...}, not= ....
概括: 接在参数列表后, 冒号开头, 以成员变量名(初始化值)或成员变量名{初始化值}格式
e.g.
1 | Student(const std::string &name_, const std::string &id_) |
- 成员按照声明的顺序初始化, 并非按照initializer list
顺序不一致会发生什么?
If the initializers appear in an order different from the declaration order, the compiler will generate a warning.
Typical mistake:
entranceYearis initialized in terms ofid, butidis not initialized yet!
1
2
3
4
5
6
7
8 class Student {
std::string name;
int entranceYear; // !!!
std::string id;
public:
Student(const std::string &name_, const std::string &id_)
: name(name_), id(id_), entranceYear(std::stoi(id.substr(0, 4))) {}
};
Q: **(重要)**为什么不在constructor函数体内部用赋值初始化? 就像这样:
1 | class Student{ |
A: 在进入函数体之前, 由于没有写initializer list, 编译器会先将成员变量都做默认初始化, 可万一某个类型不能默认初始化呢? 这不就出错了吗
不能默认初始化/被赋值的类型们:
T &引用 不能初始化 不能被赋值const对象 不能被赋值- 被设计为拒绝默认初始化的
class
默认构造函数 Default Constructors
格式: 没有输入参数的Constructor 如Point2d() : x(0), y(0) {}
作用: 规定对象的默认初始化行为
如果没有任何自定义constructor, 编译器才会synthesize a default constructor
如果有自定义constructor, 还需要synthesized default constructor,使用className() = default
Copy Constructor& Copy-assignment operator
动机: 避免shallow copy(指向同一块内存)
| type | syntax | 适用 | 备注 |
|---|---|---|---|
| Copy Constructor | className(const className &other):xxx(yyy){....} |
拷贝初始化(copy-initialization)T s2=s1(declaration statement), T s2(s1), T s2{s1} |
/ |
| Copy-assignment operator | className &operator=(const className &other) |
赋值表达式s1 = s2(expression) |
注意self-assignment safe |
- 用或删synthesized one:
= default,=delete
The rule of three
要么自定义0个, 要么自定义3个
更完备的版本: 详见Lec17 The Rule of Five
- copy constructor
- copy-assignmnet constructor
- destructor
Destructors 析构函数
动机: 当一个object生命周期结束时, 需要释放掉对应内存资源
格式: ~ClassName() {...}
What’s more: 在析构函数中, 成员变量在函数体执行后再摧毁, 摧毁顺序和declare相反
Type alias members 类型别名成员
动机: A better way of declaring type aliases
与传统C相比较
1 | // C-style |
作为class member定义时, usage: className::TypeAliasName
All standard library containers (and std::string ) define the type alias member size_type as the return type of .size()
1 std::vector<int>::size_type j = v.size();
static members
static data members
- 理解为一种”global variable”
- 属于类本身, 不属于类的任何对象, 但可以通过对象用
.来访问 - 用
className::来调用(回忆 type alias members)
static member functions
- 同静态数据成员, 属于类本身, 不属于类的任何对象, 但可以通过对象用
.来访问, 但不会绑定到this指针
friend functions
动机: 如何在一个类外定义的非成员函数中访问类内private members呢?
syntax: 随便定义在class的哪里(但通常在首尾), 照抄函数定义在{//function body...}前的内容, 前面加friend关键字, 删掉参数名
即在类内声明(declare)这个函数. 回忆: 类似function prototype (*this is not covered in CS100)
e.g.
1 | void print(const Student &stu) { |
What’s more:
- 一个
class也可以是friend - 用
::可以在类外定义成员函数
Lec17 右值引用和移动
右值引用
动机:s=a+b时, 右值a+b会被拷贝给临时变量再赋值给s, 太慢了, 能否直接steal the contents
1 | int &&rr = 42 //'rr' is an rvalue reference |
class的移动函数
move assignment operator范例
1 | class Dynarray{ |
有了如上的move assignment operator后, 右值被移动,左值被拷贝
1 | a = concat(b, c); // calls move assignment operator, |
The Rule of Five
The Rule of Five: Define zero or five of them.
- copy constructor
- copy assignment operator
- move constructor
- move assignment operator
- destructor
std::move
动机: m_label = other.m_label;这会call copy assignment operator, 因为other.m_label是左值, 那如果我想要移动呢?
Defined in <utility>
std::move将左值转换为右值, 方便我们移动(call move assignment operator)
Copy Elision&NRVO
copy elision(拷贝省略)(since C++17): 以下代码不发生拷贝或移动, 直接将a+b的结果构造在s的内存上
1 | std::string foo(const std::string &a, const std::string &b) { |
NRVO=Named Return Value Optimization 理解为特殊的copy elision
例如
1 | Named Return Value Optimization, NRVO |
Lec18 智能指针
std::unique_ptr
规范构造方法 使用std::make_unique<Type>(args)
1 | auto p = std::make_unique<std::string>(5,'c'); // 管理字符串"ccccc" |
unique所有权, 只能move不能copy
1 | auto r = std::move(p); //正确, p变为null, 管理对象所有权转交给r |
错误示范❌
1 | auto q = p; //错误! 不允许copy 不要这样写! |
std::shared_ptr
规范构造方法 使用std::make_shared<Type>(args);
1 | auto p = std::make_shared<std::string>(10,'c'); |
拥有方法use_count(),返回reference counter计数,用途是通过追踪有多少个std::shared_pointer正共享资源,当计数归零时可释放资源
Lec19 Operator Overloading
运算符重载语法
举例如下
1 | bool operator<(const Rational &lhs, const Rational &rhs) {...} |
特殊例子1(重载标准输入输出)
1 | class Rational { |
注意要点
std::ostream类型匹配std::cout,std::istream类型匹配std::cin- 操作的应当是原来的输入输出流, 因此流对象
std::cout,std::istream应该以引用&方式传入和返回
特殊例子2(++与–的prefix与postfix)
1 | class Rational { |
类型转换
区分显式转换与隐式转换: 显式转换<–>目标类型U在转换表达式中显式写出
显式转换三种形式
| 表达式 | 解释 | 举例 |
|---|---|---|
✅ what_cast<U>(expr) |
C++ named casts | static_cast<int>(3.14)将3.14转换为int |
✅U(expr) |
类比构造函数使用(constructor call) | std::string("xx") 将const char[3]转换为std::strng |
| ❗不推荐 (U)expr | C-style 兼容但不推荐在C++中继续使用 | (int)3.14 |
类型转换运算符
在自定义class中定义了转换到double的运算符operator double() const {...},
此时可以自动发生隐式转换,如double dval = r;
- 记得加
const - 若要禁止隐式转换调用, 在返回之前加
explicit关键字,如explicit operator double() const {...}
Lec20&23 Iterators and Algorithms
迭代器基础
动机: Iterator是更generalized的”pointer”, 用来获取容器成员量
每个 container 都有对应的iterator,类型为 Container::iterator,实际使用时用 auto 让编译器自动推断即可。
1 | for (auto it = str.begin(); it!= str.end(); it++){...} |
普通指针也是iterator,例如数组Type a[N], 初始迭代器为a, 终止迭代器为a+N
在<iterator>中定义了std::begin和std::end函数, 统一了返回STL容器或数组begin/end iterator的接口
数组的delete[],std::vector的push_back(),pop_back()等方法会导致原先iterator的无效化(invalidation)
迭代器的分类
Forward Iterators 只能向前移动
Bidirection Iterator 可以双向移动
Random Access Itertator 可以任意移动(
it+n,it[n],…)std::vector, 指针是经典的随机访问迭代器std::sort必须接受Random Access Iterator
用迭代器范围初始化STL容器
1 | std::vector<char> v = {'a','b','c','d','e','f','g','h','i'}; |
STL algorithms 参数格式
接受pairs of iterators来表示范围
1 | int a[N]; |
STL algorithms 返回值格式
返回值通常用迭代器表示位置, 若要取值需*解引用
“找不到”通常返回end迭代器
附: if语句也可以在condition内补scope内初始化值 方便在expression中复用
if (init_expr; condition)
STL algorithms 注意事项
STL algorithms NEVER insert or delete elements in the containers!
传入的是迭代器, 所以永远不会修改容器长度
常用STL algorithms
定义在<algorithm>的
std::replace:替换区间内所有等于指定值的元素1
std::replace(vec.begin(), vec.end(), 0, 42);
std::sort:排序区间元素,可自定义比较1
std::sort(vec.begin(), vec.end(), cmp); //若第三个可选参数传一个函数`cmp`, 会用`cmp(x,y)`代替`x<y`来比较(作为“x 是否应该排在 y 前面”的判断)
std::nth_element将第n个元素放到假设该序列排序后的位置,但:- 左边元素都 ≤ 它
- 右边元素都 ≥ 它
- 左右两边不保证有序
1 | std::nth_element(begin, nth, end, cmp);//若第四个可选参数传一个函数`cmp`, 会用`cmp(x,y)`代替`x<y`来比较(作为“x 是否应该排在 y 前面”的判断) |
std::unique:把重复项扔到最后, 没有删除, 返回迭代器pos, 使得[begin, pos)部分无重复. 常配合 erase 删除多余元素
1 | auto it = std::unique(vec.begin(), vec.end()); |
std::partition:按条件划分区间,满足的放前面, 不满足的放后面, 返回分界位置(第一个不满足的)迭代器1
auto mid = std::partition(vec.begin(), vec.end(), [](int x){ return x <= 5; });
std::iota:按递增值填充区间1
std::iota(vec.begin(), vec.end(), 1);
std::for_each:对区间每个元素执行操作1
std::for_each(vec.begin(), vec.end(), [](int& x){ x *= 2; });
std::prev_permutation:生成上一个字典序排列1
bool ok = std::prev_permutation(vec.begin(), vec.end());
std::lexicographical_compare:按字典序比较两个序列1
bool less = std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
std::equal:判断两个序列是否相等1
2bool same = std::equal(a.begin(), a.end(), b.begin()); //
bool same = std::equal(a.begin(), a.end(), b.begin(), b.end()); //检查长度相等
定义在<numeric>的
accumulate(begin, end, initValue): 从[begin, end)求和, 再加到初始值initValue上.
accumulate(v.begin(), v.end(), 0) 返回v中的元素求和 .
inner_product(begin1, end1, begin2, initValue)求向量内积, 再加到初始值initValue上.
Predicates
含义: 如find_if(begin, end, pred)找到[begin, end)范围中的第一个满足pred(element)为真的值, pred是callable object, 可以是function, function object, 也可以用lambda表达式代替
Function Objects
定义: 将operator()重载了的struct对象
fo(arg1, arg2, ...)is equivalent to fo.operator()(arg1, arg2, ...)
Lambda expressions
动机: 简化代码, 一次性使用的函数不用单独定义, 用表达式取代
1 | [capture_list](params) -> return_type { function_body } |
- capture_list “捕获”列表, 从外部scope”捕获”并存到内部scope的量(对应: function object成员变量), 避免拷贝可以加
&直接取引用 - params: 参数, 对应一般函数接收参数 记得表明类型, 放在STL algorithms时会自动将[first, end)范围内每个迭代器传入
- function_body: 对应一般函数体, 无特殊要点
Sequence containers
std::string不是container
| containers | 解释 |
|---|---|
std::vector<T> |
动态连续数组 任意位置增减慢 |
std::deque<T> |
double-ended queue 双向 头尾快速增删 |
std::array<T,N> |
container版本的built-in arrayT[N] 支持STL container方法 不会转为pointer |
std::list<T> |
双向链表 数据不连续排放 任意位置快速增删 不可快速随机访问 |
std::forward_list<T> |
单向链表 数据不连续排放 |
不同数据组织形式 Interfaces汇总
连续存储:std::string,std::vector,std::array
不支持push_front,emplace_front,pop_fornt
链表结构:std::list<T>,std::forward_list<T>
不支持:operator[],at
construct
Container c(b,e)
历年考试总结
该部分Generated by Gemini3.1 Pro
1. 原文档已覆盖的重点 (简要索引)
这类知识点在试卷中考察十分密集,但你在笔记的前半部分已经记录过:
- UB与CE判定:大量概念已被补充进“CE&UB汇总”。尤其是数组越界、指针解引用、空/野指针、强转别名违规、修改字符串常量等。
- 数组、指针与其转换:数组向指针的 Decay、指针与常量的搭配(顶层与底层
const)、向函数传多维数组参数退化为T (*)[M]等。 - 动态内存管理:
malloc/free的正确成对使用、深拷贝与浅拷贝。 - 迭代器与 STL 算法:泛型算法(
std::sort,std::count_if)、与 Lambda 表达式的组合使用。
2. 遗漏与需要重点展开的知识点
2.1 输入输出进阶与安全检查
(曾在 Spring 2025 中被重点考察)
scanf的精确匹配机制:
在scanf的格式字符串中,除了格式说明符(如%d)和空白字符外的所有普通字符,都必须与输入流中的字符逐字匹配(包括大小写、标点和空格)。如果用户少敲了一个字符或是拼错,scanf会在失败处立刻停止,导致目标变量根本不会被赋值。- 处理手段:必须检查
scanf的返回值(它返回成功匹配并赋值的变量数量)。如果有错,可以使用while ((c = getchar()) != '\n' && c != EOF);清空输入缓冲区的残留垃圾。
- 处理手段:必须检查
- **安全的字符串整行读取 (
fgets与strcspn)**:
若想稳定地读取含空格的一整行存入数组中,千万别用scanf。应使用:1
2fgets(user, sizeof user, stdin); // 安全读取,防溢出
user[strcspn(user, "\n")] = '\0'; // 替换掉行尾的换行符strcspn可以快速找到第一个\n的位置并填为终止符\0,这是 C 语言最优雅的行末处理手段。
2.2 构造函数与移动语义的极致优化
(曾在 Fall 2024 中作为 10 分大题考察)
在设计接收占内存较大的对象(如 std::string 或容器)的构造函数时,最优写法并不是传 const std::string&。
- **最佳方式 (传值 + move)**:
1
Student(std::string name) : m_name(std::move(name)) {}
- 当传入左值时:形参被拷贝构造 1 次,传入
m_name时被移动构造 1 次。(1次深拷贝 + 1次廉价移动)。 - 当传入右值时:形参被移动构造 1 次,传入
m_name时再移动构造 1 次。(0次深拷贝,2次廉价移动)。
由于整体实现代码最简洁,并且在接收右值时可彻底消灭深拷贝,这在现代 C++17 中被视为接收动态资源的标准最佳实践。
- 当传入左值时:形参被拷贝构造 1 次,传入
- 陷阱:对于
const std::string &name,即使在其上使用std::move(name),因为被标记为const,move后得到的是const std::string&&,依旧无法触发原本期望修改源的移动构造,此时只发生普通的耗时拷贝(Fall 2024 重点错题)。
2.3 设计模式:Builder(建造者)模式
(曾在 Spring 2025 考察)
当类的成员变量过多(引发包含七八个甚至更多参数的“巨型构造函数”),很容易填错顺序。此时可以使用建造者模式。
- **链式调用 (Method Chaining)**:
配置函数执行完毕后应当 **return *this;**(返回类型为Builder&)。这样能实现.name("Shaw").id(12345).email("...")接连调用的优雅形式。 - 利用
friend打通权限:
在目标类Student中声明friend class StudentBuilder;,使得 Builder 类可以毫无障碍地访问并配置其私有成员。 - 最后一步的验证与构建:
在 Builder 中,常常重载operator Student()或实现build()函数。在这里统一检查变量“是否漏填”,通过全部assert后再将中间对象std::move出去交给目标对象。
2.4 面向对象进阶、继承与多态的细节
(历年必考分析题)
- “Is-A” 关系原则 (Fall 2023分析题)**:
public继承表示“是一个 (is-a)”的关系:适用于基类的任何合法行为,必须毫无保留地适用于派生类**。经典反例:【正方形不能 public 继承长方形】,因为矩形支持“仅修改长但不改变宽”,如果作用于正方形实例上则破坏了正方形的定义逻辑。 - 防内存泄露的虚析构函数:
如果基类要在多态中使用(通过基类指针或引用操纵派生类对象),其析构函数必须显式声明为virtual(virtual ~Base() = default;)。否则delete base_ptr;时派生类的析构函数不会被触发,它特有的内存(如delete lhs,delete rhs)便会永久泄漏(UB)。 - **强制且必须显式调用的默认行为 (Spring 2024)**:
如何让派生类继承纯虚函数但同时基类又想给出一个“默认基础实现”?
一种技巧是将接口定义为纯虚函数virtual void display() = 0;(强制它重写不能静默默认拥有),但与此同时在基类通过protected: void defaultDisplay()提供代码。派生类重写display()时可以按需调用defaultDisplay()。
2.5 带有智能指针的数据结构重构
(曾在 Fall 2024 考察智能指针链表)
将普通的 C 语言版本链表结合 std::unique_ptr 重构。
- 特性:
std::unique_ptr确保资源的独占性,它自动删除了拷贝构造与拷贝赋值函数。 - 链表控制与所有权转移:
因为不可拷贝,在头部插入新节点的操作不能使用直接赋值,必须通过std::move控制所有权:1
2tmp->next = std::move(head); // 头结点的树冠交接给新节点的 next
head = std::move(tmp); // 链表起始指针易主给新节点 - 利用智能指针防浅拷贝并自发递归析构:
使用std::unique_ptr管理的链表,其List类的析构甚至可以写成= default;。销毁head会自动触发头部 Node 的析构,进而连环自动触发其内next指针析构出完整的瀑布释放效应(由于递归深度,唯需当心栈溢出)。但绝不需要我们在外部写冗长的while释放循环了。
2.6 左值与引用限定符 (Ref-qualified)
(曾在 Fall 2023, Spring 2025 考察)
- **Value Categories (值类别)**:
- “具有身份(有地址)但将要过期丢弃的值”为 **将亡值 (xvalue)**(如
std::move(obj)的返回值)。 "Shaw"等字符串面字面值常驻内存,是有身份的对象,所以是**左值 (lvalue)**。student0这样的具名变量也是左值。
- “具有身份(有地址)但将要过期丢弃的值”为 **将亡值 (xvalue)**(如
- 成员函数的左值/右值重载:
函数在后方除了加const还可以加&(针对左值)或&&(针对右值)。1
2
3
4
5// 针对被作为 lvalue 调用的重载:保留自身不动,返回个新拷贝去干活
Dynarray sorted() const & { ... return ret; }
// 针对被作为 rvalue 调用的重载:反正是要扔掉的,直接原地修改自己,连内存再利用直接 move 走
Dynarray &&sorted() && { ... return std::move(*this); }
2.7 终端命令行基础与编译期诊断
- 命令指示:利用 GCC 编译命令:
gcc -o prog power.c或针对 C++ 的g++ -std=c++17 main.cpp。重定向流的标准写法:./prog < 1.in > 1.out。 - -Wreorder 导致的诊断分析:
(常见易错考题)构造函数初始化列表里的初始化顺序,仅受该变量在“类内声明时自上而下的出现顺序”而定,跟你在InitList里用逗号排列先后顺序无关!
如果逻辑上存在前后依赖(某成员初始化需要访问上方成员),但在类的声明里被倒位了,就会触发-Wreorder警告,而且严重时会因为变量值未初始化完毕即被使用触发未定义行为!
- 标题: 26Spring CS100 Midterm Review
- 作者: AlexWei
- 创建于 : 2026-05-24 13:43:00
- 更新于 : 2026-05-24 13:43:53
- 链接: https://www.alexwei.top/2026/05/24/CS100-Midterm-Review-26Spring/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。