[读] C和指针 (Ch11 ~ Ch14)

Chapter 11

  • mallocfree维护一个内存池

    • malloc总是分配一整块内存。根据编译器的实现,实际分配的内存也有可能比请求的稍大一些
    • 可用内存池无法满足请求时,malloc先向OS申请新的内存,还是不够时返回NULL。因此malloc返回的指针必须先检查
    • free的参数必须为通过malloc等函数申请到的内存指针或NULL,它也总是释放整个分配到的内存
  • calloc:返回指针前将整块内存都初始化为0
  • realloc

    • 用于(在尾部)扩大或缩小一块已经分配到的内存,可以被用于有效地释放一块内存的尾部空间
    • 原先的内存无法修改大小时realloc可能返回一块新内存的指针(原先的内容会被复制过去),因此realloc后必须更新指针
    • 第一个参数为NULL时,与malloc行为相同

Chapter 12

  • 可以额外使用一个根结点来避免插入发生在链表头部以至于需要修改根结点的值。对于双链表,额外的根结点还可以同时维护链表的头部和尾部
  • 不过当一个节点本身的体积过大(比如存有大块数据)时,这种方式会造成浪费

Chapter 13

  • ⚠️函数指针(这里太重要了吧!!)

    此处重点在于函数调用符(即函数名后的括号)的优先级比间接访问要高,因此

    int *foo(); // foo是一个返回int*指针的函数

    foo优先与函数调用符结合,成为一个函数,然后对该函数的值(即返回值)进行间接访问得到int,可据此推论函数返回值为int*

    int (*foo)(); // foo是一个返回int的函数的指针

    foo被迫先与间接访问符结合,也就是说*foo作为整体与函数调用符结合,即*foo为函数,则foo为函数指针

    int (*foo[])(); // foo是一个函数指针数组

    foo先与[]结合,说明foo是一个数组名,接着foo[]*结合,说明foo中的元素为某种指针,最后*foo[]与函数调用符()结合,并参照类型声明为int,说明元素为返回int值的函数指针(晕了。。)

  • 函数名在表达式中的角色与数组有些类似,也是以类似指针常量的形式出现的,因此

    void f(int arg); // f为接收1个int参数,无返回值的函数
    
    void (*fPtr)(int arg) = f; // 函数名可以作为指针被直接复制给函数指针
    void (*fPtr)(int arg) = &f; // 也可以先取址再赋值给函数指针

    事实上,&只是显式地执行了编译器将隐式执行的工作

  • 转移表(jump table)常通过函数指针数组实现,此时若发生数组越界等问题debug将会非常艰难。。
  • 命令行参数

    • argc:参数个数(即argv数组的长度)
    • argv:字符串指针数组(命令行中空格分隔的每一截都为一个参数),通常通过检查字符串开头是否为-来识别选项参数
  • 字符串常量本身其实就是一个常量指针

    char *strPtr = "abcdefg";
    
    "abcdefg"[2] == 'c'; // true
    "abcdefg" + 2 == strPtr + 2; // true,因此它也可以作为一个字符串指针在printf里直接用%s输出

Chapter 14

  • 预定义符号(不是预处理指令!)

    • __FILE__%d源代码名(test.c
    • __LINE__%d文件当前行号(25
    • __DATE__%s文件被编译的日期(Jan 31 1997
    • __TIME__%s文件被编译的时间(13:07:02
    • __STDC__%d编译器是否遵循ANSI C标准(1 / 0
  • ⚠️带参数宏

    #define macro(arg1, arg2) stuff // 参数列表的左括号必须紧邻宏名,否则就会被认为是stuff的一部分

    (以前对宏有误解,以为是单纯的把一段文本替换成另一段文本,而实际上宏是先被按照符号解析再转换为文本进行替换的)

  • 宏很容易产生歧义,要格外注意括号和分号的使用
  • 宏参数和#define定义可以包含其他#define定义的符号,但宏不可以递归(大概是因为没有调用栈这种东西)
  • 利用printf会将直接相邻的字符串连接起来的特性,可以实现简单的输出包装:

    #define PRINT(FORMAT, VALUE) printf("This value is "FORMAT".\n", VALUE) // 注意没有分号!
    
    PRINT("%d", x + 4); // 则FORMAT为"%d",VALUE为x + 4
    // 实际被转换为:
    printf("This value is ""%d"".\n", x + 4); // 三个字符串被自动连接
  • ⚠️符号连接符##:产生的新符号必须合法,否则就是undefined行为

    #define ADD_TO_SUM(sum_number, value) sum##sum_number += value // 注意没有分号!
    
    ADD_TO_SUM(5, 23); // 5将作为sum_number的值被连接到sum,产生sum5这个符号,最后整个宏被替换为sum5 += 23;
    // 宏被引用时上下文中必须存在名为sum5的变量
  • ⚠️宏接收参数不指定类型,因此甚至能接受类型本身为参数

    (这就是为什么sizeof不是一个函数吧,因为类型本身不能作为参数传递,以及可变参数列表也是用宏来实现)

  • 带副作用的宏参数可能导致严重错误,比如:

    • ++ / --:改变参数本身
    • getchar():消耗I/O buffer
  • 因为宏是被直接替换的,所以反复使用同一个宏也总是生成新的代码。而函数则每次在调用时使用同一份代码
  • 命令行定义:

    • -D{name} / -D{name}=stuff === #define name stuff (未指定时stuff默认为1)
    • -U{name} === #undef name
  • 条件编译:#if / #endif / #elif / #else

    • 条件表达式将由预处理器进行求值,因此它们的值必须是在编译时就能确定的常量
    • 测试符号是否已被定义:

      • #ifdef symbol === #if defined(symbol)
      • #ifndef symbol === #if !defined(symbol)
    • ⚠️可以结合命令行定义来完成条件编译

      • 源代码test.c

        #ifndef CHOICE // 以免与命令行定义冲突
            #define CHOICE 5
        #endif
        
        int main() {
            printf("Choice: %d\n", CHOICE);
            return 0;
        }

        直接编译执行:

        > gcc test.c -o test
        > ./test
        > Choice: 5

        编译时使用命令行定义:

        > gcc -DCHOICE=2 test.c -o test # 提前定义CHOICE为5
        > ./test
        > Choice: 2
    • 可以在#endif后用注释标注它结束的是哪一个条件以提高程序可读性
    • ⚠️使用条件编译来避免由于头文件被重复包含导致的符号冲突

      #ifndef _HEADERNAME_H
      #define _HEADERNAME_H 1 // 如果没有1则_HEADERNAME_H被定义为空字符串,但仍然被定义,不会影响判断结果
      #endif
      • 尽管有办法防止冲突,多重包含仍然会拖慢编译速度,应被尽力避免
      • 但也不应该为了避免多重包含而依靠头文件自己嵌套包含,这样会导在使用make等工具时很难判断文件间的依赖关系
    • #progma指令是不可移植的,不同平台的预处理器可能会完全忽略它,或以不同的方式执行它
阅读 156

推荐阅读
Oh, Fish!
用户专栏

奇怪的知识增加了

0 人关注
6 篇文章
专栏主页