当前位置: 首页 > news >正文

第16章 C预处理器和C库

目录

  • 16.1 翻译程序的第一步
  • 16.2 明显常量:#define
        • 16.2.1 语言符号
        • 16.2.2 重定义常量
  • 16.3 在#define中使用参数
  • 16.4 宏,还是函数
  • 16.5 文件包含:#include
  • 16.6 其他指令
        • 16.6.1 #undef指令
        • 16.6.2 已定义:C预处理器的观点
        • 16.6.3 条件编译
        • 16.6.4 预定义宏
        • 16.6.5 #line和#error
        • 16.6.6 #pragma
        • 16.6.7 泛型选择(C11)
  • 16.7 内联函数
  • 16.8 _Noreturn函数(C11)
  • 16.9 C库
  • 16.10 数学库
  • 16.11 通用工具库
  • 16.12 诊断库
  • 16.13 string.h库中的memcpy()和memmove()
  • 16.14 可变参数:stdarg.h
  • 16.17 编程练习
        • 习题5
        • 习题6
        • 习题7

    编译程序之前,先由预处理器检查程序。 根据程序中使用的预处理器指令,预处理器用符号缩略语所代表的内容替换程序中的缩略语
    预处理器可以根据您的请求包含其他文件,还可以选择让编译器处理哪些代码。 预处理器不能理解C,它一般是接受一些文本并将其转换为其他文本

16.1 翻译程序的第一步

对程序作预处理之前,编译器会对它进行几次翻译处理

  • (1)编译器首先把源代码中出现的字符映射到源字符集。
    • C标准定义了一个源字符集(Source Character Set),它是编译器内部处理源代码时使用的标准字符集。这个集合包括:基本字符、数字、空白字符、图形字符。
    • 为什么需要这个映射步骤?
      • 1. 处理不同的源代码编码。源代码文件可能以各种编码格式保存,编译器需要将这些不同的外部编码统一转换为内部的标准源字符集表示,否则无法正确解析代码。
      • 2. 确保可移植性。通过映射到标准源字符集,无论源代码原本是什么编码,编译器内部处理的是统一的字符表示。
      • 3. 处理三字符组(Trigraphs)和转义序列。三元字符,C99标准中已弃用,C17中移除了。通用字符名还在使用。
      • 4. 行尾符标准化。不同的操作系统使用不同的行结束符:Unix/Linux:LF (\n)、Windows:CR+LF (\r\n)、经典Mac OS:CR (\r)映射阶段会将所有行结束符统一转换为标准的换行符,确保#line指令和错误报告的一致性这段让我联想到学习第13章文件的输入/输出时,介绍过有些操作系统的文件系统支持文本模式和二进制模式,文本模式下的换行符会有所不同,这里的源代码文件也是个文件,不同的系统中的源代码文件对应的换行符表示方法的差异需要在这一步标准化
    • 在C语言预处理之前对中文字符的处理方式,取决于中文字符出现的位置,处理结果完全不同
      • 情况一:中文字符在注释或字符串字面量中。这是最常见的情况,处理相对简单。处理过程编码识别:编译器识别源文件的编码(如UTF-8、GBK等)字节序列映射:将中文字符的多字节序列原样保留为相应的字节序列 不做字符语义解释:编译器不关心这些字节代表什么中文含义,只当作一串字节数据。为什么可以这样? 1.C编译器只需要处理基本的词法元素(标识符、关键字、操作符等)2.字符串和注释的内容在语法分析阶段不需要被"理解"3.这些字节数据最终会原样存储在生成的目标文件中
      • 情况二:中文字符在标识符中(变量名、函数名等) GCC/Clang编译器接受UTF-8编码的中文字符;MSVC编译器传统版本可能不支持中文标识符,较新版本可能有限支持,但行为不一致。C99标准引入了通用字符名机制,为Unicode字符提供了可移植的表示方式。虽然某些编译器支持,但严重影响可移植性和可维护性,还是建议避免在标识符中使用中文
  • (2)编译器查找反斜线后紧跟换行符的实例并删除这些实例。注意,在这种场合下,“换行符”代表按下回车键在源代码文件中新起一行所产生的字符,而不是符号\n代表的字符
    • 这一步目的是将源代码文件中的两个物理行转换成一个逻辑行。因为预处理表达式的长度为一逻辑行,为预处理做准备工作。
  • (3)编译器将文本划分成预处理的语言符号序列和空白字符及注释序列(术语语言符号代表由空格分隔的组)。注意,编译器用一个空格代替每一个注释



16.2 明显常量:#define

  • 预处理指令从#开始,到其后第一个换行符为止。指令的长度限于一行,但是正如前文提到的,在预处理开始前,系统会删除反斜线和换行符的组合。因此可以把指令扩展到几个物理行,由这些物理行组成单个逻辑行
  • 预处理指令可以出现在源文件的任何地方。指令定义的作用域从定义出现的位置开始直到文件的结尾。本书大量使用这个指令来定义 符号常量或明显常量
  • 每个#define行(即逻辑行)由三部分组成。
    • 第一部分为#define自身。
    • 第二部分为所选择的缩略语,这些缩略语称为宏。本例中这些宏用来代表值,它们被称为类对象宏(C还有类函数宏,后面讨论它们)。宏的名字不允许有空格,而且必须遵循C变量命名规则:只能使用字母、数字和下划线(_),第一个字符不能为数字。
    • 第三部分称为替换列表或主体。预处理器在程序中发现了宏的实例后,总会用实体代替该宏。从宏变成最终的替换文本的过程称为宏展开。注意,可以使用标准C注释方法在#define行中进行注释。正如前面提到的,在预处理器处理之前,每个注释都会被一个空格所代替
  • C编译器在编译时对所有的常量表达式(只包含常量的表达式)求值,所以实际计算过程发生在编译阶段,而不是预处理器工作阶段。预处理器不进行计算,它只是按照指令进行文字替换工作
  • 一般而言,预处理器发现程序中的宏后,会用它的等价替换文本代替宏。如果该字符串中还包括宏,则继续替换这些宏。例外情况就是双引号中的宏
  • 什么时候应该使用符号常量呢?对大多数数字常量应该使用符号常量。如果是用于计算式的常量,那么使用符号名会更加清楚。如果数字代表数组大小,那么使用符号名后更容易改变数组大小和循环界限。如果数字是系统代码,那么使用符号表示会使程序更加易于移植。记忆值的能力、易更改性、可移植性,这些功能使得符号常量很有使用价值
  • const关键字得到C的支持,这确实提供了一种创建常量更灵活的方法。使用const您可以创建全局常量和局部常量、数字常量、数组常量和结构常量。另一个方面,宏常量可以用来指定标准数组的大小并作为const值得初始化值
16.2.1 语言符号
  • 从技术方面看,系统把宏的主体当作 语言符号类型字符串,而不是 字符型字符串
  • C预处理器中的 语言符号是宏定义主体中的单独的“词(word)”。用空白字符把这些词分开
    • 例如:#define FOUR 2*2 这个定义有 一个语言符号:即序列2*2
    • 但是#define SIX 2 * 3 这个定义有 三个语言符号:2、*和3
  • 在处理主体中的多个空格时,字符型字符串和语言符号类型字符串采用不同方法。考虑这个定义#define EIGHT 4   *   8
    • 把主体解释为字符型字符串时,预处理器用4   *   8替换EIGHT。也就是说,额外的空格也当作替换文本的一部分。
    • 把主体解释为语言符号类型时,预处理器用由单个空格分隔的三个语言符号,即4 * 8来替换EIGHT。
    • 用字符型字符串的观点看,空格也是主体的一部分;而用语言符号字符串的观点看,空格只是分隔主体中语言符号的符号。在实际应用中,有些C编译器把宏主体当作字符串而非语言符号。在比这个实例更复杂的情况下,字符与语言符号之间的差异才有实际意义
  • 编译器能理解C的规则,不需要用空格来分隔语言符号。例如,C编译器把2*2当作三个语言符号。原因是C编译器认为每个2都是一个常量,而*是一个运算符。
16.2.2 重定义常量
  • 不同编译器采用不同的重定义策略。在新定义不同于旧定义时,有的编译器认为这是错误,而有些编译器可能提出警告,但允许重定义。ANSI标准采用第一种方式:只允许新定义与旧定义完全相同
    • 相同的定义意味着主体具有相同顺序的语言符号。因此下面两个定义相同,两者都有三个相同的语言符号,而且额外的空格不是主体的一部分。
      • #define SIX 2 * 3
      • #define SIX 2   *   3
    • 下面的定义则被认为是不同的。只有一个语言符号,因此与前面两个定义不同。
      • #define SIX 2*3

16.3 在#define中使用参数

  • 通过使用参数,可以创建外形和作用都与函数相似的类函数宏。类函数宏的定义中,用圆括号括起一个或多个参数,随后这些参数出现在替换部分
  • 函数调用和宏调用之间的重要差异。程序运行时,函数调用把参数的值传递给函数。而编译前,宏调用把参数的语言符号传递给程序。这是不同时间发生的不同过程。
    • 因为宏展开只是语言符号的替换,所以创建类函数宏时,对宏的主体要灵活的运用括号修饰从而达到自己想要的结果,否则会产生意想不到的输出
    • 一般来说,宏中尽量不要使用增量或减量运算符。稍不注意可能就产生一个意想不到的输出。
  • #define PSQR(X) printf(“The square of X is %d.\n”,((X)*(X))),注意引号中的字符串中的X被看作普通文本,而不是看作一个可被替换的语言符号
    • 如果你希望在字符串中包含宏参数,可以使用#符号。#符号用作一个预处理运算符,它可以把语言符号转化为字符串。该过程称为字符串化。类函数宏这样定义:#define PSQR(X) printf(“The square of” #X “is %d.\n”,((X)*(X)))
  • 和#运算符一样,##运算符可以用于类函数宏和类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号
    • #define XNAME(n) x ## n
  • 可变宏:…和__VA_ARGS__…(三个点):在宏参数列表中,它表示“此处可以接受零个或多个额外的参数”。VA_ARGS: 在宏的展开部分,它会被实际传入的“额外参数”列表所替换。
    • 宏定义中参数列表的最后一个参数为省略号(三个句号)。这样预定义宏_ VA_ARGS _就可以被用在替换部分中,以表明省略号代表什么。省略号只能代替最后的宏参数
    • 在__VA_ARGS__前面加上 ## 是一个特殊技巧,称为“逗号粘贴”或“可变参数吞掉逗号”。##VA_ARGS 的重要性:它在可变参数为空时,吞掉前面的逗号,避免语法错误。 让你的宏在有可变参数和没有可变参数的两种情况下都能正确工作,极大地提高了宏的健壮性。



16.4 宏,还是函数

  • 宏与函数之间的选择实际上是时间与空间的权衡。宏产生内联代码;也就是说,在程序中产生语句。如果使用宏20次,则会把20行代码插入程序中。如果使用函数20次,那么函数中只有一份函数语句的拷贝,因此节省了空间。另一方面,程序的控制必须转移到函数中并随后返回调用程序,因此这比内联代码花费的时间多
  • 宏的一个特点是它不检查其中的变量类型(因为宏处理的是字符型字符串,而不是实际值)。这一特点,有时是优点,有时又是缺点
    • 优点:
      • 实现泛型编程,这是最核心的优点。在没有模板的C语言中,宏是实现“通用”函数的主要手段。你可以写一个宏,让它处理各种类型的数据。一个类函数宏可以处理多种数据类型的数据。
      • 与任何类型协作。宏可以处理基础类型(int, double)、结构体、指针、数组等任何类型,只要这些类型支持宏体内使用的操作符(如 >, +, = 等)。
    • 缺点:
      • 完全缺乏类型安全,这是最严重的缺点。编译器无法检查传入的参数类型是否合理,所有类型相关的错误都要到运行时才会暴露,甚至会导致 silent error(静默错误)和未定义行为
      • 难以调试。调试器看到的是宏展开后的代码。如果宏有多行或者很复杂,错误信息会指向宏被展开后的行号,而不是宏定义本身,这使得追踪问题根源非常困难。
      • 多次求值问题。由于是替换,参数在宏体中每次出现都会被求值一次。如果参数是带有副作用的表达式(如函数调用、自增运算符),会导致意想不到的结果。
  • 使用宏需要注意的几点
    • 宏的名字不能有空格,但是替代字符串中可以使用空格。
    • 用圆括号括住每个参数,并括住宏的整体定义。确保一些复杂的表达式作为参数传送给宏时,能正确实现功能。
    • 用大写字母表示宏函数名
    • 在程序中只使用一次的宏对程序运行时间可能不会产生明显的改善,在嵌套循环中使用宏更有助于加速程序的运行。



16.5 文件包含:#include

  • 预处理器发现#include指令后,就会寻找后跟的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的#include指令,就像把被包含文件中的全部内容键入到源文件中的这个特定位置一样。
    • 在UNIX系统中,尖括号告诉预处理器在一个或多个标准系统目录中寻找文件。双引号告诉预处理器先在当前目录(或文件中指定的其他目录)中寻找文件,然后在标准位置寻找文件
  • 习惯上使用后缀.h表示头文件。包含大型头文件并不一定显著增加程序的大小。很多情况下,头文件中的内容是编译器产生最终代码所需的信息,而不是加到最终代码里的语句
    • 头文件内容最常见的形式包括:明显常量、宏函数、函数声明、结构模板定义、类型定义
    • 可以使用头文件声明多个文件共享的外部变量。
    • 需要包含头文件的另一种情况是:使用具有文件作用域、内部链接和const限定词的变量或数组。

源代码文件所在目录、工程文件目录、工作目录三者的区别

  • 源代码文件所在目录
    • 是什么:存放你编写的实际代码文件的文件夹。这些是项目的“原材料”,例如 .c, .cpp, .java, .py, .h, .hpp 等文件
    • 作用:这里是代码逻辑本身存在的地方。
    • 特点:这个目录可能很大,包含很多源文件。它通常是工程文件目录的一个子文件夹(例如 src/ 或 Source/),但也可以放在其他地方并在工程设置中指定。
  • 工程文件目录
    • 是什么:存放项目管理文件的目录。这些文件本身不是源代码,但它们告诉开发工具(如 Visual Studio, Qt Creator, CLion, Android Studio)如何组织、编译和构建你的源代码。例如 .sln, .vcxproj (VS), .pro (Qt), CMakeLists.txt (CMake), .idea/ (JetBrains IDE) 等。
    • 作用:它是一个“蓝图”或“指挥中心”,定义了:包含了哪些源代码文件。依赖哪些外部库。编译器和链接器的设置。如何生成最终的可执行文件或库
    • 特点:打开这个目录下的工程文件(如 .sln),就能加载整个项目。它通常是整个项目的根目录或顶级目录。
  • 工作目录
    • 是什么:当你的程序运行时,它认为的“当前文件夹”。程序中使用相对路径(如 "./config.ini" 或 "../data/image.jpg")读写文件时,都是相对于这个目录来查找的
    • 作用:决定程序运行时从哪里读取配置文件、加载资源、写入日志文件等。
    • 特点:它可以在IDE中独立设置,通常默认是工程文件目录,但经常被改为输出目录(如 bin/Debug/)。它与“源代码目录”和“工程目录”完全无关,除非你手动将它们设置为相同。非常重要! 如果设置不对,会导致程序运行时找不到文件而崩溃


16.6 其他指令

    程序员可能需要为不同的工作环境准备不同的C程序或C库包。代码类型的选择会根据环境的不同而各异。预处理器提供一些指令来帮助程序员编写这样的代码:改变一些#define宏的值后,这些代码就可以被从一个系统移植到另一个系统。

16.6.1 #undef指令
  • #undef指令取消定义一个给定的#define。即使前面没有给定的宏定义,#undef也是合法的。
    • 如果想使用一个特定的名字,但又不能确定前面是否已使用了该名字,为了安全起见就可以先#undef取消定义,再用#define定义。
16.6.2 已定义:C预处理器的观点
  • 预处理器在预处理指令中遇到标识符时,要么把标识符当作已定义的,要么当作未定义的。这里的已定义表示由预处理器定义
  • 如果标识符是该文件前面的#define指令创建的宏名,并且没有用#undef指令关闭该标识符,则标识符是已定义的。如果标识符不是宏,而是一个具有文件作用域的C变量,那么预处理器把标识符当作未定义
16.6.3 条件编译

    条件编译:使用指令告诉编译器,根据编译时的条件接受或忽略信息(代码)块

  • #ifdef、#else和#endif指令
    • #ifdef指令说明:如果预处理器已经定义了后面的标识符,那么执行所有指令并编译C代码,直到下一个#else或#endif出现为止。如果有#else指令,那么在未定义标识符时会执行#else和#endif之间的所有代码。
  • #ifndef指令
    • 类似于#ifdef指令,#ifndef指令可以与#else、#endif指令一起使用。#ifndef判断后面的标识符是否为未定义的,#ifndef的反义词为#ifdef。#ifndef通常用来定义此前未定义的常量。
    • 一般地,当某文件包含几个头文件,而且每个头文件都可能定义了相同的宏时,使用#ifndef可以防止对该宏重复定义。此时第一个头文件中的定义变成有效定义,而其他头文件中的定义则被忽略。
    • #ifndef指令通常用于防止多次包含同一文件
      • 为什么会多次包含同一文件呢?最常见的原因是:许多包含文件自身包含了其他文件,因此可能显示地包含其他文件已经包含的文件。为什么这会成为问题呢?因为头文件中的有些语句在一个文件中只能出现一次(如结构类型声明)。标准头文件使用#ifndef技术来避免多次包含
      • 如何确保您使用的标识符在其他任何地方都没有定义过?通常编译器提供商解决方法如下:用文件名做标识符,并在文件名中使用大写字母、用下划线代替文件名中的句点字符、用下划线(可能使用两条下划线)作前缀和后缀
      • 因为C标准保留使用下划线作前缀,所以您应避免这种用法。
  • #if和#elif指令
    • #if后跟常量整数表达式。如果表达式为非零值,则表达式为真。在该表达式中可以使用C的关系运算符和逻辑运算符。#if和#elif后面的表达式只能使用已定义的宏常量
    • 提供了另一种方法来判断一个宏是否已经定义。#if defined(VAX)。defined是一个预处理器运算符。如果defined的参数已用#define定义过,那么defined返回1;否则返回0。这种新方法的优点在于它可以和#elif一起使用
  • 条件编译的一个用途是可以使程序更易于移植。通过在文件开头部分改变几个关键定义,就可以为不同系统设置不同值并包含不同文件
16.6.4 预定义宏
意义
__DATE__进行预处理的日期("Mmm dd yyyy"形式的字符串文字)
__FILE__代表当前源代码文件名的字符串文字
__LINE__代表当前源代码文件中的行号的整数常量
__STDC__设置为1时,表示该实现遵循C标准
__STDC_HOSTED__为本机环境设置为1,否则设为0
__STDC_VERSION__当前的C标准。为C99时设置为199901L,为C17时设置为201710L。
__TIME__源文件编译的时间,格式为“hh:mm:ss”

    C99标准提供了一个名为__func__的预定义标识符。__func__展开为一个代表函数名的字符串。该标识符具有函数作用域,而宏本质上具有文件作用域因而__func__是C语言的预定义标识符,而非预定义宏

16.6.5 #line和#error
  • #line指令用于重置由__LINE___和__FILE__宏报告的行号和文件名。
  • #error指令使预处理器发出一条错误消息,该消息包含指令中的文本。可能的话,编译过程应该中断。
16.6.6 #pragma
  • 现代编译器中,可用命令行参数或IDE菜单修改编译器的某些设置。也可用#pragma将编译器指令置于源代码中
  • C99还提供了_Pragma预处理器运算符。_Pragma可将字符串转换成常规的编译提示。因为该运算符没有使用#符号,所以可以将它作为宏展开的一部分
16.6.7 泛型选择(C11)
  • 在程序设计中,泛型编程指哪些没有特定类型,但一旦指定一种类型,就可以转换成指定类型的代码。C11新增了一种表达式叫 泛型选择表达式,可以根据表达式的类型(即表达式的类型是in、double还是其他类型)选择一个值。泛型选择表达式不是预处理指令,但是在一些泛型编程中它常用作#define宏定义的一部分
  • 泛型选择表达式示例:_Generic(x,int:0,float:1,double:2,default:3)
    • _Generic是C11的关键字,后面的圆括号中包含多个用逗号分隔的项。
    • 第1个项是一个表达式(控制表达式),后面每项都由一个类型、一个冒号和一个值(结果表达式)组成。第1个项的类型匹配哪个标签,整个表达式的值就是该标签后面的值。如果没有与类型匹配的标签,那么表达式的值就是default:标签后面的值
    • 泛型选择语句与switch语句相似,只是前者用表达式类型匹配标签,后者用表达式的值匹配标签。
  • 在C语言的泛型选择表达式 _Generic 中,结果表达式有一些重要的限制和要求
    • 所有结果表达式的类型必须与整个泛型表达式的使用上下文兼容。

    int result2 = _Generic(10,
    int: “字符串”, // char* 不能安全赋给int
    double: 3.14 // double 到 int 会丢失精度
    );

    • 结果表达式必须是语法上有效的C表达式

    // ✅ 合法的结果表达式
    int a = _Generic(x,
    int: x + 5, // 算术表达式
    double: printf(“”), // 函数调用表达式
    default: (int){0} // 复合字面量
    );
    // ❌ 不合法的结果表达式
    /*
    int b = _Generic(x,
    int: int y = 5, // 错误:声明语句不是表达式
    double: if(x) { } // 错误:控制语句不是表达式
    );
    */

    • 未选择的分支中的表达式不会被执行。
    • 所有结果表达式在编译时进行类型检查
  • 在C语言的泛型选择表达式 _Generic 中,结果表达式具体限制细节
    • 不能包含不完整类型
    • 不能包含VLA(变长数组)
    • 可以包含复合字面量
    • 可以包含函数调用
  • _Generic 允许使用常量表达式作为类型关联的标签,这是它一个强大但较少被了解的特性这是一个编译器扩展特性,不是所有编译器都实现了。具体用法可查询deepseek
  • 宏必须定义为一个逻辑行,但是可以用\把一条逻辑行分隔成多个物理行
  • 对一个泛型选择表达式求值,程序不会先对第一个项求值,它只确定类型。只有匹配标签的类型后才会对表达式求值。



16.7 内联函数

  • C99这样描述:“把函数变为内联函数将建议编译器尽可能快速地调用该函数。上述建议的效果由实现来定义”。因此,使函数变为内联函数可能会简化函数的调用机制,但也可能不起作用。编译器会根据自身的优化策略来决定是否真正进行内联。
  • 创建内联函数的方法是在函数声明中使用函数说明符inline通常,首次使用内联函数前在文件中对该函数进行定义。因此,该定义也作为函数原型
  • 因为内联函数没有预留给它的单独代码块,所以无法获得内联函数的地址(实际上,可以获得地址,但这样会使编译器产生非内联函数)。
  • 内联函数应该比较短小。对于很长的函数,调用函数的时间少于执行函数主体的时间;此时,使用内联函数不会节省多少时间
  • 编译器在优化内联函数时,必须知道函数定义的内容。这意味着内联函数的定义和对该函数的调用必须在同一文件中。正因为这样,内联函数通常具有内部链接。因此在多文件程序中,每个调用内联函数的文件都要对该函数进行定义。达到这个目标最简单的方法为:在头文件中定义内联函数,并在使用该函数的文件中包含该头文件。一般不在头文件中放置可执行代码,但内联函数是个例外
    • 如何创建内联函数
      • 在头文件中定义(最常见的方式)。如果一个内联函数需要在多个源文件(.c文件)中使用,你应该将它定义在头文件(.h文件) 中,并且使用 static inline 关键字。为什么用 static inlineinline:建议编译器进行内联展开。static:确保该函数在每个包含它的.c文件中都有一个私有的、局部的定义。这避免了多个编译单元(.c文件)同时包含同一个头文件时可能产生的重复定义错误。
      • 在单个源文件中定义。如果一个内联函数只在一个.c文件中使用,你可以直接在.c文件顶部使用 inline 关键字定义它。
      • 与 extern 的组合使用 (C99标准方式)。对于需要在多个文件中使用的非静态内联函数,C99标准规定了一种更复杂但更“正确”的方法:在头文件中,用 extern inline 声明函数,告诉编译器该函数在其他地方有定义。在某一个源文件中,用 inline 定义函数,为函数生成一个非内联的副本。这种方式不常用,因为 static inline 在头文件中的方式更加简单直观,且被绝大多数编译器很好地支持。
    • 内联函数和类函数宏的区别
特性内联函数类函数宏
本质真正的函数,由编译器处理文本替换,由预处理器处理
类型检查有,严格检查参数和返回值类型无,只是文本替换,极易产生错误
参数求值传值调用,参数表达式只计算一次简单替换,参数表达式可能被多次计算
副作用安全,无意外副作用极其危险,极易因多次求值产生难以发现的Bug
调试容易,可以设置断点,有清晰的调用栈困难,在调试器中看到的是替换后的代码
语法符合函数语法,需要指定参数和返回类型使用 #define,末尾不能有分号
作用域遵守作用域规则,有链接属性无作用域,从定义点开始到 #undef 都有效
适用场景小型、频繁调用、需要安全性的函数代码片段替换、轻量级"泛型"、打印调试信息



16.8 _Noreturn函数(C11)

  • C99新增inline关键字时,它是唯一的函数说明符(extern和static是存储类别说明符,可应用于数据对象和函数)。C11新增了第2个函数说明符_Noreturn,表明调用完成后函数不返回主调函数。exit()函数是_Noreturn函数的一个实例。一旦调用exit(),它不会再返回主调函数。



16.9 C库

  • 通常可以在多个不同位置找到库函数。
  • 不同系统使用不同的方法搜索这些函数。
    • 一、自动访问 在许多系统上,您只需编译程序,一些常见的库函数自动可用。应该声明所使用的函数的类型,通常包含适当的头文件即可做到这一点
    • 二、文件包含 如果函数定义为宏,可以使用#include指令来包含拥有该定义的文件。
    • 三、库包含 在程序编译或链接的某些阶段,您可能需要指定库选项。注意,要把这个过程和包含头文件区分开来。头文件提供函数声明或原型,而库选项告诉系统到哪儿寻找函数代码
  • ANSI C使用指向void类型的指针作为通用指针。需要使用指向不同类型的指针时,可采用void指针。



16.10 数学库

  • 类型变体
    • 基本的浮点型数学函数接受double类型的参数,并返回double类型的值。当然,也可以把float或long double类型的参数传递给这些函数,因为这些类型的参数会被转换成double类型。这样做很方便但不是最好的处理方式。如果不需要双精度,那么用float类型的单精度值来计算会更快些。而且把long double类型的值传递给double类型的形参会损失精度,形参获得的值可能不是原来的值。为了解决这些潜在问题,C标准专门为float类型和long double类型提供了标准函数,即在原函数名前加上f或l前缀。因此,sqrtf()是float版本,sqrtl()是long double版本
    • 利用C11新增的泛型选择表达式定义一个泛型宏,根据参数类型选择最合适的数学函数版本
  • tgmath.h库(C99)
    • C99标准提供的tgmath.h头文件中定义了泛型类型宏,其效果与使用泛型选择表达式定义的宏类似。
    • 如果包含了tgmath.h,要调用sqrt()函数而不是sqrt()宏,可以用圆括号把被调用的函数名括起来。因为,C语言标准规定:如果一个宏名被圆括号直接包裹,则预处理器不会将其展开为宏

C 语言中一个非常重要且基础的概念,称为默认参数提升 (Default Argument Promotions)。这主要发生在两种情况中:
(1)在调用可变参数函数(如 printf)时,传递给 ... 的参数
(2)在调用没有函数原型的函数时(现代编程中很少见)
为什么会这样设计?
(1)历史原因和硬件兼容性 早期的 C 语言运行在处理器位数较低(如 16 位)的机器上。int 类型通常被设计为机器的“自然字长”,即处理器单次操作能高效处理的数据大小。int 是“最有效率”的类型:对 CPU 来说,处理一个 int 类型的数据通常比处理一个 char 或 short 更快、更直接;通过将所有小于 int 的整型都提升到 int,C 语言在函数调用时只需要处理少数几种数据类型(主要是 int, double, 指针),这使得函数调用约定(如何传递参数)变得非常简单和统一
(2)简化可变参数函数的实现 这是最主要的原因。va_arg 宏需要知道每个参数的确切大小和位置才能正确地找到下一个参数。va_arg(ap, type) 宏必须为每一种可能的基础类型生成不同的代码来计算下一个参数的位置,这非常复杂且容易出错。有了提升规则后,事情就变得简单了:整型家族:调用者只传递 int(来自 char, short)或 long(来自 int,如果 int 和 long 大小相同则可能不变)。浮点家族:调用者只传递 double(来自 float)。这使得 va_arg 只需要处理有限的几种数据大小,极大地简化了可变参数机制的实现。
(3)避免精度损失和保持一致的行为 浮点数:将 float 提升为 double 可以避免在参数传递过程中可能发生的精度损失。double 提供了比 float 更高的精度和范围,确保函数接收到的是原始值最精确的表示。整型:将小整型提升为 int 可以保证符号扩展的正确性(对于有符号类型),并提供一个标准化的、足够大的容器来存放值,避免溢出等意外情况


16.11 通用工具库

  • exit()和atexit()函数
    • atexit()函数使用函数指针作为参数进行注册,调用exit()函数时执行注册的函数
    • 由atexit()注册的函数的类型应该为不接受任何参数的void函数。通常它们执行内部处理任务,如更新程序监视文件或重置环境变量。
    • exit()函数执行了atexit()指定的函数后,将做一些自身清理工作。它会刷新所有输出流、关闭所有打开的流,并关闭通过调用标准I/O函数tmpfile()创建的临时文件
    • 注意,main()终止时会隐式地调用exit()函数。在非递归的main()函数中使用exit()函数等价于使用关键字return。但是,在main()以外的函数中使用exit()也会终止程序。
  • qsort()函数原型:qsort(void*base,size_t nmemb,size_t size,int(*compar)(const void*,const void*))
    • 第一个参数为指向要排序的数组头部的指针。ANSI C允许将任何数据类型的指针转换为void类型指针,因而qsort()的第一个实际参数可以指向任何数据类型
    • 第二个参数为需要排序的项目数量。函数原型将该值转换为size_t类型。
    • 第三个参数为数据对象的大小。因为qsort()将第一个参数转换为void指针,所以会失去每个数组元素的大小信息
    • 最后一个参数是一个指向函数的指针,被指向的函数用于确定排序顺序。这个比较函数接受两个参数,分别指向进行比较的两个项目指针。如果第一个项目的值大于第二个,那么比较函数返回正数;如果两个项目值相等,那么返回0;如果第一个项目值小于第二个,那么返回负数。

    C和C++对待void类型的指针是不同的。在两种语言中,你都可以把一个任意类型的指针赋给类型void*。但是把一个void*指针赋给一个指针或另一个类型的时候,C++需要一次强制类型转换。而C并不需要
    在C中,这种强制类型转换是可选的,在C++中则是必须的。因为强制类型转换在两种语言中都有作用,因此使用它比较有意义。如果你把程序转换到C++中,你不必留意要改变这一部分。


16.12 诊断库

  • 宏assert()向标准错误流写一条错误消息并调用abort()函数以终止程序(在头文件stdlib.h中定义了abort()函数的原型)。
  • assert()宏的作用为:标识出程序中某个条件应为真的关键位置,并在条件为假时用assert()语句终止该程序。通常,assert()的参数为关系或逻辑表达式。如果assert()终止程序,那么它首先会显示失败的判断、包含该判断的文件名和行号
  • assert()自动识别文件,并自动识别发生问题的行号。另外,还有一种无需改变代码就能开启或禁用assert()宏的机制。如果您认为已经排除了程序的漏洞,那么可以把宏定义#define NDEBUG放在assert.h包含语句所在位置前,并重新编译该程序
  • C11新增了一个特性:_Static_assert声明,可以在编译时检查assert()表达式。因此,assert()可以导致正在运行的程序中止,而_Static_assert()可以导致程序无法通过编译
    • _Static_assert()接受两个参数。第1个参数是整型常量表达式,第2个参数是一个字符串。如果第1个表达式求值为0(或false),编译器会显示字符串,而且不编译该程序。_Static_assert要求它的第1个参数是整型常量表达式,这保证了能在编译期求值(sizeof表达式被视为整型常量)
    • 根据语法,_Static_assert()被视为声明。它可以出现在函数中,或者出现在函数外部



16.13 string.h库中的memcpy()和memmove()

  • 可以用strcpy()和strncpy()复制字符数组。memcpy()和memmove()函数为复制其他类型的数组提供了类似的便利工具
    • void* memcpy(void* restrict s1,const void* restrict s2,size_t n);
    • void* memmove(void* s1,const void* s2,size_t n);
    • 两者的差别由关键字restrict造成,即memcpy()可以假定两个内存区域之间没有重叠。memmove()函数则不作这个假定,因此,复制过程类似于首先将所有字节复制到一个临时缓冲区,然后再复制到最终目的地。如果两个区域存在重叠时使用memcpy(),其行为是不可预知的。在不应该使用memcpy()时,编译器不会禁止使用memcpy()。因此使用memcpy()时,您必须确保没有重叠区域,这是程序员任务的一部分
    • 这两个函数可以对任何数据类型进行操作,两个指针参数为void类型指针,导致函数无法知道要复制的数据类型,因此这两个函数使用第3个参数来指定要复制的字节数。注意,对数组而言,字节数一般不等于元素的个数



16.14 可变参数:stdarg.h

编写可变参数函数的步骤和规则:
(1)至少一个固定参数:可变参数函数必须至少有一个明确的固定参数。这个参数通常用于指定可变参数的数量或类型(例如 printf 中的格式字符串 format)。
(2)使用省略号 …:在参数列表中使用 … 表示可变参数部分。
(3)标准流程:在函数内部,遵循 va_start -> va_arg (多次) -> va_end 的标准流程。
宏或类型用途
va_list类型。用于声明一个参数指针变量,它将指向当前的可变参数。
va_start(ap, last)宏。初始化 ap 变量,使其指向固定参数 last 之后的第一个可变参数。
va_arg(ap, type)宏。获取 ap 当前指向的参数的值,同时将 ap 移动到下一个参数。type 是参数的类型(如 int, double, char*)。
va_end(ap)宏。清理工作。在所有参数处理完毕后,必须调用此宏来结束可变参数的获取。
va_copy(dest, src)宏 (C99)。将 src 的状态拷贝到 dest。用于需要多次遍历参数列表的场景。
  • va_list代表一种数据对象,该数据对象用于存放参数列表中省略号部分代表的参量
    • 可变函数定义的起始部分应该这样:double sum(int lim,…){ va_list ap;//声明用于存放参数的变量。
  • va_start()把参数列表复制到va_list变量中。
  • va_arg()不提供后退回先前参数的放过,所以保存va_list变量的副本会是有用的。C99专门添加了宏va_cpy(),该宏的两个参数均为va_list类型变量,它将第二个参数复制到第一个参数中



16.17 编程练习

    个人觉得这章的内容优点抽象,花了好长时间才看懂。之前对源码的操作,我们都用“编译”这个词概括了。准确的对源码的操作,包括:预处理、编译、汇编和链接。这章要真正看懂就要理解预处理和编译的界限,不能对源码操作的了解停留在简单“编译”一词概括上。

习题5

    比较简单,复习了一下前面求随机数的知识。

#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>
#define NUM 40void prinSub(int *,int,int);
int getRandSub(int);int main(void)
{// 我用的Editplus写代码,需要调用此函数让控制台能支持中文SetConsoleCP(65001); // 设置标准输入的编码方式为UTF-8SetConsoleOutputCP(65001); //设置控制台输出的编码方式为UTF-8int arr[NUM];prinSub(arr,NUM*sizeof(int),10);return 0;
}void prinSub(int ele[],int aSize,int sNum)	//随机打印数组的下标,不能重复
{int sub[sNum];			//下标数组,用于存放指定次数的不重复的数组下标int i,j,temp;for (i=0;i<sNum ;i++ )			//外层循环获得指定次数的随机下标{temp=getRandSub(aSize/sizeof(int));sub[i]=temp;			//假设刚获得的随机下标不重复,存储到下标数组sub[sNum]中for (j=0;j<i ;j++ )		//内存循环判断获得的随机下标是否已经存在,如果重复,则重新获取{if (temp==sub[j])	{i--;		//重新获取的关键就是这一步,控制外层循环的i自减1,推翻原来的假设,重新获取break;}}}printf("%d个元素的数组,随机选择的下标是:",aSize/sizeof(int));for (i=0;i<sNum ;i++ ){printf("%d ",sub[i]);}printf("\n");
}int getRandSub(int eleNum)		//根据数组元素个数,得到随机的数组下标
{int randNum;srand((unsigned)time(0));	//依据自动更新的时间,设置随机数的种子randNum=rand()%eleNum;		//对随机数取余获得想要的范围内的随机数return randNum;
}
习题6

    qsort()函数的运用不太熟练,至少懂原理了,以后多写写就熟练了。对通用指针void *的使用要熟练

#include <stdio.h>
#include <windows.h>
#include <stdlib.h>struct names
{char first[40];char last[40];
};int mycomp(const void *,const void *);
void priArr(const struct names *,int);
int main(void)
{// 我用的Editplus写代码,需要调用此函数让控制台能支持中文SetConsoleCP(65001); // 设置标准输入的编码方式为UTF-8SetConsoleOutputCP(65001); //设置控制台输出的编码方式为UTF-8struct names arr[6]={{"biao","yu"},{"wei","yu"},{"yu","tao"},{"jie","chen"},{"qun","xu"},{"wenjie","ye"}};printf("排序前的数组是:\n");priArr(arr,6);qsort(arr,6,sizeof(struct names),mycomp);printf("排序后的数组是:\n");priArr(arr,6);return 0;
}int mycomp(const void * p1,const void * p2)		//先比较last若相等比较first
{int re;const struct names * a1=(const struct names *)p1;const struct names * a2=(const struct names *)p2;re=strcmp(a1->last,a2->last);if (re==0){re=strcmp(a1->first,a2->first);}return re;
}void priArr(const struct names * p,int size)
{int i;for (i=0;i<size ;i++ ){printf("%s %s\n",p[i].last,p[i].first);}
}
习题7

    动态分配内存相关的函数malloc()、calloc()和free()运用还是不熟练。

#include <stdio.h>
#include <windows.h>
#include <stdarg.h>
#include <stdlib.h>double * new_d_array(int,...);
void show_array(const double [],int);
int main(void)
{// 我用的Editplus写代码,需要调用此函数让控制台能支持中文SetConsoleCP(65001); // 设置标准输入的编码方式为UTF-8SetConsoleOutputCP(65001); //设置控制台输出的编码方式为UTF-8double * p1;double * p2;p1=new_d_array(5,1.2,2.3,3.4,4.5,5.6);p2=new_d_array(4,100.0,20.00,8.08,-1890.0);show_array(p1,5);show_array(p2,4);free(p1);free(p2);return 0;
}double * new_d_array(int n,...)
{va_list ap;va_start(ap,n);double * p=(double *)malloc(n*sizeof(double));if (p==NULL){printf("malloc内存分配失败!!!\n");exit(1);}for (int i=0;i<n ;i++ ){p[i]=va_arg(ap,double);}va_end(ap);return p;}void show_array(const double ar[],int n)
{int i;for (i=0;i<n ;i++ ){printf("%f  ",ar[i]);}printf("\n");
}
http://www.dtcms.com/a/469494.html

相关文章:

  • Vue-31-通过flask接口提供的数据使用plotly.js绘图(三)
  • vue前端面试题——记录一次面试当中遇到的题(5)
  • 90设计网站最便宜终身中小企业的网站建设
  • 杭州高端网站设计公司如何制作淘宝客网站
  • 微服务拆分以及注册中心
  • 遗留系统微服务改造(五):监控体系建设与指标收集
  • Java微服务面试实战:从电商场景看微服务架构设计与实现
  • (微服务)Dubbo 服务调用
  • Java微服务实战:从零搭建电商用户服务系统
  • Spring Cloud微服务SaaS智慧工地项目管理平台源码
  • MySQL常用API
  • DDD企业级记账软件实战二|从0-1创建用户微服务和记账微服务基于Spring Cloud
  • 昆明建设银行纪念币预约网站网站顶一下代码
  • 深入浅出 C# MVC:从基础实践到避坑指南(附完整代码示例)
  • 【网络】NAT相关知识;NAT的概念、工作机制、防火墙(Netfilter)的作用时间点;
  • JavaEE初级——Thread多线程
  • GJOI 10.9 题解
  • 如何设计一个架构良好的前端请求库?
  • 精灵图(雪碧图)的生成和使用
  • Web 开发 27
  • 网站制作主要公司学校网站开发系统的背景
  • Linux小课堂: 目录操作命令深度解析(LS 与 CD 命令)
  • 面向财经新闻的文本挖掘系统设计与实现(论文)
  • 【Redis-cli操作数据类型】Redis八大数据类型详解:从redis-cli操作到场景落地
  • linux安装海量数据库和操作
  • Redis分片+Sentinel熔断设计TP99控制在15ms内
  • 山海关城乡建设局网站佛山网络科技公司有哪些
  • 我的算法模板1(快速幂、逆元、组合数)
  • 八股-2025.10.11
  • 图片上传网站变形的处理旅游网站建设的概念