26Spring CS100 Midterm Review

26Spring CS100 Midterm Review

AlexWei Lv1

正文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
    2
    int a = 42;
    a = "hello"; // CE: 类型不匹配
  • auto 缺乏初始化:

    使用 auto 推导类型时必须提供初始值。

    1
    auto m; // CE: 无法推导类型

1.2 指针与引用限制

  • 取地址限制:

    不能对字面值或临时表达式取地址。

    1
    int *p = &42; // CE: 42 不是左值
  • 引用初始化规则:

    左值引用必须绑定到左值,且必须初始化。

    1
    2
    int &r; // CE: 必须初始化
    int &r2 = 42; // CE: 不能绑定到字面值
  • 不存在的引用类型:

    不允许定义指向引用的指针或引用的引用。

    1
    2
    int & &rr = r; // CE: 没有引用的引用
    int &*pr = &r; // CE: 没有指向引用的指针

1.3 类与对象控制

  • 访问权限违规:

    在类外部访问 private 成员。

    1
    2
    class A { int x; };
    A a; a.x = 1; // CE: x 是私有的
  • const 成员函数限制:

    const 成员函数中修改数据成员(除非是 mutable)。

    1
    void Student::foo() const { name += "a"; } // CE: const函数不能修改成员
  • 构造函数缺失:

    调用不存在的默认构造函数。

    1
    2
    class A { public: A(int x) {} };
    A obj; // CE: 已定义了带参构造,不再合成默认构造

1.4 其他语法限制

  • switch 标签要求:case 标签后必须是整型常量表达式。

    1
    2
    int n = 10;
    switch(x) { case n: ... } // CE: n 不是常量表达式
  • 数组长度:

    非 VLA 数组长度必须是编译时常量。

    1
    2
    int n; scanf("%d", &n);
    int arr[n]; // 在 C++ 或某些 C 标准下若不支持 VLA 则 CE
  • 数组初始化与赋值:
    初始化器数量不能超过数组长度;数组成员不能直接复制赋值。

    1
    2
    int b[2] = {2, 3, 5}; // CE:提供3个但预期2个
    int a[10]; int c[10] = a; // CE:数组不可直接赋值
  • 嵌套数组维度不匹配:
    向期望固定维度多维数组的函数传递其他维度数组。

    1
    2
    3
    void fun(int (*a)[5]);
    int b[3][10];
    fun(b); // CE(或编译器警告/UB):维度不匹配
  • C 风格字符串相加:
    直接使用 + 连接两个 C 风格字符串(const char*)。

    1
    "hello" + "world"; // CE:禁止直接用 + 拼接指针
  • const 未初始化或被修改:
    必须给出初值,后续不可更改。

    1
    2
    const int n; // 错误:C++中未初始化
    n = 42; // 错误:修改只读变量

2. 未定义行为 (Undefined Behavior, UB)

未定义行为是指标准未规定后果的代码,程序可能产生随机结果、崩溃或被编译器优化掉。

2.1 算术与赋值

  • 有符号整数溢出:

    有符号数超出范围是 UB。

    1
    2
    int x = 2147483647; // 假设 int 是 32 位
    x = x + 1; // UB: 有符号溢出
  • 求值顺序冲突:

    在同一个表达式中未指定顺序地多次修改变量。

    1
    2
    i = i++ + 1; // UB: 修改顺序未指定
    printf("%d %d", i, i++); // UB

2.2 指针与内存安全

  • 解引用非法指针:

    解引用空指针、野指针或悬空指针。

    1
    2
    int *p = NULL;
    *p = 10; // UB: 解引用空指针
  • 非法内存释放:

    Double free 或释放非动态分配的地址。不能逐个释放数组元素、释放偏移地址或部分释放。

    1
    2
    3
    int x = 10; free(&x); // UB: 释放非动态内存
    free(p); free(p); // UB: Double free
    free(p + 50); // UB: 释放偏移地址
  • 内存配对错误:

    new 对应 deletenew[] 对应 delete[]

    1
    2
    int *a = new int[10];
    delete a; // UB: 应该是 delete[],因为分配的是数组
  • 返回局部变量地址:

    访问已销毁的栈内存。

    1
    2
    int *foo() { int x = 42; return &x; }
    int val = *foo(); // UB: x 已销毁
  • 指针运算超出数组范围:
    即使不解引用,指针加减超出数组及其尾后(past-the-end)位置也是 UB。

    1
    2
    3
    int a[10];
    int *p = &a[0];
    p - 2; // UB: 超出数组起始
  • 严格别名违规 (Type-punned pointer dereference):
    通过实际底层类型不匹配的指针解引用(例如用 float* 操作 int 变量)。

    1
    2
    3
    int i = 42;
    float *fp = (float *)&i;
    ++*fp; // UB: 类型不匹配解引用
  • 解引用动态分配失败的空指针:
    如果 malloc 返回 NULL,解引用会导致未定义行为。

    1
    2
    int *arr = malloc(sizeof(int) * n);
    arr[0] = 42; // 若 malloc 失败,此时等于解引用 NULL (UB)

2.3 数组与容器

  • 下标越界:

    访问数组索引 i < 0i >= N

    1
    2
    int a[5];
    a[10] = 1; // UB: 越界访问
  • 字符串缺失终止符:

    处理没有 \0 的 C 风格字符串。

    1
    2
    char s[5] = "abcde"; // 数组大小为5,没空间存 '\0'
    puts(s); // UB: 会一直往后读直到崩溃
  • 迭代器失效:

    vector 扩容后使用旧迭代器。

    1
    2
    3
    auto it = v.begin();
    v.push_back(42); // 可能导致扩容
    *it = 1; // UB: 迭代器可能已失效

2.4 修改只读数据

  • 修改字符串字面值:

    尝试修改存储在只读区的数据。

    1
    2
    char *p = "hello";
    p[0] = 'H'; // UB: 尝试修改只读内存
  • 强行去除 const 修改:

    通过转换去除 const 后修改原为 const 的对象。

    1
    2
    3
    const int ci = 42;
    int *p = (int *)&ci;
    *p = 43; // UB

2.5 对象生命周期

  • 使用未初始化变量:

    使用局部非静态变量的随机值。

    1
    2
    int n;
    int m = n + 1; // UB: 使用不确定值
  • 非 void 函数缺少返回:

    函数路径未执行 return 语句且使用了返回值。

    1
    2
    int 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
2
3
4
int ival = 100000; long long llval = ival; 
long long result2 = ival * ival //发生overflow, 因为右侧表达式类型是int
long long result3 = llval * ival;//不发生overflow, 因为右侧表达类型是long long
long long result4 = llval * ival * ival //不发生overflow, `*`是左结合, 等价于(a * b) * c

自增++与自减--

  • 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
2
3
do{
//something()
}while(condition)

等价于

1
2
3
4
5
while (1){
//something()
if(!condition)
break;
}

switch-case条件选择

  • expression被计算
  • 寻找使expression == label为真的label, 执行对应case的body
  • ⚠️注意 从匹配的case起, 所有接下来的case都将不再匹配直接执行, 直到break;出现或到switch末尾
1
2
3
4
5
6
7
8
9
switch (expression) {
case label:
do_something();
break;
//......
default:
some_default_behavior();
break;
}

Conditional operator ?!

动机: 简化表达的if-else控制流

condition ? expressionT : expressionF

等价于

1
2
3
4
5
if (condition)
expressionT;
else
expressionF;
//单行expression时可省略花括号

Implicit initialization

只定义一个变量而不显式初始化? 会发生什么?

  • 局部非静态变量, 未初始化(值不确定 e.g. wild pointers)
  • 对全局变量和局部静态变量, 零初始化(空指针, 0, 0.0, …)

数组和指针 Array&Pointer

数组的初始化

花括号初始化 单层array

1
2
3
4
5
6
7
8
int a[10] = {2, 3, 5, 7}; // Correct: Initializes a[0], a[1], a[2], a[3]
int b[2] = {2, 3, 5}; // Error: Too many initializers
int c[] = {2, 3, 5}; // Correct: 'c' has type int[3].
int d[100] = {}; // 在C17中不可以❌ 但Correct in C++ and since C23.
//#initializer < 数组长度?
int a[10] = {1, 2, 3}; // {1, 2, 3 , 0,....,0}
int b[100] = {0}; // {0, 0, ..., 0} All elements of b are initialized to 0.
int c[100] = {1}; // {1, 0, 0, ..., 0} c[0] is initialized to 1, and the rest are initialized to 0.

指定位置初始化

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a[4][3] = { // array of 4 arrays of 3 ints each (4x3 matrix)
{ 1 }, // row 0 initialized to {1, 0, 0}
{ 0, 1 }, // row 1 initialized to {0, 1, 0}
{ [2]=1 }, // row 2 initialized to {0, 0, 1}
}; // row 3 initialized to {0, 0, 0}
int b[4][3] = { // array of 4 arrays of 3 ints each (4x3 matrix)
1, 3, 5, 2, 4, 6, 3, 5, 7 // row 0 initialized to {1, 3, 5}
}; // row 1 initialized to {2, 4, 6}
// row 2 initialized to {3, 5, 7}
// row 3 initialized to {0, 0, 0}
int y[4][3] = {[0][0]=1, [1][1]=1, [2][0]=1};
// row 0 initialized to {1, 0, 0}
// row 1 initialized to {0, 1, 0}
// row 2 initialized to {1, 0, 0}
// row 3 initialized to {0, 0, 0}
//同一维情况, 未显式初始化的被零初始化

数组的类型

我们声明ElemType arr[N];, 数组arr的类型是ElemType [N]

Pointer arithmetic

  • a是一个T [N] 类型的array时, 令p = &a[0], 此时p+i等价于&p[i], *(p+i)等价于a[i]

p + i returns 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
2
3
4
5
6
7
int a[10];
bool find(int value){
for(int *p = a; p < a + 10; ++p) //`p`被隐式转换为`&a[0]`, 使p初始化为指向a[0]的指针
if(*p == value)
return true;
return false;
}

向函数传递数组

BIG IDEA: 向函数传递数组的唯一方式是传递它的首元素地址

1
2
3
4
5
6
void fun(int *a);
void fun(int a[]);
void fun(int a[10]);
void fun(int a[2]);
//这些写法全部等效, 就算字面上声明为数组, 如`a[10]`, 实际参数类型仍然是`int*`, 任何符合该类型的都会被接受
//We often use another parameter to indicate the length of the array.

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]

⚠️要注意, 这里不要被一维时从arraypointer误导, 把Type[M]默认理解为等价于Type *

In other words: Type (*)[M]Type **是不一样的, 前者是指向array(成员类型为Type)的指针, 后者是指向Type *的指针

1
2
3
4
5
void fun(int (*a)[M]);
void fun(int a[][M]);
void fun(int a[2][M]);
void fun(int a[10][M]);
//这些写法全部等效, 参数类型都是`int (*)[M]`

语法辨析速查

在下列表达式中, a的类型是什么? 它接受int[N][M]类型的参数吗?

  1. void fun(int a[N][M]) : A pointer to int[M] . 接受✅

  2. void fun(int (*a)[M]) : A pointer to int[M] . 接受✅ Same as 1.

  3. void fun(int (*a)[N]) : A pointer to int[N]. 如果N == M 则接受✅, 否则拒绝❌

  4. void fun(int **a) : A pointer to int *. 拒绝❌

  5. void fun(int *a[]) : A pointer to int *. 拒绝❌

  6. void fun(int *a[N]) : A pointer to int *. 拒绝❌

  7. void fun(int a[100][M]) : A pointer to int[M] . 接受✅

  8. 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

一个指针, 指向一个存Nint的数组

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
2
3
4
5
6
7
char s[10] = "abcde"; //s = {'a','b','c','d','e','\0'}
printf("%s\n", s); // prints abcde
printf("%s\n", s + 1); // prints bcde
s[2] =';'; // s ="ab;de"
printf("%s\n", s); // prints ab;de
s[2] ='\0';
printf("%s\n", s); // prints ab

I/O

1
2
3
char str[100] = "abcdef";
scanf("%s", str);// 若读入"123", str变成{'1', '2', '3', '\0', 'e', 'f'}
printf("%s\n", str); //打印"123", 注意"\0"后的内容不会被打印

注意scanf内存不安全, 最好的alternative是fgets

1
2
char str[10];
scanf("%s", str);//如果用户输入大于9个字符 will cause disaster!
1
2
3
char str[100];
fgets(str, 100, stdin);//fgets(目标数组, 最多读取字符数, 输入流);
puts(str); //puts()可以自动补`\n`换行输出

<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),如果 destsrc 指向同一块内存,会发生什么?如果它们部分重叠呢?strcat 又如何?

A2:

  • 如果 destsrc 完全相同(同一个地址),通常结果等价于“不做任何事”。
  • 如果它们发生部分重叠(overlap),行为是未定义行为(undefined behavior)。
  • strcat 也是一样:如果源和目标区域重叠,行为未定义。

Q3: strcmp 的返回值是什么?它返回的是布尔值(true / false)吗?

A3:
不是。

strcmp(a, b) 返回的是一个整数:

  • < 0a < b
  • = 0a == b
  • > 0a > b

比较依据是字典序(lexicographical order)。

例如:

1
2
3
strcmp("abc", "abd") < 0
strcmp("abc", "abc") == 0
strcmp("abd", "abc") > 0

它不是返回 true / false 的函数。

字符串转换为数值类型

#include <stdlib.h>

strtol , strtoll , strtoul, strtoull , strtof , strtod , strtold

字符串字面值

string literals是只读量, 应当用low-level const保护, 防止意外修改的危险行为(UD/RE)

1
2
const char *str ="abcde";
str[3] ='a'; // compile-error

但可以拷贝到数组里

1
2
char arr[] = "abcde"
arr[3] = 'a'; //OK

Array of Strings

本质是array of pointers, 每个pointer指向一个字符串字面值, 不是二维数组!

1
2
3
const char *an_array_of_strings[] = {
"aaa", "bbb", "ccc"
};

Command line arguments

1
2
3
4
5
6
7
8
9
10
int main(int argc, char **argv){
for(int i = 0; i < argc; ++i)
puts(argv[i]);
}
//若以该命令参数运行`.\program one two three`
//Output:
//.\program
//one
//two
//three
  • argc是传入参数数量, 注意含程序名
  • argv指向一个数组首元素, 这个数组有argc+1个指针, 前argc个元素(从argv[0]argv[argc-1])是各参数(含程序名), 最后一个元素argv[argc]NULL

一个读取任意长度字符串的函数例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#define INITIAL_SIZE 4

char *read_string(void) {
char c = getchar();
while (isspace(c))
c = getchar();

char *buffer = malloc(INITIAL_SIZE);
int capacity = INITIAL_SIZE;
int cur_pos = 0; // The index at which we store the input character.

while (!isspace(c)) {
if (cur_pos == capacity - 1) { // `-1` is for '\0'
char *new_buffer = malloc(capacity * 2);
memcpy(new_buffer, buffer, cur_pos); // copy everything we have stored
free(buffer); // !!!!!!!!!

capacity *= 2;
buffer = new_buffer;
}

buffer[cur_pos++] = c;
c = getchar();
}

// Now `c` is a whitespace. This is not part of the contents we need.
ungetc(c, stdin);

buffer[cur_pos] = '\0'; // Remember this!!!

return buffer;
}

int main(void) {
char *content = read_string();
puts(content);
free(content);
}

struct

定义一个struct

1
2
3
4
5
6
struct Student {
const char *name;
const char *id;
int entrance_year;
int dorm;
};

实例化(注意与C++不同, 这里一定要写关键字struct)

1
2
3
4
5
6
7
8
9
struct Student stu; 
stu.name = "Alice";
stu.id = "2024533000";
stu.entrance_year = 2024;
stu.dorm = 8;
//或用初始化列表
struct Student stu1 = {"Alice", "2024533000", 2024, 8};
//建议使用指定器
struct Student stu2 = {.name = "Alice", .id = "2024533000",.entrance_year = 2024, .dorm = 8};

动态分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Student *pStu = malloc(sizeof(struct Student));
pStu->name = "Alice";
pStu->id = "2024533000";
(*pStu).entrance_year = 2024; // equivalent to pStu->entrance_year = 2024;
printf("%d\n", pStu->entrance_year);
puts(pStu->name);
free(pStu);//DON'T FORGET to `free` after use

//Compound literals
struct Student *student_list = malloc(sizeof(struct Student) * n);
for (int i = 0; i != n; ++i) {
student_list[i] = (struct Student){.name = A(i), .id = B(i), .entrance_year = C(i), .dorm = D(i)};}
//...
free(student_list);

struct实例作为参数传入函数, 或被函数所return, 语义上都是member-wise拷贝

Recursion 递归

计算n的阶乘(n!)

1
2
3
int factorial(int n) {
return n == 0 ? 1 : n * factorial(n - 1);
}

选择排序 Selection-sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void sort_impl(int *a, int k, int n) {
if (k == n - 1)
return;

int m = k;
for (int i = k + 1; i < n; ++i)
if (a[i] < a[m])
m = i;

swap(&a[m], &a[k]); // the "swap" function we defined in previous lectures
sort_impl(a, k + 1, n); // sort the rest part recursively
}

void sort(int *a, int n) {
sort_impl(a, 0, 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
2
3
4
5
6
7
#include <iostream>
int main(){
int a,b;
std::cin >> a >> b;
std::cout << "a+b= " << a+b << '\n';
return 0;
}

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
2
3
4
5
6
std::string str0 = "aaa"; //operator= overloading
std::string str1{"aaa"};//调用构造函数 equivalent(用{}更modern): std::string str1("aaa");
std::string str2(3,'a');//调用构造函数
//以上三种效果等价
std::string str3 = str0; //拷贝str0内容到str3
std::string str4; //默认初始化为空字符串(default constructor行为)

可以用+做拼接(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
2
3
4
5
std::vector<int> v{2, 3, 5, 7}; // A vector of `int`s,
// whose elements are {2, 3, 5, 7}.
std::vector<int> v2 = {2, 3, 5, 7}; // Equivalent to ↑
//补充写法:C++17 CTAD 让编译器自己deduce类型 下句会自动推断为vector<int>
std::vector v3= {2, 3, 5, 7};
Copy a std::vector
1
2
3
std::vector<int> v2 = v; // `v2`` is a copy of `v`
std::vector<int> v3(v); // Equivalent
std::vector<int> v4{v}; // Equivalent
内置方法(类成员函数)

.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
2
int& x = ival, y = ival, z = ival;
// Only `x` is a reference. `y` and `z` are of type `int`.

一个引用只能被一次性绑定到一个对象, 不可重复绑定, 但一个对象可以有多个引用

补充: * and &各种用途大汇总

Both symbols have many identities!

  • In a declaration like Type *x = expr ,* is a part of the pointer type Type *. *声明指针

  • In a declaration like Type &r = expr , & is a part of the reference type Type & . &声明引用

  • In an expression like *opnd where there is only one operand, * is the dereference operator. *解引用

  • In an expression like &opnd where there is only one operand, & is the address-of operator.&取地址

  • In an expression like a * b where there are two operands, * is the multiplication operator. *做乘法

  • In an expression like a & b where there are two operands, & is the bitwise-and operator. &二进制与

reference-to-const

动机: 三大优点如下

  1. 避免拷贝 Avoids copy.

  2. 可绑右值(注意这点与一般reference不同) Accepts temporaries and literals (rvalues).

  3. 避免错误修改应该只读的量 The const qualification prevents accidental modifications to it.

e.g.

1
2
3
4
5
6
7
8
9
10
11
int count_lowercase(const std::string &str) { // `str` is a reference-to-`const`
int cnt = 0;
for (char c : str)
if (std::islower(c))
++cnt;
return cnt;
}
std::string a = something(), b = some_other_thing();
int res1 = count_lowercase(a); // OK.
int res2 = count_lowercase(a + b); // OK.
int res3 = count_lowercase("hello"); // OK.

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
2
3
4
int ival = 42;
const int &cref = ival;
int &ref2 = const_cast<int &>(cref); // OK
int *ptr = const_cast<int *>(&cref); // OK
reinterpret_cast

转换指针类型(DANGEROUS)

注意⚠️: 勿忘实际指向的对象还是原类型, 不能当成一般转换后类型的指针用, 非必要不用reinterpret_cast

static_cast

详见Lec19 类型转换部分

auto

动机: 声明变量时, 让编译器推断类型, 简化某些very long types, 或者我们不知道编译器知道的特殊类型, 如对lambda expression

一些例子:

1
2
3
4
5
6
7
8
9
auto x = 42; // `int`
auto y = 3.14; // `double`
auto &r = x; // `int &`

auto str = "hello"; // 注意⚠️ 为了C兼容性 这里推断为`const char *`, 不是`std::string`

auto it = vs.begin();

auto lam = [](int x, int y) { return x + y; } // A lambda expression.

补充:decltype(expr)函数, 可以不执行expr(它可以是对另一个函数的调用)而推断返回类型

Function overloading

总结: 共用函数名, 具有不同参数类型, 调用时分别match

a simple example

1
2
3
4
void fun(int);
void fun(double);
fun(42); // fun(int)
fun(3.14); // fun(double)

nullptr

在C++中建议用nullptr代替NULL作为空指针

nullptr has a unique type std::nullptr_t (defined in <cstddef> ), which isneither void * 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
2
Student(const std::string &name_, const std::string &id_)
: name(name_), id(id_), entranceYear(std::stoi(id_.substr(0, 4))) {}
  • 成员按照声明的顺序初始化, 并非按照initializer list

顺序不一致会发生什么?

If the initializers appear in an order different from the declaration order, the compiler will generate a warning.

Typical mistake: entranceYear is initialized in terms of id , but id is 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
2
3
4
5
6
7
8
class Student{
//...
public:
Student(const std::string &name_, const std::string &id_){
name = name_;
id = id_;
}
}

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
2
3
4
// C-style
typedef long long LL;
// C++-style
using LL = long long;

作为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
2
3
4
5
6
7
void print(const Student &stu) {
//function body
}
class Student {
//something
friend void print(const Student &);
}

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
2
3
4
5
6
7
8
9
10
11
class Dynarray{
public:
Dynarray &operator=(Dynarray &&other) noexcept{ //勿忘noexcept move函数都需要 确保不抛出异常
if (this != &other){ //self-assignment safe! 不然移动自己就删除掉了
delete[] m_storage; // Avoid memory leaks! 先删掉this的m_storage以前指向的array 准备被新的资源move过来
m_storage = other.m_storage; m_length = other.m_length; //Steal the resources from 'other'
other.m_storage = nullptr; other.m_length = 0;//确保被转移的资源 现在只有this这唯一的owner
}
return *this;
}
}

有了如上的move assignment operator后, 右值被移动,左值被拷贝

1
2
3
a = concat(b, c); // calls move assignment operator,
// because `concat(b, c)` generates an rvalue.
a = b; // copy 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
2
3
4
std::string foo(const std::string &a, const std::string &b) {
return a + b; // a temporary
}
std::string s = foo(a, b);

NRVO=Named Return Value Optimization 理解为特殊的copy elision

例如

1
2
3
4
5
6
7
Named Return Value Optimization, NRVO
Dynarray concat(const Dynarray &a, const Dynarray &b) {
Dynarray result(a.size() + b.size());
// ...
return result;
}
Dynarray a = concat(b, c); // 事实上没有发生拷贝, 直接将result结果构造在了a的内存位置

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
2
3
4
5
6
class Rational {
friend std::ostream &operator<<(std::ostream &, const Rational &);
};
std::ostream &operator<<(std::ostream &os, const Rational &r) {
return os << r.m_num << '/' << r.m_denom;
}

注意要点

  • std::ostream类型匹配std::cout, std::istream类型匹配std::cin
  • 操作的应当是原来的输入输出流, 因此流对象std::cout, std::istream应该以引用&方式传入和返回

特殊例子2(++与–的prefix与postfix)

1
2
3
4
5
6
7
8
9
class Rational {
public:
Rational &operator++() {++m_num; simplify(); return *this; } //prefix ++aaa 先加后返回
Rational operator++(int){ //postfix aaa++ //先返回后加
auto tmp=*this;
++*this;
return tmp;
}
}

类型转换

区分显式转换与隐式转换: 显式转换<–>目标类型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::beginstd::end函数, 统一了返回STL容器或数组begin/end iterator的接口

数组的delete[],std::vectorpush_back(),pop_back()等方法会导致原先iterator的无效化(invalidation)

迭代器的分类

  1. Forward Iterators 只能向前移动

  2. Bidirection Iterator 可以双向移动

  3. Random Access Itertator 可以任意移动(it+n,it[n],…)

    std::vector, 指针是经典的随机访问迭代器

    std::sort必须接受Random Access Iterator

用迭代器范围初始化STL容器

1
2
3
std::vector<char> v = {'a','b','c','d','e','f','g','h','i'};
std::vector v2(v.begin() + 2, v.end() - 3); // {'c','d','e','f'}
std::string s(v.begin(), v.end()); // "abcdefghi"

STL algorithms 参数格式

接受pairs of iterators来表示范围

1
2
3
4
int a[N];
std::vector<int> v;
std::sort(a, a+N);
std::sort(v.begin(), v.end());

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
2
auto it = std::unique(vec.begin(), vec.end());
vec.erase(it, 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
    2
    bool 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); 清空输入缓冲区的残留垃圾。
  • **安全的字符串整行读取 (fgetsstrcspn)**:
    若想稳定地读取含空格的一整行存入数组中,千万别用 scanf。应使用:
    1
    2
    fgets(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 中被视为接收动态资源的标准最佳实践。
  • 陷阱:对于 const std::string &name,即使在其上使用 std::move(name),因为被标记为 constmove 后得到的是 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 lhsdelete 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
    2
    tmp->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 这样的具名变量也是左值
  • 成员函数的左值/右值重载
    函数在后方除了加 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 进行许可。
评论
目录
26Spring CS100 Midterm Review