the-c-programming-language-reading-notes

The C Programming Reading Notes

Created: 2023-06-06T15:59+08:00

Published: 2023-08-16T12:14+08:00

Categories: C | ReadingNotes

我看的是第二版,解决了初学 C 语言和 OS 课程的时候的一些疑惑,比如:

extern 的使用,原来 function 和 object 没有什么区别,比如下面的代码,将 afoo() 都暴露给了外部,而 static 关键词既可以用于 object,又可以用于 function,就是不暴露给外部,有点类似 OOP 的 private 的想法。

int a = 0;
int foo () { return 0; }
int main() {/*code*/}

缓冲区的概念,将 FILE 和 OS 里的 read()write() 联系在一起,通过缓冲区减少 syscall。

C 语言也可将函数作为函数的参数,分析定义的类型,可以通过由内而外的方法,比如 int *a 说明 *aint

malloccalloc 的内部结构,原来在分配好的内存前面有一段结构体……

目录

Preface

大意是说,我们随着 ANSI 的 C 标准为本书在第一版上加了一些内容。比如函数定义之类的。

因为第一版在 1978 年,而在 1983 年 ANSI 推出了标准。

Brian Kernighan(布莱恩 柯林汉)不是 C 语言的缔造者,但是他却是这本书的作者。是 Unix 缔造者的三号人物,不过一说起 Unix 缔造者大家更经常提到的是 Ken Thompson 和 Dennis Ritchie。Kernighan 是 Linux 命令中 awk 的 K。

Dennis Ritchie 是 Unix 和 C 语言的缔造者,已于 2011 年去世。

Chapter 1: A Tutorial Introduction

  • 函数:函数的定义,ANSI C 是一种进步,可以尽快查找类型错误
  • 为什么 C 语言函数调用设计成传 value,因为可以让程序紧凑
  • 字符数组的定义,数组作为函数的参数的语法
  • input 的逻辑,按下回车是 flush 缓冲区,同时回车也被送进了缓冲区。在类 Unix 系统下,按下两次 ^D 就是刷新缓冲区
  • extern 关键词的使用:区分好 definition 和 declaration,后者是不分配存储空间的。extern 既可能用于同文件跨函数之间的变量,也可能用于跨文件之间的变量。
  • \t\b 对于在终端上 printf,只是在移动光标而已,\t 背后的逻辑是移动到下一个 active 位置,所以它统计了当前这一行有多少个字符。参考 stackoverflow

1.1 Getting Started

我们调用了一个函数 printf ,这个函数在标准库里面。

我很好奇为什么标准库 include 时候用的是尖括号(angle),让我联想到 C++ 中「标准输入输出」用的也是尖括号 cout <cin >

\n 是转义字符序列(escape sequence)。

1.2 Variables and Arithmetic Expressions

  • 缩进,while 的两种写法
  • 整数除法截断
  • 自动 float 类型转换
  • 使用 % 控制输出数字的位数和小数后几位

char shot int long float double 都是取决于自己的机器的。

In either case, we will always indent the statements controlled by the while by one tab stop (which we have shown as four spaces) so you can see at a glance which statements are inside the loop.

要缩进好哦

We recommend writing only one statement per line, and using blanks around operators to clarify grouping. The position of braces is less important, although people hold passionate beliefs.

一行一个 statement

所以要写成:

while (cond)
    ++i;

而不要写成:

while(cond) ++i;

celsius = 5 * (fahr-32) / 9;,不写成 5/9 * fahr 是因为这样直接乘 0 了。

自动 float 类型转换我知道,但是书中的例子是:fahr - 32.0 而不是 fahr - 32,说这样可以强调减去的是一个 float。

关于 % 的自动输入输出,作者用两个例子来说明,分别是 %3d%3.0f%6.1f,% 后面第一个数字是说整个要多宽(wide),对于浮点数还要区分小数位数。

note. 这个我觉得蛮重要的,因为 Python 好像也用了类似的方式控制输出格式。

1.3 for

jump,没收获

1.4 Symbolic Constant

被 declare 的叫做变量,比如 int a = 0;,写到 #define 里的是「符号常量」

1.5 Character Input and Output

  • 字符流,使用 int 来接受,防止 EOF(End of File)
  • 表达式的 return value,如 while((c = getchar()) != EOF)

A text stream is a sequence of characters divided into lines; each line consists of zero or more characters followed by a newline character.

「文本流」的概念:多行字符,每行结尾有一个 \n。那么,文本流的最后一个字符就是 \n

介绍了 getchar()putchar()

1.5.1 File Copying

作者使用了一个短小精悍的例子,引入 EOF 和 Statement 的返回值:

int c;
while ((c = getchar()) != EOF) {
    putchar(c);
}

EOF 全是大写,恰好和前文的 Symbolic Constant 对应上。EOF 不属于 char 可以表示范围内,可以通过 printf("%d", EOF); 查看具体的数值。

1.5.2-1.5.4 Character Counting, Line Counting, Word Counting

引入 ++ 运算符,当然这个我们都很熟了。

double 也可以 ++,这是我没想到的。

输入 EOF 和缓冲区

关键是如何输入 EOF,这涉及到缓冲区的概念

note. 为什么有缓冲区这种设计,原因之一是避免多次系统调用。有缓冲区以后就要考虑如何刷新缓冲区。带来两个问题:

  1. 我敲下去的东西是否会刷新缓冲区?
  2. 敲下去的东西以什么值进入了缓冲区?

Linux 系统下,在一行的开头直接 ^D,就是输入了 EOF,并且触发(请容许我这么说)了 getchar()

如果直接输入 123 这类字符再敲回车\n,那么回车也被送进了输入流,并且刷新缓冲区。

要在一行内输入 EOF(也就是说一行的开头不是 ^D),要按两次 ^D,并且第一次按下的时候,会把输入缓冲区的内容复制显示在屏幕上一份;第二次按下的时候,刷新缓冲区并将 EOF 送入。

至于 Windows 系统,输入 ^Z 表示 EOF,并不是简单地把 Linux 系统下 ^D 替换成 ^Z 就可以解决的,我也不知道如何在一行内输入 EOFstackoverflow 上说,u can't。

1.6 数组

语法是 int arr[array-length],jump

1.7 Functions

形参:formal argument 是在函数定义中的参数

实参:actual argument 是在函数调用时候的参数

main 函数的返回值有什么用?是告诉执行这个程序的环境的,不要忘记了 CPP Primer 里面提到的 echo $?,可以获得 a.out 调用的返回结果。

一些函数 declare 和 define 方面的内容。涉及到了 ANSI C 的设计。

int power(int base, int n) 为例,代码结构如下:

int power(int base, int n); // (*)
int main() {
    // some code
}
int power(int base, int n) // (**)
{
    // some code
}

(*) 叫做 declaration,也叫做 function prototype,可以被写成 int power(int, int) 或者是 power(int x, int y),也可以通过编译。因为只需要知道参数位置对应的类型就好了,文中鼓励多写,是因为易读。

还介绍了早期 C 语言函数的 declaration 和 definition 语法,那种形式无法尽早地检查类型错误:

int power(); // declaration
int main()
{
    // some code
}
power(base, n) // definition
int base, n;
{
    // some code
}

所以说 ANSI C 是进步。

1.8 Arguments - Call by Value

C 语言中,传的是 value(值),而不是 reference(引用),这个和 Fortran 不一样。

为什么 C 这样设计呢?因为这样的优点是大于缺点,直接传值可以在调用的函数内部修改值而不必在意对 caller 的影响,这可以让程序更加紧凑。

如果要传引用,就要用地址或者说指针(pointer)。

1.9 Character Arrays

类比一下,输入流的结尾约定是 EOF,这个非常好记住,End of File 嘛,而字符串的结尾是 '\0'

还有就是写数组为参数的函数调用的时候,比如 int func(char s[]),不用指定数组的长度,我已经通过编译原理知道这是传入一个 address,毕竟一个寄存器里除了 value 和 reference 还能放得下什么?

1.10 External Variables and Scope

Each local variable in a function comes into existence only when the function is called, and disappears when the function is exited. This is why such variables are usually known as automatic variables, following terminology in other languages.

把这种局部变量称为 automatic 的原因。

书中还提到,static 也是局部变量,在调用后还会保持原来的值而不会消失:

(Chapter 4 discusses the static storage class, in which local variables do retain their values between calls.)

external variable

An external variable must be defined, exactly once, outside of any function; this sets aside storage for it. The variable must also be declared in each function that wants to access it; this states the type of the variable.

external 要在所有函数外定义,函数内部要使用就要说 extern,这是 declare。

extern declaration 在某些情况下可以省略,实际上大部分情况下都是省略的:在通常的做法中,所有外部变量的定义都放在源文件的开始处,这样就可以省略 extern 声明。

我们现在知道了,同一个文件中,跨函数使用同一个变量,需要依赖 extern 这个特性。还有一种情况是,需要跨文件使用同一个变量,因为 CPP 会通过头文件来组织,所以如果要跨文件使用变量,也要使用 external 特性,写 extern。

note. 或许可以理解成,extern 就是说,到别的地方去找这个变量,不要为我分配存储空间了。我记得在 OS 课上看到过 extern 这个语法。

Chapter 2 Types, Operators and Expressions

  1. 变量名的规定,必须字母和下划线(当然这个我知道),让我感兴趣的是,external variable 取决于 assembler 和 linker,所以只能保证 6 个长度内有效。

  2. 数据类型,首先是 math and code,type 是 set,operation 是 function。其次是 basic type 只有 char int float 和 double,short 和 long 是 qualifiers,省略后才说 short 和 long 类型。还有 signed 和 unsigned 类型。表示范围:

    1. short 至少 16 bits,long 至少 32 bits
    2. char 可能可以存储负数,machine-independent
    3. 具体范围可以在 limits.h 中的宏找到
    4. 知道这个有什么用啊?我觉得小心 char 可能是负数就够了
  3. 常量,常量也需要用底层的数据类型来表示,比如一个 integer 总需要 fit 进一个 type 里面

    1. 数字常量,默认是 int 和 double
    2. 后缀表示法和 '' 表示法。比如后缀有 L, U, F, UL,'???, \x??' 八进制和十六进制
    3. 字符串常量,结尾的 '\0' 就是 0,因为数字常量表示法
    4. 枚举常量,要知道枚举常量的作用,说是替代了 #define,具体的赋值方式是自动递增。
  4. declaration:

    1. automatic 和 extern 的默认值是不一样的,想一想编译原理,automatic 需要显示赋值
    2. const这个 qualifier,以及 const 数组表示内部元素不可修改,array assignment 不被允许的原因,是语言设计上的问题。
  5. Arithmetic Operators

    1. 负数的 / 和 % machine dependent
    2. 浮点数没有 %
  6. Relational and Logical Operators

    1. relational > equality, && > ||
    2. 典型例子 getchar
  7. Type Conversion:当引入类型、算符和算子,就会发生类型转换

    1. 低精度向高精度转换
    2. 如果引入符号类型,将更加复杂,比如 -1L > 1UL,因为 Long 被转换为 UL 了,要记住:有符号会被转换为无符号,毕竟无符号是需要人去指定的。
    3. char 到 int 符号不保证,所以使用 unsigned char 或者 signed char 如果真的想要存储整数
  8. Increment and Decrement:记住典型案例,concatenate two strings,只需要两个 while

  9. Bitwise Operators:位运算不难,难的是它的应用

    1. 得到 mask:前导/后置 n 位 0/1 的得到
    2. 循环右/左移使用逐位移动的技巧
    3. 输出一个数字的二进制表示:if (x & 1/2/4/8) '1':'0'
    4. delete lats 1: (x & (x - 1)
  10. Assignment Operators and Expressions

    1. 定义:像 op= 这类都叫做赋值运算符
    2. form/syntax,expr1 op= expr2,所以 x *= y+1 使用 y+1
    3. 返回类型是左操作数,返回值是操作后的结果,所以常常看见 while(n/=10) 这类代码
    4. 好处:看起来更加简洁,甚至可以帮助编译器生成更好的代码
  11. 条件表达式 expr1? expr2:expr3,返回类型是 expr2,两个 trick:换行和复数追加 s

  12. 优先级和结合律

    1. bitwise 低于 equality,所以 mask 操作要用括号
    2. 「结合律」:用于规定优先级相等的算符在一起的计算顺序,比如 x + y + z 要先算 x + y
    3. 不要写有 side effect 的代码,比如 f() + g() 没有规定计算顺序

declaration 规定了 variable 的 type,而 type 就是集合(set),operation 就是函数(function),expression 结合 variable 和 constant 得到新的 value。

note. 这个 set 和 function 的观点来自 FCG。

ANSI C 拓展了很多:

  1. 每个 integer 都有 unsigned 类型
  2. 枚举类型

2.1 Variable Names

变量名的长度上也是有要求的,比如 internal name 约定长度在 31 以内是有效的;而 external names 和汇编器(assembler)和链接器(linker)有关,就不是 compiler 能够控制的了。

2.2 Data Types and Sizes

我们平时说话有 short int long,这些只能是 types,而不是 basic types,basic types 只有:

  1. char:表示字符
  2. int
  3. float
  4. double

然后在 basic types 上加 qualifier(限定符)得到新的类型,比如 short 和 long,然后我们才有 short intlong int,这个时候,int 可以被省略,所以才有了 shortlong 这两种类型,其实它们只是 qualifiers。

关于大小,只有三条:

  1. short 至少 16 bits
  2. long 至少 32 bits
  3. short 长度不超过 int,int 长度不超过 long

还有限定符 signedunsigned,可以给 char 和 整数前加上,要注意的是:

Whether plain chars are signed or unsigned is machine-dependent, but printable characters are always positive.

没人规定 char 一定可以全部表示正数,虽然叫做 char,但是也有可能表示负数。比如前面 while((c = getchar()) != EOF),就是用 int c 来接受的。但是我在 Windows 上用 GCC 写 char c 来接受 EOF 也是能跑的。

具体数据类型表示的范围取决于编译器,在 #include<limits.h> 中有对应的宏,比如一下来自 GitHub 的代码:

#include <stdio.h>
#include <limits.h>

int main(void)
{
  printf("#################### CHAR #####################\n");
  printf("bits: %d\n", CHAR_BIT);
  printf("unsigned char max: %d\n", UCHAR_MAX);
  printf("signed char min: %d\n", SCHAR_MIN);
  printf("signed char max: %d\n", SCHAR_MAX);
  printf("\n");

  printf("##################### INT #####################\n");
  printf("unsigned int max: %u\n", UINT_MAX);
  printf("signed int min: %d\n", INT_MIN);
  printf("signed int max: %d\n", INT_MAX);
  printf("\n");

  printf("################## SHORT INT ##################\n");
  printf("unsigned short int max: %u\n", USHRT_MAX);
  printf("signed short int min: %d\n", SHRT_MIN);
  printf("signed short int max: %d\n", SHRT_MAX);
  printf("\n");

  printf("################## LONG INT ###################\n");
  printf("unsigned long int max: %lu\n", ULONG_MAX);
  printf("signed long int min: %ld\n", LONG_MIN);
  printf("signed long int max: %ld\n", LONG_MAX);
  printf("\n");

  printf("################ LONG LONG INT #################\n");
  printf("unsigned long long int max: %llu\n", ULLONG_MAX);
  printf("signed long long int min: %lld\n", LLONG_MIN);
  printf("signed long long int max: %lld\n", LLONG_MAX);
  printf("\n");

  return 0;
}

// NOTE: The limits.h header contains all the necessary constants machine
// dependent for types sizes.

2.3 Constant

哪怕是一个数字也有对应的类型,我的理解是数字也要被存储到底层,然后就涉及到两个问题:用多大的存储空间?符号/精度?

换而言之,常量也要和现有提供的类型兼容。

数字常量

普通的数字是 int,L 或者 l 后缀是 long,u 或者 U 是 unsigned,ul 就是 unsigned long

普通小数是 double,f 后缀是 float,L 后缀是 long double。

还可以以 '' 表示数字,如 'x' 就取决于编译器自带的字符集的定义,以及将 ? 替换为进制对应小的数字,\??? 就是八进制表示,'\x??'

note. 我的思考,为什么八进制是三位?我想是 char 类型一般 8 bits,二进制转换为八进制,三个三个数字一转就好了,比如 \(123_{(8)}\) 就是 \(001-010-011_{(2)}\),二进制转十六进制就是四个四个数字一转就好了。

note. 关于十六进制之间的转换在 CSAPP 中又被提及

所以,我们平时看到往字符串的结尾写一个 \0,就是在写 0,等价于数字 0, '\0' == 0

字符串常量

string literal,用双引号括起来。

String constants can be concatenated at compile time:

"hello, " "world" equals to "hello, world"

枚举常量

一种 #define 的替代方式。

Enumerations provide a convenient way to associate constant values with names, an alternative to #define with the advantage that the values can be generated for you.

自动递增数字

2.4 Declarations

automatic 和 external 变量初始化区别以及为什么

我们常常认为 main() 外面的变量,也就是 external variable 的初始化和 main() 内部变量的初始化逻辑是一样的,就是一个赋值语句而已,其实并非如此。

main() 或者其他函数内部的变量,这种前文提到,叫做 automatic variable,进入函数时候创建,函数退出后消失,所以是有语句来执行赋值的,而 main() 外部编译完以后可没有赋值语句,所以书里说:

If the variable in question is not automatic, the initialization is done once only, conceptionally before the program starts executing, and the initializer must be a constant expression.

非 automatic 变量是有默认值的,为 0。关于 automatic variable,仔细想一想,编译原理课上,没有为这类变量的 declaration 生成赋值语句,所以里面初始值是 garbage。

note. 或许这就是编译课程上的 .bss 段的内容。

const

曾经有一个问题让我混淆,就是 const int a 还是 int const a,现在说明白了,const 作为 qualifier,想一想 short intshort,也是 qualifier,所以在前面。

数组的 const 意思是,数组内部的元素不能被改变,如 const char msg[] = "hello world",不能修改 msg 数组内部的信息:

#include <stdio.h>

int main()
{
    const char msg0[] = "hello";
    const char msg1[] = "world";
    // msg0[0] = 'a'; // error
    printf("%s\n", msg0);
    return 0;
}

note. 我注意到 array assignment 是不被允许的,因为 array 可以被视作 pointer 方便理解,但是实际上因为 struct 内部可以有 array,实际上是 track array 的地址来实现的,array is not pointer。参考 StackOverflow 上的回答:Why do C and C++ support memberwise assignment of arrays within structs, but not generally?

允许 array assignment 会带来麻烦,究竟是 member-wise 赋值呢,还是修改指向呢?

2.5 Arithmetic Operators

The % operator cannot be applied to a float or double. The direction of truncation for / and the sign of the result for % are machine-dependent for negative operands, as is the action taken on overflow or underflow.

关于 operand 能不能是小数

还有优先级

2.6 Relational and Logical Operators

precedence: relational operators > equality operators(比大小高于是否相等)

&& 高于 ||

典型的例子:

while(i < limit && (c=getchar()) != '\n' && c != EOF)

首先检查能否容纳,不能的话,连 getchar 都省了,然后是必须要先 getchar 再判断,不然使用的是上一次的 char。

有 relational 和 logical operators 的话,直接以 logical 分割开来。

2.7 Type Conversion

Expressions that don't make sense, like using a float as a subscript, are disallowed.

没意义的,比如把 float 作为下标的,被 disabled

一般来说,都是小精度转换为高精度,比如 float + int,就是把 int 转换为 float

而 char 作为最小的单位,可以放心使用,但是注意 char 没保证正负,所以发生 char 到 int 的转换的时候,比如 0xff,是正数还是负数?varies from machine to machine

所以如果想要用 char 来存储数字,并且保证可移植性(portability),指定 signed 和 unsigne

关于逻辑运算结果,非 0 即 1(毕竟是逻辑运算),但是在 if, while 中,使用 non-zero,这两点不一样。

-1L > 1UL,因为 -1L 被扩展为 UL

  1. 负数还是整数被强制转换?
  2. 负数强制转换是否变成正数了?

note. 这一章好烦啊

赋值语句也可能导致类型转换。

2.8 Increment and Decrement Operators

有一些典型的案例可以记住。

concatenate two strings:

void concat(char s[], char t[]) {
    int i, j;
    i = j = 0;
    while (s[i] != '\0')
        i++;
    while ((s[i++] = t[j++]) != '\0')
        ;
}

2.9 Bitwise Operators

为什么叫做 bitwise,因为是「逐位」,施加于整数

六种位运算:&, |, ^, ~, <<, >>

>> 施加于符号和无符号数有区别,无符号填充 0,有符号填充 1。

技巧:就是保留掩码,我们可通过 ~0 构造四个函数,建立更高层次的抽象

关键是:

  1. 是前导的个数确定还是后置个数确定?
  2. 前导或后置的是 0 还是 1?
unsigned head_n_0(int n)
{
    return (~(unsigned)0) >> n;
}

unsigned head_n_1(int n)
{
    return ~head_n_0(n);
}

unsigned last_n_0(int n)
{
    return (~0) << n;
}

unsigned last_n_1(int n)
{
    return ~(last_n_0(n));
}

循环右移(rightrot)不是使用掩码,比如 x 循环右移 5 位,x 最右端的 5 位获取不难,但是要移动到最左边,我们不知道 x 的总长度的。使用多次移动一位来实现移动多位:

if (x & 1)
{
    x = (x >> 1) | msb_1; // I guess "msb" means "most significant bit"
}
else
{
    x = (x >> 1);
}

2.10 Assignment Operators and Expressions

赋值运算符不仅仅指的是 x = 1 里面的 =,像 i+=1 里面的 += 也算赋值运算符。

expr1 op= expr2 相当于 expr1 = (expr1) op (expr2),所以 x *= y+1 就是 x = x * (y+1)

赋值运算符的好处是使得看起来更加简洁(succinct)

In all such expressions, the type of an assignment expression is the type of its left operand, and the value is the value after the assignment.

assignment expression 返回左操作数的类型,返回操作后的结果作为 value,所以我们常常见到把 assignment expression 作为 condition。

In all such expressions, the type of an assignment expression is the type of its left operand, and the value is the value after the assignment.

2.11 Conditional Expressions

语法是 (epxr1)? expr2: expr3,类型是 expr2

好处是看起来 compact:

一个 trick 是用这个来换行:

for (i = 0; i < n; ++i)
	printf("%6d%c", a[i], (i%10 == 9 || i == n)? '\n', ' ');

还有一个是复数的输出追加 "s"

printf("you have %d item%s", n, (n==1)? "", "s");

2.12 Precedence and Order of Evaluation

算符的优先级和结合律表:

note. 我觉得很难记啊,只能记住作者说:

Note that the precedence of the bitwise operators &, ^, and | falls below == and !=. This implies that bit-testing expressions like
if ((x & MASK) == 0) ...
must be fully parenthesized to give proper results.

以及算符的结合律,指的是 x op y op z 的计算顺序,要有多个 operators:

In programming language theory, the associativity of an operator is a property that determines how operators of the same precedence are grouped in the absence of parentheses.

Operator associativity

比如 x + y + z 表示 (x + y) + z,但是对于单个 x + y 没有约定计算顺序,类似的例子还有 a[i] = i++; 等,这类运行结果依赖于具体的 compiler 实现,会有 side effect,要规避。

Chapter 3 - Control Flow

可以阅读 Appendix B.2 了解下 ctype.h 这个标准库,是为了 test characters

  1. Statements and Blocks:使用 {} 作为 block,让多个语句看起来像一条语句
  2. if-else:jump
  3. else-if:jump
  4. switch:
    1. 注意 case 穿透,如果利用这个特点,这是 mixed blessing,需要 comment
    2. 好的风格会在 default 后面追加 break,防止以后 default 变成 case 忘记
  5. Loops - While and For:逗号运算符,也要谨慎使用
  6. Loops - Do While: 要知道 Do While 的常用场景:
    1. itoa(), itob() 这一类,因为哪怕是一个 0 也要先写入到 array 中。
    2. 符号,尤其是负数的取余,被定义为 -1 % 10 = -1
    3. 注意结果需要 reverse。
  7. Break and Continue: continue 对于 for 还会执行 expr3
  8. Goto: goto 在一些情况下也有用处,如 error handle 或者从 nested 中 break,但是还是要少用。

3.1 Statements and Blocks

An expression such as x = 0 or i++ or printf(...) becomes a statement when it is followed by a semicolon.

expression 加上分号 ; 变成 statement

Block 通过 {} 来定义,为什么要有 block,就是语法上充当一个大的 statement。

3.2 If-Else

jump

3.3 Else-If

jump

3.4 Switch

case 穿透要注意,这很少利用到这个特点,是 mixed blessing,如果使用要 comment

最后 default 也要在结尾添加 break 语句,因为可以防止以后再尾部追加的时候,忘记加上 break。

3.5 Loops - While and For

逗号运算符,返回的结果和类型是最右边的操作数。

逗号运算符也要谨慎使用,常常用在初始化和递增上,比如:for(i = 0, j = strlen(s); i < j; ++i, --j)

3.6 Loops - Do While

典型的案例是 itoa() (integer to array)把数字转换为字符串,使用 do-while 最简便,因为哪怕只有一个 0,也需要写入 string 中。

关于取余:\(a \% b, (a < 0, b > 0)\) 的结果在 \([-(b-1), (b-1)]\) 之间。

书中的练习题有一个注意的地方,就是负数的 itoa 不能反转符号,因为 two's complement 表示法的范围不是对称的。可以直接对负数取余再 abs。

3.7 Break and Continue

jump

3.8 Goto

goto 在某几种特定的情况下也有自己的用武之地,但是还是建议不要使用,而是通过一些方法改写。

Chapter 4 - Functions and Program Structure

  1. basics of functions:我只记得不说返回类型就 assume 是 int
  2. Functions Returning Non-integers
    1. 若 caller 和 callee 在同一个源文件种,可以做 return type 的类型检查:要求 declare 和 definition 必须相同
    2. 分离式编译无法做 return type 的 match 检查
    3. 如果不给出 function prototype,将会认为返回 int,并且不假定参数的类型和个数(won't assume arguments)
    4. 定义空参数的函数要使用 <function-name> (void),因为如果不写 void,编译器为了兼容,不会 assume arguments
  3. External Variables:
    1. 定义在任何函数外的 variables 就是 external variables,functions 永远是 external 的
    2. external variables 具有全局 scope,可以作为函数之间通信的数据
  4. Scope Rules: 介绍 extern 的作用:在 a.cextern int i; 就是说到别的文件的 external variable 中寻找 i
  5. Header Files: Jump
  6. Static Variables: 文件 - 函数 - 变量
    1. 对 external variable 加上 static 关键字,就是屏蔽其他文件对其的访问,function 本身作为一种 external,同理
    2. 对 internal variable 加上 static,就不是 automatic 了,permenent storage for function
  7. Register Variables: 告诉 compiler 这个 variable 放到 machine register 里,但是 compiler 可以 ignore
  8. Block Structure: Jump
  9. Initialization:
    1. external 和 static 变量初始化要在程序运行前完成,所以必须要用 constant expression 初始化,否则有默认的值 0
    2. automatic 的初始化是赋值语句的 shorthand,写过编译器 demo 可以理解,就是对 stack 里面赋值,所以不初始化里面是 undefined
  10. Recursion: Jump
  11. The C Preprocessor
    1. Include: 就是 copy,"" 在同级目录下找,<> 和编译器实现有关
    2. Macro: 既可以简单的文本替换,也可以带参数,但是带参数要注意 pitfalls,比如 max(++i, ++j),还有 dprint 例子

4.1 basics of functions

If the return type is omitted, int is assumed.

4.2 Functions Returning Non-integers

关于函数的返回类型,不仅仅是写上 <return-type> <function-name> 那么简单,ANSI C 的一大进步就是允许定义好函数的参数类型来做类型检查。

关于函数的返回类型,如果 caller 和 callee 在同一个源文件中,就可以通过 return type 在编译阶段进行检查,以要求 declare 和 definition 的 return type 一致。如果采用分离式编译,就无法做这种检查。

note. why?为什么分离编译就无法检查到 return type 不一致?

上下文也可以声明函数,此时认为声明了一个 int 类型的函数:

If a name that has not been previously declared occurs in an expression and is followed by a left parentheses, it is declared by context to be a function name, the function is assumed to return an int, and nothing is assumed about its arguments.

空参数请使用 void 作为 argument,如果参数为空,表示不对 argument assume,这为了兼容而采取的操作。

note. 所以我们会看见定义函数的时候有 int func(void); 这样的语句

4.3 External Variables

external 是相对于 internal 而言的,external 有 function 和 variables

function 一定是 external 的,因为 C 不允许函数内定义函数

定义在任何 function 外的 variable 就是 external 的

external linkage:哪怕是单独编译的,只要名字相同,链接到一块的时候用的就是同一个变量。

external 的作用:

  1. scope 为全局,所以可以被所有函数看到,所以可以作为函数通信的一种方式
  2. lifetime 长

4.4 Scope Rules

automatic 的 scope 非常简单,重点是 external variable 的。

declaration 只是声明类型,不分配存储空间,definition 才分配空间

如何书写 declaration?如何分布?

假设:int i, arr[MAXLEN];a.c 中,并且写在任何函数外面,就是一个 external variable 的 definition,如果 b.c 要使用它,那么肯定不能写 int i;,那样和 a.c 中就没有任何区别了,而是要写 extern int i, arr[];

4.5 Header Files

jump

4.6 Static Variables

static 施加于 external 变量上,说明该变量只在该 source file 内可见,其他 file 使用 extern 关键字也无法获取到这 static external variable。

如下代码:buf 和 bufp 要在 getch()ungetch() 两个函数之间共享,所以必须是 external,但是又不希望被其他编译的文件访问到,所以加上 static

static char buf[BUFSIZE]; /* buffer for ungetch */
static int bufp = 0; /* next free position in buf */
int getch(void) { ... }
void ungetch(int c) { ... }

同理,static 关键词也可以用于函数(函数本身就是一种 external),防止函数被其他 file 看到。

static 用于 internal variable 时候:

they remain in existence rather than coming and going each time the function is activated. This means that internal static variables provide private, permanent storage within a single function.

相当于函数的一个永久的存储空间。

4.7 Register Variables

usage: tell compiler the variables should be place in machine registers, but the compilers are free to ignore them

limits: on automatic and formal varibales

4.8 Block Structure

jump

4.9 Initialization

In the absence of explicit initialization, external and static variables are guaranteed to be initialized to zero; automatic and register variables have undefined (i.e., garbage) initial values.

For external and static variables, the initializer must be a constant expression; the initialization is done once, conceptionally before the program begins execution.

对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前进行初始化)

In effect, initialization of automatic variables are just shorthand for assignment statements. Which form to prefer is largely a matter of taste.

automatic 变量的初始化相当于赋值语句。

4.10 Recursion

jump,写过编译器哪怕是 demo 的人都能理解

4.11 The C Preprocessor

4.11.1 File Inclusion

#include "filename" 或者 #include <filename> 做的就是 copy 的事情

"filename" 在 source file 的同目录下查找,<filename> 如何查找和编译器具体实现有关。

4.11.2 Macro Substitution

definition is for token,所以 string 和 非空白符分割是不会被替换的。

#undef derivative 用于取消一个 #define,可以用来确保某一个东西的实现不是宏。

宏定义可以带参数,效果类似于函数,却有所不同:

definition 的 pitfall,比如 #define square(x) x * x 无法处理 square(z+1),所以常常看见很多括号,以及 square(x++) 会导致 x 加两次。

如果给参数带上 # 前缀,表示用引号将其括起来:

#define dbg_print(expr) printf(#expr " = %g\n", expr) // correct
#define dbg_print(expr) printf("expr = %g\n", expr) // wrong

至于 ## 用于 concatenate 多个参数,见 appendix。

4.11.3 Conditional Inclusion

Jump

Chapter 5 - Pointers and Arrays

  1. Pointers and Addresses: pointer 就是存储了地址的 variable,助记 int *p 表示 *p 是 int 类型

  2. Pointers and Function Arguments: 如果要将函数的返回值作为函数的执行状态,可以使用指针作为参数传递执行结果,比如 int getint(int *p)

  3. Pointers and Arrays:

    1. pointer 和 array 在 index 和 + 上的操作完全一致。比如 arr + i 表示地址,p[i] 来 index 数组元素
    2. 但是 array 的名字不是变量,我认为是一个 <type> * const,不能修改一个数组名字的指向
  4. Address Arithmetic:地址的运算

    1. 同类型地址的赋值
    2. 加减整数 i,表示移动 i 个单位
    3. 地址减去地址,表示相差的元素个数:pi0 - pi1
    4. 地址比较,高地址大
    5. 其余运算都是不合法的,尤其是地址的加法
  5. Character Pointers and Functions: Jump

  6. Pinter Arrays; Pointers to Pointers: array store pointers,multi-lines 的例子

  7. Multi-Dimensional Arrays:

    1. 定义
    2. 多维数组作为函数参数,第一个维度被忽略
  8. Initialization of Pointer Arrays: jump

  9. Pointers vs. Multi-dimensional Arrays: 知道 int *b[10]; 是什么意思就好

  10. Commend-line Arguments:argcargv 的含义,就是命令行传参,char *argv[]

  11. Pointers to Functions

    1. 定义一个函数指针:int (*pf)(); 表示对于 pf 这个名字,先 * 后得到函数,然后 () 调用函数,返回 int 类型
    2. 传入函数名字即可作为函数指针,& 可以省略
  12. Complicated Declarations: C 因为定义繁杂被诟病,因为没法从左往右读,函数调用运算符 () 优先级高于解指针 *,引入递归下降程序,将定义分解为 *, [], ?(), (?) 几个算子

5.1 Pointers and Addresses

关于指针的定义,可以「助记」为:int *p 表示 *p 是 int 类型。

其他内容 jump

5.2 Pointers and Function Arguments

使用 pointer 作为函数的参数,一种情形是:函数的放回值表示函数的执行情况,比如 int getint(),需要 return -1 表示没有获得到一个合法的 int,所以使用指针作为获得到 int 的结果:int getint(int *)

5.3 Pointers and Arrays

首先是关于 pointer 和 array 的通用性:

int a[5], *pa;
// equivalence
pa = &a[0];
pa = a;

// equivalence
*(a+i);
*(pa+i);
a[i];
pa[i];

然后是不同之处:数组不是变量,没有一个真正的变量 a 存储着地址,所以不能修改 a 的指向:

There is one difference between an array name and a pointer that must be kept in mind. A pointer is a variable, so pa=a and pa++ are legal. But an array name is not a variable; constructions like a=pa and a++ are illegal.

note. a 好像 C++ 里的 Reference

如果确定数组不越界,也可以 p[-1] 来 index

5.4 Address Arithmetic

地址的加减法和比较

众所周知,加法 p+i 就是沿着高地址方向,移动 i 个对应的元素,而减法的 operand 就可以不是 integer 了,比如两个 operand 都是地址,p - q + 1 表示指针 p 到 q 包含的元素的个数,同时包括 p 和 q 所指的元素。下面的 strlen 用的就是减法:

int strlen(char *s)
{
    char *p = s;
    while (*p != '\0')
        ++p;
    return p - s;
}

The valid pointer operations are:

  1. assignment of pointers of the same type
  2. adding or subtracting a pointer and an integer
  3. subtracting or comparing two pointers to members of the same array
  4. assigning or comparing to zero.

All other pointer arithmetic is illegal.

5.5 Character Pointers and Functions

JUMP

大概就是用指针替换数组重写原来那些 strlen srcpy 函数

5.6 Pointer Arrays; Pointers to Pointers

jump

5.7 Multi-dimensional Arrays

使用闰年月份天数的例子,初始化多维数组:

static char daytab[2][13] = {
    {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};

多维数组作为参数的时候,第一个维度大小可以被忽略,剩下维度必须被指定:

More generally, only the first dimension (subscript) of an array is free; all the others have to be specified.

note. 我认为,这么做的原因是,允许地址的加法。

5.8 Initialization of Pointer Arrays

jump

5.9 Pointers vs. Multi-dimensional Arrays

int *b[10] 意思是初始化 10 个空间放指针,每个指针用来指向 int 类型,所以可以指向一个 int 数组开始的元素。

5.10 Command-line Arguments

main 执行的时候可以带参数,argc 就是参数个数,默认自己名字也算一个,argv 就是前面提到的 char* 的数组,每一个都指向参数。

首先使用 echo 的例子介绍 argcargv

#include <stdio.h>
/* echo command-line arguments; 2nd version */
main(int argc, char *argv[])
{
    while (--argc > 0)
        printf("%s%s", *++argv, (argc > 1) ? " " : "");
    printf("\n");
    return 0;
}

第二个例子是 find [-n] [-x] <pattern>,支持 any order of optional arguments find -nx pattern

就是一个遍历 argv,再在内部遍历对应字符串的例子

5.11 Pointers to Functions

例子是自定义 qsort 的比较方法,于是 qsort 定义是:

void qsort(void *v[], int left, int right, int (*comp)(void *, void *));

注意 int (*comp)(void *, void *),首先,comp 本身表示函数的指针,通过 *comp 取出对应的函数,然后通过括号运算符 () 来调用函数,返回是 int 类型。

在调用 qsort 时候,因为 comp 已经知道是函数类型,所以可以不用写 & 去取一个函数的地址,直接将函数的 identifier 作为参数传入即可。

/* qsort: sort v[left]...v[right] into increasing order */
void qsort(void *v[], int left, int right,
			int (*comp)(void *, void *))
{
	int i, last;
	void swap(void *v[], int, int);
	if (left >= right) /* do nothing if array contains */
		return; /* fewer than two elements */
	swap(v, left, (left + right)/2);
	last = left;
	for (i = left+1; i <= right; i++)
		if ((*comp)(v[i], v[left]) < 0) // (*comp) get function then call
			swap(v, ++last, i);
	swap(v, left, last);
	qsort(v, left, last-1, comp);
	qsort(v, last+1, right, comp);
}

切记不能写成 *comp(arg1, arg2),正确的写法是 (*comp)(arg1, arg2)

5.12 Complicated Declarations

不直观的原因:

  1. 无法从左往右读
  2. 过度使用括号

int *f();int (*pf)(); 前者定义函数,后者定义函数的指针。

然后手写递归下降……,可见引入指针后,对于 type define 会有比较复杂的要求。

dcl 递归下降的文法就是:

  1. * 表示 a pointer to ...,比如 int *<something>dcl: [*+] <direct-dcl>
  2. <something>() 表示 return a type of sth, direct-dcl: direct-dcl()
  3. (<something>) direct-dcl: (dcl)
  4. direct-dcl[optional-size] 表示对结果取 index,direct-dcl: direct-dcl[optional-size]

note. 就是看对 name 进行什么操作最后返回了 base type,这个递归下降还要回想以前编译器的知识,我选择 jump

Chapter 6 - Structures

  1. Basics of Structures:

    1. 语法:list of declarations enclosed in braces
    2. 定义对应结构的变量
    3. 对变量赋值
    4. 获取变量的成员
  2. Structures and Functions:定义了在 structure 上允许的操作和 structure 作为函数参数

    1. 结构体的赋值、作为函数参数就是整个拷贝进去,所以当结构体空间大时候,造成不必要的开销
    2. 可以将结构体的指针作为函数的参数传入
    3. 因为结构体的指针使用得是如此频繁,所以有 (*p).member 的 shorthand p->member
    4. 取结构体成员的优先级最高,和函数调用 () 和数组下标 [] 一样高
  3. Arrays of Structures: 使用 sizeof() 这个算符,在 compiler time 计算出 struct 对应的大小,返回类型为 size_t

  4. Pointers to Structures: Jump

  5. Self-referential Structures: 通过二叉树的例子介绍

  6. Table-Lookup: jump

  7. Typedef: CPP Primer 讲过了,这个还可以用来定义函数的类型,还有一大作用是定义兼容性的那些类型,比如各种整数

  8. Unions: 一块内存区域,可以被解释成不同的类型,但是需要程序员自己去维护解析成为何种类型

    1. 内部结构上,一个 Union 就是刚好可以存放最大的类型的大小
    2. 只能用第一个类型来初始化它
    3. 访问、写入和 struct 一样,具体表示何种类型需要程序员自己维护、
  9. Bit-fields:自定义 struct 中内存的布局,比如这个类型占据多少 bit,用 : 分割

The main change made by the ANSI standard is to define structure assignment - structures may be copied and assigned to, passed to functions, and returned by functions.

6.1 Basics of Structures

declaration 决定了一个 structure 的基本结构,比如:

struct point {
    int x;
    int y;
};

如果在结尾的 ; 前面给定 identifier 就会为这些变量分配空间

定义对应 struct 变量语法为 struct point pt;struct point 就像 base type 一样。

初始化 pt = {1,2};

获取成员(member):pt.x;

6.2 Structures and Functions

The only legal operations on a structure are

  1. copying it or assigning to it as a unit
  2. taking its address with &
  3. accessing its members

这里定义了结构体的赋值就是拷贝,以及结构体作为函数的参数传入也是拷贝一整个结构体。

当结构体的空间占用较大时候,由此导致的拷贝可能比较耗费性能,所以结构体的指针就被大量使用。

(*p).member 需要一个 shorthand,就是 p->member

The structure operators . and ->, together with () for function calls and [] for subscripts, are at the top of the precedence hierarchy and thus bind very tightly.

取成员的操作符优先级和 function call () 以及 array subscript [] 一样高,都是最高的,所以:

  1. char *keyword[len] 表示先取 [] 然后 * 得到 char 类型
  2. ++p->member 是对 p 的 member 自增
  3. int *f() 表示函数 f 调用后,再 * 得到 int 类型返回值

6.3 Arrays of Structures

例子是使用 keytab 这个结构体数组统计输入文件中 C 语言中各个关键词出现的个数

为了遍历这个数组,使用了 sizeof() 这个 operator,能够在 compiler time 计算出占用字节数,sizeof() 返回的是 size_t 类型的数据,定义在 stddefl.h 这个头文件中。

6.4 Pointers to Structures

jump

6.5 Self-referential Structures

通过介绍二叉树,引入概念:

struct node {
    char* word;
    int count;
    struct node *left;
    struct node *right;
}

note. 在我看来这个东西用链表就能讲清楚,但是作者用了二叉树,可以串联前面的「递归」。

Alignment requirements can generally be satisfied easily, at the cost of some wasted space, by ensuring that the allocator always returns a pointer that meets all alignment restrictions. The alloc of Chapter 5 does not guarantee any particular alignment, so we will use the standard library function malloc, which does. In Chapter 8 we will show one way to implement malloc.

note. 原来 alloc 分配的地址是不对齐的,而标准库函数 malloc 是内存对齐的。

6.6 Table Lookup

jump

6.7 Typedef

简单用法 typedef char *String;,对于自定义的类型,用一个大写来和原来的类型区分开。

typedef 还能做 #define 做不到的事情,比如定义一个函数类型:

// define pointer points to a funcion takes two char * arguments returning int
typedef int (*PF)(char *, char *);
PF strcmp; numcpy;

typedef 的除了「美学」以外的好处:

  1. 兼容性,定义那些可能导致兼容性的类型,这样每次程序需要迁移时候,只用修改 typedef,比如定义各种整数类型,如 size_t
  2. 易于理解

6.8 Unions

In effect, a union is a structure in which all members have offset zero from the base, the structure is big enough to hold the ``widest'' member, and the alignment is appropriate for all of the types in the union. The same operations are permitted on unions as on structures: assignment to or copying as a unit, taking the address, and accessing a member.

一块内存区域,用于 interpret 不同类型。至于如何 interpret,交给程序员自己:

It is the programmer's responsibility to keep track of which type is currently stored in a union; the results are implementation-dependent if something is stored as one type and extracted as another.

比如专门用一个变量来存储当前 union 存储的是什么类型。

只能用第一个类型来初始化:

A union may only be initialized with a value of the type of its first member.

#include <stdio.h>
union u_tag
{
    int ival;
    float fval;
};

typedef unsigned char *byte_pointer;

void show_byte(byte_pointer start, size_t len)
{
    int i;
    for (i = 0; i < len; ++i)
    {
        printf("%.2x ", start[i]);
    }
    printf("\n");
}

int main(int argc, char *argv[])
{

    union u_tag u = {3.14};                      // initialize by 3
    printf("interpret as integer:%d\n", u.ival); // 3
    printf("interpret as float:%f\n", u.fval);   // 0.0
    show_byte((byte_pointer)&u, sizeof(u)); // 03 00 00 00

    u.fval = 3.14;
    printf("interpret as integer:%d\n", u.ival); // 1078523331
    printf("interpret as float:%f\n", u.fval);   // 3.14000
    show_byte((byte_pointer)&u, sizeof(u)); // c3 f5 48 40
    return 0;
}

note. 这就是 CSAPP 中所谓的 information is bits + context 吧,大家都共用一个存储空间,就看如何去访问了。

6.9 Bit-fields

从 mask 引入,一个 struct 不仅能放置 base type,还能够自定义结构体中每一段 bit-field 的长度,比如:

#include <stdio.h>

struct fields
{
    unsigned int is_keyword : 1;
    unsigned int is_extern : 1;
    unsigned int is_static : 1;
};

int main(int argc, char *argv[])
{
    struct fields f = {0,1,0};
    printf("size of fields: %d\n", sizeof(struct fields)); // 4 bytes
    printf("%d %d %d\n", f.is_keyword, f.is_extern, f.is_static);
    return 0;
}

具体的大小是 machine independent 的,内部只能是 int 类型,无法被取地址

Chapter 7 - Input and Output

  1. Standard Input and Output: 标准输入输出,介绍了 < > | redirect 和 pipeline 机制
  2. Formatted Output - printf: printf 最后的 f 就是格式输出的意思,涉及左右对齐、宽度、精度等
  3. Variable-Length Argument Lists:如何编写变长变量的函数,讲了一个头文件 stdarg,用一个 va_list 类型的指针,表示指向的是参数列表,然后用函数 va_start() 初始化这个指针,通过函数 va_arg 移动指针
  4. Formatted Input -Scanf:
    1. 输入就是由一个个 field 组成的,field 通过空白符分割,像 %d\n%d%d%d 没有区别
    2. 解析的方式
    3. scanf 返回解析成功的个数
  5. File Access:
    1. fopen 返回 FILE*,记得 fclose
    2. 操作系统默认三个流,in out err
    3. 一切皆文件,所以 fprintf 配合 stdin 就是 printf
  6. Error Handling - Stderr and Exit: stderr 可以避免错误信息被 pipeline 到其他程序,exit 可以在任意函数内调用终止程序并提供一个 value
  7. Line Input and Output:
    1. int fgets(char *s, int maxline, FILE *fp)int fputs(char *line, FILE *fp)
    2. getsputs 就是将前两者连接到标准输入输出上,但是在处理换行符的行为有所不同

The properties of library functions are specified in more than a dozen headers; we have already seen several of these, including <stdio.h>, <string.h>, and <ctype.h>.

7.1 Standard Input and Output

介绍了 stdio.h 中三个函数,getchar(), putchar(), printf()

还介绍了使用 <, >, | 来重定向和 pipe 输入,箭头 <, > 用于将 file 的内容作为标准输入和输出,| 是连接 program 的输入和输出。

note. 在 MIT6S081 里介绍了这种机制

最后提到了这些「函数」其实是宏,来避免调用的开销。

7.2 Formatted Output - printf

Each conversion specification begins with a % and ends with a conversion character.

对齐方式,宽度,精度,以及如何解析(d, i, x, X, o, ...

7.3 Variable-length Argument Lists

如果要写变长参数列表函数,那么如何引用变长的参数?

使用头文件 stdarg.h,里面定义了一些类型与宏:

  1. va_list: argument pointer 的类型
  2. va_start(<va_list>, <named_argument>): 宏,给定一个起了名字的变量,通过它索引到下一个变量
  3. va_arg(<va_list>, <type>): macro,take a step by the given type
#include <stdarg.h>
/* minprintf: minimal printf with variable argument list */
void minprintf(char *fmt, ...)
{
    va_list ap; /* points to each unnamed arg in turn */
    char *p, *sval;
    int ival;
    double dval;
    va_start(ap, fmt); /* make ap point to 1st unnamed arg */
    for (p = fmt; *p; p++)
    {
        if (*p != '%')
        {
            putchar(*p);
            continue;
        }
        switch (*++p)
        {
        case 'd':
            ival = va_arg(ap, int);
            printf("%d", ival);
            break;
        case 'f':
            dval = va_arg(ap, double);
            printf("%f", dval);
            break;
        case 's':
            for (sval = va_arg(ap, char *); *sval; sval++)
                putchar(*sval);
            break;
        default:
            putchar(*p);
            break;
        }
    }
    va_end(ap); /* clean up when done */
}

note. 这里要强调的是压栈顺序,一般来说,从右往左压栈,fmt 位于栈的底部。

汇编中,callee 知道当前栈指针(sp)指向栈底,如果从左往右压栈,那么 sp 指向最后一个参数,但是却不知道它类型,就没办法索引到前面的固定参数位置。
因为栈是从高地址向低地址增长的,每次 callee 要确定最后一个压栈导致的参数的类型,才能索引到其他参数。

int sum(int x, ...) {
    int *p = &x;
}

int main(void) {
    sum(1, 1.0, 2.0);
    sum(1, 1.0);
}

如果从左往右压栈,sum() 的汇编中分别就是 sp + 2 * sizeof(double)sp + sizeof(double),可是 sum 采用分离编译,他才不知道调用者给了哪些参数。
所以固定参数必须被 sp 指着才能直接取地址。

7.4 Formatted Input - Scanf

scanf(char *format, ...) 从标准输入读取,根据 format 将解析到的值赋值(assignment)给 argument list 中的变量。

scanf 何时 return 取决于 format,如果 format 匹配完了输入的字符,就 return,或者 format 无法匹配,也 return。

返回值:

  1. 成功解析的个数,0 表示一个都没有解析到。
  2. -1: 一个都没解析到,而且还遇到了 EOF

sscanf(char *string, char *format, arg1, arg2) 是从 string 中解析。

A conversion specification directs the conversion of the next input field. Normally the result is places in the variable pointed to by the corresponding argument. If assignment suppression is indicated by the * character, however, the input field is skipped; no assignment is made. An input field is defined as a string of non-white space characters; it extends either to the next white space character or until the field width, is specified, is exhausted. This implies that scanf will read across boundaries to find its input, since newlines are white space. (White space characters are blank, tab, newline, carriage return, vertical tab, and formfeed.)

输入是由 input field 组成的,input field 的定义是非空白符组成的,所以 %d%d 可以直接输入 1\n2 来匹配,因为 1 和 2 这俩 input field 中间已经有了空白符,scanf 会去寻找下一组 input field,而且 format 中的空白符也会被忽略掉,%d\n\n %d%d%d 没区别。

至于 conversion specifications,有 * 表示跳过解析到的,不赋值,比如 scanf("%d%*d%d", &a, &b); 赋值的是第一个和第三个。h 表示用 short 存储,l 或者 L 表示用 long 存储。

7.5 File Access

open 文件的接口:FILE *fopen(char *name, char *mode) 返回的是一个 pointer to FILE,FILE 是一个 typedef 出来的东西

mode 有 read, write, append,都是调用接口

一切皆文件,所以其实 stdinstdout 也是 FILE,C program 运行,操作系统默认打开的三个文件是:stdinstdoutstderr,in 连接到 keyboard,out 和 err 连接到 screen

读写文件的接口是 int getc(FILE *fp)int putc(int c, FILE *fp),scanf 和 printf 也是默认连接了

#define getchar() getc(stdin)
#define putchar(c) putc((c), stdout)
int fscanf(FILE *fp, char *format, ...)
int fprintf(FILE *fp, char *format, ...)

读写文件结束最后要 fclose,有两个原因:

  1. 操作系统可以管理的资源是有限的
  2. 会 flush 缓冲区

程序结束也会自动调用 fclose

7.6 Error Handling - Stderr and Exit

如果不使用 stderr,那么错误信息有可能被 pipe。

使用 fprintf(stderr, ...) 向 stderr 输出

exit(value) 让程序终止执行,和 return 的区别是,exit 可以在任何一个函数内部调用。

同时返回给调用者 value

note. 注意 main 中 return 的 value 也是可以通过某种方式 access 的。

ferror(FILE *fp)feof(FILE *fp) 检查 fp 对应的流

7.7 Line Input and Output

char *fgets(char *line, int maxline, FILE *fp)

maxline 允许的范围内,line 最后两个字符肯定是 \n \0。关于返回值:

Normally fgets returns line; on end of file or error it returns NULL. (Our getline returns the line length, which is a more useful value; zero means end of file.)

note. Unix 默认文件结尾都要有 \n,所以 EOF 前面不会有普通字符,这涉及到 Windows 和 Unix 关于行的定义

fputs 只是输出一个 string,但是 getsputs\n 上的行为,书中认为是 confusingly

The library functions gets and puts are similar to fgets and fputs, but operate on stdin and stdout. Confusingly, gets deletes the terminating '\n', and puts adds it.

7.8 Miscellaneous Functions

7.8.1 String Operations

7.8.2 Character Class Testing and Conversion

<string.h> 是字符串处理库,<ctype.h> 字符测试和转换

7.8.3 Ungetc

jump

7.8.4 Command Execution

system(char *s),返回执行的程序的 exit(value) 的值

7.8.5 Storage Management

void *malloc(size_t n):未初始化的 n bytes

void *calloc(size_t n, size_t size):用于分配一个动态数组,初始化为 0

返回结果都需要强制转换,使用结束后需要 free

一个要小心的地方就是,被 free 的空间就不能再次使用了,比如链表中 next 字段需要在 free 前保存。

7.8.6 Mathematical Functions

<math.h> 中,三角对指幂

7.8.7 Random Number generation

随机数生成,stdlib.h

Chapter 8 - The UNIX System Interface

  1. File Descriptors: program 只知道自己在对 fd 对应的文件读写,却不知道 fd 具体对应哪些文件,这个对应关系由操作系统维护。一个程序在运行之初就打开了三个文件,操作系统知道是 stdin、stdout 和 stderr

  2. Low Level I/O - Read and Write: read 和 write 系统调用只需要 fd,不需要文件名,实现对 fd 对应文件的读写

  3. Open, Creat, Close, Unlink

    1. open 打开文件,需要文件名、打开方式和权限,creat 同理。权限 9 个 bit,owner、group 和 others 的 read、write 和 execute。
    2. close 关闭文件,回收 fd
    3. unlink:删除文件
  4. Lseek,移动读取的位置,比如 read 是 sequential 的,只能一个一个读取,现在要跳到中间第 512 个 byte 开始读取,就用 lseek

  5. Example - An implementation of Fopen and Getc:在 read()write() 等 syscall 的基础上,实现对文件的抽象:FILE 结构体。FILE 内部有 fd、flag 和 buffer,就是预先读取来减少 read 或者 write 的调用。

    1. buffer 的意思比如先 syscall read 一堆 bytes 到 buffer,需要多少就从已经 read 的 buffer 中去取。如果 buffer 被读完了就要重新调用 read。尤其是 getc,每次只读取一个,完全可以先读 512 个 bytes,然后每次从 heap 上取一个。
    2. stderr 就是 unbuffed
  6. Example - Listing Directories:「文件」一词有两个意思:一是「一切皆文件」,二是与「目录」一词相对的文件

    1. 认识 stat 这个 syscall,用于获取文件(一切皆文件)的信息,比如最后一次修改时间等,也可以判断文件是不是目录
    2. 目录作为文件,内容是 filename, inode-number 的数组,通过 read(dirname) 得到 dir 文件内容,然后用 / 拼接 filename 调用 stat 就能实现 dir walk,遍历目录下所有文件。
  7. Example - A Storage Allocator:

    1. 使用 free block 的链表管理可被 malloc 的地址,用户每次 malloc 就是在链表里的 block 找符合大小的
    2. 组成 block 的 bytes 是通过 sbrk 调用得来的,链表结构是通过 block 内部前面放置 header 实现的。header 存储其所在 block 的 size 和 next free block 地址
    3. 使用 union 来保证对齐,每次 malloc 返回给用户的都是 block 内部 header 后面一段
    4. free 接受 block 地址,计算 header 地址,遍历 free block list,检查是否有合并 free block 的可能

8.1 File Descriptors

有一种东西叫做 file descriptor。program 一启动,就打开了三个文件,fd 分别是 0 1 2,操作系统知道其分别对应 stdin、stdout 和 stderr,但是 program 本身不知道,它只知道自己在读 0,写 1 和 2。

prog < infile > outfile 只是修改了 0 和 1 fd 的指向,2 是 stderr,操作系统将其与屏幕对应,所以在重定向的时候,也能看到 error message。

8.2 Low Level I/O - Read and Write

使用 read 和 write 构建简单的读写函数

read 和 write 需要 fd

open 文件,得到其 fd,需要文件名、打开方式(read,write,……)和 permission

#include <fcntl.h>
int fd;
int open(char *name, int flags, int perms);
fd = open(name, flags, perms);

It is an error to try to open a file that does not exist.

创建文件用 creat(char *name, int perms),如果文件已经存在,截断其长度为 0

关于 perms,指定 owner、group 和 others 对文件的 read、write 和 execute 权限,所以 9 个 bits,用八进制三位数字,比如 chmod 777 file

还提到了 vprintf,v 是 variable 的意思,fprintf(FILE *file, char *fmt, ...)vprintf(FILE *file, char *fmt, va_list args),需要一个指针指向可变参数

#include <stdio.h>
#include <stdarg.h>
/* error: print an error message and die */
void error(char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    fprintf(stderr, "error: ");
    vprintf(stderr, fmt, args);
    fprintf(stderr, "\n");
    va_end(args);
	exit(1);
}

note. 这些都是 OS 的接口了。

8.4 Random Access - Lseek

read 或者 write 都是顺序的,比如只能从头开始读,lseek(int fd, long offset, int origin) 允许改变 cursor,从跳转到的位置开始读写。

origin 取值为 0 表示开始,1 表示当前位置,2 表示文件结尾。

go back to begin lseek(fd, 0L, 0);

jump to end: lseek(fd, 0L, 2);

8.5 Example - An implementation of Fopen and Getc

FILE 内部的实现都是通过 readwrite 实现的,这两个函数都需要一个 buffer 来保存读取或者写入的字节数。

FILE 的抽象包含一个对 buffer 的指针,读写文件的时候,这个空间在 heap 上被分配。

如果是 buffered 就说明用一个数组存储已经读好的数据,这样可以避免多次调用 read 这个 syscall。比如一次读个 512 bytes,每次 getc 直接从 base 指向的 buffer 数组中,通过 ptr 去取。取到以后 cnt 自减。这样的后果就是,取 byte 前需要先检查 cnt,如果不够了,就要重新 read 来 fill buffer。

typedef struct _iobuf {
    int cnt; /* characters left */
    char *ptr; /* next character position */
    char *base; /* location of buffer */
    int flag; /* mode of file access */
    int fd; /* file descriptor */
} FILE;

fopen 内部调用 syscall creatopen,对 flag 和 fd 修改

flag 除了常用的读模式、写模式,还有是否 buffered,文件打开的状态不仅仅是读和写。

enum _flags {
    _READ = 01, /* file open for reading */
    _WRITE = 02, /* file open for writing */
    _UNBUF = 04, /* file is unbuffered */
    _EOF = 010, /* EOF has occurred on this file */
    _ERR = 020 /* error occurred on this file */
};

note. C++ 中,对于 while(cin >> i) 也判断了 stdin 的状态

note. 我们知道 stdinstdout 是 buffer 的,像读入 \n 或者 写入 \n 才会被刷新,而 stderr 这就是在用户态实现了接口的原理。

8.6 Example - Listing Directories

为什么需要 stat 这种 syscall?比如文件传输的时候,需要知道文件的总大小显示给用户,这和 read 就不一样。

所以 stat 不需要 file descriptor 作为参数,没必要打开文件才知道文件的一些附加信息,只需要文件名就够了。

Let us begin with a short review of UNIX file system structure.

A directory is a file that contains a list of filenames and some indication of where they are located.

The "location" is an index into another table called the "inode list".

The inode for a file is where all information about the file except its name is kept.

A directory entry generally consists of only two items, the filename and an inode number.

note. 精要的定义。

目录也是文件,目录的文件内容(或者说字节流)是 filename, inode-number 的数组,操作系统通过 file descriptor 表示打开了一个文件,通过 inode index 表示后面真正的文件。

note. 目录作为一个文件的内容是什么?当我们在电脑上双击一个目录,进入了它的内部的时候,其实看到的就是这个目录文件的内容:每个 filename,以及每个 file 对应信息。

为了获得一个目录下的文件信息,肯定要打开这个目录,读取其中的文件名,然后调用 stat("<dir>/<filename>") 进行 dir walk

note. 怎样算删除了一个文件(与目录相对的意思)呢?如果所有目录下都没有其 filename, inode-number 对,该文件对应的 inode 就没了。一个文件可以在多个目录下出现,只要目录有一个 filename?, inode-number 就行。这就是硬链接。

8.7 Example - A Storage Allocator

底层是 sbrk 返回的 n 个 bytes,连续的 free bytes 构成一个 block,标准库通过 链表 来管理这些 blocks

通过在 block 内部的前面加上 header,指明当前的 block 大小和下一个 block 的地址。

img

typedef long Align; /* for alignment to long boundary */
union header { /* block header */
	struct {
		union header *ptr; /* next block if on free list */
		unsigned size; /* size of this block */
	} s;
	Align x; /* force alignment of blocks */
};
typedef union header Header;

当用户申请空间的时候,标准库在 free block 组成的链表里查找满足大小的 block,然后切割并返回地址。

一个 free block 的组成包括 header 和可供使用的空间,每次分配给用户的是可使用空间的起始地址。

img

当没有 block 满足条件,调用 sbrk,得到新的 block,插入链表

当用户 free,遍历链表,检查能否合并,不能的话根据 heap 的地址放到开头或者结尾

Appendix A - Reference Manual

A.1 Introduction

jump

A.2 Lexical Conventions

当 preprocess 结束后,macro 被展开或者替换,得到 a sequence of tokens

A.2.1 Tokens

There are six classes of tokens:

  1. identifiers
  2. keywords
  3. constants
  4. string literals
  5. operators
  6. other separators

A.2.5 Constants

constant:
integer-constant
character-constant
floating-constant
enumeration-constant

note. 这些在 CSAPP 中说过了,不再赘述。

整数常量是何种类型,按顺序 fit,int, long int, unsigned long int,还取决于 suffix

字符常量就是 \ooo, \xhh,还有 escape(转义)字符,扩展表示的 suffix 是 L,类型是 wchar_t,主要用于表示亚洲文字。

A.2.6 String Literals

是 static,修改字符串常量的行为是未定义。

A string has type ``array of characters'' and storage class static (see Par.A.3 below) and is initialized with the given characters. Whether identical string literals are distinct is implementation-defined, and the behavior of a program that attempts to alter a string literal is undefined.

A.3 Syntax Notation

jump

A.4 Meaning of Identifiers

生存周期和类型和 scope

An object, sometimes called a variable, is a location in storage, and its interpretation depends on two main attributes: its storage class and its type. The storage class determines the lifetime of the storage associated with the identified object; the type determines the meaning of the values found in the identified object.

A.4.1 Storage Class

分为 static 和 automatic。inside block 又没有特定的 storage class keyword,就是 automatic。

outside any block 和通过 static 就是 static 类型。对 outside any block 使用 extern 就会触发 external linkage。

note. 所以在函数里面使用 static 就是指定变量为 static 类型。

A.4.2 Basic Types

A.4.3 Derived types

数组、函数、结构体可以认为依赖于基本类型存在

note. 让我想起 CPP 里的 reference 和 pointer 的描述

A.5 Objects and Lvalues

左值就是表示 object 的 expr,表示内存里的一块区域,最简单的左值就是变量。

A.6 Conversions

A.6.3 Integer and Floating

如果没法 fit,就是未定义行为

When a value of floating type is converted to integral type, the fractional part is discarded; if the resulting value cannot be represented in the integral type, the behavior is undefined. In particular, the result of converting negative floating values to unsigned integral types is not specified.

A.7 Expressions

A.7.6 Multiplicative Operators

Otherwise, it is always true that (a/b)*b + a%b is equal to a. If both operands are non-negative, then the remainder is non-negative and smaller than the divisor, if not, it is guaranteed only that the absolute value of the remainder is smaller than the absolute value of the divisor.

所以没有规定 \(\%\) 的结果要是正数还是负数,只做绝对值的要求。比如 -5 = -3 * -1 + -2 也可以说 -5 = -3 * -2 + 1

note. CSAPP 中提到,负数除以 2 的幂次,C 语言里向 0 方向取整,所以前者被实现。

A.7.9 Relational Operators

If P points to the last member of an array, then P+1 compares higher than P, even though P+1 points outside the array. Otherwise, pointer comparison is undefined.

note. 引入了 CPP 中 .end() 的概念

A.7.18 Comma Operator

expression:
    assignment-expression
    expression , assignment-expression

A pair of expressions separated by a comma is evaluated left-to-right, and the value of the left expression is discarded. The type and value of the result are the type and value of the right operand.

A.8 Declarations

Declarations specify the interpretation given to each identifier; they do not necessarily reserve storage associated with the identifier. Declarations that reserve storage are called definitions.

对 declaration 和 definition 做出了精确的定义。declaration 只是解释 identifier。

我比较关心 declaration-specifiers

\[<declaration> \rightarrow <declaration\_specifiers>\ <init\_declarator\_list_{opt}>; \\ declaration\_specifiers:\\ <storage\_class\_specifier>\ <declaration\_specifiers_{opt}> | \\ <type\_specifier>\ <declaration\_specifiers_{opt}> |\\ <type\_qualifier>\ <declaration\_specifiers_{opt}> \]

A.8.1 Storage Class Specifiers

storage class 就是说这个变量要放到哪里,有 auto, register, static, externtypedef

对于 register,只是 hint 该变量会被频繁使用,不一定会放到 register 里,而且定义为了 register 就不能被取地址,不论编译后是否在 stack 上还是 register 里。

函数内 static,同时导致 definition;函数外,看 A.11.2

函数内 extern,就是说 defined elsewhere;函数外,看 A.11.2

默认的规则:

At most one storage class specifier may be given in a declaration. If none is given, these rules are used: objects declared inside a function are taken to be auto; functions declared within a function are taken to be extern; objects and functions declared outside a function are taken to be static, with external linkage. See Pars. A.10-A.11.

注意,不仅仅是 object inside function is auto,outside is static,function 本身也有 storage class。

A.8.2 Type Specifiers

type-qualifier 是 ANSI 新定义的。包括 constvolatile,前者是要求 object 所在内存只读,后者要求避免编译器优化。书中的例子是:

for a machine with memory-mapped input/output, a pointer to a device register might be declared as a pointer to volatile, in order to prevent the compiler from removing apparently redundant references through the pointer.

A.8.3 Structure and Union Declarations

jump,太复杂,我也用不着

A.8.6.3 Function Declarators

说明:到目前为止,带形式参数原型的函数声明符是 ANSI 标准中引入的最重要的一个语言变化。它们优于第 1 版中的“旧式”声明符,因为它们提供了函数调用时的错误检查和参数强制转换,但引入的同时也带来了很多混乱和麻烦,而且还必须兼客这两种形式。为了保持兼容,就不得不在语法上进行一些处理,即采用 void 作为新式的无形式参数函数的显式标记。

A.9 Statements

A.9.3 Compound Statement

Initialization of static objects are performed only once, before the program begins execution.

比如函数内部的 static 声明,就只执行一次。

前文提到了:Except as described, statements are executed in sequence.

static 就是一个特殊的声明。

A.10 External Declarations

The unit of input provided to the C compiler is called a translation unit; it consists of a sequence of external declarations, which are either declarations or function definitions.

提供给 C 编译器处理的输入单元称为翻译单元。它由一个外部声明序列组成,这些外部声明可以是声明,也可以是函数定义。

A.10.2 Exteranl Declarations

external 表示不在任何函数内部,比如下面程序的 a, b, c 都是 exteranl,要强调的是,external 不一定是 extern

int a, b;
static int c;
int main() {/* code */}

If the first external declarator for a function or object includes the static specifier, the identifier has internal linkage; otherwise it has external linkage.

static 关键字,就用内部链接,否则用外部链接。

如果一个对象的外部声明带有初值,则该声明就是一个定义。
如果一个外部对象声明不带有初值,并且不包含 extern 说明符,则它是一个临时定义。
如果对象的定义出现在翻译单元中。则所有临时定义都将仅仪被认为是多余的声明;
如果该翻译单元中不存在该对象的定义,则该临时定义将转变为一个初值为 0 的定义。

意思就是,external 的 int a; 被当作 tentative definition,如果整个翻译单元都没出现带初值的定义,临时定义就被转换。

下面这段代码中,int a 虽然出现了两次,但是是合法的,遇到了第三行带有初值的定义,第二行就被抛弃。

#include<stdio.h>
int a; // tentative definition
int a = 2; // occur this, a definition, the first tentative definiton gets discard
int main()
{
    printf("%d\n", a); // print 2
    return 0;
}

下面这段代码也是合法的,大家都是 tentative definition。

#include<stdio.h>
int a; // tentative definition
int a; // tentative definition
int main()
{
    printf("%d\n", a); // print 0
    return 0;
}

A.11 Scope and Linkage

A.11.2 Linkage

文件夹下两个文件,main.ctest.c,编译命令:gcc main.c test.c,Windows 下得到 a.exe,输出为 a: 2

// main.c
#include<stdio.h>

extern int a;

int main()
{
    printf("a: %d\n", a);
    return 0;
}
// test.c
int a = 2;

As discussed in Par.A.10.2, the first external declaration for an identifier gives the identifier internal linkage if the static specifier is used, external linkage otherwise.

不用 static 关键字,那么这个变量就被暴露给了外部程序,就是采用了 exteranl linkage。

如果 test.c 中改为 static int a = 2;,那么编译就直接报错。

A.12 Preprocessing

讲的大多数都是 macro,比如一些没见过指令,如 #lineerror 等。

A.12.4 File Inclusion

A control line of the form # include <filename> causes the replacement of that line by the entire contents of the file filename.

就是单纯的拷贝,而 #include "filename" 是先在源文件位置找,找不到的话,按照 #include <filename> 的方式找。

Appendix B - Standard Library

B.1 Input and Output: <stdio.h>

A stream is a source or destination of data that may be associated with a disk or other peripheral.

「流」就是数据,当被 open 后就和 FILE 关联。

B.1.1 File Operations

除了常见的 read、write,还可以为 FILE 设置 buffer 的大小。

B.2 Chracter Class Tests: <ctype.h>

为什么叫它 tests,因为一堆函数名字都是 is*,比如 isalpha()isalnum() 之类的。

还有两个转换函数 tolower()toupper()

B.3 String Functions: <string.h>

对连个字符串比较、粘连、找前缀、找后缀、split 等。

以及 mem*

posted @ 2023-08-16 12:17  dutrmp19  阅读(25)  评论(0编辑  收藏  举报