C与指针读书笔记

第一章 快速上手

1.1 简介

  1. 使用#if 0 与#endif 比//能更好的完成逻辑上删除代码的功能。
  2. 预处理指令由预处理器解释,预处理器读入源代码,根据预处理指令对其进行修改,然后把修改过得源代码交给编译器。
  3. C 函数的参数传递规则:所有传给函数的参数都是值传递。

1.2

  1. 常见的字段函数:putchar、getchar、ctrcpy、ctrncpy、strcat、strchr、strstr

第三章 数据

3.1 基本数据类型

  1. 四种:整形、浮点型、指针和聚合类型。(注意没有布尔类型,任何非零值都是 true,包括负数)。
  2. 整形包括字符、短整型、整形和长整型。都可以再分为 signed 和 unsigned。

3.4 常量

  1. int const *p可以修改指针的值,不能修改它指向的值。
  2. int * const p指针不能修改,但是可以修改它指向的值。
  3. #define 也是创建常量的手段。

3.5 作用域

  1. 代码块作用域、文件作用域、原型作用域、函数作用域
  2. 链接属性:external、internal、none。extern 和 static 可以用于修改链接属性。具有 external 属性的实体常称为全局变量。
  3. 存储类型:普通内存、运行时堆栈、硬件寄存器。static 使存储类型变成静态。
  4. 文件作用域内声明的变量默认链接属性是 external。存储类型是 static。局部变量的链接属性是 none,存储类型是 auto。external 的一定 static。
  5. 关于 static:当用于函数定义或者是代码块之外的变量声明时,作用是将标识符的链接属性从 external 修改为 internal,但标识符的存储类型和作用于不受影响,此时该标识符只能在该源文件中访问。当用于代码块内部的变量声明时,static 将自动变量修改为静态变量,但是链接属性和作用域不受影响。此时该变量在执行之前创建,在整个执行期间存活。静态变量只会初始化一次,如果没有手动初始化,默认赋值为 0。
  6. 尽管全局变量在程序执行之前创建,但是其作用域仍然在声明之后。

第四章 语句

第六章 指针

第七章 函数

7.2 函数声明

  1. 在头文件内定义函数原型(函数声明),并且将其 include 至函数定义文件与函数使用文件,这样的好处在于:函数原型具有文件作用域,避免多次调用不匹配,修改时只修改原型
  2. 没有 return 或者 return 没有语句的函数被称为过程

7.3 函数参数

  1. 所有参数都是传值调用,这意味着函数会获得参数值的一份拷贝。

7.4 ADT 与黑盒

  1. 设计与实现抽象数据类型(ADT),通过限制函数和数据定义的作用域,这个技巧也被称为黑盒设计。使用头文件声明函数接口原型,然后在实现文件中定义 static 函数限制其作用域在该文件内部。

7.5 递归

  1. 注意递归和堆栈的互相替代性。
  2. 尾递归最好转换为循环避免堆栈开销。

7.6 可变参数列表

  1. 使用 stdarg 宏:函数声明内的参数位置加上省略号,函数内预先定义va_list类型变量,使用va_start获取这个可变参数,使用va_arg依次获取va_list中的参数值,最后用va_end关闭变量。

第八章 数组

8.1 一维数组

  1. 数组名的类型是指向该数组类型的指针,数组名的值是第一个元素的地址。但是数组与指针不能完全画等号,例如数组具有确定数量的元素而指针只是一个标量值,只有当数组名在表达式中使用时编译器才会为他产生一个指针常量。注意是指针常量!

    1
    2
    3
    4
    int a[10];
    int b[10];
    a = b;
    int *c;
  2. 上面的语句错误,由于在表达式中数组名是指针常量,编译器会提示左值不可修改。另外*a 表达式合法,然而*b 却是非法的因为*b 将会访问内存中一个不确定的位置。c++可以通过编译,a++却不行,理由同样是因为 a 在表达式中是一个常量。

  3. 只有在两种情况下,数组名并不用指针常量表示:
  4. 下标引用与间接访问除了操作优先级以外完全等同。因此 2[a]和 a[2]是完全一样的,因为他们都等于*(a+2)或者*(2+a),但是由于编译的原因,在遍历数组的情况下,使用指针的方式更快。
  5. 如果一个函数形参接受一个数组,那么把这个形参声明为指针或者数组都是完全等价的,函数内部会得到一个指针的拷贝,因此声明为指针更准确一些。因此在函数内去对数组形参进行 sizeof 操作得到的只是指针的大小而不是数组的长度。

    1
    2
    3
    4
    5
    unsigned char *a;
    printf("%d",sizeof(a));//4

    unsigned char a[30];
    printf("%d",sizeof(a));//30
  6. 使用花括号初始化数组,花括号内元素小于数组大小时,后面不足的元素默认为 0。使用大括号初始化数组时,数组长度可以省略,数组大小将自动匹配大括号内元素个数。

  7. 在一个代码块内部多次运行到大数组的初始化代价不可忽视,如果没有必要,可以将其声明为 static。

    1
    2
    3
    4
    5
    6
    7
    8
    //1
    char a[] = {'a','b','c'};

    //2
    char b[] = "abc";

    //3
    char *c = "abc";
  8. 上述两种对于字符串的初始化方法等价,“ABC”在上述情况下不是字符串常量,而是字符列表的简略写法。

8.2 多维数组

  1. 多维数组可以认为是数组的数组。

    1
    2
    3
    4
    5
    6
    /*
    使用{1,2,3,4,5,6,7,8,9,10,11,12}初始化也可以,不过下面的方式更好
    另外,提供初始值的时候,第一位长度可以省略。
    */
    int d[][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
    printf("%d\n", ++d[0][3]); //output:5
  2. 多维数组是行主序的,如上述代码,到一行的最后一个元素时,继续+1 会自动转到下一行。

  3. 多维数组的数组名是一个指向数组的指针。
  4. 下标引用只是间接访问表达式的一种伪装形式。这句话对于多维数组同样适用。例如 a[2][3]实际上等价于((a+2)+3)。
  5. 指向数组的指针:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //right
    int v[10], *vp = v;

    //error
    int m[3][10], *mp = m;

    //right
    //p 是一个指向整形数组的指针,并指向 m 的第一行
    int m[3][10];
    int (*p)[10] = matrix;

    //do not write like this
    //也许编译器不能指出错误,但是当 p 与整数进行运算时,比如 p+a,a 要乘以指针元素的长度,但是此时指针元素长度为 0。
    int (*p)[] = matrix;
  6. 作为函数参数的多维数组:(前两种正确,第三种不正确)

    1
    2
    3
    4
    5
    int m[3][10];
    ...
    void func1(int (*p)[10]);
    void func2(int p[][10]);
    void func3(int **p);
  7. 指针数组与指向数组的指针:这么理解,第一个式子中*p 先结合,说明 p 是指针,然后是(指向数组的)指针,第二个式子中,下标操作符优先级更高,所以 a 先是一个数组,然后是(数组内元素是指针的)数组。

    1
    2
    3
    4
    //1
    int (*p)[10];
    //2
    int *a[10];
  8. 逗号下标有时不会报错,因为逗号表达式的值为最后一个逗号后面的值,所以 a[1,2]在有些机器中等价于 a[2].

第九章 字符串、字符和字节

9.1 字符串基础

  1. 字符串的长度不包括 UNL 字节。
  2. include string.h 并非必须,但是加上是好的。因为有了头文件中定义的原型,编译器可以更好地执行错误检查。

9.2 字符串长度

  1. strlen 返回的是无符号整形,尽量不要使用无符号整形进行运算,因为计算出来的结果永远是非负数,如果你想计算出负数,那程序就会有错。同样无符号数与有符号数一起计算还是有问题,最好的方法就是强制类型转换。

9.3 不受限制的字符串函数

  1. 参数是 char*
  2. 字符串复制:strcpy,字符串连接 strcat,字符串比较 strcmp(返回值是两个字符串的字典序比较)。
  3. 避免源字符串和目标字符串位置发生重叠,注意由于不检查长度,只通过\0判断结尾,因为是不安全的,可能溢出。
  4. 返回值是目标函数的指针,这使得这类函数可以嵌套调用,但是这样会使可读性变差。

9.4 长度受限的字符串函数

  1. 与不受限制的区别是这里的三个函数多了一个 size 参数,比如复制函数会恰好复制过去 size 个字节,不够补零,过了就不要,注意不自动填充结束符。strcat 会添加结束符。

9.5 字符串查找

  1. 单个字符查找:strchr 和 strrchr
  2. 字符组查找:strpbrk
  3. 子串查找:strstr

9.6 高级字符串查找

  1. 字符串前缀查找:strspn 和 strcspn
  2. 查找标记:strtok

9.7 错误信息

  1. `char *strerror(int error_code);

9.8 字符操作

  1. ctype.h 文件中,一种用于字符分类,一种用于字符大小写测试与转换。

内存操作

  1. 字符串操作遇到\0就会停止,如果想直接操作内存,使用 mem 系列函数

第十章 结构和联合

10.1 结构基础知识

  1. 结构变量在表达式中不是指针,可以作为返回值,类似于数组,使用花括号初始化。
  2. 结构声明,下面两种定义几乎等价。区别在于 Simple 是类型名还是结构标签。在后续的声明终会体现出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    typedef struct {
    int a;
    char b;
    float c;
    } Simple1;

    struct Simple2{
    int a;
    char b;
    float c;
    };

    struct Simple1 s1;
    Simple2 s2;
  3. .操作符对结构成员进行直接访问,->操作符对结构指针的成员进行间接访问。

  4. 结构的自引用与互引用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //error
    struct SELF1 {
    struct SELF1 s1;
    }

    //right
    struc SELF2 {
    struct SELF2 *s2;
    }

    //right
    struct A;
    struct B {
    struct A *a;
    }
    struct A {
    struct B *b;
    }

10.3 结构的存储分配

  1. 当结构需要字对齐时,对结构体内各个成员进行重拍会有更高的空间利用率。
  2. sizeof 得到一个结构体的占用空间,offsetof 宏(定义于 stddef.h)确定某个成员的偏移量。

    1
    2
    3
    4
    5
    6
    7
    #include <stddef.h>
    struct ALIGN {
    char a;
    int b;
    char c;
    }
    offset(struct ALIGN, b); // output: 4

10.4 作为函数参数的结构

  1. 值传递效率比较低,要先复制到堆栈中,然后丢弃。传指针的话更好,因为只需要压栈一个 4 字节。如果对这个指针的间接访问超过三四次,把指针声明为寄存器变量更有效率。若不希望修改,就声明为 const。此时声明为:void func(register Struct const *p);

10.5 位段(bit field)

  1. 位段的声明和结构类似,但他的成员是一个或多个位的字段,这些字段实际存储于整型中。因此位段成员必须声明为 int、signed int 或者 unsigned int,其次在成员名后加上冒号和整数,指定位数。
  2. 在源代码中使用位段更为清晰,但是在目标代码中无论是否使用位段,移位操作和屏蔽操作都是必须的,位段提供的唯一优点是简化源代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 假设已知磁盘寄存器在 0xc0200142 位置进行访问

    // 通过位段方式
    #define DISK_REGISTER ((struct DISK_REGISTER_FORMAT *)0xc0200142)
    struct DISK_REGISTER_FORMAT {
    unsigned command :5;
    unsigned sector :5;
    unsigned track :9;
    }

    DISK_REGISTER->sector = new_sector;

10.6 联合

  1. 联合的所有成员引用的是内存中的相同位置。如果联合中的成员具有不同的长度,联合的长度就是它最长成员的长度。
  2. 联合变量可以被初始化,但是这个初始值必须是联合中第一个成员的类型,并且在一堆花括号里面。

第十一章 动态内存分配

11.1 为什么使用动态内存分配

  1. 正常时候声明数组时要用一个编译时就能确定的常量指定数组长度。

11.2 molloc 和 free

  1. 这两个函数维护一个可用内存池,molloc 提取一块连续的内存并返回一个指向这块内存的(void*)指针,返回后根据使用目的进行强制类型转换。两个函数都在 stdlib.h 中声明。如果可用内存池不足以满足请求,molloc 就向操作系统请求更多内存,如果操作系统无法提供,返回值就是 NULL。参数的单位是字节。

11.3 calloc 和 realloc

  1. calloc:void* calloc(size_t num_elements, size_t element_size)。和 molloc 的区别在于首先参数不同,其次 calloc 会将内存初始化为 0。
  2. realloc:void realloc(void *ptr, size_t new_size)。改变内存大小。

11.4 使用动态分配的内存

11.5 常见错误

  1. 对一个动态分配返回的指针创建了一个拷贝,然后其中一个 free 后,其他的指针仍然试图访问这块内存。

第十二章 使用结构和指针

12.1 链表

12.2 单链表

12.3 双链表

第十三章 高级指针话题

13.1 进一步探讨指向指针的指针

13.2 高级声明

原则是:在 C 中,声明以表达式推论的形式进行分析。

  1. int *f;把 f 声明为指向整形的指针。其具体声明过程是将(f)声明为整型,因此,`int f, g;`语句把 f 声明为指针,g 声明为一般整型。
  2. 同理int *f();认为 f 先和具有更高优先级的函数调用操作符结合,然后间接访问得到一个整型,因此,f 的返回值是 int 的指针。
  3. int (*f)();中 f 先进行间接访问,然后再执行函数调用,因此,f 是函数指针。这样的话int *(*f)();也好理解了,是一个返回 int 指针的函数指针。
  4. int *f[];因为下标优先级更高,先对 f 进行数组元素访问,再进行间接访问,因此 f 是指针的数组。
  5. 最后两个较复杂的int (*f)(int, float);int *(*g[])(int, float);前者把 f 声明为一个返回 int 的函数指针,后者把 g 声明成一个函数指针数组,指向的函数返回值是 int 指针。
  6. 使用 unix 程序 cdecl 帮助理解

13.3 函数指针

  1. 主要的两个用途是转换表(jump table)和作为参数传递给另外一个函数。函数指针同样需要初始化,初始化时的&操作符可选,因为函数名被使用时总是由编译器将其转换为函数指针。在函数指针的初始化之前具有 f 的原型是很重要的,否则编译器无法检查 f 的类型是否与 pf 所指向的类型一致。

    1
    2
    int f(int);
    int (*pf)(int) = &f;
  1. 使用三种方式调用函数 f

    1
    2
    3
    4
    5
    int ans;

    ans = f(25);
    ans = (*pf)(25);
    ans = pf(25);
    • 第一条语句简单的执行函数,然而真正的执行过程是 f 首先被转换为一个函数指针指向函数在内存中的位置,然后通过调用操作符调用该函数,执行开始于这个地址的代码。
    • 第二条语句先通过间接访问将函数指针转换为函数名。然而这个转换不是真正需要的,因为编译器在执行函数时又会转化为函数指针。
    • 第三条语句显示了函数指针是怎样使用的,因为编译器需要的是一个函数指针,因此直接执行效果相同。
  2. 回调函数使回调函数的参数是 void*, 在回调函数内部进行强制类型转换。
  3. 转移表:根据不同的操作数调用不同的函数,详见原书。

13.4 命令行参数

(略)

13.5 字符串常量

  1. 当一个字符串常量出现在表达式中时它的值是一个指针常量。编译器把这些指定字符的一份拷贝存放在内存的某个位置,并存储指向第一个字符的指针。因此"xyz"+1的值是指向’y’的指针

第十四章 预处理器

预处理是编译的第一个阶段,包括删除注释、插入#include 内容,定义和替换#define 内容以及一些条件编译的工作。

14.1 预定义符号

14.2 #define

  1. #define 包括了一个规定,允许把参数替换到文本中,这种实现通常称为。宏的声明方式:#define name(param_list) stuff。所有宏定义最好按照希望的执行顺序加上括号,包括最外面也要加括号,以防可能错误的执行顺序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #define SQUARE(x) x * x

    printf("%d\n", SQUARE(5)); //output 25

    int a = 5;
    printf("%d\n", SQUARE(a + 1)); //output 11


    /*
    * 第二次执行失败是因为宏被替换为 a + 1 * a + 1 = 11
    * 在宏中加括号可以正确运行
    */
  2. 宏和函数:

    • 宏的好处是没有调用和返回的代价,不需要确定的参数类型,不好的地方在于宏太长并且大量替换的时候会大幅增加程序长度。另外,一部分功能只能通过宏实现:
      1
      2
      #define MALLOC(num, type) \
      ((type*)malloc((n)*sizeof(type)))

  1. 使用#undef 移除一个宏定义
  2. 使用#define 的流程:检查是否已经被定义,如果是,他们会被替换;替换文本;全部替换后检查是否还有被#define 的符号,如果有,返回上一步。

14.3 条件编译

  1. 基本结构是#if#endif#if 后面必须是一个常量表达式。通过条件编译可以更好地完成调试的工作。
  2. 条件编译的另一个用途是选择不同的代码部分,需要配合#elif#else
  3. #indef#ifndef

14.4 文件包含

14.5 其他指令

本站总访问量