CS100 学习笔记 - C语言部分

Coinred 的 手稿们 / 2024-04-20 / 原文

CS100 学习笔记 - C语言部分

记录一些规范和自己不知道的特性。

Lesson 1

main 函数

main 的“调用者”是谁?——程序的调用者/“宿主环境” (hosted environment)

C语言有以下三种 main 函数定义。

int main (void) { body }	                //(1)	
int main (int argc, char *argv[]) { body }	//(2)	
/* 其他由实现定义的签名 */                    //(3)	(C99 起)	

为什么推荐以 void 为参数呢?可以看一下课件后的notes。

在 C23 以前,一个函数的参数列表如果是空的,它表示“接受未知的参数”,而非不接受参数。这一点和 C++、Python 等语言不同,是 C 特有的历史遗留问题。例如

void foo();
int main(void) {
  foo(1, 2.0, "hello"); // 合法
  foo(42); // 合法
}

以上代码中,声明函数 foo 时没有说明它接受什么参数,因此下面调用 foo 的时候无论如何传参都是合法的。这是十分危险的行为,它放弃了对于参数的类型检查。如果后面我们定义 foo 的时候仅接受一个 int 参数,那么第一个调用 foo(1, 2.0, "hello") 就是 undefined behavior:

void foo();
int main(void) {
  foo(1, 2.0, "hello"); // undefined behavior. 2.0 和 "hello" 会被如何处理?
  foo(42); // ok
}
void foo(int x) {
  // do something with x
}

所以在 C23 以前,参数列表空着和写一个 void 的含义是不同的。许多人习惯声明 main 的时候写成 int main(),这在 C++ 中以及 C23 以后是合法的,而在 C23 以前如果要说它合法,只能将它理解为属于 "another implementation-defined signature"。

还有一些人可能会把 main 函数的返回值类型 int 省略,即直接写成 main(void) { ... },甚至和上一条的情况组合变成 main() { ... }。C89 规定,如果声明了一个函数而没有写出它的返回值类型,则返回值类型默认为 int,但 C99 删除了这一规则。编译器为了向后兼容可能仍然会默许这一行为,但它绝不是一个好的编程习惯。我们始终要编写符合标准的代码。

此外,还有一个广泛讨论的问题就是 void main(void) 到底合不合法。这种写法通常来自于非常老的代码(那时语言的标准化尚未完善和普及),或者来自谭浩强那样的课本,或者来自于一些嵌入式系统代码。在那些特殊的系统中,程序并不需要返回一个状态值给 hosted environment,人们就理所当然地认为 main 函数应该没有返回值了。无论如何,在那些需要程序返回状态值的运行环境中,这种写法都会导致返回值是无法预知的,调用者也就不知道这个程序是否正常运行了。比方说在 CS100 的 OJ 上,这样的程序几乎肯定会被判定为 Runtime Error。

那么 void main 到底合法还是不合法?标准对于 main 函数的签名的规定中,第三条写的是 "another implementation-defined signature",也就是说只要编译器和机器支持这种写法,它就符合这一条,也就符合标准。值得一提的是,C++ 标准在这个地方的规定是 "another implementation-defined signature, with int as return type",所以 void main 必然不是合法的 C++ 代码。

所以一个程序被视为正常退出当且仅当 main 函数返回 0

而且 main 函数结尾的 return 0; 是可以省略的。因为根据标准:

If the return type is compatible with int and control reaches the terminating } ,
the value returned to the environment is the same as if executing return 0;

(不过出于强迫症我还是会写就是了())

另一个更糟糕的历史遗留问题(in r1)

如果你调用了一个未经声明的函数,C++ 编译器会给出一个十分正常的报错: "... was not declared in this scope",而 C 编译器会允许,并且给出一个令人困惑的 warning: "implicit declaration"。
C 标准认为你“隐式地声明” (implicitly declare) 了这个函数,于是压力全都给到链接器。
当然,它是无法运行的,链接器会抱怨“找不到名为 my_mysterious_function 的函数”。

这一规则 C99 起被删除,但为了向后兼容 (backward compatibility),编译器很可能仍然支持,只是给一个 warning。

Lesson 2

变量名

可读性很重要。

[Best practice] Declare the variable when you first use it!

  • If the declaration and use of the variable are too separated, it will become much more difficult to figure out what they are used for as the program goes longer.

[Best practice] Use meaningful names!

  • The program would be a mess if polluted with names like a, b, c, d, x, y, cnt, cnt_2, flag1, flag2, flag3 everywhere.
  • Use meaningful names: sumOfScore, student_cnt, open_success, ...

Readability is very important. Many students debug day and night simply because their programs are not human-readable.

初始化

[Best practice] Initialize the variable if possible. Prefer initialization to later assignment.

变量类型

cppreferance 的变量定义内容: 注意 C 标准定义的是每种类型至少几位。

  • 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
  • sizeof(signed T) == sizeof(unsigned T) for every T \(\in\{\) char, short, int, long, long long \(\}\)
  • short and int are at least 16 bits. long is at least 32 bits. long long is at least 64 bits.

[Best practice] Avoid magic numbers 保证可读性。

char, signed charunsigned char三种不同的类型.

C99 后,C语言使用 bool 类型需要 #include <stdbool.h>.

字面值

整型字面值不写后缀,默认是 int,如果 int 不够大就是 long,还不够大就是 long long
还不够大的话:

  • 如果编译器支持 __int128 并且它够大,那就是 __int128
  • 否则报错 (ill-formed)。

整型字面值后缀有 u (unsigned), l (long), ll (long long)。大小写不敏感,但是不可以是 lLLl
十六进制字面值:0xBAADF00D,八进制字面值:052,以及 C23/C++14 的二进制字面值:0b101010

字符字面值:'a'。但是它的类型是 int !(C++ 里它就是 char 了)

scanfprintf

scanf 的格式指示符 :

  • %hd for short. %x for hex num. %zu for size_t .
  • %f for float, %lf for double, and %Lf for long double.

scanfprintf 中,conversion specifier 与对应的变量类型不匹配是未定义行为。

Lesson 3

变量与运算符

运算符隐式转换 //很杂,只要求常用的

整除是向零取整,但同时取模仍然满足 \(a \bmod b = a- \left\lfloor\dfrac{a}{b}\right\rfloor\cdot b\)

有符号整型变量溢出是未定义行为。无符号整型自然溢出啥事没有(就是模 \(2^N\))。

大数位运算记得用 1ull(NOIP 格雷码、动物园的痛())

* 不要混用带符号数和无符号数

未定义行为 and so on

C语言的几种行为:

  • 未定义行为(undefined behavior) - 程序的该行为没有限制。未定义行为的例子是越过数组边界的访问、有符号整数溢出、空指针解引用、在表达式中超过一次修改标量而其中无顺序点、通过不同类型的指针访问对象,等等。编译器不要求诊断未定义行为(尽管多数简单情形是得到诊断的),且编译后的程序不要求做任何有意义的事
  • 未指定行为(unspecified behavior ) - 容许二种或多种行为,且不要求实现规范每种行为。例如,求值顺序、同样的字符串字面量是否有别,等。每个未指定行为导致一组合法结果之一,并且可以在同一程序中重复时产生不同结果。
  • 实现定义行为(implementation-defined behavior) - 在未指定行为之上,实现规范了如何选择。例如,字节中的位数,或有符号整数右移是算术还是逻辑。
  • 本地环境限定行为(locale-specific behavior) - 依赖于当前选择的本地环境的实现定义行为。例如, islower 对任何 26 个小写拉丁字母外的字符是否返回 true。
    (注意:严格遵从的程序不依赖任何未指定、未定义或实现定义行为)

循环控制语句

[Best practice] Use a formatter! If you use VSCode, press Shift+Alt+F (Windows), ⇧⌥F (MacOS) or Ctrl+Shift+I (Linux) to format the code.

  • Other editors may also have this functionality.

[Best practice] Always declare and initialize the iteration variable inside the for statement, if possible.

while (n--){} 执行 n 次。

Lesson 4

运算符优先级与结合顺序

C语言运算符优先级

运算符优先级与结合顺序不决定 求值顺序!

求值顺序与未定义行为

优先级,结合性,求值顺序

如果两个表达式 AB 的求值顺序是 unspecified 的,而它们

  • 都修改了一个变量的值,或者
  • 一个修改了某个变量的值,另一个读取了那个变量的值,

那么这就是 undefined behavior。

  1. 若对一个标量对象的副效应与另一个对同一标量对象的副效应相对无顺序,则行为未定义。
i = ++i + i++; // 未定义行为
i = i++ + 1; // 未定义行为
f(++i, ++i); // 未定义行为
f(i = -1, i = -1); // 未定义行为
  1. 若一个标量对象上的副效应与另一个使用同一标量对象之值的值计算相对无顺序,则行为未定义。
f(i, i++); // 未定义行为
a[i] = i++; // 未定义行为

If the evaluation order of A and B is unspecified, there are two possibilities:

  1. evaluations of A and B are unsequenced: they may be performed in any order and may overlap.
  2. evaluations of A and B are indeterminably-sequenced: they may be performed in any order but may not overlap: either A will be complete before B, or B will be complete before A.

To constitute undefined behavior, the evaluations of A and B should be unsequenced. An example is f() + g() where both f and g modify a global variable: The C standard says that two functions calls are indeterminably sequenced, so f() + g() is not undefined behavior.

常见求值顺序确定情形:&&||,?:,(后自增自减)。

[Best practice] Use one pair of parentheses when two binary logical operators meet. 不易确定优先级

[Best practice] Avoid more than two levels of nested conditional operators. 可读性

[Best practice] Avoid unnecessary if 可读性&不容易出事

switchdo-while

在 do-while 循环体内声明的变量,无法在 cond 部分使用。

switch 不要忘了 break !

switch 的大括号内的所有变量都会被包括到同一作用域,要隔离需要对需要的 case 加大括号。

函数

函数定义与声明:

  • A function should have only one definition, but can be declared many times.
  • A definition is also a declaration, since it contains all the information that a declaration has.
  • When a function is called, its declaration must be present.

变量作用域

[Best practice] Declare a variable right before the use of it. Declare it in a scope as small as possible.
[Best practice] Don't worry about the same names in different scopes.

In short, argument passing is an assignment with type qualifiers on the parameter type ignored, instead of initialization.

Lesson 5

static

局部 static 变量的行为就像一个全局变量,但它的名称位于函数内部,不会污染全局名称空间。

隐式初始化

  • For local non-static variables, they are initialized to indeterminate values. In other words, they are uninitialized.
  • For global or local static variables, they are empty-initialized

Intuitively, such variables are initialized to some kind of "zero". This is called zero-initialization in C++.

使用未初始化的变量的值是未定义行为

[Best practice] Always initialize the variable.

const 变量在初始化后不能修改。但是C语言可以不初始化 const 变量,不过这必然带来未定义行为("non-stop ticket to undefined behavior")。

指针

声明时:

  int *p1, p2, *p3; // p1 和 p3 是 int *,但 p2 是 int
  int* q1, q2, q3;  // 只有 q1 是 int *,q2 和 q3 都是 int

[Best practice] Either PointeeType *ptr or PointeeType* ptr is ok. Choose one style and stick to it. But if you choose the second one, never declare more than one pointers in one declaration statement.

空指针 是指针的“零值”,表示指针没有指向任何对象。

如果一个指针没有指向一个实际的对象:

  • 它可能是未初始化的(俗称“野指针(wild pointers)”)
  • 可能是空指针
  • 可能是指向的内存刚被释放掉(“空悬指针(dangling pointer)”)
  • 或者其它无意义的地址:int *p = 123;

试图解引用这个指针是 undefined behavior,并且通常是严重的运行时错误

[Best practice] Avoid wild pointers. 记得初始化

数组

数组 ElemType arr[N]; 的类型是 ElemType [N] 包含了元素类型 ElemType 及其长度 N
N 必须是常量表达式:它的值能确定在编译时已知。
数组下标越界是未定义行为

如果数组 a 的大小是运行期确定的,它就是一个 Variable-Length Array (VLA)。关于VLA 。

数组初始化

数组初始化的两种方式

  • Initialize the beginning few elements:
    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] = {};          // Correct in C++ and since C23.
    
  • 初始化器 Initialize designated elements (since C99):
    int e[10] = {[0] = 2, 3, 5, [7] = 7, 11, [4] = 13};
    
    (只有C语言有这种数组初始化器,C++没有,原因 https://stackoverflow.com/a/53250500/8395081)

二维数组:

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}

Lesson 6

指针运算

两个指针的差值实际上是 ptrdiff_t 类型,声明于 <stddef.h>,是一个有符号整形类型,其大小是实现定义的。

对于 ElemType [N] 数组 p,指针指向的数组元素的地址只能从 pp+N (“past-the-end”),否则行为未定义(包括 p-1)。两个不同数组的指针运算也是未定义行为。

不难发现数组与指针实际上是不同类型的变量,只是他们的隐式转换非常常见。

  • 数组向指向首元素指针的隐式转换(退化):
    • Type [N] 会退化为 Type *
  • “二维数组”其实是“数组的数组”:
    • Type [N][M] 是一个 N 个元素的数组,每个元素都是 Type [M]
    • Type [N][M] 退化为“指向 Type [M] 的指针”

C语言中不同类型的指针可以互相隐式转换,但这并不安全(C++里直接报错)。解引用一个指向 T2 类型的 T1* 指针绝大多数都是未定义行为。(例外:严格别名使用)

指针与函数

往函数中传递数组的唯一方式是传入指向数组首位的指针,故以下几种声明等价:

void fun(int *a);
void fun(int a[]);
void fun(int a[10]);
void fun(int a[2]);

并且最好同时传入一个变量代表数组的大小。

如果要限制传入数组大小的话可以传入指向数组的指针:

void print_array_10(int (*pa)[10]) {
  for (int i = 0; i < 10; ++i)
    printf("%d\n", (*pa)[i]);
}
int main(void) {
  int a[10], b[100], c = 42;
  print_array_10(&a); // OK
  print_array_10(&b); // Error
  print_array_10(&c); // Error
}

函数不能直接返回局部数组变量(因为存储其数据的栈空间会在函数调用完毕后立即释放),所以最好将需要的返回值的地址作为参数传入。

int (*parr)[N]; 是指向 长为 N 的整形数组 的指针,类型名为 int (*)[N]
int *arrp[N]; 是 整型指针组成的 长为 N 的数组,类型名为 int *[N]
(如果要 typedef 可以 typedef (*A)[N]; A parr;,把新类型名写在原本变量的位置)

以下往函数中传入二维数组的方式是等价的,其中必须要说明第二维的长度,第二维的长度也决定了参数的类型:

void fun(int (*a)[N]);
void fun(int a[][N]);
void fun(int a[2][N]);
void fun(int a[10][N]);

并不等价于:

void fun(int **a)
void fun(int *a[])
void fun(int *a[N])

Lesson 7

const

const T * (等价于 T const *) 表示该变量指向一个 const T 变量。这种用法叫做底层 const

这里的 const 相当于给变量上了一个锁,即使他指向一个普通的 T 变量,这个变量也不能通过这个指针来直接修改(可以认为是指针觉得自己指向的是一个 const T 变量,从而不允许修改)。
同样的,你也可以通过显式转换用一个 T * 指针指向一个 const T 变量,并且可以通过这个指针间接修改那个 const T 变量(相当于把加在 const T 变量上的锁强行破除),但这是一个未定义行为

T const* 表示该变量是指向 T 变量的指针,但是他不能被修改。这种用法叫做顶层 const

[Best practice] Use const whenever possible.

void *

void * 是一种特殊的指针。任何类型的指针可以被隐式地转换为 void *void *也可以被隐式地转换为任何类型的指针。所以经常被用来表示 “可指向任何东西的指针”,“某块内存的地址”,甚至是 “任意对象”。

但我们也不难看出,void * 是 C 类型系统真正意义上的天窗,这样的隐式转换可能造成安全隐患,尽管C语言允许这种操作。

动态内存

动态内存,从堆空间申请内存:

void *malloc(size_t size);
void *calloc(size_t num, size_t each_size);
void free(void *ptr);

size_t 是无符号整型类型,可代表一个对象占用的空间,其大小是实现定义。
内存申请失败会返回 NULL所以解引用前记得特判。
申请 0 大小空间是实现定义行为:可能返回 NULL,也可能返回一个不能解引用的指针(也得 free)。

Due to the alignment requirements, the number of allocated bytes is not necessarily equal to num * each_size.

为了防止内存泄漏,申请的空间记得 free 掉!
free 中传入 NULL 是无害的。
free 中传入不指向动态内存首位的指针是未定义行为。(指向中间某一位也不行)
释放内存后访问该内存是未定义行为。
释放内存后原来的指针被称为悬空指针("dangling pointer"),解引用它是未定义行为。

动态内存在申请完之后在释放之前是可以全局访问的,可以延长变量的生命周期。

动态二维空间的两种方式:

int **p = malloc(sizeof(int *) * n);
for (int i = 0; i < n; ++i)
  p[i] = malloc(sizeof(int) * m);
for (int i = 0; i < n; ++i)
  for (int j = 0; j < m; ++j)
    p[i][j] = /* ... */
// ...
for (int i = 0; i < n; ++i)
  free(p[i]);
free(p);
int *p = malloc(sizeof(int) * n * m);
for (int i = 0; i < n; ++i)
  for (int j = 0; j < m; ++j)
    p[i * m + j] = /* ... */ // This is the (i, j)-th entry.
// ...
free(p);

字符串

char[] 字符串结尾必须要有一个 \0,否则行为未定义。所以记得加额外的空间。
cppreference - 空终止字节字符串

gets() 已于 C11 被移除,现在如果要用最好用 fgets(str, len, stdin);,文件尾出现、发现换行符 或 已读入超过 len-1个字符 停止读取,且包含结尾换行符。

  • strlen(str): Returns the length of the string str. (返回 size_t
  • 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.

strcpy 包含 \0 的复制。参数字符串长度不足或重叠都会导致未定义行为。
strcmp 的结果的符号是被比较的字符串中首对不同字符(都转译成 unsigned char)的值间的差的符号。

字符串相关的函数如果需要遍历字符串,可以考虑直接移动字符指针。

下面这个是 \(O(n^2)\) 的不要这样写。

for (size_t i = 0; i < strlen(s); ++i) // very slow
  // ...

字符串字面量的类型是 char [N+1],其中 N 是其长度,+1 包含结尾的空。(C++中是 const char [N+1]
没有 const 修饰,但是存放在只读内存。
所以你可以试图通过 char * 修改字符串字面量,但行为未定义。最好通过 const char * 指向字符串字面量,或者赋值给 char[]

const char *translations[] = {
  "zero", "one", "two", "three", "four",
  "five", "six", "seven", "eight", "nine"
}; //这是指向字符串字面量的指针的数组,不是二维数组。

Lesson 8

printf(NULL); 是未定义行为。注意区分 the null character '\0', the empty string "" and the null pointer NULL.

main() 的第二种形式:

int main(int argc, char **argv) { // or char *argv[]
  for (int i = 0; i < argc; ++i)
    puts(argv[i]);
}

Run: .\program one two three
Output:

.\program
one
two
three

用动态内存实现一个读入未知字符串的函数

Recitations 3

预处理指令

#include 的含义极其简单:文本替换。它会按照某种规则,找到被 #include 的那个文件,将其中的内容原封不动地复制粘贴过来。

#define 的含义也是文本替换。将其中的内容做原封不动的替换。

Lesson 9

结构体

C语言要声明 struct 变量必须用 struct Name Var; 的形式。(所以以前OI里用C的常用 typedef struct {/*...*/} Name; 来声明结构体类型)

ptr->mem 等价于 (*ptr).mem (而不是 *ptr.mem!).

结构体变量的大小满足(因为有对齐要求所以是不等式):

\[\mathtt{sizeof(struct\ \ X)}\geqslant\sum_{\mathtt{member}\in\mathtt{X}}\mathtt{sizeof(member)}. \]

结构体初始化

结构体初始化的两种方式:

  • 初始化列表:
struct Student stu = {"Alice", "2024533000", 2024, 8};
  • C99 初始化器: (highly recommended)
struct Student stu = {.name = "Alice", .id = "2024533000",
                      .entrance_year = 2024, .dorm = 8}; //.mem=xxx => 指派符

[Best practice] Use designators, especially for struct types with lots of members. 极大提高可读性

复合字面量

使用复合字面量简化赋值:

struct Student *student_list = malloc(sizeof(struct Student) * n);
for (int i = 0; i != n; ++i) {
  student_list[i].name = A(i); // A, B, C and D are some functions
  student_list[i].id = B(i);
  student_list[i].entrance_year = C(i);
  student_list[i].dorm = D(i);
}
// 与下面等价
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)};
}

(数组也可以用复合字面量:int *p = (int[]){2, 4};

结构体拷贝

结构体的赋值,传参与函数返回值都是 “Member-wise copy” ,相当于每个成员拷贝到对应的值(包括数组也是逐元素的拷贝过来)(结构体可以直接 struct A b=a; 赋值拷贝,而数组直接 int b[10]=a; 赋值会报错,需要 memcpy)(可能没用但是贴在这里)

所以函数返回结构体可能会发生两次拷贝行为,编译器会通过NRVO优化减少返回时发生的赋值拷贝。
为了减少传参时发生的拷贝,我们可以考虑传入 const struct Name * 类型的指针来避免直接拷贝结构体。

但同样的,指针也会被直接拷贝从而指向同一个区域,造成不必要的麻烦,需要根据情况处理。
如:

struct Vector {
  double *entries;
  size_t dimension;
};
struct Vector create_vector(size_t n) {
  return (struct Vector){.entries = calloc(n, sizeof(double)),
                         .dimension = n};
}
void destroy_vector(struct Vector *vec) {
  free(vec->entries);
  // Do we need to free(vec)?
}

指针存储的是一块动态内存数组,所以为了拷贝,需要释放原空间,开辟新空间,再逐元素拷贝数据。(注意特判相等)

void vector_assign(struct Vector *to, const struct Vector *from) {
  if (to == from) return;
  free(to->entries); // Don't forget this!!
  to->entries = malloc(from->dimension * sizeof(double));
  memcpy(to->entries, from->entries, from->dimension * sizeof(double));
  to->dimension = from->dimension;
}

Lesson 10

标准库总结

  • IO library <stdio.h>: scanf, printf, fgets, puts, putchar, getchar, ...
  • String library <string.h>: strlen, strcpy, strcmp, strchr, ...
  • Character classification <ctype.h>: isdigit, isalpha, tolower, ...
  • <stdlib.h>: Several general-purpose functions: malloc/free, rand, ...
  • <limits.h>: Defines macros like INT_MAX that describe the limits of built-in types.
  • <math.h>: Mathematical functions like sqrt, sin, acos, exp, ...

C语言实现Vector类

Recitations 5

IO流

“流” (stream) :这些字符像水流一样,流过便不再回头。(丢回流里可 unget()

stdin, stdout : 标准输入流、标准输出流

  • 当我们在终端运行一个程序的时候,在默认情况下, stdinstdout 都被绑定到这个终端。
  • ./program < my_input_file.txt : 将 stdin 重定向my_input_file.txt
  • ./program > my_output_file.txt : 将 stdout 重定向my_output_file.txt
    • 如果这个文件不存在,就创建出来。
    • 如果这个文件存在,它原有的内容会被覆盖掉。
    • ./program >> my_output_file.txt : 追加,而非覆盖。
  • ./program < testcases/3.in > output.txt

enum & Bitfield & union

定义一个新的类型,其值是某几个特定的(有名字的)值之一。

//- 不好的设计:使用 magic numbers
struct Text {
  int color; // 0 黑色, 1 红色, 2 绿色
  char *contents;
};

// in some function
struct Text text = something();
text.color = 0; // 设为黑色

//- 好的设计:使用 `enum` 。
enum TextColor { Black, Red, Green };
struct Text {
  enum TextColor color;
  char *contents;
};

// in some function
struct Text text = something();
text.color = Black;

enum 实际上也是一种整数。

const char *colorToString(enum TextColor color) {
  switch (color) { // 可以放在 switch 里
    // enum items 是整型编译期常量,可以放在 case label 上
    case Black: return "Black";
    case Red:   return "Red";
    case Green: return "Green";
    default:    return "unknown color";
  }
}
//等价于
const char *colorToString(enum TextColor color) {
  const char *colors[] = {"Black", "Red", "Green"};
  return colors[color]; // color 可以作为下标
}

Bitfield 按位分配成员内存。

struct Instruction {
  unsigned imm : 7;
  unsigned r2 : 3;
  unsigned r1 : 3;
  unsigned opcode : 3;
}; // sizeof(struct Instruction) == 7 + 3 + 3 + 3

union 是成员的“叠加”,这些成员共用一片内存。

\[ \mathtt{sizeof(union\ \ X)}\geqslant\max_{\mathtt{member}\in\mathtt{X}}\mathtt{sizeof(member)}. \]