文章目录
引言
C语言标准库是一组内置的函数、常量,被定义在15个头文件中,提供了C语言中最基础的功能
stdio.h 即标准输入输出,同时涵盖了文件读写的全部过程。
所谓标准输入输出,直观一点就是键盘和显示屏:键盘将标准输入传递给终端,终端将内容打印在显示屏上。关于这种基于字符串的人机交互,stdio.h
主要提供了两组函数,一组是get
和put
,另一组是print
和scan
。
操作一个文件的流程大致为:从fopen
打开文件,到fread, fwrite
读写文件,再到fflush
强制写入缓存,最后fclose
关闭文件;如需精确定位文件的字节位置,可通过fseek
或fgetpos
。
stdlib.h 即是standard library
,标准库中无法归入其他类别的统统放到这里,主要分为四个部分:内存、系统、字符串转数值和数学算法。
string.h:提供字符串的查询、比较、复制等功能
ctype.h:用于数据类型判断
math.h:提供最基础的数学运算
时间与货币:C标准库提供了针对不同国家的本地化头函数,具体表现在时间和货币上。C标准库对不同地区的时间和货币的表达形式进行了本地化的描述,分别封装在time.h
和locale.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 1024 | setbuf 函数缓冲区字节数 | __BUFSIZ__ |
FOPEN_MAX 20 | 系统可同时打开的文件数量 | __FOPEN_MAX__ |
FILENAME_MAX 1024 | 文件名最大长度 | __FILENAME_MAX__ |
L_tmpnam FILENAME_MAX | tmpnam 创建的临时文件名的最大长度 | __L_tmpnam__ |
TMP_MAX 26 | tmpnam 可生成最多独立文件名 |
stdio.h
中定义了三个FILE
类型的指针,
#define stdin (_REENT->_stdin)
#define stdout (_REENT->_stdout)
#define stderr (_REENT->_stderr)
其中,_REENT
在reent.h
由_impure_ptr
定义,而_impure_ptr
则为_reent
的指针。
在_reent
中定义了三个FILE
型的指针__FILE *_stdin, *_stdout, *_stderr;
,分别代表标准输入、标准输出和标准错误。
打开文件
fopen
和freopen
均为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+” | 打开一个用于读取和追加的文件 |
fopen
和freopen
的返回值为FILE
指针,刚好可以通过stdio.h
中的close
进行关闭。
stdio.h
中定义了三个单参函数用以调控文件的写入,其输入均为FILE *stream
,若成功则返回1,失败则返回0。
返回类型 | 函数 | 功能 |
---|---|---|
int | fclose | 关闭stream,刷新缓冲区 |
int | fflush | 刷新stream的输出缓冲区 |
int | ferror | 测试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;
}
结果如下,可见getpos
和fseek
的单位是一致的,fseek
移动了10个字节,则fgetpos
也获取了位置10.
>gcc pos.c
>a.exe
file pointer : 0
file pointer : 10
结合fgetpos
和fsetpos
,可完成类似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为文件结束标识符。
返回类型 | 功能 | |
---|---|---|
void | clearerr | 清除stream的文件结束和错误标识符 |
int | feof | 返回stream的文件结束标识符,若未设置,则返回0 |
long int | ftell | 返回stream的文件位置,如果发生错误,则返回-1L,全局变量errno被设置为一个正值。 |
void | rewind | 设置文件位置为stream的开头 |
int | fgetc getc | 从stream获取下一个字符,并把位置标识符往前移动 |
缓存
在打开文件后还没有做其他操作的时候,可以通过ssetvbuf
来设置缓冲格式,其声明为
int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
其中stream
为文件流;buffer
为分配给用户的缓冲;size
为缓冲的字节数;mode
为缓冲模式,有三种
宏 | 值 | 类别 | 说明 |
---|---|---|---|
_IOFBF | 0 | 全缓冲 | 输出时,数据在缓冲填满时被一次性写入 输入时,缓冲在请求输入且缓冲为空时被填充。 |
_IOLBF | 1 | 行缓冲 | 输出时,在换行处或缓冲填满时写入数据 输入时,缓冲至下一个换行符 |
_IONBF | 2 | 无缓冲 | 不使用缓冲。I/O操作即时写入,忽略buffer和size |
setbuf
是setvbuf
的一个特例,其中mode
为_IONBF
,size
为BUFSIZ
。
路径操作
stdio.h
还提供了两个操作文件的函数,分别是删除文件remove
和重命名文件rename
,其声明分别为:
int remove(const char *filename)
int rename(const char *old_filename, const char *new_filename)
get和put
get
和put
有互为反函数的感觉,例如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
则与get
和put
有关的函数如下表所示
get | fget | put | fput |
---|---|---|---|
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) |
其中,除fgets
和gets
的返回值为字符指针,其余均为整型,getchar()
也会将输入的字符转为整型。
所有的get
和put
函数均用于字符的输入输出,后缀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",fPI | 3.1415927410 | float 一般只有6位精度 |
"%.10f",dPI | 3.1415926536 | double 可保证15位精度 |
"%010.5f",dPI | 0003.14159 | 5位精度,10个字符,左侧补0 |
"%015.5E",dPI*1e5 | 0003.14159E+005 | width指字符个数 |
"%#010.0f",dPI*1e5 | 000314159. | # 强制输出小数点 |
"%+f",dPI | +3.141593 | + 强制输出正号 |
"%.2G",dPI | 3.1 | |
"%.2G",dPI*1e10 | 3.1E+010 | 此时显然E模式更短 |
"%010.2G\n%010.4G",dPI | 00000003.1 000003.142 | 默认右对齐 |
"%-010.2G\n%-010.4G",dPI | 3.1 3.142 | - 模式下左对齐,所以0没了 |
整型测试
printf | 输出 | 说明 |
---|---|---|
%u,IPI | 31415926 | 作为无符号整型输出 |
%+d,IPI | 31415926 | + 模式强制显示正号 |
%o,IPI | 167657166 | 输出为8进制 |
%x,IPI | 1df5e76 | 输出为十六进制 |
%#X,IPI | 0X1DF5E76 | # 模式下显示0X |
在stdio
中,定义了一系列printf
函数和scanf
函数,通过在前面添加一个字母来表示不同的行为:
输出位置 | 使用参数列表 | 读取输入 | |
---|---|---|---|
标准输出(屏幕) | printf | vprintf | scanf |
stream | fprintf | vfprintf | fscanf |
字符串 | sprintf | vsprintf | sscanf |
为了便于阅读,下面默认
#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
中的字符串转换函数如下表所示
返回值类型 | 简单 | 高级 |
---|---|---|
double | atof(cSTR) | strtod(cSTR, END) |
int | atoi(cSTR) | |
long int | atol(cSTR) | strtol(cSTR, END, int base) |
unsigned long int | strtoul(cSTR, END, int base) |
其中,若endptr
不为空,则会保存转换数值之后的指针位置;base
介于2和36(包含)之间,表示转换整型的基数。
此外,stdlib
中还有4个用于不同宽度的字符数组之间的转化函数,主要是char
类型和wchar_t
之间的转化。
在C++中,除了char
作为字符类型的保留字之外,还有三个不同宽度的字符类型作为表达式:wchar_t
, char16_t
, char32_t
;到了C++11,又新增了char16_t
和char32_t
。
在C语言中,尽管只有一个char
是保留字,单并不妨碍定义其他数据类型,wchar_t
被定义在stddef.h
中,本质上是一个int
。字符串和wchar_t
之间的转换函数包括
size_t mbstowcs(wchar_t *pwcs, cSTR, size_t n)
:str转为pwcssize_t wcstombs(char *str, const wchar_t *pwcs, size_t n)
:pwcs转为strint wctomb(char *str, wchar_t wchar)
:wchar转为strint 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
中封装了四个用于整型或长整型的数学函数,包括两个绝对值函数abs
和labs
,分别用于整型和长整型;两个除法div
和ldiv
,也是分别用于整型和长整型。
此外,还有一个随机数生成器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
中相似。
如果key
在base
数组中,则返回数组中等于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)
其功能为通过delim
将str
分解为一组小字符串,所谓分解,其实就是将分割符号替换为\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) | str 前n 个字节中首次出现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) | 比较str1 和str2 的前n个字节 |
int strncmp(cSTR1, cSTR2, size_t n) | 比较str1 和str2 的前n个字符 |
int strcmp(cSTR1, cSTR2) | 比较str1 和str2 |
size_t strcspn(cSTR1, cSTR2) | str1开头连续不含str2中字符的个数 |
注意
有关字符串str1
和str2
的比较中:
- 若二者相等,则返回0
- 若
str1<str2
,则返回值小于0 - 若
str1>str2
,则返回值大于0
复制和追加
下面用于字符串复制的函数,均返回一个指向目标字符串的指针。
复制 | |
---|---|
char *strcpy (char *dest, vSTR) | 将str 复制到dest |
char *strncpy (char *dest, cSTR, size_t n) | 把str 前n 个字符复制到 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.h
中LC_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-122 | 30-39, 41-5A, 61-7A |
isalpha | 字母 | 65-90, 97-122 | 41-5A, 61-7A |
isupper | 大写字母 | 65-90 | 41-5A |
islower | 小写字母 | 97-122 | 61-7A |
iscntrl | 控制字符 | 0-31,127 | 00-1F, 7F |
isdigit | 十进制数字 | 48-57 | 30-39 |
isxdigit | 十六进制数字 | 48-57, 48-53, 97-102 | 30-39,41-46, 61-66 |
isgraph | 图形字符 | 33-126 | 21-7E |
isprint | 可打印 | 32-126 | 20-7E |
ispunct | 标点符号 | 33-47, 58-64 91-96, 123-126 | 21-2F, 3A-40 5B-60, 7B-7E |
isspace | 空白字符 | 9-13,32 | 09-0D, 20 |
判别函数之间可由下图表示
此外,ctype
中还封装了两个大小写转换的函数:int tolower(int c)
和int toupper(int c)
,这两个函数很容易实现,只要看一下十六进制下的ASCII即可发现。
前32个字符为控制符
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | NUL | SOH | STX | ETX | EOT | ENQ | ACK | BEL | BS | HT | LF | VT | FF | CR | SO | SI |
1 | DLE | DC1 | DC2 | DC3 | DC4 | NAK | SYN | ETB | CAN | EM | SUB | ESC | FS | GS | RS | US |
32-126为可打印字符,其中20
为空格,大小写字母正好差了0x20
。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | ! | " | # | $ | % | & | ’ | ( | ) | * | + | , | - | . | / | |
3 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
4 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
5 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ |
6 | ` | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
7 | p | q | r | s | t | u | v | w | x | y | z | { | | | } | ~ | 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; // 夏令时
};
gmtime
和localtime
函数可将时间戳转为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
代表将要设置的函数类别,其输入参数包括
值 | 宏 | 说明 | 影响的函数 |
---|---|---|---|
0 | LC_ALL | 下面的所有选项 | |
1 | LC_COLLATE | 字符串比较 | strcoll 和 strxfrm |
2 | LC_CTYPE | 字符分类和转换 | 所有字符函数 |
3 | LC_MONETARY | 货币格式 | localeconv() |
4 | LC_NUMERIC | 小数点分隔符 | localeconv() |
5 | LC_TIME | 日期和时间格式 | strftime() |
6 | LC_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
其中,grouping
和mon_grouping
均为字符串,分别表示在常规数值和货币中每组数字大小。字符串中每个字符都代表一个整数,用以指定当前组的位数。
当前区域的货币符号cs
与货币值之间的排版方式为
值为1 | 值为0 | |
---|---|---|
p_cs_precedes | cs在正货币值之前 | cs在正货币值之后 |
p_sep_by_space | cs和正货币值之间用空格 | cs和正货币值之间不使用空格 |
n_cs_precedes | cs在负货币值之前 | cs在负货币值之后。 |
n_sep_by_space | cs和负货币值之间使用空格 | cs和负货币值之间不使用空格 |
货币中正负号的位置通过p_sign_posn
和n_sign_posn
来调节,二者均有5个取值。
对于-1美元,其n_sign_posn
取值从0-5,n_cs_precedes
分别为0,1
时,表示方法分别如下
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
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_list
为stdarg.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_t
,wchar_t
和ptrdiff_t
。
其中size_t
是sizeof
的结果,一般是long unsigned int
;wchar_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
中有两个函数,分别是setjmp
和longjmp
。
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=1∑pfkb−k,emin<e≤emax
其中, 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=1pfkb−k,则可写为
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<e≤emax
在float.h
中,FLT_RADIX
表示基数,即b
,这个b
对所有浮点数类型都有效。而float.h
的define
的值中,绝大多数可分为三类,分别对应float
、double
和long double
类型:
float | double | long double | 简要说明 |
---|---|---|---|
FLT_MANT_DIG | DBL_MANT_DIG | LDBL_MANT_DIG | b 进制下M 的最大长度 |
FLT_DIG( ≥ 6 \ge6 ≥6) | DBL_DIG( ≥ 10 \ge10 ≥10) | LDBL_DIG( ≥ 10 \ge10 ≥10) | 小数点后精确数字位数 |
FLT_MIN_EXP | DBL_MIN_EXP | LDBL_MIN_EXP | b 进制下e 的最小值 |
FLT_MAX_EXP | DBL_MAX_EXP | LDBL_MAX_EXP | b 进制下e 的最大值 |
FLT_MIN_10_EXP | DBL_MIN_10_EXP | LDBL_MIN_10_EXP | 十进制下 e min ≤ − 37 e_{\min}\le-37 emin≤−37 |
FLT_MAX_10_EXP | DBL_MAX_10_EXP | LDBL_MAX_10_EXP | 十进制下 e max ≥ 37 e_{\max}\ge37 emax≥37 |
FLT_MAX | DBL_MAX | LDBL_MAX | 浮点数最大值 ⩾ 1 0 37 \geqslant10^{37} ⩾1037 |
FLT_MIN | DBL_MIN | LDBL_MIN | 浮点数最小值 ⩽ 1 0 − 37 \leqslant10^{-37} ⩽10−37 |
FLT_EPSILON ( ⩽ 1 0 − 5 \leqslant10^{-5} ⩽10−5) | DBL_EPSILON ( ⩽ 1 0 − 9 \leqslant10^{-9} ⩽10−9) | LDBL_EPSILON ( ⩽ 1 0 − 9 \leqslant10^{-9} ⩽10−9) | 最小差值 |
其中,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。
类型 | 最大值 | 最小值 | 一般取值 |
---|---|---|---|
char | CHAR_MAX | CHAR_MIN | |
singed char | SCHAR_MAX | SCHAR_MIN | ± 127 \pm127 ±127 |
unsinged char | UCHAR_MAX | 255 255 255 | |
short int | SHRT_MAX | SHRT_MIN | ± 32767 \pm32767 ±32767 |
unsigned short int | USHRT_MAX | 65536 65536 65536 | |
int | INT_MAX | INT_MIN | ± 32767 \pm32767 ±32767 |
unsigned int | UINT_MAX | 65536 65536 65536 | |
long int | LONG_MAX | LONG_MIN | ± ( 2 31 − 1 ) \pm(2^{31}-1) ±(231−1) |
unsigned long int | ULONG_MAX | 2 32 − 1 2^{32}-1 232−1 | |
long long int | LLONG_MAX | LLONG_MIN | ± ( 2 63 − 1 ) \pm(2^{63}-1) ±(263−1) |
unsigned long long int | ULLONG_MAX | 2 64 − 1 2^{64}-1 264−1 |
此外,CHAR_MIN
的值为0或SCHAR_MIN
;此外,CHAR_MAX
的值为SCHAR_MAX
或UCHAR_MAX
。
这些宏并非毫无意义,而是定义了整型在C语言中的编码方式。
首先,最简单的数据类型是无符号整型,乃至于几乎不存在“编码”,只需把整数写为二进制,例如1011
就是11。其规范的定义方式为
∑ i N x i 2 i \sum^N_ix_i2^i i∑Nxi2i
其中,
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 1⋅23+0⋅22+1⋅21+1⋅20=11
由于在物理层面并不存在类似-1111
的负地址,所以负数,或者带有符号的整型需要编码,计算机中常用补码表示负数,其编码方式为
− x N 2 N + ∑ i N − 1 x i 2 i -x_N2^N+\sum^{N-1}_ix_i2^i −xN2N+i∑N−1xi2i
其中,最高位
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 −1⋅23+0⋅22+1⋅21+1⋅20=−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<OSTOP) */
#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 //用户自定义