【C语言篇】C预处理器和C库

友情链接:C/C++系列系统学习目录

知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
 
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上


文章目录

  • 一、C预处理器
    • ⛳(一)#define
      • 1.概述
      • 2.在#define中使用参数
      • 3.其它知识点
        • (1)宏参数创建字符串
        • (2)记号
        • (3)变参宏:...和_ \_VA_ARGS_ _
    • ⛳(二)#include
      • 1.用法
      • 2.为什么要包含文件
      • 3.使用头文件
    • ⛳(三)其它指令
      • 1.#undef指令
      • 2.条件编译
        • (1)#ifdef、#else和#endif指令
        • (2)#ifndef指令
        • (3)#if和#elif和#endif指令
        • (4)#line和#error指令
        • (5)#pragma
    • ⛳(四)C语言泛型编程
  • 二、C库
    • ⛳(一)访问C库
      • 1.自动访问
      • 2.文件包含
      • 3.库包含
    • ⛳(二)库函数的变化
    • ⛳(三)一些特殊的函数
      • 1.数学库
      • 2.通用工具库
        • (1)exit()和atexit()函数
        • (2)qsort()函数
      • 3.断言库
      • 4.string.h库中的memcpy()和memmove()


一、C预处理器

C标准不仅描述C语言,还描述如何执行C预处理器、C标准库有哪些函数,以及详述这些函数的工作原理。

C预处理器在程序执行(编译)之前查看程序(故称之为预处理器),C语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释(比如之前使用的#include文件包含指令),产生一个新的源程序(这个过程称为编译预处理),之后再进行通常的编译

1.C语言由源码变成可以运行的可执行文件包括四个阶段,分别是:

- 预处理阶段(预处理器)
- 编译阶段(编译器)
- 汇编阶段(汇编器)
- 链接阶段(链接器)

.c为后缀的文件(源代码)—> 编译器编译(编译包括预编译、编译和汇编)—> 形成目标文件(在VS2019中是.obj为后缀的文件,在Linux中是以.o为后缀的文件)—> 链接器链接 —> 形成可执行程序(在VS2019中是.exe为后缀的文件,在Linux中是a.out文件)

【C语言篇】C预处理器和C库_第1张图片

2.而在预处理之前,编译器还必须对该程序进行一些翻译处理。

  • 首先,编译器把源代码中出现的字符映射到源字符集。该过程处理多字节字符和三字符序 列——字符扩展让C更加国际化

  • 第二,编译器定位每个反斜杠后面跟着换行符的实例,并删除它们。

    printf("That's wond\
    erful!\n");
    //转换成一个逻辑行(logical line):
    printf("That's wonderful\n!");
    

    由于预处理表达式的长度必须是一个逻辑行,所以这一步为预处理器做好了准备工作。一个逻辑行可以是多个物理行。

  • 第三,编译器把文本划分成预处理记号(记号是由空格、制表符或换行符分隔的项)序列、空白序列和注释序列。编译器将用一个空格字符替换每一条注释。

    int/* 这看起来并不像一个空格*/fox;
    //将变成:
    int fox;
    

最后,程序已经准备好进入预处理阶段,预处理器查找一行中以#号开始的预处理指令。

⛳(一)#define

1.概述

在这里插入图片描述

  1. 宏名称中不允许有空格,必须遵循C变量的命名规则

  2. 宏可以用来表示符号常量(明示常量),这是之前所涉及到的,宏也可以表示任何字符串,甚至可以表示整个C表达式,甚至宏定义还可以包含其它宏

  3. 一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替换,从宏变成最终替换文本的过程称为宏展开。如果替换的字符串中还包含宏,则继续替换这些宏。唯一例外的是双引号中的宏属于正常字符串不会被替换。

  4. 替换体的多个物理行可以使用反斜杠延续,但是注意,第2行要与第1行左对齐。第2行开始到tive之间的空格也算是字符串的一部分:

    #define OW "Consistency is the last refuge of the unimagina\
        tive.- Oscar Wilde"
    
    //将输出:
    Consistency is the last refuge of the unimagina tive.- Oscar Wilde
    

拓展:

1.ANSI和后来的标准都允许#号前面有空格或制表符,而且还允许在#和 指令的其余部分之间有空格。但是旧版本的C要求指令从一行最左边开始, 而且#和指令其余部分之间不能有空格。

2.宏常量(符号常量)可用于指定标准数组的大小和const变量的初始值。

#define LIMIT 20
const int LIM = 50;

static int data1[LIMIT]; // 有效
static int data2[LIM]; // 无效

const int LIM2 = 2 * LIMIT; // 有效
const int LIM3 = 2 * LIM; // 无效

在C中,非自动数组的大小应该是整型常量表达式,这意味着表示数组大小的必须是整型常量的组合、枚举常量和sizeof表达式,不包括const声明的值(这也是C++和C的区别之一,在C++中可以把const值作为常量表达式的一部分)。但是,有的实现可能接受其他形式的常量表达式。例如,GCC 4.7.3不允许data2的声明, 但是Clang 4.6允许。

2.在#define中使用参数

在#define中使用参数可以创建外形和作用与函数类似的类函数宏。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中

【C语言篇】C预处理器和C库_第2张图片

#define SQUARE(X) X*X

z = SQUARE(2); //宏展开为:z = 2*2;

注意:预处理器不做计算、不求值,只简单的替换字符序列:

  1. 第一种情况:

    若x的值为5,你可能认为SQUARE(x+2)应该是 7*7,即 49。但是,输出的结果是 17,

    //预处理器把出现x的地方都替换成x+2。因此,x*x变成了x+2*x+2。如果x为5,那么该表达式的值为:
        
    5+2*5+2 = 5 + 10 + 2 = 17
    
    
    //解决方案:在替换字符串中使用圆括号即可:
        
    #define SQUARE(x) (x)*(x)
    SQUARE(x+2)变成了(x+2)*(x+2),该表达式的值为:
        
     (5+2)*(5+2) = 49
    
  2. 第二种情况:

    100/SQUARE(2)将变成: 100/2*2,

    //根据优先级规则,从左往右对表达式求值:
    
    (100/2)*2,即50*2,得100//解决方案:把SQUARE(x)定义为下面的形式可以解决这种混乱:
        
    #define SQUARE(x) (x*x)
    这样修改定义后得100/(2*2),该表达式的值为:
        
    100/4,得25

因此,必要时要使用足够多的圆括号来确保运算和结合的正确顺序。

3.其它知识点

(1)宏参数创建字符串

#define PSQR(X) printf("The square of X is %d.\n", ((X)*(X)));

PSQR(8);  //输出为:The square of X is 64.
  • 双引号字符串中的X被视为普通文本,而不是一个可被替换的记号。

  • C允许在字符串中包含宏参数。在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串:如果x是一个宏形参,那么#x就是转换为字符串"x"的形参名。这个过程称为字符串化

    #define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x)))
    
    PSQR(2 + 4); //The square of 2 + 4 is 36.  用"2 + 4"替换#x,最后通过字符串组合成一个字符串
    

(2)记号

从技术角度来看,可以把宏的替换体看作是记号(token)型字符串, 而不是字符型字符串。C预处理器记号是宏定义的替换体中单独的“词”。用空白把这些词分开。例如:

#define FOUR 2*2  //该宏定义有一个记号:2*2序列
#define SIX 2 * 3 //有3个记号:2、*、3。

如果预处理器把替换体解释为字符型字符串,则把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符。在实际应用中,一些C编译器把宏替换体视为字符串而不是记号。

C编译器处理记号的方式比预处理器复杂。由于编译器理解 C语言的规则,所以不要求代码中用空格来分隔记号。例如,C编译器可以 把2*2直接视为3个记号,因为它可以识别2是常量,*是运算符。

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

##运算符把两个记号组合成一个记号。例如:

#define XNAME(n) x ## n  //宏XNAME(4)将展开为x4。
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);


int XNAME(1) = 14; // 变成 int x1 = 14;
PRINT_XN(1); // 变成 printf("x1 = %d\n", x1);

(3)变参宏:…和_ _VA_ARGS_ _

一些函数(如 printf())接受数量可变的参数。stdvar.h 头文件(本章后 面介绍)提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。

用法:

#define PR(...) printf(_ _VA_ARGS_ _)

PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);

对于第1次调用,_ _VA_ARGS_ _展开为1个参数:“Howdy”。 对于第2次调用,_ _VA_ARGS_ _展开为3个参数:“weight = %d, shipping = $%.2f\n”、wt、sp。因此,展开后的代码是:

printf("Howdy");
printf("weight = %d, shipping = $%.2f\n", wt, sp);

举例:

#define PR(X, ...) printf("Message " #X ": " __VA_ARGS__)

PR(1, "x = %g\n", x);
//X的值是1,所以#X变成"1"。展开后成为:
print("Message " "1" ": " "x = %g\n", x)
//然后,串联4个字符,把调用简化为:
print("Message 1: x = %g\n", x);

注意:省略号只能代替最后的宏参数:

#define WRONG(X, ..., Y) #X #_ _VA_ARGS_ _ #y //不能这样做

⛳(二)#include

1.用法

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

//尖括号告诉预处理器在标准系统目录中查找该文件
#include   ←文件名在尖括号中    

//双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录
#include "mystuff.h" ←文件名在双引号中   	
#include "/usr/biff/p.h" ←查找/usr/biff目录									  

集成开发环境(IDE)也有标准路径或系统头文件的路径。许多集成开 发环境提供菜单选项,指定用尖括号时的查找路径。在 UNIX 中,使用双引 号意味着先查找本地目录,但是具体查找哪个目录取决于编译器的设定。有 些编译器会搜索源代码文件所在的目录,有些编译器则搜索当前的工作目 录,还有些搜索项目文件所在的目录。

2.为什么要包含文件

因为编译器需要这些文件中的信息。例如,stdio.h 文件中通常包含EOF、NULL、getchar()和 putchar()的定义。getchar()和 putchar()被定义为宏函数。此外,该文件中还包含C的其他I/O函数。

C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信 息。头文件经常包含一些预处理器指令。有些头文件(如stdio.h)由系统提供,当然你也可以创建自己的头文件。

包含一个大型头文件不一定显著增加程序的大小。在大部分情况下,头 文件的内容是编译器生成最终代码时所需的信息,而不是添加到最终代码中的材料。

3.使用头文件

可执行代码通常在源代码文件中,而不是在头文件中。像#define指令、结构声明、 typedef和函数原型等这些内容是编译器在创建可执行代码时所需的信息,而不是可执行代码。

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

  1. 明示常量——例如,stdio.h中定义的EOF、NULL和BUFSIZE(标准I/O 缓冲区大小)。
  2. 宏函数——例如,getc(stdin)通常用getchar()定义,而getc()经常用于定义较复杂的宏,头文件ctype.h通常包含ctype系列函数的宏定义。
  3. 函数声明——例如,string.h头文件(一些旧的系统中是strings.h)包含字符串函数系列的函数声明。在ANSI C和后面的标准中,函数声明都是函数原型形式。
  4. 结构模版定义——标准I/O函数使用FILE结构,该结构中包含了文件和与文件缓冲区相关的信息。FILE结构在头文件stdio.h中。
  5. 类型定义——标准 I/O 函数使用指向 FILE 的指针作为参数。通常, stdio.h 用#define 或typedef把FILE定义为指向结构的指针。类似地,size_t和 time_t类型也定义在头文件中。
  • 另外,还可以使用头文件声明外部变量供其他文件共享。例如,如果已经开发了共享某个变量的一系列函数,该变量报告某种状况(如,错误情况),这种方法就很有效。这种情况下,可以在包含这些函数声明的源代码 文件定义一个文件作用域的外部链接变量:

    虽然源代码文件中包含该头文件后也包含了该声明, 但是只要声明的类型一致,在一个文件中同时使用定义式声明和引用式声明没问题。

int status = 0; // 该变量具有文件作用域,在源代码文件

//然后,可以在与源代码文件相关联的头文件中进行引用式声明:

extern int status; // 在头文件中
  • 需要包含头文件的另一种情况是,使用具有文件作用域、内部链接和 const 限定符的变量或数组。const 防止值被意外修改,static 意味着每个包含 该头文件的文件都获得一份副本。因此,不需要在一个文件中进行定义式声 明,在其他文件中进行引用式声明。

⛳(三)其它指令

1.#undef指令

#undef指令用于“取消”已定义的#define指令。也就是说,假设有如下定义:

#define LIMIT 400

//然后,下面的指令:

#undef LIMIT

将移除上面的定义。现在就可以把LIMIT重新定义为一个新值。即使原 来没有定义LIMIT,取消LIMIT的定义仍然有效。如果想使用一个名称,又 不确定之前是否已经用过,为安全起见,可以用#undef 指令取消该名字的定 义。

2.条件编译

可以使用其他指令创建条件编译(conditinal compilation)。也就是说, 可以使用这些指令告诉编译器根据编译时的条件执行或忽略信息(或代码) 块。

(1)#ifdef、#else和#endif指令

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

这里使用的较新的编译器和 ANSI 标准支持的缩进格式。如果使用旧的编译器,必须左对齐所有的指令或至少左对齐#号

#ifdef指令说明,如果预处理器已定义了后面的标识符(MAVIS),则执行#else或#endif指令之前的所有指令并编译所有C代码(先出现哪个指令就执行到哪里)。如果预处理器未定义MAVIS,且有 #else指令,则执行 #else和#endif指令之间的所有代码。

#ifdef #else很像C的if else。两者的主要区别是,预处理器不识别用于标记块的花括号({}),因此它使用#else(如果需要)和#endif(必须存在) 来标记指令块。这些指令结构可以嵌套。也可以用这些指令标记C语句块:

这里的已定义表示由预处理器定义。如果标识符是同一个文件中由前面的#define指令创建的宏名,而且没有用#undef 指令关闭,那么该标识符是已定义的。

#ifdef JUST_CHECKING
	printf("i=%d, running total = %d\n", i, total);
#endif

(2)#ifndef指令

#ifndef指令与#ifdef指令的用法类似,也可以和#else、#endif一起使用, 但是它们的逻辑相反。#ifndef指令判断后面的标识符是否是未定义的,常用 于定义之前未定义的常量。

#ifndef SIZE
	#define SIZE 100
#endi

通常,包含多个头文件时,其中的文件可能包含了相同宏定义。#ifndef 指令可以防止相同的宏被重复定义。既然如此,#ifndef指令通常也用于防止多次包含一个文件:

假设该文件被包含了多次。当预处理器首次发现该文件被包含时, THINGS_H_是未定义的,所以定义了THINGS_H_,并接着处理该文件的其 他部分。当预处理器第2次发现该文件被包含时,THINGS_H_是已定义的, 所以预处理器跳过了该文件的其他部分。

/* things.h */
#ifndef THINGS_H_
    #define THINGS_H_
    /* 省略了头文件中的其他内容*/
#endif

许多被包含的文件中都包含着其他文件,所以显式包含的文件中可能包含着已经包含的其他文件,而在被包含的文件中有某些项(如,一些结构类型的声明)只能在一个文件中出现一次。C标准头文件使用#ifndef技巧避免重复包含。但是,这存在一个问题:如何确保待测试的标识符没有在别处定义。通常,实现的供应商使用这些方法解决这个问题:用文件名作为标识符、使用大写字母、用下划线字符代替文件名中的点字符、用下划线字符做前缀或后缀(可能使用两条下划线)

标准保留使用下划线作为前缀,所以在自己的代码中不要这样写,避免与标准头文件中的宏发生冲突

(3)#if和#elif和#endif指令

#if指令很像C语言中的if。#if后面跟整型常量表达式,如果表达式为非零,则表达式为真。可以在指令中使用C的关系运算符和逻辑运算符:

#if SYS == 1
    #include "ibmpc.h"
#elif SYS == 2
    #include "vax.h"
#elif SYS == 3
    #include "mac.h"
#else
    #include "general.h"
#endif

较新的编译器提供另一种方法测试名称是否已定义,即用#if defined (VAX)代替#ifdef VAX。如果它的参数是用#defined定义过,则返回1;否则返回0。这种新方法的优点是,它可以和#elif一起使用。

(4)#line和#error指令

预定义宏:C标准规定了一些预定义宏

【C语言篇】C预处理器和C库_第3张图片

C99 标准提供一个名为_ _func_ _的预定义标识符,它展开为一个代表函数名的字符串(该函数包含该标识符)。那么_ _func_ _必须具有函数作用域,而从本质上看宏具有文件作用域。因此,_ func _是C语言的预定 义标识符,而不是预定义宏。

#include 
void why_me();

int main()
{
 printf("The file is %s.\n", __FILE__);
 printf("The date is %s.\n", __DATE__);
 printf("The time is %s.\n", __TIME__);
 printf("The version is %ld.\n", __STDC_VERSION__);
 printf("This is line %d.\n", __LINE__);
 printf("This function is %s\n", __func__);
 why_me();

 return 0;
}

void why_me()
{
 printf("This function is %s\n", __func__);
 printf("This is line %d.\n", __LINE__);
}

//下面是该程序的输出:
The file is predef.c.
The date is Sep 23 2013.
The time is 22:01:09.
The version is 201112.
This is line 11.
This function is main
This function is why_me
This is line 21.

#line指令重置_ _LINE_ _和_ _FILE_ _宏报告的行号和文件名。可以这样使用#line:

#line 1000 // 把当前行号重置为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 newish.c  //如果编译器只支持旧标准,则会编译失败,如果支持C11标准,就能成功编译。
$

(5)#pragma

它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。

语法:

#pragma Para
//(其中Para 为参数)


//比如:
#pragma once  //指定在创建过程中该编译指示所在的文件仅仅被编译程序包含(打开)一次。

⛳(四)C语言泛型编程

C语言本身不支持真正意义上的泛型编程,但是却在一定程度上可以“实现泛型编程”,即C11标准的 _Generic 关键字(C++不支持该关键字)。在 C11 标准中,_Generic 关键字让 C 语言如同 C++ 等面向对象程序设计语言一样,支持轻量级的泛型编程

泛型编程(generic programming):是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

_Generic 关键字_Generic 关键字提供了一种在 编译时 根据 赋值表达式 的类型在 泛型关联表 中选择一个表达式的方法,因此可以将一组不同类型却功能相同的函数抽象为一个统一的接口,以此来实现泛型

C11新增了一种表达式,叫作泛型选择表达式 (generic selection expression),可根据表达式的类型(即表达式的类型是 int、double 还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define宏定义的一部分。

语法形式如下:

_Generic ( assignment-expression , generic-assoc-list )
  • assignment-expression:赋值表达式,可以认为是变量var

  • generic-assoc-list:泛型关联表,其语法为:

    type-name : expression, type-name : expression, ..., default : expression
    
    1. default不是必须的
    2. generic-assoc-list中的expression仅仅是普通的宏替换

例如:

_Generic(x, int: 0, float: 1, double: 2, default: 3)
#define GET_TYPENAME(var) _Generic((var),\
    int:"int", \
    char:"char", \
    float:"float", \
    double:"double", \
    char *:"char *", \
    default:"other type")

int main(int argc, char const *argv[])
{
    int x;
    int* x1;
    char s[10];
    printf("type: x = %s\n",GET_TYPENAME(x));
    printf("type: x1 = %s\n",GET_TYPENAME(x1));
    printf("type: s = %s\n",GET_TYPENAME(s));
    return 0;
}

//运行结果:
type: x = int
type: x1 = other type
type: s = char *

二、C库

最初,并没有官方的C库。后来,基于UNIX的C实现成为了标准。ANSI C委员会主要以这个标准为基础,开发了一个官方的标准库。在意识到C语 言的应用范围不断扩大后,该委员会重新定义了这个库,使之可以应用于其 他系统。

⛳(一)访问C库

用#include引入头文件只是复制了库函数的声明,实际使用时寻找库函数的实现是由编译器或链接器实现的。不同系统使用不同的方法搜索库函数,有三种可能性。

1.自动访问

许多系统上,你只需编译程序,一些常见的库函数自动可用(甚至不用引入头文件)

2.文件包含

如果函数被定义为宏,那么可以通过#include 指令包含定义宏函数的文件。通常,类似的宏都放在合适名称的头文件中。例如,许多系统(包括所有的ANSI C系统)都有ctype.h文件,该文件中包含了一些确定字符性质(如 大写、数字等)的宏。

3.库包含

在程序编译或链接的某些阶段,您可能需要指定库选项。因为即使在自动检查标准库的系统上,也有可能有不常用的函数库。必须使用编译时选项来显示地指定这些库。

注意,这个过程与包含头文件不同。头文件提供函数声明或原型,而库选项告诉系统到哪里查找函数代码。虽然这里无法涉及所有系统的细节, 但是可以提醒读者应该注意什么。

例如:

你使用了#include<math.h>

//然后在函数中使用了sqrt函数(求平方),但是在编译时却得到了类似下面的消息:

Undefined:_sqrt
或:
'sqrt':unresolved external
或是类似的消息,这表明编译器的链接器没有找到数学库。

此时你可以在命令行手动的让链接器找到数学库:

cc a.c -lm  //UNIX系统要求使用-lm标记,其中cc是编译器的名字,a.c是源文件的名字
gcc a.c -lm  //Linux的gnu编译器也使用同样的形式

⛳(二)库函数的变化

可以在多个地方找到函数文档。阅读文档的关键是看懂函数头。许多内容随时间变化而变化。下面是旧 的UNIX文档中,关于fread()的描述:

#include 

fread(ptr, sizeof(*ptr), nitems, stream)
FILE *stream;

首先,给出了应该包含的文件,但是没有给出fread()、ptr、sizeof(*ptr) 或nitems的类型。过去,默认类型都是int,但是从描述中可以看出ptr是一个 指针(在早期的C中,指针被作为整数处理)。后来,上面的描述变成了:

#include 

int fread(ptr, size, nitems, stream;)
char *ptr;
int size, nitems;
FILE *stream;

所有的类型都显式说明,ptr作为指向char的指针。ANSI C90标准提供了下面的描述:

#include 
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 首先,使用了新的函数原型格式。
  • 其次,改变了一些类型。size_t 类型 被定义为 sizeof 运算符的返回值类型——无符号整数类型,通常是unsigned int或unsigned long。stddef.h文件中包含了size_t类型的typedef或#define定义。 其他文件(包括stdio.h)通过包含stddef.h来包含这个定义。
  • 另外,ANSI C把指向void的指针作为一种通用指针,用于指针指向不同类型的情况。

C99/C11标准在以上的描述中加入了新的关键字restric:

#include 
size_t fread(void * restrict ptr, size_t size,size_t nmemb, FILE * restrict stream);

⛳(三)一些特殊的函数

1.数学库

数学库中包含许多有用的数学函数。math.h头文件提供这些函数的原型

【C语言篇】C预处理器和C库_第4张图片

拓展:

1.类型变体:

基本的浮点型数学函数接受double类型的参数,并返回double类型的 值。当然,也可以把float或 long double 类型的参数传递给这些函数,它们仍 然能正常工作,因为这些类型的参数会被转换成double类型。这样做很方 便,但并不是最好的处理方式。如果不需要双精度,那么用float类型的单精 度值来计算会更快些。而且把long double类型的值传递给double类型的形参 会损失精度,形参获得的值可能不是原来的值。为了解决这些潜在的问题, C标准专门为float类型和long double类型提供了标准函数,即在原函数名前 加上f或l前缀。因此,sqrtf()是sqrt()的float版本,sqrtl()是sqrt()的long double 版本。

2.tgmath.h库:

C99标准提供得tgmath.h头文件定义了泛型类型宏。比如在math.h中为一个函数定义了3中类型(float,double和long double)的版本,那么tgmath.h文件就创建一个泛型类型宏,与原来的float,double和long double版本的函数名同名

如果要调用sqrt()函数而不是sqrt()宏,可以用圆括号把被调用的函数名括起来

#include 
…
 Float x = 44.0;
 Double y;
 Y = sqrt(x);  //调用宏,因为x是float,所以是sqrtf(x)
 Y = (sqrt)(x);  //调用函数sqrt()

2.通用工具库

通用工具库包含各种函数,包括随机数生成器、查找和排序函数、转换函数和内存管理函数,在ANSI C标准中,这些函数的原型都在stdlib.h头文件中。

(1)exit()和atexit()函数

atexit()函数的用法:

函数参数使用函数指针,只需把退出时要调用的函数地址传递给 atexit()即可。然后,atexit()注册函数列表中的函数,当调用exit()时就会执行这些函数。(执行顺序与列表中的函数顺序相反,即最后添加的函数最先执行)。

atexit()注册的函数应该不带任何参数且返回类型为void。通常,这些函数会执行一些清理任务,例如更新监视程序的文件或重置环境变量。

注意,输入失败时,会调用sign_off()和too_bad()函数;但是输入成功时 只会调用sign_off()。因为只有输入失败时,才会进入if语句中注册 too_bad()。另外还要注意,最先调用的是最后一个被注册的函数。

即使没有显式调用exit(),还是会调用sign_off(),因为main()结束时会隐式调用exit()。

exit()函数的用法:

exit()执行完atexit()指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数tmpfile()创建的临时文件。然后 exit()把控制权返回主机环境,如果可能的话,向主机环境报告终止状态。

UNIX程序使用0表示成功终止,用非零值表示终止失败。UNIX返回 的代码并不适用于所有的系统,所以ANSI C为了可移植性的要求,定义了一个名为EXIT_FAILURE的宏表示终止失败。类似地,ANSI C还定义了 EXIT_SUCCESS表示成功终止。不过,exit()函数也接受0表示成功终止。在 ANSI C中,在非递归的main()中使用exit()函数等价于使用关键字return。尽 管如此,在main()以外的函数中使用exit()也会终止整个程序

(2)qsort()函数

对较大型的数组而言,“快速排序”方法是最有效的排序算法之一。该算法由C.A.R.Hoare于1962年开发。它把数组不断分成更小的数组,直到变成单元素数组。首先,把数组分成两部分,一部分的值都小于另一部分的值。 这个过程一直持续到数组完全排序好为止。

快速排序算法在C实现中的名称是qsort()。qsort()函数排序数组的数据对象,其原型如下:

 void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
  • 第1个参数是指针,指向待排序数组的首元素。

  • 第2个参数是待排序项的数量

  • 由于qsort()把第1个参数转换为void指针,所以qsort()不知道数组中每个 元素的大小。为此,函数原型用第 3 个参数补偿这一信息,显式指明待排序 数组中每个元素的大小。

  • 最后,qsort()还需要一个指向函数的指针,这个被指针指向的比较函数 用于确定排序的顺序。该函数应接受两个参数:分别指向待比较两项的指 针。如果第1项的值大于第2项,比较函数则返回正数;如果两项相同,则返 回0;如果第1项的值小于第2项,则返回负数。qsort()根据给定的其他信息计算出两个指针的值,然后把它们传递给比较函数。例如:

    /* 按从小到大的顺序排序值 */
    int mycomp(const void * p1, const void * p2)
    {
        /* 使用指向double类型的指针访问值 */
        const double * a1 = (const double *) p1;
        const double * a2 = (const double *) p2;
        if (*a1 < *a2)
        	return -1;
        else if (*a1 == *a2)
        	return 0;
        else
        	return 1;
    }
    

3.断言库

assert.h 头文件支持的断言库是一个用于辅助调试程序的小型库。它由 assert()宏组成,接受一个整型表达式作为参数。如果表达式求值为假(非 零),assert()宏就在标准错误流(stderr)中写入一条错误信息,并调用 abort()函数终止程序(abort()函数的原型在stdlib.h头文件中)。

int a;

scanf("%d",&a);
assert(a >= 0);
//如果输入的a小于0,会显示失败的调试、包括测试的文件名和行号

在包含assert.h的位置前面写上:#define NDEBUG可以开启或关闭assert()的机制

4.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);
  • 这两个函数都从 s2 指向的位置拷贝 n 字节到 s1 指向的位置,而且都返 回 s1 的值。
  • 所不同的是, memcpy()的参数带关键字restrict,即memcpy()假设两个内存区域之间没有重叠;而memmove()不作这样的假设,如果出现重叠,出现的结果是未定义的
  • 拷贝过 程类似于先把所有字节拷贝到一个临时缓冲区,然后再拷贝到最终目的地。

例如:

int values[SIZE] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int target[SIZE];

memcpy(target, values, SIZE * sizeof(int)); //target为1 2 3 4 5 6 7 8 9 10
memmove(values + 2, values, 5 * sizeof(int)); //values为1 2 1 2 3 4 5 8 9 10

行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

你可能感兴趣的:(C\C++基本知识,c语言,基础知识)