【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 = 100z = SQUARE1(2);//z = 100 / (2*2) = 25z = 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 <stdio.h>              //查找系统目录
      #include "mystuff.h"            //查找当前工作目录
      #include "/usr/biff/mystuff.h"  //查找/usr/biff/目录
      
    3. 头文件实例

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

        // 常量
        #include <string.h>
        #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 <stdio.h>
        #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';          //在此处放一个空字符elsewhile (getchar() != '\n')continue;          //处理输入行中的剩余字符}return ret_val;
        }
      3. useheader.c————使用names_st结构

        #include <stdio.h>
        #include "names_st.h"
        //记得链接names_st.cint 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 <stdio.h>
    #define JUST_CHECKING
    #define LIMIT 4int main(void)
    {int i;int total = 0;for (i = 1; i <= LIMIT; i++){total += 2*i*i + 1;
    #ifdef JUST_CHECKINGprintf("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 <stdio.h>
    #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. 预定义宏

    在这里插入图片描述

  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 <stdio.h>
    #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类型,输出intprintf("%s\n", MYTYPE(2.0*d)); //2.0*d是double类型,输出doubleprintf("%s\n", MYTYPE(3L));    //3L是long类型,输出otherprintf("%s\n", MYTYPE(&d));    //&d是int * 类型输出otherreturn 0;
    }
    
  14. 内联函数(C99)

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

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

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

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

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

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

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

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

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

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

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

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

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

    //  file1.c
    #include <stdio.h>
    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 <stdio.h>
    double square(double x) { return (int) (x*x); }
    void spam(double v)
    {double kv = square(v);printf("%.2f\n", kv);return;
    }
    
    //  file3.c
    #include <stdio.h>
    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. 关于库以及一些函数的使用跳过

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/314055.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中科亿海微UART协议

引言 在现代数字系统设计中&#xff0c;通信是一个至关重要的方面。而UART&#xff08;通用异步接收器/发送器&#xff09;协议作为一种常见的串行通信协议&#xff0c;被广泛应用于各种数字系统中。FPGA&#xff08;现场可编程门阵列&#xff09;作为一种灵活可编程的硬件平台…

鸿蒙开发第1篇__网络请求

先访问 OpenAtom OpenHarmony &#xff0c; 浏览 Http数据请求&#xff0c;

【MATLAB】【数字信号处理】基本信号的仿真与实现

目的 1、用MATLAB软件实现冲激序列 2、用MATLAB软件实现阶跃序列 3、用MATLAB软件实现指数序列 4、用MATLAB软件实现正弦序列 内容与测试结果 1、用MATLAB软件实现冲激序列 程序如下&#xff1a; % 1 冲激序列 clc; clear all; n0 -10; nf 50; ns 1; A 1;%起点为-1&…

国家开放大学形成性考核 统一考试 资料参考

试卷代号&#xff1a;11141 工程经济与管理 参考试题 一、单项选择题&#xff08;每题2分&#xff0c;共20分&#xff09; 1.资金的时间价值&#xff08; &#xff09;。 A.现在拥有的资金在将来投资时所能获得的利益 B.现在拥有的资金在将来消费时所付出的福利损失 C.…

实验六——cache模拟器实验

前言 本次实验的主要目的是熟悉cache的原理。加深对cache的映像规则、替换方法、cache命中与缺失的理解。通过实验对比分析映像规则对cache性能的影响。 实验内容一&#xff1a;熟悉模拟程序 阅读给出的cache模拟程序&#xff08;cachesimulator.cpp&#xff09;&#xff0c;…

Feign远程调用丢失请求头问题处理--异步任务执行远程请求线程丢失请求属性问题处理

在关于Feign远程调用丢失请求头问题处理中解决了远程调用发送请求丢失老请求中请求头的问题。A方法接收浏览器中的请求&#xff0c;B方法是A方法中嵌套方法用来发送Feign远程调用。如果B方法是在异步任务CompletableFuture.runAsync(()->{},Executor)中执行并启用线程池分配…

从0到1入门C++编程——02 通讯录管理系统

文章目录 一、创建结构体及菜单显示二、添加联系人三、显示联系人四、删除联系人五、查找联系人六、修改联系人七、清空联系人八、退出通讯录 本文通过C实现一个通讯录管理系统&#xff0c;系统要实现的功能如下。 1、添加联系人&#xff1a;向通讯录中添加新人&#xff0c;信息…

Zookeeper-Zookeeper选举源码

看源码方法&#xff1a; 1、先使用&#xff1a;先看官方文档快速掌握框架的基本使用 2、抓主线&#xff1a;找一个demo入手&#xff0c;顺藤摸瓜快速静态看一遍框架的主线源码&#xff0c;画出源码主流程图&#xff0c;切勿一开始就陷入源码的细枝末节&#xff0c;否则会把自…

【分布式微服务专题】SpringSecurity快速入门

目录 前言阅读对象阅读导航前置知识笔记正文一、Spring Security介绍1.1 什么是Spring Security1.2 它是干什么的1.3 Spring Security和Shiro比较 二、快速开始2.1 用户认证2.1.1 设置用户名2.1.1.1 基于application.yml配置文件2.1.1.2 基于Java Config配置方式 2.1.2 设置加密…

71内网安全-域横向网络传输应用层隧道技术

必备知识点&#xff1b; 代理和隧道技术的区别&#xff1f; 代理主要解决的是网络访问问题&#xff0c;隧道是对过滤的绕过&#xff0c; 隧道技术是为了解决什么 解决被防火墙一些设备&#xff0c;ids&#xff08;入侵检测系统&#xff09;进行拦截的东西进行突破&#xff0…

图像特征提取之Hog特征提取

HOG全称&#xff08;histogram of oriented gradients&#xff09;,方向梯度直方图&#xff0c;可以用来提取表示图像的特征&#xff0c;本质就是一行高维特征。 HOG特征提取步骤 图像预处理&#xff08;gamma校正和灰度化&#xff09;【option】 计算每一个像素点的梯度值&am…