Flowe2 / fkAlgorithm

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

fkAlgorithm

@个人C语言学习笔记 / 数据结构复习 @Flowe2



C语言

书籍介绍 课后练习题答案
全书源码查看


基本概念

  1. 编译&链接 预处理 -> 编译 -> 链接

  2. 程序结构(详见 程序结构)

    #include <...>;
    int main() {
        // ...
        return 0;
    }
    // ...
    type function() {
        // ...
        }
    1. 预处理器指令
    2. 主函数 函数
    3. 语句 & 表达式
    4. 常量 #define MAX_NUM 10
    5. 变量 int a=10;
  3. 注释

    • 单行: // content
    • 多行: /* content */
  4. 变量


格式化输出

printf & scanf

  • 转换说明, %-m.pX格式
    • -: 指定左对齐, 无则默认右对齐
    • m: 最小字段宽度
    • p: 精度
    • X: 转换说明符, d - 十进制, e - 指数形浮点数, f - 定点十进制浮点数, g - 指数形式/定点十进制浮点数
    • 那么如何显示%? 使用%%即可print一个%
  • 转义序列
    • 常见: \a - 警报符, \b - 退回符, \n - 换行符, \t - 水平制表符
    • 详见 数据类型


表达式

  1. 算术运算符

    type description
    + 相加(一元时为正号)
    - 相减(一元时为符号)
    * 相乘
    / 相除
    % 模运算/取余
    ++ 自增(区别作为前缀和后缀时)
    -- 自减(区别作为前缀和后缀时)
  2. 赋值运算符

    type description
    =` 最简单基础的赋值运算符
    += 加且赋值运算符, C += A 相当于 C = C + A
    -= 减且赋值运算符, C -= A 相当于 C = C - A
    *= 乘且赋值运算符, C *= A 相当于 C = C * A
    /= 除且赋值运算符, C /= A 相当于 C = C / A
    %= 求模且赋值运算符, C %= A 相当于 C = C % A
    <<= 左移且赋值运算符, C <<= 2 等同于 C = C << 2
    >>= 右移且赋值运算符, C >>= 2 等同于 C = C >> 2
    &= 按位与且赋值运算符, C &= 2 等同于 C = C & 2
    ^= 按位异或且赋值运算符, C ^= 2 等同于 C = C ^ 2
    ` =`
  3. 关系运算符 (逻辑表达式)

    type description
    == 是否相等
    != 是否不等
    > 左值是否大于右值
    < 左值是否小于右值
    >= 左值是否大于或等于右值
    <= 左值是否小于或等于右值
  4. 逻辑运算符 (逻辑表达式)

    type description
    && 与运算符
    `
    ! 非运算符

短路赋值

int a = 3, b = 3;

(a=0) && (b=2);
// a=0, b=3
(a=1) || (b=5);
// a=1, b=3

---

### 数据类型

1. **整数**
    | type           | size        | rage                                               |
    | :------------- | :---------- | :------------------------------------------------- |
    | char           | 1 字节      | -128 ~ 127  0 ~ 255                              |
    | unsigned char  | 1 字节      | 0 ~ 255                                            |
    | signed char    | 1 字节      | -128 ~ 127                                         |
    | int            | 2  4 字节 | -32,768 ~ 32,767  -2,147,483,648 ~ 2,147,483,647 |
    | unsigned int   | 2  4 字节 | 0 ~ 65,535  0 ~ 4,294,967,295                    |
    | short          | 2 字节      | -32,768 ~ 32,767                                   |
    | unsigned short | 2 字节      | 0 ~ 65,535                                         |
    | long           | 4 字节      | -2,147,483,648 ~ 2,147,483,647                     |
    | unsigned long  | 4 字节      | 0 ~ 4,294,967,295                                  |

2. **浮点数**
    | type        | size    | rage                  | accuracy  |
    | :---------- | :------ | :-------------------- | :-------- |
    | float       | 4 字节  | 1.2E-38 ~ 3.4E+38     | 6 位小数  |
    | double      | 8 字节  | 2.3E-308 ~ 1.7E+308   | 15 位小数 |
    | long double | 16 字节 | 3.4E-4932 ~ 1.1E+4932 | 19 位小数 |

3. **void**
    | type          | description                                                                                                                                    |
    | :------------ | :--------------------------------------------------------------------------------------------------------------------------------------------- |
    | 函数返回为空  | 不返回值的函数的返回类型为空例如 void exit (int status);                                                                                     |
    | 函数参数为空  | 不带(不接受)参数的函数可以接受一个 void例如 int rand(void);                                                                                  |
    | 指针指向 void | 类型为 void * 的指针代表对象的地址, 而不是类型例如, 内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针, 可以转换为任何数据类型。 |

4. 转义序列
   | type | description                        | ASCII value(DEC) |
   | :--- | :--------------------------------- | :--------------- |
   | `\a` | 响铃(BEL)                          | 007              |
   | `\b` | 退格(BS), 将当前位置移到前一列     | 008              |
   | `\f` | 换页(FF), 将当前位置移到下页开头   | 012              |
   | `\n` | 换行(LF), 将当前位置移到下一行开头 | 010              |
   | `\r` | 回车(CR), 将当前位置移到本行开头   | 013              |
   | `\t` | 水平制表(HT)                       | 009              |
   | `\v` | 垂直制表(VT)                       | 011              |
   | `\'` | 单引号                             | 039              |
   | `\"` | 双引号                             | 034              |
   | `\\` | 反斜杠                             | 092              |

---

### 判断与循环

1. 判断
   * **if** - (else if) - else
        ```C
        if( boolean_expression 1 )
        {
            /* 当布尔表达式 1 为真时执行 */
        }
        else if( boolean_expression 2)
        {
            /* 当布尔表达式 2 为真时执行 */
        }
        else 
        {
            /* 当上面条件都不为真时执行 */
        }
        ```
        > 判断boolean_expression, 为真则执行对应语句块  
        > 没有 {} 辅助时, else默认与其最近的if匹配  
   * **switch** - case - default
        ```C
        switch(expression)
        {
            case constant-expression 1 :
                statement(s);
                break; /* 可选的 */
            case constant-expression 2 :
                statement(s);
                break; /* 可选的 */
            /* 您可以有任意数量的 case 语句 */
            default : /* 可选的 */
                statement(s);
        }
        ```
        > 根据expression判断, 跳转执行对应constant-expression下的语句块, 若无匹配则执行default下的语句块  
        > 也可以不写defaul情况  
        > 漏写break会使下一条件的语句被执行  
        > 有时也故意不写来达到几个条件分支共享代码的目的  
   * **条件运算符** (三元运算符)
        ```C
        Exp1 ? Exp2 : Exp3;
        ```
        > 若Exp1为真, 则执行Exp2, 否则执行Exp3  

2. 循环
   * **while**
        ```C
        while(condition)
            {
            statement(s);
            }
        ```
        > 判断condition, 为true则执行statement(s), 否则结束循环  
   * **do...while**
        ```C
        do
        {
            statement(s);
        }while( condition );
        ```
        > 执行statements(s), 然后判断condition, 为true则继续执行, 否则结束循环  
   * **for**
        ```C
        for ( init; condition; increment )
        {
            statement(s);
        }
        ```
        > 1. init 首先执行, 且仅执行一次, 声明并初始化任何循环控制变量 (也可以不在这里写任何语句, 只要有一个分号出现即可)  
        > 2. 判断 condition, 如果为真, 则执行循环主体, 为假则不执行并结束循环  
        > 3. 执行 statement(s) , 会执行 increment 语句, 更新循环控制变量 (该语句也可以留空, 只要在条件后有一个分号出现即可)  
        > 4. 同2.再次判断循环条件, 重复上述2~4  
   * **循环控制语句**
        | type          | size                                                             |
        | :------------ | :--------------------------------------------------------------- |
        | break 语句    | 终止循环或`switch`语句, 继续执行紧接着循环或`switch`的下一条语句 |
        | continue 语句 | 立刻停止本次循环迭代, 重新开始下次循环迭代                       |
        | goto 语句     | 将控制转移到被标记的语句 (但不建议使用)                          |
        > goto 语句配对的标记语句为"xxx:", e.g.:`goto myGoto; Goto:`, 其坏处在于过多使用会使程序逻辑看上去逻辑混乱, 毕竟goto可往前也可往后跳转  

---

### 数组

* 变量: 标量 & 聚合
  * 标量: 保存单一数据项
  * 聚合: 可储存一组一组的数据, 数组&结构

* 一维度数组
  * 声明: `type arrayName [ arraySize ];`
  * 初始化: 
    * 声明时初始化语句 (大小可省略): `type arrayName [ (arraySize) ] = {ele 1, ele 2, ..., ele n}; //若初始化数组短语length, 则剩余元素都为0`
    * 指定初始化: `type arrayName [ (arraySize) ] = { [i] = ele i, [j] = ele j, ..., [n] = ele n}; //其余初始化为0`
    * 声明后初始化, 逐一访问进行初始化
  * 访问: `arrayName[i] = xxx;`
  * 下标: 长度为 n 的数组下标从 0 ~ n-1

* 多维数组
  * 数组可有任意维数
  * 声明: `type arrayName [ oneDSize ][ twoDSize ];`
  * 初始化 (二维为例):
    * 嵌套一维数组: `type arrayName [ oneDSize ][ twoDSize ] = {{ele 1, ele 2, ... , ele n}, ..., {...}};`
    * 若嵌套初始化式不够填满行数, 则后面的行默认初始化为`{0, 0, ..., 0}`
    * 若某一嵌套初始化式不够填满当前行, 同一维, 其余位默认为`0`
    * 甚至可以省略掉嵌套的括号 (因为**行主序**)
  * 访问: `arrayName[i][j] = xxx;`, 考点: `arrayName[i, j]`逗号运算符, 实际等于`arrayName[j]`
  * 存储: 二维为例, 在内存中也为`行主序`, 

* C99 支持变长数组
  * 声明: `type arrayName [ n ]; // n为变量`;
    * `n`可以为表达式
    * 常见于除`main`外的函数 (main也可用)

---

### 函数

* 定义:
    ```C
    return_type function_name( parameter list )
    {
        body of the function
    }
    ```
  * 返回类型: `return_type` 是函数返回的值的数据类型若不返回值 return_type  `void`, 未给出的话编译器会警告, 但不报错, 默认`int`
  * 函数名称: `function_name`, 函数名和参数列表一起构成了*函数签名*
  * 参数: `parameter list` 形式参数参数, 可选, 需要**按顺序以<类型, 形参名>的形式**给出, 当函数被调用时, 传递的值为实际参数
  * 函数主体函数主体包含一组定义函数执行任务的语句, 函数的实际主体可以单独定义

* 声明
    ```C
    return_type function_name( parameter list ); // 注意与函数完整定义不同, 声明语句需要以";"结尾
    ```
  * C语言并未规定函数需在调用点前定义, 对于为给出定义的函数, 编译器会给一个**隐式声明** (返回值默认为int型)
  * **函数原型**: 即函数声明, 早期C语言中, 函数的声明是简单的`return_type function_name()`, 函数原型的名字即与早期这种声明区分

* 调用
    ```C
    function_name( actual_parameters )
    ```
  * 实际参数转换: C语言允许实参类型与形参类型不匹配的调用, 
    * 调用前遇到原型: 自动转为相应类型
    * 调用前未遇到原型: 将进行默认参数提升, `float``double`, `char``short``int`
  * 数组型参数: 函数无法知道其长度, 需通过额外参数提供数组长度
  * C99新特性
    * 支持变长数组形式参数
    * 支持在数组参数声明中使用`static`
    * 支持复合字面量(做实际参数), 适用于仅使用一次的(临时)变量, 有效避免浪费, 调用: `func_name((int []){1, 2, 3, 4}, 4);`

* Return
  * `void`型函数外, 函数需要有一个返回值

* 递归
  * 若函数调用自身, 称其为**递归**

---

### 程序结构

* 局部变量
  * 特性
    1. 自动存储期限
      > 存储期限/存储长度: 在变量存在期间内程序执行部分
      > 局部变量在函数调用时自动分配, 结束时自动回收
    2. 块作用域
      > 从声明开始至函数结尾
  * 静态局部变量
    * 使用`static`关键词可将局部变量变量声明为静态局部变量
    * 在程序执行期间永久存在 (可以用来记录程序调用了几次)
    * 但仍是**块作用域**, 对程序其他部分不可见
  * 形式参数
    * 和局部变量一样的性质: **自动存储期限**, **块作用域**
    * 与局部变量的区别: 每次函数调用时, 会对形参进行自动初始化

* 外部变量/全局变量
  * 特性
    1. 静态存储期限
      > 在程序执行期间永久存在
    3. 文件作用域
      > 从声明开始至文件结尾

* 程序块
  * 形如 `{ 多条语句 }` 的代码块
  * 程序块中的声明变量是局部变量

* 作用域
  * 若一标识符已经是可见的了, 在程序块内命名相同标识符时, 新的声明会隐藏旧的声明
  * 然后在程序块末尾, 标识符重新获得旧的含义

---

### 指针

> Byte = 8 bit  
> 每个字节都有地址

* 指针的声明: `type *pointer_name;`
  * 同基本变量声明一样, 只是多了一个星号`*`

* 指针的使用
  * 取地址运算符: `&`
    > `&a` 即取变量`a`的地址  
  * 间接寻址运算符: `*`
    > `*p` 即取指针`p`所指地址的变量值 (`*p`是实际变量`a`的别名, 改变`*p`也会改变`a`)
  * 在声明时可以将实际变量与指针一起声明并赋值, 不过要求实际变量先声明: `int a=0, *p=&a`

* 指针赋值
  * 对两指针使用赋值运算, 即进行指针的复制 **(前提: 两指针具有相同类型)**

* 指针作为函数参数
  * 定义方法即将对应指针类型形参添加`*`即可
  * C语言的函数无法改变普通的实际变量
  * 使用指针变量作为函数参数传入则可以达到改变实参的目的
  * 若不希望函数改变指针指向变量, 则可以使用`const`关键词来保护参数,  `const type * pointer`

* 指针作为返回值
  * 定义方法即将函数返回值后添加`*`即可
  * 函数除返回传入指针参数外, 还可以返回指向外部变量/声明为`static`的局部变量指针
  * 可以用于返回数组
  * **但是**不能返回自动局部变量指针, 因为函数执行完后, 其就不存在了

* 指针与数组  
  > `int a[10], *p`
  * 赋值
    > `p = a`, `p`指向数组`a`的起始地址, 等价于`p = &a[0]`  
    > 实际上数组名`a`可以一个指针, `*a``a[0]`, `*(a+2)``a[2]`
    > 不过, 试图给数组名`a`赋值的操作是错误的, 例如`a++`, 可先将`a`赋给另一指针变量再来改变 (⭐)
  * 指向复合常量的指针
    > 例如: `int *p = (int []){1, 2, 3}` (C99 supported), 可省略声明数变量的过程  
  * 运算 (仅支持三种)
    > 加上整数: 可以通过对指针`p`做运算, 得到数组元素, `p = a`, `p += 2``p`指向`a[2]`, 绝不能`指针+指针`  
    > 减去整数: 同理, 但可以`指针-指针`  
    > 两指针相减: 即得两指针间的距离
  * 比较
    > 关系运算符&判等运算符可用, 判断的是两指针的相对位置
  * 处理数组
    > 可用指针来遍历数组, 自增指针即可 `for (p = a; p < &a[N]; p++) {...}` (可用`a + N`代替`&a[N]`)
    > 可以用间接寻址运算符`*`和自增运算符`++`等组合
  * 用指针做数组名
    > 既然可用数组名做指针, C语言也允许将指针看做数组名, 直接进行取下标的操作, `p[i]``*(p+i)`
  * 指针处理多维数组
    * 由于C语言对于多维数组仍然是按**行主序**存储的, 在遍历多维数组时, 可将嵌套循环用指针递增来代替
    * 处理行: 
      ```C
        int a[ROWS][COLS], *p, i; // i为待处理行号
        for ( p = &a[i][0]; p < &a[i][0] + COLS; p++ ) 
          { *p = ...; }
      ```
      > 注意声明的`p`为普通`int`指针  
      > `p = &a[i][0]`简写`p = a[i]`  
    * 处理列:
      ```C
        int a[ROWS][COLS], (*p)[COLS], i; // i为待处理列号
        for ( p = &a[0]; p < &a[ROWS]; p++ )
          { (*p)[i] = ...; }
      ```
      > 声明`p`为指向长度为`COLS``int`数组的指针  
      > `(*p)[COLS]`使用时需要带括号, 若无括号, 编译器会认为p为指针数组并解释为`*(p[COLS])`, 而非指向数组的指针  
      > `p++`此时就会自增一个`COLS`长度, 从而实现对列遍历
    * 多维数组名做指针时需小心
  * 指针与变长数组
    > 一维好用, 多维需维度一致  

---

### 指针(高级应用)

* **动态存储分配**
  * **内存分配函数**
    * `void *malloc(int num);`: 分配内存块, 但不对其进行初始化
    * `void *calloc(int num, int size);`: 分配内存块, 并将其清零
    * `void *realloc(void *address, int newsize);`: 调整先前分配的内存块大小
      * 当拓展内存块时, 不会对其进行初始化
      * 当拓展失败时, 会返回空指针, 且原有内存块内数据不变
      * 若调用时第一个参数为空指针, 则实际效果等同`malloc`
      * 若调用时第二个参数为`0`, 则会释放掉内存块
    * `void free(void *address);`: 该函数释放 address 所指向的内存块,释放的是动态分配的内存空间, address必须是由内存分配函数返回的指针
  * 空指针
    * 不指向任何地方的指针, 一个区别于有效指针的特殊值
    * 用名为`NULL`的宏来表示, `<locale.h> / <stddef.h> / <stdio.h> / <stdlib.h> / <string.h> / <time.h> / <wchar.h>(C99)`中都有定义, 程序包含任一即可
  * 释放存储空间
    * 内存分配函数所获得的的内存块都来自大小有限的``
    * `垃圾`: 程序不可再访问到的内存块
      * 若程序没有回收自身产生的垃圾, 则该程序存在`内存泄漏`现象
      * `垃圾收集器`: 有些高级语言提供, 但C没有, C语言要求程序自行回收各自的垃圾
    * `悬空指针`: 调用`free(p)`之后, 并不会改变`p`本身, 此时`p`成为悬空指针, 再试图修改`p`所指的内存会导致严重错误

* 指向指针的指针
  * 例如`char *`数组, 指向其元素的指针`char **`
  * 链式数据结构中也常用到
  * 当希望函数通过指针指向别处来改变数据时也会用到

* 指向函数的指针
  * 是指向函数的指针变量
  * 可以像一般函数一样, 用于调用函数/传递参数
  * 声明: `typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型`

* 受限指针(C99)
  * C99中, 允许使用`restrict`关键字来修饰指针声明, `type * restrict p`, `p`指向的对象不允许除`p`外的任何方式访问
  * `别名`: 同一个对象的不同访问方式

* 链表
  * 详见[数据结构](#数据结构)部分
  * `->`运算符: 利用指针访问结构成员, `p->member`等价`(*p).member` 

---

### 字符串

* 字符串字面量
  > `string literal`, 是一用一对双引号`""`括起来的字符序列
  1. 可包含转义序列
  2. 一行太长, 则可用`\`来延续字符串, 也可以直接使用`"string 1" (换行) "string 2"`, C语言会自动拼接二者
  3. 存储: C语言将字符串字面量作为数组存储, 类型`char *`, 长度为`n+1`, 末位为空字符`\0`
  4. ==操作: 允许取下标, **不允许改变字符串字面量**==
     * 因为部分编译器仅为相同字面量在内存中保存一个副本, 改变后, 所有指向该字面量的指针都会受影响 
  5. 与字符常量区别: 字符串字面量是用指针表示的, 而字符常量(`"a"`)是用整数(ASSIC码)表示

* 字符串变量
  * 声明&初始化:
    `char str[length + 1] = "my string"`
    * 因为末位有空字符, 故长度应为字符串长度+1
    * 长度不一定一直为`length`, 只要以`\0`结尾就是结束, 即长度实际取决于空字符位置
    * 编译器会自动在末位追加`\0`, 长度不够时, 余下元素也会被初始化为`\0`
    * 若初始化时省略长度, 则编译器会自动计算, `char test[] = "test string"`

* 字符数组&字符指针
  * `char str[] = "string"` & `char *str = "string"`
  * 前者声明的是数组, 后者声明的是指针
  * 对于任何期望传递字符数组/指针的函数, 都能接受这两种形式声明的`str`作为参数
  * 但是二者实际存在较大差异:
    * 声明为数组时, 可以随意改动; 但声明为指针时, 不允许随意改动
    * 声明为数组时, `str`是数组名; 但声明为指针时, `str`是变量, 可指向其他字符串

* (输出)字符串`printf`&`puts`
  * `printf("%s", str);`
    > `printf`函数会数个输出`str`的字符, 直到遇到`\0`, 若字符串的`\0`丢失, 函数会越过字符串末尾继续输出内存内容, 直到遇到`\0`
  * `puts(str);`
    > `puts`函数只有一个参数, 在输出完之后, 会自动添加换行符

* (读入)字符串`scanf`&`gets`
  * `scanf("%s", str);`
    * `scanf`函数, 调用时不需要对`str`使用取址符`&`, 因为`str`为数组, 函数会默认将其视为指针处理
    * 会跳过前面的空白字符, 当遇到`换行符`/`空格符`/`制表符`会停下
  * `gets(str);`
    * `gets`函数, 会一次性读入一整行, 包括前后的空字符等, 直到遇到`换行符`
    * 并且会忽略`换行符`, 不会将其存入`str`
  * **自定义函数, 逐字符读入**

* 访问字符串中的字符
  ```C
  // example
  int count_str (const char * str) {
    int n = 0;
    for( ; *str != '\0'; str++){
      if( *str != ' ') {
        n ++;
      }
    }
  }
  • 因为字符串以数组方式存储, 所以可以直接取下标

  • 可用const来保护, 可读不可写

  • C语言字符串库string.h

    • 引用: #include <string.h>
    • strcpy函数
      • 原型: char *strcpy(char *s1, const char *s2);
      • s2指向的字符串复制给s1指向的字符串中, 并返回s1
      • 通常忽略返回值 (即, 直接调用函数, 不管返回值)
      • 考虑安全: 使用strncpy函数 (速度会略慢于strcpy), 限制str2str1长度部分复制给str1; 但若str2的长度大于str1的长度, 将导致str1没有终止的空字符\0, 故最好如下两句一起使用
        • strncpy(str1, str2, sizeof(str1) - 1);
        • str1[sizeof(str1)] = '\0';
    • strlen函数
      • 原型: size_t strlen(const char *s);
      • 返回字符串s第一个空字符前的长度 (即终止符\0前), size_t是C语言中一种无符号整型
      • strlen不是返回s本身长度, 而是返回s指向的字符串的实际长度 (且不含\0)
    • strcat函数
      • 原型: char *strcat(char *s1, const char *s2);
      • 将字符串s2的内容追加s1后面, 并返回s1
      • 通常忽略返回值
      • 考虑安全: 使用strncat函数 (速度会略慢于strcat), 限制str2str1空余长度部分追加给str1
        • strncat(str1, str1, sizeof(str1)-strlen(str2)-1
    • strcmp函数
      • 原型: int strcmp(const char *s1, const char *s2);
      • 比较字符串s1s2, 返回一个小于/等于/大于0的值
  • 字符串数组

    • char * stringArr [] = {"string1", "string2", ..., "stringN"};
    • 这种"参差不齐"的数组, 不会造成空间浪费
    • 访问: 访问某一字符串, 只需对stringArr去下标即可
  • 命令行参数

    • 为了能够够访问命令行参数, 需要把main函数定义为含有两个参数的函数
    • int main(int argc, char *argv[])
    • argc: 参数计数, 命令行参数数量 (含程序名本身)
    • argv: 参数向量, 命令行参数的具体指令数组

预处理器

  • 工作原理:

    • C源码 👉 预处理器 👉 修改后的C程序(不再含预处理指令) 👉 编译器 👉目标代码(机器码)
  • 预处理指令

    instruction description
    #define 定义宏
    #include 包含一个源代码文件
    #undef 取消已定义的宏
    #ifdef 如果宏已经定义, 则返回真
    #ifndef 如果宏没有定义, 则返回真
    #if 如果给定条件为真, 则编译下面代码
    #else #if 的替代方案
    #elif 如果前面的 #if 给定条件不为真, 当前条件为真, 则编译下面代码
    #endif 结束一个 #if……#else 条件编译块
    #error 当遇到标准错误时, 输出错误消息
    #pragma 使用标准化方法, 向编译器发布特殊的命令到编译器中
  • 宏定义 #define 标识符 替换列表

    • 替换列表可包括: 标识符/关键字/数值常量/字符常量/字符串字面量/操作符/排列
    • 作用:
      • 程序更易读
      • 程序更易修改
      • 可避免前后输入不一致导致的错误
      • 对C语言语法做小修改
      • 对类型重命名
      • 控制条件编译
    • 带参数的宏定义 #define 标识符(x1, x2, ..., xN) 替换列表
      • 例如: #define (IS_EVEN(n)) ((n)%2==0), 调用时if(IS_EVEN(i)) i++;
      • 作用:
        • 程序可能优化变快
        • 宏更通用
      • 缺点:
        • 编译后的文件通常变大
        • 无法用指针指向宏
        • 宏可能需要不止一次的计算其参数
    • 宏定义的#运算符
      • 将宏的参数转换为字符串字面量
    • 宏定义的##运算符
      • 记号粘合
      • 若其中一个操作数为宏参数, 粘合会在相应形式参数被实际参数替换后发生
      • 例如: #define MAKE_ID(n) i##n, 调用时int MAKE_ID(1), MAKE_ID(2), ...;int i1, i2, ...;
    • 宏的通用属性
      • 可以被其他宏调用
      • 作用范围从出现到所在文件末尾
      • 宏不可被定义两遍 (除非新旧定义一致)
      • 可以用#undef 标识符来取消定义
      • 一定使用(), 避免发生意料之外的情况
    • 一些预定义宏:
      macro description
      __LINE__ 这会包含当前行号, 一个十进制常量
      __FILE__ 这会包含当前文件名, 一个字符串常量
      __DATE__ 当前日期, 一个以 "MMM DD YYYY" 格式表示的字符常量
      __TIME__ 当前时间, 一个以 "HH:MM:SS" 格式表示的字符常量
      __STDC__ 当编译器以 ANSI 标准编译时, 则定义为 1
      (C99)新增
    • C99 支持空的宏参数
    • C99 支持可变数量的宏参数, __VA_ARGS__指代未定的可变的参数
    • __func__标识符, 指代所处的函数的名称
  • 条件编译

    #if 常量表达式
      {...}
    #elif 常量表达式
      {...}
    #else 
      {...}
    #endif
    • 使用条件编译的好处
      1. 提高程序的可移植性
      2. 可适应不同的编译器
      3. 为宏提供默认定义
      4. 临时屏蔽包含注释的代码
    • defined运算符
      • defined(标识符)defined 标识符
      • 若标识符为定义过的宏, 则返回1, 否则返回0
    • #ifdef&#ifndef指令
      • 测试一个标识符是否已经/还未定义为宏
      • #ifdef 标识符 等价于 #if defined 标识符
      • #ifndef 标识符 等价于 #if !defined 标识符
  • 其他指令

    • #error 指令
      • #error message
      • message不需要用引号包裹
      • 当预处理器遇到#error时会显示一条包含message的报错消息
    • #line 指令
      • #line n#line n "文件"
      • 用于改变程序行编号, 这条指令会导致程序后续行被编号为n, n+1, ...
      • 若带文件, 则后续行会被编号为认为来自文件, 行号由n开始
      • 会改变__LINE__, 甚至__FILE__
    • #program 指令
      • #program 记号
      • 该指令要求编译器执行某些特殊操作
      • 对大程序/需要使用指定编译器特殊功能的程序很有用
    • _Pragama 运算符
      • _Pragama (字符串字面量)
      • 表达式结果被视为出现在#program
      • 预处理器通过移除字符串两端的双引号并分别用字符"\代替转义字符序列\"\\

结构and联合

  • 结构struct

    • 一种用户自定义的可用的数据类型, 允许存储不同类型的数据项
    • 声明
      struct struct_tag { 
        memeber_type member_name;
        memeber_type member_name;
        ...
      } struct_variable1, struct_variable2, ... ;
      • struct {...}指明变量类型, struct_variable指明为具有该变量类型的变量
      • struct_tag是结构标记
        • 用于标识某种特定结构的名称, 定义一个结构类型
        • 一旦标记tag, 便可以用struct tag来声明变量了
        • tag不是类型名, 单独使用没有任何意义, 只有当与struct一起时标识结构类型
        • 结构标记可以与结构变量的声明合并(如上), 如不跟struct_variable则为单独定义结构类型
        • C语言也支持typedef形式来定义 typedef struct {...} tag, 使用typedef时, tag视为一种变量类型, 直接使用, 不需要跟struct
      • 结构的成员在内存中是按声明的顺序存储的, 成员间没有间隙
    • 初始化
      struct ...{
        ...
      } variable_name1 = { member1, member2, ... },
        variable_name2 = { member1, member2, ... };
      • 同普通变量一样, 可以在声明时直接初始化, 将初始化成员变量依次放入{}即可
        • 同数组类似, 未初始化的成员会默认为0或空字符串""
      • 指定初始化: struct {} variable_name1 = { .member1 = member1, ...};
        • .和成员名组合成为指示符
    • 访问
      • 通过struct_variable.struct_member来访问struct中的成员
      • .运算符的优先级几乎高于其他所有运算符
      • struct允许整体复制, struct_variable1 = struct_variable2;, 但仅限于结构兼容的struct
    • 作为参数和返回值
      • 参数return_type func_name (struc tag parameter) {...};
      • 返回值struct tag func_name () {}
    • C99支持作为复合字面量
      • (struct tag) { member1, ...}
      • 未初始化的默认0
      • 适用于仅使用一次的(临时)变量, 有效避免浪费
    • 嵌套结构&结构数组
      • 结构允许嵌套操作, 某结构变量作为另一结构变量的成员
      • 结构数组可以作为简单的数据库, struct tag variable[N];
      • 结构数组的初始化, struct tag variable[(N)] = { { member1.1, member 1.2, ...}, { member2.1, member2.1, ...}, ... }
    • typedef详解
      • 用法1:
        typedef struct newType{
          int data[10];
          int size;
        };
        // 使用时, struct newType作为新的数据类型
        struct newType variableType;
      • 用法2:
        typedef struct newType{
          int data[10];
          int size;
        }myType;
        // 上述操作实现两步, 定义新的数据类型struct newType, 并取别名为myType
        // 使用时, 可以直接使用数据类型别名myType来定义
        myType variableType;
      • 更多讲解
  • 联合union

    • 一种特殊的数据类型, 允许在相同的内存位置存储不同的数据类型
    • 由一个/多个成员构成, 但编译器只为联合中最大的成员分配足够内存, 联合的成员在该空间内彼此覆盖
    • structunion性质操作等几乎一样, 不同的是前者同时储存成员变量, 后者只能同时存储某一成员变量
    • union主要用来节省空间, 也可以用来构造混合数据结构 (例如浮点数&整数数组)
    • 可以通过将union嵌套到struct中, 来为联合添加标记字段, 告诉外部联合里面所存变量类型
    • 声明(其余基本相同)
      union union_tag {
        memeber_type member_name;
        memeber_type member_name;
        ...
      } union_variable1, union_variable2, ... ;
  • 枚举enum

    • 一种基本数据类型,它可以让数据更简洁,更易读
    • 声明: enum enum_tag { element1, element2, ... } enum_variable;
      • 也可以和struct/union一样, 先声明, 再定义; 或省略tag作为一次性的类型
    • 枚举作为整数
      • 其中的元素默认从0开始, 依次递增1
      • 可以单独指明首个元素, 改变枚举始量, enum tag { element1 = 10, element2, ... }; (此时枚举量为10, 11, ...)
      • 也可以指明每个元素, 对于没有指定的值总比前一个大1
      • C语言允许枚举变量与普通整数混合
    • 枚举非常适合用来做union的标记字段

大型程序

  • 源文件

    • 可以把程序分为任意个.c源文件
    • 优点:
      • 使程序结构更清晰 (将相关变量&函数分组放在同一文件中)
      • 可有效节约时间 (可分别对每个源文件单独编译, 对于大规模程序而言尤为重要)
      • 利于复用
  • 头文件

    • #include指令指向的.h文件
    • 引用方式
      1. #include <xxx.h> - 用于属于C语言自身库的头文件, 搜寻系统头文件所在(多个)目录
      2. #include "xxx.h" - 用于其他头文件/自己编写的文件, 先搜寻当前目录, 然后搜寻系统头文件所在(多个)目录
      3. #include 记号 - 记号即任意预处理标识符, 会自动完成宏替换, 配合#if-#elif-#else可大大提高系统的可移植性
    • 共享宏定义&类型定义&共享函数原型&共享变量声明
    • 头文件允许嵌套包含
    • 保护头文件: 在待保护头文件中, 用#ifndef&#endif来防止头文件被多次包含编译

TIPS

  • 访问结构的成员时使用点运算符, 而通过指针访问结构的成员时, 则使用箭头运算符

  • 逗号运算符,

    exp1, exp2; 一行执行
    只有一个表达式的情况下执行多条表达式

  • 空语句

    ;
    什么也不做, 常用于编写空循环体的循环

  • 函数如何返回数组的三种方法:

    1. 函数外初始化数组, 并传入地址, 返回地址
    2. 使用静态局部变量数组(static)
    3. 使用结构体(结构体成员深拷贝)

数据结构

About


Languages

Language:JavaScript 80.8%Language:C 16.2%Language:C++ 1.7%Language:HTML 1.3%