C语言标准库深度整理

引言

C语言标准库是一组内置的函数、常量,被定义在15个头文件中,提供了C语言中最基础的功能

stdio.h 即标准输入输出,同时涵盖了文件读写的全部过程。

所谓标准输入输出,直观一点就是键盘和显示屏:键盘将标准输入传递给终端,终端将内容打印在显示屏上。关于这种基于字符串的人机交互,stdio.h主要提供了两组函数,一组是getput,另一组是printscan

操作一个文件的流程大致为:从fopen打开文件,到fread, fwrite读写文件,再到fflush强制写入缓存,最后fclose关闭文件;如需精确定位文件的字节位置,可通过fseekfgetpos

stdlib.h 即是standard library,标准库中无法归入其他类别的统统放到这里,主要分为四个部分:内存、系统、字符串转数值和数学算法。

string.h:提供字符串的查询、比较、复制等功能

ctype.h:用于数据类型判断

math.h:提供最基础的数学运算

时间与货币:C标准库提供了针对不同国家的本地化头函数,具体表现在时间和货币上。C标准库对不同地区的时间和货币的表达形式进行了本地化的描述,分别封装在time.hlocale.h中。

单功能库 有很多库十分简单,故统一归类到单一功能库下,包括

  • stdarg.h:可变参数支持
  • assert.h:用于报错
  • stddef.h:指针相减
  • setjmp.h:跳转

常量库 有的标准库只定义了一些宏,单并未声明函数,故将这类库统一归类到常量库中。

  • 系统错误码errno.h
  • 信号signal.h
  • 浮点型限制信息float.h
  • 整型限制信息limits.h

stdio.h

std是标准的,io即输入输出,合起来stdio就是标准输入输出。

常量和指针

#define功能操作系统默认
EOF (-1)文件结束符
BUFSIZ 1024setbuf函数缓冲区字节数__BUFSIZ__
FOPEN_MAX 20系统可同时打开的文件数量__FOPEN_MAX__
FILENAME_MAX 1024文件名最大长度__FILENAME_MAX__
L_tmpnam FILENAME_MAXtmpnam创建的临时文件名的最大长度__L_tmpnam__
TMP_MAX 26tmpnam可生成最多独立文件名

stdio.h中定义了三个FILE类型的指针,

#define	stdin	(_REENT->_stdin)
#define	stdout	(_REENT->_stdout)
#define	stderr	(_REENT->_stderr)

其中,_REENTreent.h_impure_ptr定义,而_impure_ptr则为_reent的指针。

_reent中定义了三个FILE型的指针__FILE *_stdin, *_stdout, *_stderr;,分别代表标准输入、标准输出和标准错误。

打开文件

fopenfreopen均为C语言标准库stdio.h中的函数,分别用于打开和重新打开某个stream,二者均返回一个FILE指针。

  • FILE *fopen(const char *filename, const char *mode)
  • FILE *freopen(const char *filename, const char *mode, FILE *stream)

其中filename为文件路径,mode为文件打开模式,freopen中输入的的stream为现存的流,freopen将新打开的文件注入stream中,同时关闭旧文件。

其中mode的取值如下,第一列为常规模式,第二列为二进制模式,在二进制模式下,读取的是二进制文件,其他与常规模式相同。

mode模式说明
“r”“rb”只读打开的文件必须存在。
“w”“wb”写入创建空文件,若文件已存在,会删除原有内容。
“a”“ab”追加向文件末尾追加数据,若文件不存在,则创建文件。
“r+”“rb+”更新打开一个文件进行读写,该文件必须存在。
“w+”“wb+”读写创建一个用于读写的空文件。
“a+”“ab+”打开一个用于读取和追加的文件

fopenfreopen的返回值为FILE指针,刚好可以通过stdio.h中的close进行关闭。

stdio.h中定义了三个单参函数用以调控文件的写入,其输入均为FILE *stream,若成功则返回1,失败则返回0。

返回类型函数功能
intfclose关闭stream,刷新缓冲区
intfflush刷新stream的输出缓冲区
intferror测试stream的错误标识符

如果在没有完成写入的情况下调用close,可能会致使数据丢失,故stdio.h中提供了fflush函数,用于强制将缓冲区的内容写入文件。而fclose则综合了二者的功能,先flush,再close。

FILE结构体

FILE是C语言标准库stdio.h中定义的一个结构体,用于数据缓存,一般写为

//stdio.h
typedef struct _iobuf
{
    char*  _ptr;        //文件输入的下一个位置
    int    _cnt;        //当前缓冲区的相对位置
    char*  _base;       //文件初始位置
    int    _flag;       //文件标志
    int    _file;       //文件有效性
    int    _charbuf;    //缓冲区是否可读取
    int    _bufsiz;     //缓冲区字节数
    char*  _tmpfname;   //临时文件名
} FILE;

其中,_bufsiz为缓冲区字节数,一般由宏来定义

#define BUFSIZ 1024

临时文件名_tmpfname可通过函数tmpnam进行设置,或者通过tmpfile创建二进制文件。

为了理解这些字段的含义,可以写一个小例子

//test.c
#include<stdio.h>

//这个函数用来打印FILE内部的字段
void printFILE(FILE *fp){
    printf("fp->_ptr=%s\n",fp->_ptr);
    printf("fp->_cnt=%d\n",fp->_cnt);
    printf("fp->_base=%s\n",fp->_base);
    printf("fp->_flag=%d\n",fp->_flag);
    printf("fp->_file=%d\n",fp->_file);
    printf("fp->_charbuf=%d\n",fp->_charbuf);
    printf("fp->_bufsiz=%d\n",fp->_bufsiz);
    printf("fp->_tmpfname=%s\n",fp->_tmpfname);
    printf("-------------------------------------");
    printf("-------------------------------------\n");
}

int main(){
    FILE* fp = fopen("test.c","r"); //打开test.c文件,即本文件
    printFILE(fp);

    char buf[20];
    fread(buf,1,5,fp);      //读取fp中的数据
    printFILE(fp);

    fclose(fp);             //关闭fp
    printFILE(fp);
    return 0;
}

得到其输出如下,为了便于阅读,用///**/对输出进行注释

>gcc test.c
>a.exe
fp->_ptr=(null)     //打开文件后,由于未作操作,故该指针为空
fp->_cnt=0          //当前缓冲区相对位置为0
fp->_base=(null)    //文件初始位置也是一个空指针
fp->_flag=1         
fp->_file=3         
fp->_charbuf=0      //缓冲区文件
fp->_bufsiz=0       //缓冲区是空的
fp->_tmpfname=(null)   
--------------------------------------------------------------------------
// 下面的输出是在执行fread之后,
// 由于读取了5个字节,所以指针跳过了#incl
fp->_ptr=ude<stdio.h>

void printFILE(FILE *fp){
    /*
    这一段将test.c中的内容原封不动地打印了出来
    因为太长,所以省略不写了
    */
    printFILE(fp);
    return 0;
}
LE(fp);
    return 0;
}

fp->_cnt=665        #当前缓冲区的相对位置是65
fp->_base=#include<stdio.h>     #指向文件初始位置的指针

void printFILE(FILE *fp){
    /*
    这一段将test.c中的内容原封不动地打印了出来
    因为太长,所以省略不写了
    */
    printFILE(fp);
    return 0;
}
LE(fp);
    return 0;
}

fp->_flag=9
fp->_file=3
fp->_charbuf=0
fp->_bufsiz=4096        //缓冲区尺寸为4096
fp->_tmpfname=(null)    //并没有临时名字
--------------------------------------------------------------------------
// 关闭文件后,一切又变得未知,但缓冲区尺寸并未变化
fp->_ptr=(null)
fp->_cnt=0
fp->_base=(null)
fp->_flag=0
fp->_file=3
fp->_charbuf=0
fp->_bufsiz=4096
fp->_tmpfname=(null)
--------------------------------------------------------------------------

在上面的输出中,_flag值在打开文件后为1,读取文件后变为9,关闭文件后变为0.

_file则一直为3。

stdio.h中定义了一系列常量用于描述文件流的状态

#define __SLBF  0x0001   /* 行缓冲 */
#define __SNBF  0x0002   /* 无缓冲 */
#define __SRD   0x0004   /* 可读 */
#define __SWR   0x0008   /* 可写 */
#define __SRW   0x0010   /* 可读写 */
#define __SEOF  0x0020   /* 发现 EOF */
#define __SERR  0x0040   /* 发现 error */
#define __SMBF  0x0080   /* _buf来自内存(malloc) */
#define __SAPP  0x0100   /* fdopen()ed in append mode */
#define __SSTR  0x0200   /* 是一个sprintf/snprintf字符串 */
#define __SOPT  0x0400   /* 进行fseek()优化 */
#define __SNPT  0x0800   /* 不进行fseek()优化 */
#define __SOFF  0x1000   /* set iff _offset is in fact correct */
#define __SMOD  0x2000   /* true => fgetln modified _p text */
#define __SALC  0x4000   /* 动态分配字符串内存*/
#define __SIGN  0x8000   /* 在_fwalk是忽略本文件*/

_flag的值为1,表示启用行缓冲;为9,则是__SLBF | __SWR,说明可写。

文件读写和定位

stdio.h中定义了读、写还有查找的函数,其中fread用于将文件中的数据写入内存;fwrite将内存中的数据写入文件;fseek则操作文件指针,使之偏移,从而可以更加灵活地读取。

  • 读:size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
  • 写:size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
  • 移:int fseek(FILE *stream, long int offset, int whence)

其中,ptr为指向某个内存块的指针;stream为文件的数据流;nmemb为元素个数,size为每个元素的字节数;whence为添加偏移的位置,offset为相对于whence的偏移量。

stdio.h中为whence定义了宏,其含义如下

#define SEEK_SET 0从0开始
#define SEEK_CUR 1从当前位置开始
#define SEEK_END 2从EOF开始

读写相对来说比较直观,接下来主要测试一下fseek,说明均在注释中。

//test.c
#include<stdio.h>
void printChar(char *buffer, int len){
	for(int i = 1; i < len; i++)
		printf("%c",buffer[i-1]);
	printf("\n-------------------------\n");
}
int main(){
    FILE* fp = fopen("test.c","r"); //打开test.c文件,即本文件

    char buf[20];
    fread(buf,1,20,fp);          //读取fp中的数据
    printf("printf the first 20 bytes:");
    printChar(buf,20);

    fseek(fp, 24, SEEK_CUR);    //从当前位置开始偏移
    fread(buf,1,20,fp);
    printf("fseek 24 from now:");
    printChar(buf,20);

    fseek(fp, 24, SEEK_SET);    //从0开始偏移
    fread(buf,1,20,fp);
    printf("fseek 24 from 0:");
    printChar(buf,20);

    fseek(fp, -20, SEEK_END);    //从结束位置向前偏移
    fread(buf,1,20,fp);
    printf("fseek 20 from END:"); 
    printChar(buf,20);

    fclose(fp);         //关闭fp
    return 0;
}

输出结果为

>gcc test.c

E:\Documents\00\1217>a.exe
printf the first 20 bytes://test.c
#include<s
-------------------------
fseek 24 from now:har *buffer, int le
-------------------------
fseek 24 from 0:.h>
void printChar(
-------------------------
fseek 20 from END:
    return 0;
}
r(
-------------------------

文件定位

除了fseek可以对文件指针进行移动之外,fsetpos可以直接对文件指针进行定位。相应地,fgetpos可以获取文件指针的位置,二者声明为:

  • int fgetpos(FILE *stream, fpos_t *pos)
  • int fsetpos(FILE *stream, const fpos_t *pos)

二者的返回值均为设置之后的位置,关于输入参数,FILE *stream是大家非常熟悉的文件流,而fpos_t是一个结构体,代表相对位置,通常定义为

typedef struct
{
    unsigned long _off;
}fpos_t;

接下来做一个简单的测试

//pos.c
#include <string.h>
#include <stdio.h>
int main(void)
{
    fpos_t pos;
    FILE* fp = fopen("pos.c", "r"); //打开pos.c文件,即本文件
    fgetpos(fp, &pos);              //获取指针位置
    printf("file pointer : %ld\n", pos);
    
    fseek(fp,10,0);         // 移动指针位置
    fgetpos(fp, &pos);      // 获取指针位置并存入&pos所指向的对象
    printf("file pointer : %ld\n", pos);
    fclose(fp);
    return 0;
}

结果如下,可见getposfseek的单位是一致的,fseek移动了10个字节,则fgetpos也获取了位置10.

>gcc pos.c
>a.exe
file pointer : 0
file pointer : 10

结合fgetposfsetpos,可完成类似fseek的操作

//setpos.c
#include <string.h>
#include <stdio.h>
int main(void)
{
    fpos_t pos;
    FILE* fp = fopen("pos.c", "r");   //打开pos.c文件,即本文件
    printf("file pointer : %ld\n", fgetpos(fp, &pos));
    
    pos += 10;
    fsetpos(fp, &pos);      //设置指针位置
    printf("file pointer : %ld\n", fgetpos(fp, &pos));
    fclose(fp);
    return 0;
}

测试结果为

>gcc setpos.c
>a.exe
file pointer : 0
file pointer : 10

文件和路径的其他操作

流控制

输入为FILE *stream的单参函数,EOF为文件结束标识符。

返回类型功能
voidclearerr清除stream的文件结束和错误标识符
intfeof返回stream的文件结束标识符,若未设置,则返回0
long intftell返回stream的文件位置,如果发生错误,则返回-1L,全局变量errno被设置为一个正值。
voidrewind设置文件位置为stream的开头
intfgetc
getc
从stream获取下一个字符,并把位置标识符往前移动

缓存

在打开文件后还没有做其他操作的时候,可以通过ssetvbuf来设置缓冲格式,其声明为

int setvbuf(FILE *stream, char *buffer, int mode, size_t size)

其中stream为文件流;buffer为分配给用户的缓冲;size为缓冲的字节数;mode为缓冲模式,有三种

类别说明
_IOFBF0全缓冲输出时,数据在缓冲填满时被一次性写入
输入时,缓冲在请求输入且缓冲为空时被填充。
_IOLBF1行缓冲输出时,在换行处或缓冲填满时写入数据
输入时,缓冲至下一个换行符
_IONBF2无缓冲不使用缓冲。I/O操作即时写入,忽略buffer和size

setbufsetvbuf的一个特例,其中mode_IONBFsizeBUFSIZ

路径操作

stdio.h还提供了两个操作文件的函数,分别是删除文件remove和重命名文件rename,其声明分别为:

  • int remove(const char *filename)
  • int rename(const char *old_filename, const char *new_filename)

get和put

getput有互为反函数的感觉,例如getchar()从标准输入stdin获取一个字符,而putchar(int c)将字符c送到标准输出。

例如

#include <stdio.h>
int main ()
{
   char ch;
   printf("getchar: ");
   ch = getchar();
   printf("putchar: ");
   putchar(ch);
   return 0;
}

测试结果为

>gcc gets.c
>a.exe
getchar: a
putchar: a

为了便于对比阅读,下面令

#define FS FILE *stream
#define IC int char
#define CS char *str
#define CCS const char *str

则与getput有关的函数如下表所示

getfgetputfput
getchar()putchar(IC)
getc(FS)fgetc(FS)putc(IC, FS)fputc(IC, FS)
gets(CS)fgets(CS, int n, FS)puts(CCS)fputs(CCS, FS)
ungetc(IC, FS)

其中,除fgetsgets的返回值为字符指针,其余均为整型,getchar()也会将输入的字符转为整型。

所有的getput函数均用于字符的输入输出,后缀c表示从文件输入或输出到文件,没有c则表示基于标准输入输出流。前缀f表示是否由宏来实现,例如getc通常由宏来实现且经过高度优化,故常作为首选,但目前来说与fgetc在速度相差无几。

ungetc把一个无符号字符推入到指定的流stream中,以便接下来被读取。

接下来做一些简单的示例

//fget.c
#include <stdio.h>
 
int main ()
{
    FILE *fp;
 
    fp = fopen("file.txt", "w+");
    //将字符逐一写入文件
    for(int ch = 50; ch <= 100; ch++)
        fputc(ch, fp);
    fclose(fp);

    //读取刚刚写入的文件
    fp = fopen("file.txt","r");
    while(!feof(fp))
        printf("%c",getc(fp));
    
    fclose(fp);
    return 0;
}

测试为

>gcc fget.c
>a.exe
23456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcd

需要注意的是,C11标准中移除了gets函数,代之以gets_s,区别在于后者对输入字符个数进行了限制。然而,虽然C11已经出了10年了,但存在感不是很高的样子。

printf

printf是绝大多数人接触的第一个C语言函数,但绝大多数人对printf的认识也就到了hello world为止了,大部分人甚至看不懂这个函数的声明:

int printf(const char *format, ...)

关键是字符串格式比较复杂,可以表示为%format[flags][m.n]specifier

例如

#include<stdio.h>
#define PI 3.14159265358979323846264338327950288

int main(){
	printf("%6.2f",PI);
	return 0;
}

其输出为

E:\Documents\00\1110>a.exe
  3.14

其中,%6.2f可分为3部分,6表示输出六个字符;.2表示保留2位小数;f表示输出的是浮点数。由于PI在保留2位小数之后,只有4个字符,所以在3.14左侧补上了空格。

各参数的取值与含义可见于下表,如果觉得不够直观,可直接跳到测试部分。

specifier格式化符号
整数%d〗〖%i〗〖无符号%u
无符号不同进制〖八进制%o〗〖十六进制%x〗〖大写十六进制%X
浮点数%f〗〖科学计数法%e, %E〗〖%g%f%e中较短的那个〗
字符和字符串〖字符%c〗〖字符串%s
指针%p输出指针地址

其中,

  • i, d, o, u, x, X可通过h修饰,从而输出短整型;通过l修饰,从而输出长整型
  • e, E, f, g, G可通过l修饰,表示长双精度型,但在Windows下可能没什么差别。
flags标识描述
-左对齐,默认是右对齐
+强制显示正负号
#与 o、x、X连用时,非零值前面分别显示0、0x 或 0X
与e、E、f连用时,强制包含小数点
与g、G连用时,结果与e、E时相同,但不会移除尾部的零
0在数字左边补充0

m.n中,m表示输出字符的最小数目,若字符长度短于m,则用空格填充;n表示小数点后的位数。

下面随机抽选一些表达式,列出其输出结果,事先声明一些变量

#define PI 3.14159265358979323846264338327950288
#define IPI 31415926
int main(){
    float fPI = PI;
    double dPI = PI;
    printf("%.10f",fPI);
    return 0;
}

浮点型测试

printf输出说明
"%.10f",fPI3.1415927410float一般只有6位精度
"%.10f",dPI3.1415926536double可保证15位精度
"%010.5f",dPI0003.141595位精度,10个字符,左侧补0
"%015.5E",dPI*1e50003.14159E+005width指字符个数
"%#010.0f",dPI*1e5000314159.#强制输出小数点
"%+f",dPI+3.141593+强制输出正号
"%.2G",dPI3.1
"%.2G",dPI*1e103.1E+010此时显然E模式更短
"%010.2G\n%010.4G",dPI00000003.1
000003.142
默认右对齐
"%-010.2G\n%-010.4G",dPI3.1
3.142
-模式下左对齐,所以0没了

整型测试

printf输出说明
%u,IPI31415926作为无符号整型输出
%+d,IPI31415926+模式强制显示正号
%o,IPI167657166输出为8进制
%x,IPI1df5e76输出为十六进制
%#X,IPI0X1DF5E76#模式下显示0X

stdio中,定义了一系列printf函数和scanf函数,通过在前面添加一个字母来表示不同的行为:

输出位置使用参数列表读取输入
标准输出(屏幕)printfvprintfscanf
streamfprintfvfprintffscanf
字符串sprintfvsprintfsscanf

为了便于阅读,下面默认

#define FORMAT const char *format

printf族函数声明分别为

  • int fprintf(FILE *stream, FORMAT, ...)
  • int sprintf(char *str, FORMAT, ...)
  • int vfprintf(FILE *stream, FORMAT, va_list arg)
  • int vprintf(FORMAT, va_list arg)
  • int vsprintf(char *str, FORMAT, va_list arg)
  • int snprintf(char *str, size_t size, FORMAT, ...)

其中snprintf中的size表示要写入字符的最大数目,超过size会被截断。

scanf族函数声明为

  • int fscanf(FILE *stream, FORMAT, ...)
  • int scanf(FORMAT, ...)
  • int sscanf(cSTR, FORMAT, ...)

stdlib.h

内存分配

最简单的内存分配函数为void *malloc(size_t size),输入内存字节数,返回指向这篇内存区域的指针,如果创建失败,则返回NULL

void *calloc(size_t nitems, size_t size)malloc的基础上对内存空间进行了初始化,令所有字节均置零;此外,还可以分配多块内存,nitems就表示所分配的内存块数。

void *realloc(void *ptr, size_t size)用于重新分配内存,其中ptr是一个指向已经被分配的内存的指针。

最后,通过free可以释放由malloc, calloc, realloc等分配的空间。

系统交互

一般情况下,在C语言中退出一个程序用return,如果在main函数中,return在清理局部对象之后,会调用exit函数。

return相比,exit并不会销毁局部对象,而是会销毁所有静态与全局对象、清空缓冲区,关闭IO通道。终止前则会调用atexit()所定义的函数。

atexit也在stdlib.h中,其声明式为

int atexit(void (*func)(void))

当程序中止时,就会调用函数指针func。

//atexit.c
#include <stdio.h>
#include <stdlib.h>

void funcA (){
   printf("call from atexit\n");
}

int main (){
   atexit(funcA);   //将funcA注册为中止函数
   printf("exiting...\n");
   return 0;
}

测试结果如下

>gcc atexit.c
>a.exe
exiting...
call from atexit

如果希望什么都不做就退出程序,可以使用abort(),将abort();插入到return 0前面,则其输出结果如下,并不会调用funcA

>a.exe
exiting...

system函数

system,顾名思义,就是调用系统命令行,其输入为字符串,然后把这个字符串输出给命令行,让命令行执行。

为了测试其特性,可以做一个小程序

//system.c
#include<stdlib.h>
#include<stdio.h>
#include<string.h>

int main(){
	char cmd[100];
	while(1){
		printf("input code: ");
		gets(cmd);
		if(strcmp(cmd,"exit")==0)
			break;  //当输入exit时退出
		system(cmd);
	}
	return 0;
}

然后开始

>gcc system.c
>a.exe
input code: asdfasdf
'asdfasdf' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
input code: date
当前日期: 2021/12/19 周日
输入新日期: (年月日)
input code: date
当前日期: 2021/12/19 周日
输入新日期: (年月日)
input code: HELP
有关某个命令的详细信息,请键入 HELP 命令名
ASSOC          显示或修改文件扩展名关联。
ATTRIB         显示或更改文件属性。
BREAK          设置或清除扩展式 CTRL+C 检查。
BCDEDIT        设置启动数据库中的属性以控制启动加载。
# 由于太长,且和命令行中输入HELP的结果是一样的,所以这里就省略了
有关工具的详细信息,请参阅联机帮助中的命令行参考。
input code: exit        #退出

通过system,可以做一个增强版的命令行。

而除了这些终端提供的命令之外,可能还需要一些自定义的语句,这些语句都被存放在环境变量中,getenv可以获取名字对应的环境变量

char *getenv(const char *name)

例如

#include <stdio.h>
#include <stdlib.h>

int main ()
{
   printf("PATH : %s\n", getenv("PATH"));
   return 0;
}

其运行结果为

E:\Documents\00\1220>a.exe
PATH : C:\Program Files\Microsoft\jdk-11.0.12.7-hotspot\bin;C:\Python310\Scripts\;C:\Python310\;C:\Program Files\Common Files\Oracle\Java\javapath;D:\CS\ImageMagick;(x86)\Common Files\Intel\Shared 
....//因为太多所以后面的就不写了

字符串函数

stdlib中定义了6个将字符串转为数值的函数,分别用于浮点、整型、长整型和无符号长整型,为了书写上的便捷,下文中令

#define cSTR const char *str
#define END char **endptr

其中,str为一个字符串,endptr则为字符串指针,则stdlib.h中的字符串转换函数如下表所示

返回值类型简单高级
doubleatof(cSTR)strtod(cSTR, END)
intatoi(cSTR)
long intatol(cSTR)strtol(cSTR, END, int base)
unsigned long intstrtoul(cSTR, END, int base)

其中,若endptr不为空,则会保存转换数值之后的指针位置;base介于2和36(包含)之间,表示转换整型的基数。

此外,stdlib中还有4个用于不同宽度的字符数组之间的转化函数,主要是char类型和wchar_t之间的转化。

在C++中,除了char作为字符类型的保留字之外,还有三个不同宽度的字符类型作为表达式:wchar_t, char16_t, char32_t;到了C++11,又新增了char16_tchar32_t

在C语言中,尽管只有一个char是保留字,单并不妨碍定义其他数据类型,wchar_t被定义在stddef.h中,本质上是一个int。字符串和wchar_t之间的转换函数包括

  • size_t mbstowcs(wchar_t *pwcs, cSTR, size_t n):str转为pwcs
  • size_t wcstombs(char *str, const wchar_t *pwcs, size_t n):pwcs转为str
  • int wctomb(char *str, wchar_t wchar):wchar转为str
  • int mbtowc(whcar_t *pwc, cSTR, size_t n):str转为pwc

此外,stdlib.h中还定义了一个计算字符串长度的函数mblen,和string.h中的strlen的区别在于,mblen中多了一个最大字节数的参数n

int mblen(cSTR, size_t n)

数学函数和算法

stdlib中封装了四个用于整型或长整型的数学函数,包括两个绝对值函数abslabs,分别用于整型和长整型;两个除法divldiv,也是分别用于整型和长整型。

此外,还有一个随机数生成器rand(),将会生成一个范围在 0 到 RAND_MAX 之间的伪随机数。若想指定随机数种子,可使用函数srand(unsigned int seed)

stdlib中还有两个基本的查找和排序函数,分别是二分查找bsearch、伪快排qsort

排序

stdlib.h中的排序函数为

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))

其中,

  • base为指向数组第一个元素的指针
  • nitems为待排序数组中元素个数
  • size为数组中每个元素的字节数
  • compar是个函数指针,为用于比较两个元素的函数

其中,用于比较元素的compar符合我们的直觉,可定义为

int cmpInt (const void * a, const void * b);

当其返回值大于0时,表示a>b,等于0时,表示a=b,小于0时表示a<b

这种函数指针出现在C语言标准库中感觉还是很炫酷的,连带着让qsort也显得高级感十足,下面举个例子来测试一下

//sortList.c
#include <stdio.h>
#include <stdlib.h>

int cmpInt (const void * a, const void * b){
   return (*(int*)a - *(int*)b);
}

void printList(int *lst, int N){
   for(int n = 0 ; n < N; n++ )
      printf("%d ", lst[n]);
    printf("\n");
}

int main(){
    int N = 8;
    int lst[N];
    for(int i=0; i<N; i++)
        lst[i] = rand();
    printf("the original list: ");
    printList(lst, N);
    
    qsort(lst, N, sizeof(int), cmpInt);
    printf("the qSorted list: ");
    printList(lst, N);
    
    return 0;
}

输出结果为

>gcc sortList.c
>a.exe
the original list: 41 18467 6334 26500 19169 15724 11478 29358
the qSorted list: 41 6334 11478 15724 18467 19169 26500 29358

二分查找

所谓二分查找,前提是有一个有序数组。假设这个数组是升序数组,那么先将这个数组从中间分开,变成两份,此之谓二分。然后用中间值和待查元素比较,如果中间值大于待查元素,那么就舍弃大份,用小份和待查元素继续二分查找,知道分完或者找到待查元素。

stdlib.h中的二分查找函数为

void *bsearch(const void *key, const void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *))

其中,

  • key指向要查找的元素
  • base指向被查数组的第一个对象
  • nitems为数组中元素个数
  • size为数组中每个元素的字节数
  • compar用来比较两个元素的函数,和qsort中相似。

如果keybase数组中,则返回数组中等于key的值的指针,否则返回NULL

举个例子

//searchList.c
#include <stdio.h>
#include <stdlib.h>

int cmpInt (const void * a, const void * b){
   return (*(int*)a - *(int*)b);
}

void printPos(int* lst, int* pos, int key){
    if(pos == NULL)
        printf("%d is not in lst\n", key);
    else
        printf("Found %d at %d\n", *pos, pos-lst);
}

int main(){
    size_t N = 6;
    int lst[N];
    for(int i=0; i<N; i++)
        lst[i] = rand();
    int *p;

    //对乱序lst中的值进行查找
    printf("----search from unsorted values-----\n");
    for(int i = 0; i<N; i++){
	   p = (int*)bsearch(lst+i, lst, N, sizeof(int), cmpInt);
	   printPos(lst, p, lst[i]);
    }

    //对排序后的list进行查找
    printf("---------search from sorted values---------\n");
    qsort(lst, N, sizeof(int), cmpInt);
    for(int i = 0; i<N; i++){
	   p = (int*)bsearch(lst+i, lst, N, sizeof(int), cmpInt);
	   printPos(lst, p, lst[i]);
    }

    //对不在lst中的值进行查找
    printf("------search rand value ----------\n");
    int rd;
    for(int i = 0; i<3; i++){
        rd = rand();
        p = (int*)bsearch(&rd, lst, N, sizeof(int), cmpInt);
        printPos(lst, p, rd);
    }
   
   return 0;
}

得到其结果为

>gcc test.c
>a.exe
----search from unsorted values-----
Found 41 at 0
18467 is not in lst
Found 6334 at 2
26500 is not in lst
Found 19169 at 4
15724 is not in lst
---------search from sorted values---------
Found 41 at 0
Found 6334 at 1
Found 15724 at 2
Found 18467 at 3
Found 19169 at 4
Found 26500 at 5
------search rand value ----------
11478 is not in lst
29358 is not in lst
26962 is not in lst

必须要注意的一点是,二分查找输入的数组必须是有序的,bsearch只会机械地按照比较函数进行二分,而并没有排序的义务。如果输入是乱序,那么可事先通过qsort进行排序。

string.h

为了看上去规整简洁,令

#define cSTR const char *str
#define vSTR const void *str

由于字符串自身存在终止符\0,所以本节所有提及对字符串前n个字符的操作,均默认n小于字符串长度;若n大于字符串长度,则对字符串整体进行操作。

C语言中的字符串无非是终止于\0的字符数组,但并未提供任何长度信息,所以要有一个函数来计算字符串长度,此即

size_t strlen(cSTR)

然后认识一下用于分割字符串的strtok

char *strtok(char *str, const char *delim)

其功能为通过delimstr分解为一组小字符串,所谓分解,其实就是将分割符号替换为\0;然后返回被分割之后的第一个字符串。

strtok对字符串分割之后,可通过表达式strtok(NULL, delim),将被分割后的字符串逐一调出。

//teststrtok.c
#include<stdio.h>
#include<string.h>

int main(){
	char oldStr[50] = "I am tiny cold";
	const char s[2] = " ";
	printf("str length: %d\n", strlen(oldStr));
	char* newStr = strtok(oldStr,s);
	while(newStr != NULL){
		printf("the newStr is 【%s】 with length : %d\n", newStr, strlen(newStr));
		newStr = strtok(NULL, s);
	}
	return 0;
}

测试结果为

>gcc testStrtok.c
>a.exe
str length: 20
the newStr is 【I】 with length : 1
the newStr is 【am】 with length : 2
the newStr is 【tiny】 with length : 4
the newStr is 【cold】 with length : 4     

接下来将string.h中的函数分为四个类别,分别是查询比较复制和追加以及本地函数

由于字符串是存在长度的,所以在本文中,所有字符串中前n个字符中的n默认不大于字符串长度。

查询函数

查询函数返回类型
void *memchr(vSTR, int c, size_t n)strn个字节中首次出现c的位置指针
char *strchr(cSTR, int c)str首次出现c的位置指针
char *strrchr(cSTR, int c)str最后出现c的位置指针
char *strstr(cSTR1, cSTR2)str1中首次次出现字符串str2的位置指针
char *strpbrk(cSTR1, cSTR2)str1中首个个属于str2的字符的位置指针
size_t strspn(cSTR1, cSTR2)str1中第一个不属于str2的字符的索引整数
char *strerror(int errnum)根据错误号errnum索引错误名
详见errno.h

比较函数

比较函数返回
int memcmp(vSTR1, vSTR2, size_t n)比较str1str2的前n个字节
int strncmp(cSTR1, cSTR2, size_t n)比较str1str2的前n个字符
int strcmp(cSTR1, cSTR2)比较str1str2
size_t strcspn(cSTR1, cSTR2)str1开头连续不含str2中字符的个数

注意

有关字符串str1str2的比较中:

  • 若二者相等,则返回0
  • str1<str2,则返回值小于0
  • str1>str2,则返回值大于0

复制和追加

下面用于字符串复制的函数,均返回一个指向目标字符串的指针。

复制
char *strcpy(char *dest, vSTR)str复制到dest
char *strncpy(char *dest, cSTR, size_t n)strn个字符复制到 dest
void *memset(void *str, int c, size_t n)str的前n个字符设为c
void *memcpy(void *dest, vSTR, size_t n)
void *memmove(void *dest, vSTR, size_t n)
str的前n个字节复制到dest

其中,strncpy是对字符串的操作,而memcpy是对内存块的操作。

如果内存块发生重叠,memmove可以保证源字符串被覆盖之前,将重叠区域的字节复制到目标区域,所以比memcpy更加安全。

所谓追加,无非是将一个字符串的内容复制到另一个字符串的结尾,本质上也可称为广义的复制。

追加
char *strcat(char *dest, cSTR)str追加到dest结尾。
char *strncat(char *dest, cSTR, size_t n)str的前n个字符追加到dest结尾

本地函数

所谓本地函数,就是受到locale.hLC_COLLATE影响的函数,其功能和返回值取决于当前所在的地区。string.h中共有两个本地函数,分别是用于字符串比较的strcoll和用于更改字符串格式的strxfrm

对于前者,若我们想对一组汉字按照拼音进行排序,那么就要用到strcoll,其声明为

int strcoll(cSTR1, cSTR2)

做一个测试

//testStrcoll.c
#include <stdio.h>
#include <string.h>
#include <locale.h>

void printStrcoll(const char *str1, const char *str2){
    char flag = strcoll(str1,str2)>0 ? '>' : '<';
    printf("%s%c%s\n",str1, flag, str2);
}

int main (void)
{
    printf ("默认比较:");
    printStrcoll("甲","乙");

    setlocale (LC_ALL, "");
    printf ("拼音比较:");
    printStrcoll("甲","乙");

    return 0;
}

由于我实在不熟悉汉字编码顺序,所以选择了一个不太能说明问题的两个字符,其结果为

>gcc testStrcoll.c
>a.exe
默认比较:甲>乙
拼音比较:甲>

需要注意,本程序用的是gcc11.2进行编译的,低于10的版本可能会汉字乱码。

另一个本地函数声明为

size_t strxfrm(char *dest, cSTR, size_t n)

其功能就是简单的将str的前n个字符转换为本地形式后复制到dest中,返回值是被转换的字符长度。

ctype.h

作为强类型语言,C语言自其诞生以来,就通过类型来为初学者增加重重阻碍。为了避免各种麻烦,早在上古时期,C语言的程序员们就写了大量判别变量类型的代码段,这些代码段也很快就走进了标准库。

ctype中共有11个鉴别字符类型的函数,这些函数的输入输出均为int型,但又不完全是int型。输入的int是通过强转得到的;输出为0和1,即布尔型。

返回1的情况ASCII码位置十六进制
isalnum字母和数字48-57, 65-90, 97-12230-39, 41-5A, 61-7A
isalpha字母65-90, 97-12241-5A, 61-7A
isupper大写字母65-9041-5A
islower小写字母97-12261-7A
iscntrl控制字符0-31,12700-1F, 7F
isdigit十进制数字48-5730-39
isxdigit十六进制数字48-57, 48-53, 97-10230-39,41-46, 61-66
isgraph图形字符33-12621-7E
isprint可打印32-12620-7E
ispunct标点符号33-47, 58-64
91-96, 123-126
21-2F, 3A-40
5B-60, 7B-7E
isspace空白字符9-13,3209-0D, 20

判别函数之间可由下图表示

isxdigit
A-F a-f
isdigit
isprint
isgraph
isalnum
0-9
isalpha
isupper
A-Z
islower
a-z
ispunct
! # $ % & '等
space
isspace
FF NL CR HT VT
iscntrl
BEL BS

此外,ctype中还封装了两个大小写转换的函数:int tolower(int c)int toupper(int c),这两个函数很容易实现,只要看一下十六进制下的ASCII即可发现。

前32个字符为控制符

0123456789ABCDEF
0NULSOHSTXETXEOTENQACKBELBSHTLFVTFFCRSOSI
1DLEDC1DC2DC3DC4NAKSYNETBCANEMSUBESCFSGSRSUS

32-126为可打印字符,其中20为空格,大小写字母正好差了0x20

0123456789ABCDEF
2!"#$%&()*+,-./
30123456789:;<=>?
4@ABCDEFGHIJKLMNO
5PQRSTUVWXYZ[\]^_
6`abcdefghijklmno
7pqrstuvwxyz{|}~DEL

math.h

double型的数学运算包,封装了最常用的一些数学函数,其中三角函数均用弧度制。

不太需要解释的函数
三角函数acos, asin, atan, cos, cosh, sin, atan2(y,x)= arctan ⁡ y x \arctan\frac{y}{x} arctanxy
双曲函数cosh, sinh, tanh
指数对数exp, log, log10, sqrt, pow(x,y)= x y x^y xy
取整函数向上取整ceil,向下取整floor
其他函数绝对值fabs, 求余fmod, ldexp(x, y)= x × 2 y x\times2^y x×2y

modf(double x, double *integer)返回值为小数部分,并设置 integer 为整数部分。例如令x=2.5,则输出为0.5,然后让integer=2

frexp(double x, int *exponent)可将x分解成尾数和指数,返回值是尾数,并将指数存入exponent中。所得的值是 x = mantissa * 2 ^ exponent。

#include<stdio.h>
#include<math.h>

int main(){
	float y,x;
	int a;
	x = 25.5;
	y = frexp(x,&a);
	printf("%f=%f*2^%d",x,y,a);
	return 0;
}

运行之后得到

>gcc testMath.c
>a.exe
>25.500000=0.796875*2^5 (x=y*2^a)

时间与货币

time.h

一般计算机中通过时间戳来代表时间,所谓时间戳就是从1970年11日0时0分0秒开始到当前时刻所经历的秒数。在C语言中,time_t就是专门表示这个时间戳的数据类型,在如今的操作系统中,一般time_t都是64位整型。

函数time()可以获取当前的时间戳,并将其编码位time_t格式。通过ctime函数,可将时间戳转为便于阅读的年月日字符串。

例如

#include<stdio.h>
#include<time.h>

int main(){
	time_t seconds = time(NULL);            //获取当前时间戳
	printf("timestamp: %ld\n", seconds);    //输出时间戳
	printf("time:%s\n", ctime(&seconds));   //输出时间字符串
	return(0);
}

编译运行之后为

>gcc testTime.c
>a.exe
timestamp: 1639375539
time:Mon Dec 13 14:05:39 2021   

time_t相似,time.h中还设置了clock_t用来描述处理器时间,通过无参数函数clock()可以直接返回一个clock_t类型的数据。

time.h中还定义了tm结构,用于存储日期和时间。

struct tm {
   int tm_sec;         // 秒,范围从 0 到 59
   int tm_min;         // 分,范围从 0 到 59
   int tm_hour;        // 小时,范围从 0 到 23
   int tm_mday;        // 一月中的第几天,范围从 1 到 31
   int tm_mon;         // 月,范围从 0 到 11
   int tm_year;        // 自 1900 年起的年数
   int tm_wday;        // 一周中的第几天,范围从 0 到 6
   int tm_yday;        // 一年中的第几天,范围从 0 到 365
   int tm_isdst;       // 夏令时
};

gmtimelocaltime函数可将时间戳转为tm结构,前者返回的是格林尼治标准时间,后者返回的是本地时间。然后,和ctime相似,asctime可以把tm结构转为方便阅读的字符串。

反过来,通过mktime可将tm结构按照本地时间的方式转化为time_t格式的时间戳。

测试程序如下

#include<stdio.h>
#include<time.h>

int main(){
	time_t seconds = time(NULL);
	struct tm *tLocal;
	tLocal = localtime(&seconds);
	printf("Local: %2d:%02d\n",tLocal->tm_hour, tLocal->tm_min);
	printf("Local asctime: %s\n", asctime(tLocal));
	return(0);
}

输出为

Local: 14:26
Local asctime: Mon Dec 13 14:26:45 2021    

如果对ctime的返回值不满意,还可以通过strftime函数自定义时间格式,其声明为

size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)

其中,timeptr为传入的tm指针;str为为时间日期字符串,用于输出;maxsize表示被复制到str的最大字符个数。而输入参数中,最关键的则是format这个字符串。

format中,每个字符的含义如下

星期%a为缩写星期名称,%A为完整星期名称, %w表示星期(星期日为0)
星期%U, %W表示一年中的第周,二者分别以首个周日、首个周一为首周首日
月份%b为缩写月份名称,%B为完整月份名称,%m表示第
年份%y为年份后两个数字,%Y为完整年份
日期%d为一月中的第天,%j为一年中的第
小时%H为24小时格式,%I为12小时格式
分秒%M表示分,%S表示秒
符号%p表示’AM’或’PM’,%Z表示时区的名称或缩写,%%为%转义
格式%x为日期表示法:08/19/12%X为时间表示法:02:50:06
%c为日期和时间表示法 Sun Aug 19 02:56:02 2012

locale.h

locale.h对时间和货币的书写格式进行了封装,从而符合不同地区的使用习惯,故而locale.h中设计了两个用于本地化的函数

char *setlocale(int category, const char *locale)
struct lconv *localeconv(void)

前者用于设置或读取本地化信息,locale即代表某个区域的字符串,category代表将要设置的函数类别,其输入参数包括

说明影响的函数
0LC_ALL下面的所有选项
1LC_COLLATE字符串比较strcoll 和 strxfrm
2LC_CTYPE字符分类和转换所有字符函数
3LC_MONETARY货币格式localeconv()
4LC_NUMERIC小数点分隔符localeconv()
5LC_TIME日期和时间格式strftime()
6LC_MESSAGES系统响应

其中,localeconv()函数被封装在locale.h中,其返回值是一个lconv结构,主要用于描述货币的表示方法,每个字段的含义在下面的注释中说明。在注释中,cs表示当前区域的货币符号。

typedef struct {
   char *decimal_point;     //常规数值的小数点字符
   char *thousands_sep;     //常规数值的千位分隔符
   char *grouping;          //常规数值中每组数字大小的字符串
   char *int_curr_symbol;   //国际货币符号使用的字符串。前三个字符由 ISO 4217:1987 指定,第四个字符用于分隔货币符号和货币量。
   char *currency_symbol;   //当前区域的货币符号,后文用cs表示
   char *mon_decimal_point; //货币的小数点字符
   char *mon_thousands_sep; //货币的千位分隔符
   char *mon_grouping;      //货币数值中每组数字大小的字符串
   char *positive_sign;     //货币的正号
   char *negative_sign;     //货币的负号
   char int_frac_digits;    //国际货币值中小数点后要显示的位数
   char frac_digits;        //货币值中小数点后要显示的位数。
   char p_cs_precedes;      //cs在正货币值中的位置
   char p_sep_by_space;     //cs与正货币值之间是否使用空格
   char n_cs_precedes;      //cs在负货币值中的位置
   char n_sep_by_space;     //cs与负货币值之间是否使用空格
   char p_sign_posn;        //表示正货币值中正号的位置
   char n_sign_posn;        //表示负货币值中负号的位置
} lconv

其中,groupingmon_grouping均为字符串,分别表示在常规数值和货币中每组数字大小。字符串中每个字符都代表一个整数,用以指定当前组的位数。

当前区域的货币符号cs与货币值之间的排版方式为

值为1值为0
p_cs_precedescs在正货币值之前cs在正货币值之后
p_sep_by_spacecs和正货币值之间用空格cs和正货币值之间不使用空格
n_cs_precedescs在负货币值之前cs在负货币值之后。
n_sep_by_spacecs和负货币值之间使用空格cs和负货币值之间不使用空格

货币中正负号的位置通过p_sign_posnn_sign_posn来调节,二者均有5个取值。

对于-1美元,其n_sign_posn取值从0-5,n_cs_precedes分别为0,1时,表示方法分别如下

01234
0(1.00$)-1.00$1.00$-1.00-$1.00$-
1($1.00)-$1.00$1.00--$1.00$-1.00
说明括号数值和cs之前数值和cs之后cs之前cs之后

单功能库

C语言标准库中有很多十分简单,故统一归类到单一功能库下,包括

stdarg.h可变参数支持
stddef.h指针相减
setjmp.h跳转

assert.h

经典断言

感觉现在很多人都不用assert了,毕竟IDE都这么智能,assert的功能也非常简单,如果输入一个0(False),那么就会打印一条错误信息。

#include<stdio.h>
#include<assert.h>

int main(){
	assert(1);
	assert(0);
}

这样一个简单的案例,在命令行中通过gcc编译,果然在第6行报错了。

E:\Documents\00\1101>gcc assert1.c

E:\Documents\00\1101>a.exe
Assertion failed: 0, file assert1.c, line 6

查看insert.h的源文件,会发现下面几行代码

#undef assert       //取消已有的assert定义
#ifdef NDEBUG
#define assert (test) ((void)0)
#else
#define assert (test) ...
#endif

这说明,当NDEBUG被定义的时候,会将assert自动转化为一个空函数,从而取消断言。

gcc-D指令可以添加宏代码,则下面的代码并不会报错

E:\Documents\00\1101>gcc -DNDEBUG assert1.c
E:\Documents\00\1101>a.exe

C11静态断言

assert非常简单,简单到可以写成条件语句

void assertIf(int i){
    if(!i){
        printf("error");
        abort();            //自stdlib中调用,可终结函数
    }
}

assert的好处是,可以自动返回出错的行数,并且能够通过define NDEBUG将其禁止。不足之处在于,只能在运行时执行。C11推出了一个新的函数_Static_assert,顾名思义为静态断言,可以在编译时执行:

//sa.c
#include<assert.h>
int main(){
	_Static_assert(0);
}

结果为

E:\Documents\00\1101>gcc sa.c
sa.c: In function 'main':
sa.c:10:2: error: static assertion failed: "ERROR"
  _Static_assert(0,"ERROR");

stdarg.h

stdarg.h提供了C语言对可变参数的支持,先举一个简短的例子

//testStdArg.c
#include <stdarg.h>
#include <stdio.h>

void printIntList(int N, ...){
    va_list args;       //存放...所代表的参数
    va_start(args, N);  //初始化变量args
    for (int idx = 1; idx <= N; ++idx)
        printf("param %d: %d, ", idx, va_arg(args, int));
    printf("-----\n");
    va_end(args);
}

int main(void)
{
    printIntList(4,1,2,3,4);
    printIntList(4,1,2,3);
    printIntList(3,1,2,3,4);
}
>gcc testStdArg.c
>a.exe
param 1: 1, param 2: 2, param 3: 3, param 4: 4, 
-----
param 1: 1, param 2: 2, param 3: 3, param 4: 4, 
-----
param 1: 1, param 2: 2, param 3: 3, 
-----

其中,va_liststdarg.h中声明的数据类型,用以存放...所代表的参数,在printIntList中,定义了va_list类型的args用于存储变量。

stdarg.h中声明了三个函数,在上面的案例中都用上了,下面逐一解析

void va_start(va_list ap, last_arg)

  • 用于初始化ap变量,last_arg为最后一个参数的下标
  • 对于函数printIntList(4,1,2,3,4)而言,总共输入了5个参数,故其last_arg应该为4。

type va_arg(va_list ap, type)

  • 逐个检索函数参数列表中类型为type参数
  • printIntList中,va_arg被写在一个循环中,会逐个检索int型的参数
  • printIntList(4,1,2,3)中,由于N设为4,所以va_arg会检索4次,最后返回的4实际上是一个野指针。

void va_end(va_list ap)

  • 在参数调用结束之后,应该调用va_end来释放ap

stddef.h

定义了三个数据类型:size_twchar_tptrdiff_t

其中size_tsizeof的结果,一般是long unsigned intwchar_t用于描述宽字符,一般是int

ptrdiff_t是指针相减的数据类型,stddef.h还定义了一个宏函数offsetof,用于确定结构的某个成员到起始位置的偏移字节。

对于前者,例如

#include <stdio.h>
#include <stddef.h>

int main(){
    char x[20];
    ptrdiff_t nx = &x[5]-&x[0];
    printf("%d\n", nx);    //输出为5
    long int y[20];
    ptrdiff_t ny = &y[5]-&y[1];
    printf("%d\n", ny);    //输出为4
    return(0);
}

通过指针相减,当然可以得到结构中某个字段距离结构体初始指针的距离

#include <stdio.h>
#include <stddef.h>
typedef struct TEST{
    int a,b;
}test;

int main(){
    test x;
    static size_t off = (char*)&x->b - (char*)&x;
}

如果想在不创建示实例的情况下得到字段b到test初始值的偏离,可以可以采用offsetof函数

static size_t off = offsetof(test, b)

setjmp.h

setjmp.h中有两个函数,分别是setjmplongjmp

setjmp相当于回城标记,而longjmp则是个传送阵。调用longjmp会跳转到setjmp所在的位置——就如goto一样,但goto只能在函数内跳转,longjmp则可跨越函数。

setjmp一般被写为

int setjmp(jmp_buf env)

其输入参数env可理解为是系统地图,setjmp将当前位置刻录在地图中。jmp_buf是一个结构体,用来描述当前的地图。

那么当程序继续运行,想要回城的时候,被调用的longjmp也不可避免地需要导入这个地图——env。此外,我们不能平白无故地回城,必须得传达一些信息,longjmp这个传送阵允许我们传送一个整型,其定义为

void longjmp(jmp_buf env, int value)

当使用longjmp之后,就会跳转到最近一次执行的setjmp所在行,同时把value传递给setjmp,作为setjmp的返回值。

接下来举一个最直观的例子

//testJmp.c
#include <stdio.h>
#include <setjmp.h>
int main(){
    jmp_buf env;
    int ret;
    ret = setjmp(env);
    printf("ret=%d\n",ret);
    if(ret==0){
        printf("we'll execute the longjmp and set env=1\n");
        longjmp(env,1);
    }else
        printf("we'll not excute the longjmp because env=1");
    return 0;
}

编译运行的结果为

>gcc testJmp.c
>a.exe
ret=0
we'll execute the longjmp and set env=1
ret=1
we'll not excute the longjmp because env=1

常量库

  • float.h,描述浮点数并提供了浮点算术的基本信息
  • limits.h,描述整型并提供了整数计算的基本信息
  • errno.h,表明错误类型
  • signal.h:定义了信号宏

float.h

float.h用以描述浮点数并提供了浮点算术的基本信息,这些信息大多是通过宏的形式定义的。

在C语言中,一个浮点数可以表示为

x = s b e ∑ k = 1 p f k b − k , e min ⁡ < e ≤ e max ⁡ x=sb^e\sum_{k=1}^pf_kb^{-k}, e_{\min}<e\leq e_{\max} x=sbek=1pfkbk,emin<eemax

其中, s s s为符号,取值为 ± 1 \pm1 ±1 b b b是大于1的整数,表示基数; e e e为指数; p p p为精度; f k f_k fk表示有效数字。记 M = ∑ k = 1 p f k b − k M=\sum_{k=1}^pf_kb^{-k} M=k=1pfkbk,则可写为

x = s × M × b e , e min ⁡ < e ≤ e max ⁡ x=s\times M\times b^e, e_{\min}<e\leq e_{\max} x=s×M×be,emin<eemax

float.h中,FLT_RADIX表示基数,即b,这个b对所有浮点数类型都有效。而float.hdefine的值中,绝大多数可分为三类,分别对应floatdoublelong double类型:

floatdoublelong double简要说明
FLT_MANT_DIGDBL_MANT_DIGLDBL_MANT_DIGb进制下M的最大长度
FLT_DIG( ≥ 6 \ge6 6)DBL_DIG( ≥ 10 \ge10 10)LDBL_DIG( ≥ 10 \ge10 10)小数点后精确数字位数
FLT_MIN_EXPDBL_MIN_EXPLDBL_MIN_EXPb进制下e的最小值
FLT_MAX_EXPDBL_MAX_EXPLDBL_MAX_EXPb进制下e的最大值
FLT_MIN_10_EXPDBL_MIN_10_EXPLDBL_MIN_10_EXP十进制下 e min ⁡ ≤ − 37 e_{\min}\le-37 emin37
FLT_MAX_10_EXPDBL_MAX_10_EXPLDBL_MAX_10_EXP十进制下 e max ⁡ ≥ 37 e_{\max}\ge37 emax37
FLT_MAXDBL_MAXLDBL_MAX浮点数最大值 ⩾ 1 0 37 \geqslant10^{37} 1037
FLT_MINDBL_MINLDBL_MIN浮点数最小值 ⩽ 1 0 − 37 \leqslant10^{-37} 1037
FLT_EPSILON
( ⩽ 1 0 − 5 \leqslant10^{-5} 105)
DBL_EPSILON
( ⩽ 1 0 − 9 \leqslant10^{-9} 109)
LDBL_EPSILON
( ⩽ 1 0 − 9 \leqslant10^{-9} 109)
最小差值

其中,XXX_EPSILON表示1和大于1的最小浮点数之间的差值。

此外,还有三个独立的常量:

FLT_ROUNDS表示浮点加法的舍入模式:

  • -1:无法确定
  • 0:趋向于零
  • 1:趋向最近的值
  • 2:趋向于正无穷
  • 3:趋向于负无穷

FLT_EVAL_METHOD:指明表达式求值时是否需要提升浮点数类型

  • -1:不确定
  • 0:使用当前类型
  • 1:将float提升到double
  • 2:将浮点数提升到long double

DECIMAL_DIG:用于long double序列化和反序列化时的十进制精度。简单地说,以不损失精度为前提,能够将long double转换成至少DECIMAL_DIG个十进制数字;反过来,也能将至少DECIMAL_DIG个十进制数字转换成long double

limits.h

float.h类似,limits.h中定义了整型数据的取值范围,对于char类型,还额外给出了CHAR_BIT表示char所包含的位数,取值大于等于8,一般为8。另有MB_LEN_MAX表示一个字节字符最多能包含的字节数,需大于等于1。

类型最大值最小值一般取值
charCHAR_MAXCHAR_MIN
singed charSCHAR_MAXSCHAR_MIN ± 127 \pm127 ±127
unsinged charUCHAR_MAX 255 255 255
short intSHRT_MAXSHRT_MIN ± 32767 \pm32767 ±32767
unsigned
short int
USHRT_MAX 65536 65536 65536
intINT_MAXINT_MIN ± 32767 \pm32767 ±32767
unsigned intUINT_MAX 65536 65536 65536
long intLONG_MAXLONG_MIN ± ( 2 31 − 1 ) \pm(2^{31}-1) ±(2311)
unsigned
long int
ULONG_MAX 2 32 − 1 2^{32}-1 2321
long long intLLONG_MAXLLONG_MIN ± ( 2 63 − 1 ) \pm(2^{63}-1) ±(2631)
unsigned
long long int
ULLONG_MAX 2 64 − 1 2^{64}-1 2641

此外,CHAR_MIN的值为0或SCHAR_MIN;此外,CHAR_MAX的值为SCHAR_MAXUCHAR_MAX

这些宏并非毫无意义,而是定义了整型在C语言中的编码方式。

首先,最简单的数据类型是无符号整型,乃至于几乎不存在“编码”,只需把整数写为二进制,例如1011就是11。其规范的定义方式为

∑ i N x i 2 i \sum^N_ix_i2^i iNxi2i

其中, i i i表示二进制的位数, x i x_i xi表示第 i i i位的二进制值,可取0或者1。从而1011写为

1 ⋅ 2 3 + 0 ⋅ 2 2 + 1 ⋅ 2 1 + 1 ⋅ 2 0 = 11 1\cdot2^3+0\cdot2^2+1\cdot2^1+1\cdot2^0=11 123+022+121+120=11

由于在物理层面并不存在类似-1111的负地址,所以负数,或者带有符号的整型需要编码,计算机中常用补码表示负数,其编码方式为

− x N 2 N + ∑ i N − 1 x i 2 i -x_N2^N+\sum^{N-1}_ix_i2^i xN2N+iN1xi2i

其中,最高位 x N x_N xN为符号位,当 x N = 1 x_N=1 xN=1时,上式即为负数,例如1011表示

− 1 ⋅ 2 3 + 0 ⋅ 2 2 + 1 ⋅ 2 1 + 1 ⋅ 2 0 = − 5 -1\cdot2^3+0\cdot2^2+1\cdot2^1+1\cdot2^0=-5 123+022+121+120=5

所以,在C语言中,int a=-1,则a的二进制编码为1111 1111 1111 1111

errno.h

errno.h定义了整数变量errno,用于表明错误类型,程序启动时,errno为0。

如果该值不为0,说明发生了错误,操作系统会定义各种错误码所对应的错误类型,例如2表示未找到文件或文件夹等,而错误号所对应的错误类型被封装在string.h中,可通过函数strerror()来搜索。

#include<stdio.h>
#include<errno.h>
#include<string.h>

int main(){
	for(int i=0; i<10; i++)
		printf("%d:%s\n",i,strerror(i));
	return 0;
}

编译运行

>gcc testErr.c
>a.exe
0:No error
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted function call
5:Input/output error
6:No such device or address
7:Arg list too long
8:Exec format error
9:Bad file descriptor

测试一下,若调用一个不存在的文件或文件夹,其main函数改为

int main(){
	FILE *fp;
	fp = fopen("test.txt","r");
	if (fp==NULL)
		printf("%d:%s",errno, strerror(errno));
	return 0;
}
>gcc errFile.c
>a.exe
2:No such file or directory

位于stdio.h中的函数void perror(const char *str)可以把字符串str写入标准错误stderr,例如

#include<stdio.h>
#include<string.h>
#include<errno.h>
int main(){
	FILE *fp;
	fp = fopen("test.txt","r");
	if (fp==NULL){
        perror("Error");
    }
	return 0;
}

输出为

>gcc errFile.c
>a.exe
Error: No such file or directory

signal.h

signal.h中定义了一系列的信号宏,如下面代码所示。

#define	SIGHUP	1	//挂起
#define	SIGINT	2	//中断
#define	SIGQUIT	3	//退出
#define	SIGILL	4	//非法指令
#define	SIGTRAP	5	//调试异常
#define	SIGABRT 6	//调用abort时产生,表示程序异常终止
#define	SIGIOT	SIGABRT	//实现相关的硬件异常
#define	SIGEMT	7	//EMT指令
#define	SIGFPE	8	//浮点运算时的异常
#define	SIGKILL	9	//无法处理或忽略时中止某个进程
#define	SIGBUS	10	//硬件异常导致的总线错误
#define	SIGSEGV	11	//非法内存访问
#define	SIGSYS	12	//非法系统调用
#define	SIGPIPE	13	//在reader终止之后写入管道
#define	SIGALRM	14	//timer(alarm)或interval timer(setitimer)超时
#define	SIGTERM	15	//请求中止进程而无法调用Kill时
#define	SIGURG	16	/* urgent condition on IO channel */
#define	SIGSTOP	17	/* sendable stop signal not from tty */
#define	SIGTSTP	18	/* stop signal from tty */
#define	SIGCONT	19	//从stop恢复时发送
#define	SIGCHLD	20	//子进程结束时发给其父进程
#define	SIGCLD	20	/* System V name for SIGCHLD */
#define	SIGTTIN	21	/* to readers pgrp upon background tty read */
#define	SIGTTOU	22	/* like TTIN for output if (tp->t_local&LTOSTOP) */
#define	SIGIO	23	//IO信号
#define	SIGPOLL	SIGIO	//向可调用设备发送信息时
#define	SIGXCPU	24	//CPU时间间隔超时
#define	SIGXFSZ	25	//文件字节数超限
#define	SIGVTALRM 26	//interval timer(setitimer)超时
#define	SIGPROF	27	//由Setitimer指定的计时器发出
#define	SIGWINCH 28	//终端窗口变化
#define	SIGLOST 29	//资源丢失
#define	SIGPWR  SIGLOST	//掉电
#define	SIGUSR1 30	//用户自定义
#define	SIGUSR2 31	//用户自定义
  • 20
    点赞
  • 117
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

微小冷

请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值