C Primer Plus 学习笔记 第16章 C预处理器和C库

这一章很多内容可以作为参考。

全书共分17章,这是关于本书第16章内容的博客。本章主要介绍了关于C预处理器和C库的内容。本章的一些内容不会细写。博客的目录和书上目录是相似的。此系列博客的代码都在Visual Studio 2022环境下编译运行。 

我目前大一刚刚结束,水平有限,博客中若有错误或者总结不到位的地方也请见谅。

目录

16.1 翻译程序的第一步

16.2 明示常量:#define

16.2.1 记号

16.2.2 重定义常量

16.3 在#define中使用参数

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

16.3.2 预处理器黏合剂:##运算符

16.3.3 变参宏:...和__VA_ARGS__

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 内联函数(C99)

16.8 _Noreturn函数(C11)

16.9 C库

16.9.1 访问C库

16.9.2 使用库描述

16.10 数学库

16.11 通用工具库

16.11.1 exit()和atexit()函数

16.11.2 qsort()函数

16.12 断言库

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

16.14 可变参数:stdarg.h


16.1 翻译程序的第一步

在预处理之前,编译器对程序进行一些翻译处理。首先编译器把源代码出现的字符映射到源字符集,此过程处理多字节字符和三字符序列。其次编译器定位每个反斜杠后跟着换行的示例,并删除它们(换行指的是写程序的过程中按Enter键产生的,不是\n),生成逻辑行。然后编译器把文本划分成预处理记号序列,空白序列和注释序列(记号是空格、换行符或制表符分隔的项)。编译器用空格替换每一条注释。随后程序进入预处理阶段。

16.2 明示常量:#define

#define指令可以出现在源文件的任何地方,定义从指令出现的地方到该文件末尾有效。

我们可以使用#define指令定义明示常量。

预处理器指令从#开始,到第一个换行符为止。

#define命令行由三部分组成,#define 宏名 替换体。宏的名称遵循变量命名规则。程序中的宏会被替换成替换体。从宏到替换的过程是宏展开

代表值的宏称为类对象宏

宏可以表示任何字符串,甚至是一个表达式。

预处理器不做计算,不对表达式求值,只进行替换。

宏定义可以包含其他宏。

双引号内的宏不会被替换。

用明示常量可以表示一些数字常量,这样更容易移植和更改。

16.2.1 记号

可以把宏的替换体看作记号型字符串。C预处理器记号是宏定义的替换体中单独的词,用空白分隔开。解释为记号字符串,把空格视为替换体中各个记号的分隔符。可以不用空格分隔记号。

16.2.2 重定义常量

如果对一个宏进行多次定义,称为重定义常量。只有新定义和旧定义完全相同(记号和顺序完全相同)才允许重定义。

16.3 在#define中使用参数

在#define中使用参数可以创建类函数宏。宏名后面的圆括号中可以有一个或多个参数,随后参数出现在替换体中。

使用类函数宏时可以是变量,常量或表达式。

预处理器不计算、不求值,只进行替换。因此可能会得到意外的值,要使用圆括号保证正确的计算顺序。不要在宏中使用递增或递减运算符。

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

字符串中的宏名会被视为普通文本。

在类函数宏的替换体中,#后跟参数名可以将其转换为字符串,字符串的内容是形参名。这个过程是字符串化。

16.3.2 预处理器黏合剂:##运算符

##运算符可用于类函数宏和类对象宏的替换部分。把运算符两边的记号组合成一个记号。

16.3.3 变参宏:...和__VA_ARGS__

把宏参数列表中最后的参数写成...实现变参宏,替换部分中用__VA_ARGS__表明省略号代表什么。

16.4 宏和函数的选择

有些任务可以用函数完成,也可以用带参宏完成。

使用带参宏容易产生副作用,占用空间比较大,但是效率相对高,不用关心变量类型。

使用函数花费时间多,但是占用空间少。

16.5 文件包含:#include

预处理器发现#include指令,会查看后面的文件名并将内容包含到当前文件中。

文件名用尖括号括起来表示从标准系统目录中查找文件,双引号括起来表示先在当前目录中查找该文件,查不到再从标准系统目录中查找。

包含文件因为编译器需要此文件的信息。C语言用.h后缀表示头文件。头文件包含需要放在程序顶部的信息。有些头文件由系统提供,也可以自己写头文件。

大部分情况下头文件的内容是编译器生成最终代码时所需信息。

头文件可以包含明示常量,宏函数,函数声明,结构模板定义,类型定义,外部变量等。

#include和#define是最常用的C预处理器特性。

16.6 其他指令

16.6.1 #undef指令

#undef指令用于取消已定义的#define指令。#undef后跟一个已定义的宏会移除此宏的定义。即使没有定义,取消定义依然有效。

#undef可以用于确定一个名字是否用过时。

16.6.2 从C预处理器角度看已定义

预处理器在发现一个标识符时会当成已定义或未定义。已定义表示预处理器定义。如果已经用#define创建,且没有用#undef取消则表示已定义。不是宏的标识符是未定义。取消定义后也是未定义。

已定义宏可以是类对象宏或类函数宏。

#define宏定义的作用域从声明处开始,到#undef取消或者文件结尾。通过头文件引入的宏的位置取决于#include指令的位置。

16.6.3 条件编译

可以通过一些指令创建条件编译,告诉编译器根据编译时的条件执行或忽略信息和代码。

#ifdef指令说明,如果已经定义后面的标识符,则执行到#else#endif指令(先遇到哪个就是哪个)前的的所有指令。如果未定义且有#else指令,则执行#else和#endif指令之间的所有代码。(没有#else就什么也不做)

这类似ifelse语句,但是预处理器不识别花括号,因此使用#else和#endif指令。#endif必须存在。

指令结构可以嵌套,可以用于标记C语句块。

#ifdef指令可以用于调试程序,根据不同情况使用不同代码。

#ifndef指令判断后面标识符是否未定义,如果未定义执行到#else或#endif之前的指令。否则如果有#else指令执行#else到#endif之间的指令,没有就什么也不做。#endif指令必不可少。

常用于定义以前未定义的常量,防止相同的宏被重复定义,防止文件被重复包含。

#if指令后跟一个整型常量表达式,如果表达式非零则为真。可以按照if else的格式使用#elif,但是不需要花括号,结尾需要#endif。

defined是一个预处理运算符,参数已定义则返回1,否则返回0。

条件编译让程序更容易移植。

16.6.4 预定义宏

C语言提供一些预定义宏,这些宏不能被取消。

__DATE__表示预处理的日期(Mmm dd yyyy形式的字符串),__FILE__是表示当前源代码文件名的字符串,__LINE__表示在当前源代码文件中的行号(整型)。__STDC__设置为1表明遵循C标准。__STDC_HOSTED__本机环境为1,否则为0。__STDC_VERSION__在支持C99情况下值为199901L,支持C11则为201112L。__TIME__表示翻译代码的时间,格式为hh:mm:ss。

__func__展开为一个代表函数名的字符串,是预定义标识符不是宏。

16.6.5 #line和#error

#line重置__LINE__和__FILE__的行号和文件名,#line 数字重置行号,#line "文件名"重置文件名。

#error让预处理器发出一条错误消息,该消息包含指令中的文本。

16.6.6 #pragma

#pragma把编译器指令放入源代码中。一般编译器有自己的编译指示集。

_Pragma把字符串转换为普通的编译器指示。

16.6.7 泛型选择(C11)

泛型编程指没有特定类型,但是指定一种类型就可以转换成指定类型的代码。

这部分内容后期才用到,省略。

16.7 内联函数(C99)

函数调用通常有一定的开销。C99提供内联函数避免一些开销。

把函数变成内联函数意味着尽可能快地调用该函数,具体效果由实现定义。编译器可能会用内联代码替换函数调用,执行一些优化,也可能没有。创建内联函数最简单地方法是用函数说明符inline和存储说明符static。

内联函数应定义在首次使用的文件中,相当于函数原型。内联函数应该比较短小,否则节约不了多少时间。

16.8 _Noreturn函数(C11)

_Noreturn表明调用完成后函数不返回主调函数。

16.9 C库

16.9.1 访问C库

如何访问C库取决于实现。一般只要进行头文件包含后编译就可以使用一些常见的库函数。

16.9.2 使用库描述

一般可以在一个位置找到编译器的头文件。

16.10 数学库

数学库包含许多有用的数学函数,原型一般在math.h中。数学函数的参数和返回值一般都是double类型。

一些数学函数如sin(x)返回x的正弦值,cos(x)返回x的余弦值,tan(x)返回x的正切值。这三个函数的参数是弧度制,不是角度制。

exp(x)返回e的x次幂,log(x)返回ln x的值,log10(x)返回lg x的值,pow(x,y)返回x的y次幂,fabs(x)返回x的绝对值。

C99标准提供的tgmath.h头文件定义了泛型定义宏。

16.11 通用工具库

16.11.1 exit()和atexit()函数

main()函数返回系统时会自动调用exit()函数。

atexit()函数通过注册要退出时调用的函数来指定执行exit()时调用的函数。

atexit()函数有一个参数,是一个函数指针。要使用此函数只需要把退出时调用的函数地址传递给atexit()即可。函数名作为函数参数时相当于该函数的地址。随后atexit()注册函数列表中的函数,调用exit()时执行这些函数。最后添加的函数先执行。

atexit()注册的函数应该不带任何参数,且返回类型为void,这些函数通常执行一些清理任务。

exit()函数在执行完atexit()指定的函数后,会进行一些清理工作,然后结束函数。

exit()函数有一个参数,参数为0或宏EXIT_SUCCESS表示成功终止。非零值和EXIT_FAILURE表示终止失败。

在任意地方使用exit()都会终止整个程序。

exit()和atexit()定义在stdlib.h头文件中。

16.11.2 qsort()函数

qsort()函数定义在stdlib.h头文件中。该函数使用快速排序法进行排序,没有返回值。

qsort()函数有四个参数,第一个参数是指针,指向待排序数组的首元素。第二个参数是待排序项的数量。第三个参数是每个元素的大小。第四个参数是一个指向函数的指针。这个被指针指向的函数用于确定排序的顺序。该函数有两个参数,分别指向待比较两项的指针。如果第一项大于第二项返回正数,第一项小于第二项返回负数,否则如果两项相等返回零。(如果第一个参数位置在第二个参数前代表升序,否则为降序)

16.12 断言库

assert.h头文件支持的断言库是一个用于辅助调试程序的小型库。由assert()宏组成,接受一个整型表达式作为参数,如果表达式为0,就在stderr中写入一条错误信息,并调用abort()函数终止程序。终止程序后通常显示一些信息,包括文件名和行号。

使用#define NDEBUG指令可以禁用程序中所有assert()语句。

C11新增了_Static_assert声明,可以在编译时检查assert()表达式,因此会导致程序无法通过编译,而不是运行时终止。

_Static_assert()有两个参数,第一个是一个整型常量表达式,第二个是一个字符串。如果第一个表达式是0则显示第二个参数字符串,不编译程序。

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

可以使用memcpy()memmove()函数处理任意类型数组。这两个函数都有三个参数,前两个是void*类型的指针,第三个是待拷贝的字节数。

这两个函数都从第二个指针指向的位置开始,拷贝第三个参数大小的字节到第一个指针指向的位置。返回第一个指针的值。

但是memcpy()函数假定两个区域没有重叠,如果重叠则结果是未定义的。而memmove()函数中两个区域可以重叠(重叠区域的原数据被覆盖)。

16.14 可变参数:stdarg.h

stdarg.h头文件为函数提供了一个可变函数的功能。

使用比较复杂,但应用广。后期用到的内容,省略。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值