【C Primer Plus第六版 学习笔记】 第十六章 C预处理器和C库

有基础,进阶用,个人查漏补缺

第十五章的内容之前学过,跳过

  1. 预处理之前,编译器必须对该程序进行一些翻译处理

    1. 首先把源代码中出现的字符映射到原字符集

    2. 其次编译器定位每个反斜杠后面跟着换行符的实例,并删除它们(把由于写代码时,一行太长,会用反斜杠\把一行逻辑行变成两个物理行)

    3. 然后编译器把文本划分为预处理记号序列、空白序列和注释序列。此处需要注意的是,编译器将用一个空格字符替换每一条注释,如

      int/* 注释*/fox;
      //将变成
      int fox;//中间的注释变成了一个空格
      
  2. C预处理器在程序执行之前查看程序,故称之为预处理器。根据程序中的预处理器指令,预处理器把符号缩写替换成其表达的内容。

  3. 明示常量:#define

    1. 指令从#开始运行,到第1个换行符结束,针对的是一个逻辑行(可以用\进行物理换行)

    2. 类对象宏定义的组成:宏的名称中不允许有空格,需要遵循C变量的命名规则

      #define PX printf("x is %d\n", x)
      //预处理指令 宏 替换体
      
    3. 预处理器不做计算,不对表达式求值,只进行替换

    4. 记号:可以把宏的替换体看作是记号(token)型字符串

      #define FOUR 2*2    //有一个记号2*2,但对于C编译器来说是3个记号
      #define six 2 * 3   //有三个记号2、*、3,额外的空格也是替换体的一部分
      
    5. 重定义常量:假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。

      //ANSI标准在新定义和旧定义完全相同时才允许重定义
      #define six 2 * 3
      #define six 2 * 3
      
    6. 在#define中使用参数,即类函数宏:

      1. 为保证运算顺序,要多使用圆括号

        #define SQUARE(X) X*X
        #define SQUARE1(X) (X*X)
        #define SQUARE2(X) (X)*(X)
        int x = 5;
        z = SQUARE(x);//z=25
        z = SQUARE(x+2);//z= x+2*x+2 = 5+2*5+2 = 17
        z = 100 / SQUARE(2);//z = 100/2*2 = 100/2*2 = 100
        
        z = SQUARE1(2);//z = 100 / (2*2) = 25
        
        z = SQUARE2(x+2);//z = (x+2)*(x+2)
        
        
      2. 避免使用++x等这种递增或者递减

      3. 用宏参数创建字符串:#运算符

        #define PSQR(X) printf("The square of X is %d.\n", ((X)*(X)))
        PSQR(8);//输出The square of X is 64.**注意双引号中的X被视为普通文本,不是记号**
        
        #define PSQR(x) printf("The square of " #x " is %d.\n", ((x)*(x)))
        int y = 5;
        PSQR(y);//输出The square of y is 25
        PSQR(2 + 4);//输出The square of  2 + 4 is 36
        
      4. 预处理黏合剂:##运算符

        #define XNAME(n) x ## n
        int XNAME(1) = 14;//变成int x1 = 14
        
      5. 变宏参:…和__VA_ARGS__

        #define PR(...) prinf(__VA_ARGS__)
        pr("Hoedy");//等于 prinf("Hoedy")
        
        #define PR(X, ...) prinf("Message " #X ": " __VA_ARGS__)
        int x = 2;
        PR(1, "x = %d\n", x);//即prinf("Message " "1" ": " "x = %d\n", x)
        //输出Message 1: x = 2
        
  4. 宏和函数的选择

    1. 宏生成内联代码,即在程序中生成语句,调用20次宏就生成20行代码。而调用函数20次,在程序中只有一份函数语句的副本,节省空间

    2. 调用函数时,程序控制必须跳转到函数内,再返回主调程序,比内联代码更费时间

    3. 宏不用担心变量类型

    4. 在嵌套循环中使用宏更有助于提高效率

    5. 对于简单的函数,通常使用宏

      #define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
      #define ABS(X,Y) ((X) < 0 ? -(X) : (X))
      #DEFINE ISSIGN(X) ((X)  == '+' || (X) == '-' ? 1 : 0)
      
  5. 文件包含:#include

    1. 当预处理器发现#include指令时会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。

    2. #include指令有两种形式:文件名在尖括号或者双引号中

      //在unix系统中
      #include               //查找系统目录
      #include "mystuff.h"            //查找当前工作目录
      #include "/usr/biff/mystuff.h"  //查找/usr/biff/目录
      
    3. 头文件实例

      1. names_st.h————names_st结构的头文件

        // 常量
        #include 
        #define SLEN 32
        
        // 结构声明
        struct names_st
        {
            char first[SLEN];
            char last[SLEN];
        };
        
        // 类型定义
        typedef struct names_st names;
        
        // 函数原型
        void get_names(names *);
        void show_names(const names *);
        char * s_gets(char * st, int n);
        
      2. names_st.c————定义names_st.h中的函数

        #include 
        #include "names_st.h"     //包含头文件
        
        //函数定义
        void get_names(names * pn)
        {
            printf("Please enter your first name: ");
            s_gets(pn->first, SLEN);
            
            printf("Please enter your last name: ");
            s_gets(pn->last, SLEN);
         }
        
        void show_names(const names * pn)
        {
            printf("%s %s", pn->first, pn->last);
        }
        
        char * s_gets(char * st, int n)
        {
            char * ret_val;
            char * find;
            
            ret_val = fgets(st, n, stdin);
            if (ret_val)
            {
                find = strchr(st, '\n');   //查找换行符
                if (find)                  //如果地址不是NULL,
                    *find = '\0';          //在此处放一个空字符
                else
                    while (getchar() != '\n')
                        continue;          //处理输入行中的剩余字符
            }
            return ret_val;
        }
        
        
      3. useheader.c————使用names_st结构

        #include 
        #include "names_st.h"
        //记得链接names_st.c
        
        int main(void)
        {
            names candidate;
            
            get_names(&candidate);
            printf("Let's welcome ");
            show_names(&candidate);
            printf(" to this program!\n");
            return 0;
        }
        
      4. 注意:

        1. 两个源文件.c都使用names_st类型结构,所以它们都必须包含names_st.h头文件
        2. 必须编译和链接names_st.c和useheader.c源代码文件
        3. 声明和指令放在names_st.h头文件中,函数定义放在names_st.c源代码文件中
    4. 使用头文件

      头文件中最常用的形式如下:

      1. 明示常量
      2. 宏函数
      3. 函数声明
      4. 类型定义
      5. 使用头文件声明外部变量供其他文件共享
  6. #undef 指令

    用于取消已定义的 #define 指令

    #define LIMIT 40
    #undef LIMIT      //可以移除上面的定义
    

    现在可以将LIMIT重新定义为一个新值,即使原来没有定义LIMIT,该取消也依旧有效;

    如果想使用一个名称,又不确定是否之前已经用过,为安全起见,可以使用 #undef 取消该名称的定义

  7. 条件编译——#ifdef、#else、#endif

    预处理器不识别用于标记块的花括号{}

    缩进与否看个人风格

    #ifdef MAVIS          //如果已经用#define定义了MAVIS,则执行下面的指令
    	#include "horse.h"
    	#define STABLE 5
    #else                 //如果没有用#define定义了MAVIS,则执行下面的指令
    	#include "cow.h"
    	#define STABLE 5
    #endif                //必须存在
    

    也可以用这些指令标记C语句块

    #include 
    #define JUST_CHECKING
    #define LIMIT 4
    
    int main(void)
    {
        int i;
        int total = 0;
    
        for (i = 1; i <= LIMIT; i++)
        {
            total += 2*i*i + 1;
    #ifdef JUST_CHECKING
            printf("i=%d, running total = %d\n", i, total);
    #endif
        }
        printf("Grand total = %d\n", total);
        
        return 0;
    }
    

    输出:

    i=1, running total = 3
    i=2, running total = 12
    i=3, running total = 31
    i=4, running total = 64
    Grand total = 64
    

    如果省略JUST_CHECKING定义(把#define JUST_CHECKING放在注释中,或者使用#undef指令取消它的定义),并重新编译该程序,只会输出最后一行。该方法可用来调试程序。

  8. 条件编译——#ifndef 指令

    #ifndef 和#ifdef 用法类似,也是和#else、#endif一起使用,只是它们的逻辑相反

    有arrays.h

    #ifndef SIZE
    	#define SIZE 100
    #endif
    

    有代码

    #define SIZE 10
    #include "arrays.h" //当执行到该行时,跳过了#define SIZE 100,故SIZE为10
    

    故可以使用#ifndef 技巧避免重复包含

    #ifndef NAMES_H_
    #define NAMES_H_
    
    // constants
    #define SLEN 32
    
    // structure declarations
    struct names_st
    {
        char first[SLEN];
        char last[SLEN];
    };
    
    // typedefs
    typedef struct names_st names;
    
    // function prototypes
    void get_names(names *);
    void show_names(const names *);
    char * s_gets(char * st, int n);
    
    #endif
    

    用以下代码进行测试,是没有问题的。但是如果把上面的.h中的#ifndef 删除,程序会无法通过编译

    #include 
    #include "names.h"
    #include "names.h"   //不小心第2次包含头文件
    
    int main()
    {
        names winner = {"Less", "Ismoor"};
        printf("The winner is %s %s.\n", winner.first, winner.last);
        return 0;
    }
    
  9. 条件编译——#if 和 #elif

    类似于if语句。#if 后面跟着整型常量表达式

    #if SYS == 1
    	#include "a.h"
    #elif SYS == 2
    	#include "b.h"
    #elif SYS == 3
    	#include "c.h"
    #else
    	#include "d.h"
    #endif
    

    另一个新的用法测试名称是否已经定义

    #if defined (ABC)
    	#include "a.h"
    #elif defined (DE)
    	#include "b.h"
    #elif defined (FG)
    	#include "c.h"
    #else
    	#include "d.h"
    #endif
    
  10. 预定义宏

    【C Primer Plus第六版 学习笔记】 第十六章 C预处理器和C库_第1张图片

  11. #line 和 #error

    #1ine 指令重置__LINE__和__FILE__宏报告的行号和文件名。可以这样使用Iine:

    #line 1000 11      //把当前行号重置为1000
    #line 10 "cool.c"  //把行号重置为10,把文件名重置为 cool.c
    

    #error 指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断。可以这样使用#error 指令:

    #if __STDC_VERSION__!= 201112L
    #error Not C11
    #endif
    
    //编译以上代码生成后,输出如下:
    $ gcc newish.c
    newish.c:14:2: error: #error Not C11
    $ gcc -std=c11 zewish.c
    $
    

    如果编译器只支持旧标准,则会编译失败,如果支持 CI1 标准,就能成功编译。

  12. #pragma

    在现在的编译器中,可以通过命令行参数或 IDE 菜单修改编译器的一些设置。#pragma 把编译器指令放入源代码中。例如,在开发 C99 时,标准被称为 C9X,可以使用下面的编译指示 (pragma)让编译器支持 C9X:

    #pragma c9x On
    
  13. 泛型选择(C11)

    在程序设计中,泛型编程(generic programming)指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。

    例如,C++在模板中可以创建泛型算法,然后编译器根据指定的类型自动使用实例化代码。C没有这种功能。然而,C11 新增了一种表达式,叫作泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是int、double 还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define 宏定义的一部分。

    下面是一个泛型选择表达式的示例:

    _Generic (x, int: 0, float: 1, double: 2, default: 3)
    

    _Generic 是C11 的关键宇。_Generic 后面的國括号中包含多个用逗号分隔的项。

    1. 第1个项是一个表达式,后面的每个项都由一个类型、一个冒号和一个值组成,如float:1
    2. 第1个项的类型匹配哪个标签,整个表达式的值是该标签后面的值。例如,假设上面表达式中× 是int 类型的变量,× 的类型匹配int :标签,那么整个表达式的值就是0。
    3. 如果没有与类型匹配的标签,表达式的值就是 default:标签后面的值。
    4. 泛型选择语句与 switch 语句类似,只是前者用表达式的类型匹配标签,而后者用表达式的值匹配标签。
    #include 
    #define MYTYPE(X) _Generic((X),\
    int: "int",\
    float : "float",\
    double: "double",\
    default: "other"\
    )
    
    int main(void)
    {
        int d = 5;
        printf("%s\n", MYTYPE(d));     //d是int类型,输出int
        printf("%s\n", MYTYPE(2.0*d)); //2.0*d是double类型,输出double
        printf("%s\n", MYTYPE(3L));    //3L是long类型,输出other
        printf("%s\n", MYTYPE(&d));    //&d是int * 类型输出other
        return 0;
    }
    
  14. 内联函数(C99)

    通常,函数调用都有一定的开销,因为函数的调用过程包括建立调用、传递参数、跳转到函数代码并返回。使用宏使代码内联,可以避免这样的开销。

    C99还提供另一种方法:内联函数(inline function)。把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用。

    标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。

    因此,最简单的方法是使用函数说明符inline和存储类别说明符static。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。如下所示:

    #include 
    inline static void eatline ()   // 内联函数定义/原型
    {
    	while (getchar() != '\n')
    		continue;
    }
    int main()
    {
    	...
    	eatline();              //函数调用
    	...
    }
    

    编译器查看内联函数的定义(也是原型),可能会用函数体中的代码替换 eatline)函数调用。也就是说,效果相当于在西数调用的位置输入函数体中的代码:

    #include 
    inline static void eatline ()   // 内联函数定义/原型
    {
    	while (getchar() != 'In')
    		continue;
    }
    int main()
    {
    	...
    //函数调用之处相当于插入代码块
    	while (getchar() != "\n')
    		continue;              
    	...
    }
    

    由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。

    内联函数应该比较短小。把较长的函数变成内联并未节约多少时间,因为执行函数体的时间比调用函数的时间长得多。

    编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。鉴于此,一般情况下内联函数都具有内部链接。

    因此,如果程序有多个文件都要使用某个内联函数,那么这些文件中都必须包含该内联函数的定义。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。

    一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联两数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。

    与C++不同的是,C还允许混合使用内联函数定义和外部函数定义(具有外部链接的两数定义)。例如,一个程序中使用下面了个文件:

    //  file1.c
    #include 
    inline static double square(double x) { return x*x; }
    void spam(double);
    void masp(double);
    int main()
    {
        double q = square(1.3);
        
        printf("%.2f\n", q);
        spam(12.6);
        masp(1.6);
        
        return 0;
    }
    
    //  file2.c
    #include 
    double square(double x) { return (int) (x*x); }
    void spam(double v)
    {
        double kv = square(v);
        printf("%.2f\n", kv);
        return;
    }
    
    //  file3.c
    #include 
    inline double square(double x) { return (int) (x * x + 0.5); }
    void masp(double w)
    {
        double kw = square(w);
        printf("%.2f\n", kw);
        return;
    }
    

    如上述代码所示,3 个文件中都定义了square() 函数。

    1. file1.c文件中是 inline static定义
    2. file2.c 文件中是普通的函数定义(因此具有外部链接)
    3. file3.c 文件中是 inline 定义,省路了static
    • 3个文件中的函数都调用了 sguare() 函数,这会发生什么情况?
      1. file1.c文件中的main()使用square()的局部static 定义。由于该定义也是inline 定义,所以编译器有可能优化代码,也许会内联该函数。
      2. file2.c文件中spam函数使用该文件中square()函数的定义,该定义具有外部链接,其他文件也可见。
      3. file3.c 文件中,编译器既可以使用该文件中square()函数的内联定义,也可以使用file2.c文件中的外部链接定义。如果像 file3.c 那样,省路file1.c 文件 inline 定中的 static,那么该 inline 定义被视为可替换的外部定义。
  15. 关于库以及一些函数的使用跳过

你可能感兴趣的:(C语言,c语言,学习,笔记)