标准库(Standard Library)是C语言重要的一部分,不过学习C语言这么长时间,都没有细致的了解过标准库到底中包含哪些内容,这几天打算来仔细看看这部分内容。
C语言标准库有各种不同的实现,比如最著名的glibc, 用于嵌入式Linux的uClibc,还有ARM公司的自己的C语言标准库及精简版的MicroLib等。不同标准库的实现并不相同,而且提供的函数也不完全相同,不过有一个它们都支持的最小子集,这也就是最典型的C语言标准库。
这个C语言标准库中一共包含15个头文件,粗略的按常用程度排序列举如下:
Header File | Content |
---|---|
|
输入和输出 |
|
最常用的一些系统函数 |
|
字符串处理 |
|
数学函数 |
|
字符类测试 |
|
时间和日期 |
|
可变参数列表 |
|
信号 |
|
断言 |
|
非局部跳转 |
|
定义错误代码 |
|
一些常数、类型和变量 |
|
本土化 |
|
浮点数运算 |
|
定义整数数据类型的取值范围 |
本文总结的是不完整的C标准库,仅列举一些常用且最重要的部分。
日期和时间操作。需要特别注意的是,书中使用的time_t
时间戳标准是从1900年1月1日午夜开始的,这与目前广泛使用的UNIX时间戳不一样,也和Glibc的实现不一样,书中是通过_TBIAS
这个宏定义偏置量来解决这个问题的,为了简单起见,此处对此进行了改写,忽略了偏置问题,直接将其修改为与UNIX时间戳一样。
通常使用time(NULL)
获取一个time_t
类型的UNIX时间戳,这一般是一个32位整数(signed int),指的是从1970年1月1日午夜至今的秒数,大约可以表示到2038年。如果要获取更精确的时间,可使用clock()
函数。
其余函数用于在几种不同数据结构间进行转换,根据需要选取即可,其中tm
类型的定义一般是这样的:
1 2 3 4 5 6 7 8 9 10 11 |
struct tm { int tm_sec; /* [0, 60], 1 leap second */ int tm_min; /* [0, 59] */ int tm_hour; /* [0, 23] */ int tm_mday; /* [1, 31] */ int tm_mon; /* [0, 11] */ int tm_year; /* Years since 1900 */ int tm_wday; /* [0, 6], Sunday, Monday... */ int tm_yday; /* [0, 365], days since January 1th */ int tm_isdst; /* 夏令时标志,无效则为0 */ } |
需要注意的是,以上只是time_t
的最小实现,实际Glibc 2.23版本的代码中除了上述成员外还添加了其它字段。tm_year
是从1900年开始的,并不是和UNIX时间戳相同的1970年。
time()
和clock()
函数是依赖于具体实现的,此处不作分析。
difftime()
函数返回两个时间戳之间的差值,考虑到time_t
可能会被定义为无符号整数,故需要先比较二者的大小:
1 2 3 |
double difftime(time_t t1, time_t t0) { return (t0 <= t1 ? (double)(t1 - t0) : -(double)(t0 - t1)); } |
tm
与time_t
间的转换函数是
中的重点,这里主要来看一下gmtime()
和mktime()
的实现方法。下列代码在书中给出的代码基础上进行了些改写,主要是做了些精简,没有考虑夏令时等问题。虽然以下两段代码比Glibc中的实现要简单得多,不过经测试完全可以正常使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
static const short lmos[] = { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 }; static const short mos[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; #define MONTAB(year) ((year & 0x03) == 2 ? lmos : mos) struct tm * gmtime(time_t * timer) { static struct tm ts; int year; int days; int secs; secs = *timer; days = secs / 86400; // 获取天数 ts.tm_wday = (days + 4) % 7; // 1970年1月1日是星期四 int dDay; /* days / 365 先求出year的初步估计,因为闰年的存在不一定准确(可能会多1年) */ /* (year + 1) / 4 求出因闰年多出来的天数 */ /* days与year年初的天数比较,若days小于它,说明year估计有误,需要减去1年 */ for (year = days / 365; days < (dDay = (year + 1) / 4 + 365 * year);) year--; days -= dDay; // 将days变成1年中的天数 ts.tm_year = year + 70; // tm_year是从1900年开始的 ts.tm_yday = days; // 总天数减去年初的天数 /* 从最后一个月开始,逐步向前寻找正确的月份,pm[mon]得到月初的天数 */ int mon; const short * pm = MONTAB(year); int tmp = (year & 0x03) == 2; for (mon = 11; days < pm[mon]; mon--); ts.tm_mon = mon; ts.tm_mday = days - pm[mon] + 1; /* 根据secs依次求出小时、分钟和秒 */ secs %= 86400; ts.tm_hour = secs / 3600; secs %= 3600; ts.tm_min = secs / 60; ts.tm_sec = secs % 60; return &ts; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
static const short lmos[] = { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 }; static const short mos[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; #define MONTAB(year) ((year & 0x03) == 2 ? lmos : mos) time_t mktime(struct tm * timeptr) { int year, days, secs; // 检查参数有效性,不使用tm_yday & tm_wday #ifndef NDEBUG if (timeptr->tm_hour < 0 || timeptr->tm_hour > 23) return -1; if (timeptr->tm_mday < 1 || timeptr->tm_mday > 31) return -1; if (timeptr->tm_min < 0 || timeptr->tm_min > 59) return -1; if (timeptr->tm_mon < 0 || timeptr->tm_mon > 11) return -1; if (timeptr->tm_sec < 0 || timeptr->tm_sec > 60) return -1; if (timeptr->tm_year < 70 || timeptr->tm_year > 138) return -1; #endif year = timeptr->tm_year - 70; days = (year + 1) / 4 + 365 * year; days += MONTAB(year)[timeptr->tm_mon] + timeptr->tm_mday - 1; secs = 3600 * timeptr->tm_hour+ 60 * timeptr->tm_min + timeptr->tm_sec; return (86400 * days + secs); } |
需要指出的是,上面这个mktime()
函数没有考虑时区的问题,而标准的mktime()
函数实现的是将由tm
表示的地方时转换为time_t
表示的GMT时间,所以二者并不等价。
在1970~2038年这个范围内,闰年规律符合简单的4年一闰,所以可以用(year & 0x03) == 2
来进行闰年判断。
其余asctime()
、ctime()
等函数用于返回格式化的时间字符串,其原理和sprintf()
等函数大同小异,在此不作分析。
包含字符测试及大小写转换函数。
提供了若干isxxxx()
函数用于判断字符类型,并提供了大小写转换函数,具体函数列表见:C语言标准库总结
需要说明的是,字符集的具体定义和区域设置有关,不过常用的就是英文的情况,因为这些函数也无法处理中文编码。另外,函数接受的参数是一个int
类型的整数,不过只有unsigned char
类型所能表示的值加上EOF
宏定义的值(一般为-1)是有效的,传入其它值的行为是未定义的。
出于效率考虑,标准库中的实现方法是基于转换表的,这里不列举具体使用的转换表了,仅描述一下设计思路。
首先将整个字符集合划分为若干个合理设计的子集,如数字(0
~9
)、小写字母(a
~z
)、大写字母(A
~Z
)等,每一类用一个比特位来表示,这样就可以得到如下宏定义:
1 2 3 4 |
#define _XD 0x01 /* '0'-'9', 'A'-'F', 'a'-'f' */ #define _UP 0x02 /* 'A'-'Z' */ #define _SP 0x04 /* space */ // ...... |
任何一个字符都属于某一子集(或某几个子集)中,这样就可以根据以上宏定义得到这个字符的编码了,将全体字符编码构成一个数组,这就是所谓的转换表,书中这个数组的名字叫做_Ctype
。这样一来,要判断某个字符是否属于某个子集就很简单了,只要检查这个字符在转换表中对应值的特定位是否被置位了就可以了,比如检查一个字符是否是大写字母:
1 2 3 |
int isupper(int c) { return (_Ctype[c] & _UP); } |
大小写间的转换也是基于转换表的,这个转换表相当于在原始ACSII表的基础上将大写字母替换为小写字母(或相反)得到的。
关于区域编码的问题此处从略。
用于处理可变参数。
可变参数函数的定义类似这样:
1 2 3 4 5 6 7 8 9 10 11 |
#include |
必须要有至少一个固定参数,习惯上把最后一个固定参数叫做parmN
。在函数中先调用va_start()
初始化va_list
,之后就可以通过va_arg()
依次获取各参数,最后调用va_end()
即可。需要注意的是,在可变参数中,应用的是“加宽”原则,也就是float
会被扩展成double
,char
、short
等会被扩展成int
,也就是说,函数中只该使用以下这些表达式:
1 2 3 |
va_arg(ap, double); va_arg(ap, int); va_arg(ap, unsigned int); |
1 2 3 4 5 |
typedef char * va_list; #define va_start(ap, A) (void)((ap) = (char *)&A) #define va_end(ap) (void)(0) #define va_arg(ap, T) (*(T *))((ap += sizeof(T)) - sizeof(T)) |
这里给出的代码是简化版代码,没有考虑存储空隙及对齐问题,仅用来说明基本原理。
提供断言。
在需要使用断言的地方加入assert(x)
即可,x
是一个int
,若x
为零断言成立,此时程序会向标准错误流输出一条包含出错行号等的错误信息并调用abort()
函数终止程序的运行。assert(x)
返回void
。
一般只有在程序调试时才需要终止程序运行,发布时应该去掉这个功能,为实现这一目的,可通过定义NDEBUG
这个宏来实现,一般使用编译器预定义。
为了对NDEBUG
作出正确回应,头文件的基本结构如下:
1 2 3 4 5 6 |
#undef assert /* remove existing definition */ #ifdef NDEBUG #define assert (test) ((void) 0) /* passive form */ #else #define assert (test) ... /* active form */ #endif |
其中active form
的定义如下:
1 2 3 4 |
void _Assert(char *); #define _STR(x) _VAL(x) #define _VAL(x) #x #define assert(test) ((test) ? (void) 0 : _Assert(__FILE__":"_STR(__LINE__)" "#test)) |
_Assert()
是一个隐藏库函数,用于调用
中的其它库函数输出错误信息并调用abort()
函数,这个很简单,没有什么问题,上述代码的关键在于后面几行宏定义上。
__FILE__
及__LINE__
这两个宏是由编译器定义的,代表当前文件名及当前代码行号,__FILE__
是一个字符串,而__LINE__
是一个十进制整数。
_STR()
及_VAL()
这两个宏神奇的实现了将一个整数常量转换为字符串字面量的功能,二者缺一不可,也就是说,下面这个写法是错误的:
1
|
#define _STR(x) #x
|
使用这个写法的话,_STR(__LINE__)
得到的是"__LINE__"#就是把宏参数进行字符串处理
输入和输出。
在其中定义了以下一些常用的类型及常量:
Name | Comment |
---|---|
FILE |
文件指针 |
EOF |
End Of File,表示文件的结尾 |
stderr |
标准错误流 |
stdin |
标准输入流 |
stdout |
标准输出流 |
其中stderr
、stdin
、stdout
为宏定义,是指向FILE
类型的指针。
中的函数有很多,大致可分为对标准输入输出流的操作、对文件流的操作、对标准错误流的操作、对字符串的操作这几大类。
其实从stdin
与stdout
的定义中也可以看到,标准输入输出流也就是文件,只是一般情况下已经默认定义为键盘和屏幕。这与Linux中一切皆文件的思想一脉相承。
常用的函数有以下这些:
Name | Comment |
---|---|
int printf(const char * format, ...) |
格式化输出数据至stdout |
int scanf(const char * format, ...) |
由stdin 读取格式化输入数据 |
int putchar(int c) |
向stdout 输出一个字符 |
int getchar(void) |
由stdin 读入一个字符 |
int puts(const char * s) |
向stdout 输出一串字符串 |
char * gets(char * s) |
由stdin 读入一串字符串 |
另外,vprintf()
函数主要用于需要自己实现一些类似printf()
的函数时使用,关于这个函数的用处可参考StackOverflow上的讨论,用于文件流的vfprintf()
与用于字符串的vsprintf()
的用处也是相似的。
对文件的操作是
中的核心,其他函数均可视为对特定文件的操作,大部分函数均以f****()
命名。
最重要的函数是以下这几个:
Name | Comment |
---|---|
FILE * fopen(const char * filename, const char * mode) |
打开文件,失败返回NULL |
int fclose(FILE * stream) |
关闭文件,成功返回0,失败返回EOF |
size_t fread(void * ptr, size_t size, size_t nmemb, FILE * stream) |
读取文件内容 |
size_t fwrite(cosnt void * ptr, size_t size, size_t nmemb, FILE * stream) |
写入文件内容 |
只使用这4个函数就可以完成基本的文件读写操作了,其它函数可以视为是为了更方便的进行文件读写而引入的。在Linux中,文件不仅仅是指磁盘上的一个file,也有可能是一个设备等,不过都可以以统一的方式进行读写。常用的打开模式有r
(读)、w
(写)、a
(附加)、b
(二进制)等。
与标准输入输出流的操作相同,对文件的操作也有以下这些函数:
Name | Comment |
---|---|
int fprintf(FILE * stream, const char * format, ...) |
格式化输出数据至文件 |
int fscanf(FILE * stream, cosnt char * format, ...) |
由文件读取格式化输入数据 |
int putc(int c, FILE * stream) |
向文件输出一个字符 |
int getc(FILE * stream) |
由文件读入一个字符 |
int fputc(int c, FILE * stream) |
向文件输出一个字符 |
int fgetc(FILE * stream) |
由文件读入一个字符 |
int fputs(const char * s, FILE * stream) |
向文件输出一串字符串(或比特流) |
char * fgets(char * s, int n, FILE * stream) |
由文件读入一串字符串(或比特流) |
其中putc()
与fputc()
、getc()
与fgetc()
的区别在于前者可能是使用宏定义实现的,而后者一定是函数,具体分析可以参考这篇文章。
用于对文件进行修改(如删除文件等)的函数有以下这些:
Name | Comment |
---|---|
int remove(const char * filename) |
删除文件,成功返回0 |
int rename(const char * old, const char * new) |
更改文件名称或位置,成功返回0 |
FILE * tmpfile(void) |
以wb+形式创建一个临时二进制文件 |
其中tmpfile()
创建的临时文件在调用fclose()
关闭时会被自动删除。
对文件流的定位通常使用以下这些函数:
Name | Comment |
---|---|
int fseek(FILE * stream, long int offset, int fromwhere) |
移动文件流的读写位置,错误返回非0 |
long int ftell(FILE * stream) |
取得文件流的读取位置 |
void rewind(FILE * stream) |
重设读取目录的位置为开头位置 |
int feof(FILE * stream) |
检测文件结束符 |
whence
可设置为SEEK_SET
、SEEK_END
或SEEK_CUR
。
使用这两个函数处理读写文件流操作中的错误:
Name | Comment |
---|---|
int ferror(FILE * stream) |
检查流是否有错误 |
void clearerr(FILE * stream) |
复位错误标志 |
与缓冲(Buffer)机制有关的函数常用的有以下这两个:
Name | Comment |
---|---|
void setbuf(FILE * stream, char * buf) |
把缓冲区与流相联 |
int fflush(FILE * stream) |
更新缓冲区,成功返回0,错误返回EOF |
对stderr
的操作通过以下函数完成:
Name | Comment |
---|---|
void perror(const char * s) |
打印出错误原因信息字符串 |
此函数将上一个函数发生错误的原因输出到stderr
,此错误原因依照全局变量errno
的值来决定要输出的字符串,errno
在
中声明。
对字符串也提供了格式化输入输出函数:
Name | Comment |
---|---|
int sprintf(char * s, const char * format, ...) |
格式化字符串复制 |
int sscanf(const char * s, const char * format, ...) |
格式化字符串输入 |
最常用的一些系统函数。
在其中定义了以下一些常用的类型及常量:
Name | Comment |
---|---|
size_t |
sizeof 运算符产生的数据类型,一般是一个无符号整数 |
wchar_t |
一个宽字符的大小 |
NULL |
空 |
RANDMAX |
rand() 的最大返回值 |
下面分类整理一下其中的重要函数。
最常用的是以下两个函数:
Name | Comment |
---|---|
void * malloc(size_t size) |
从堆上动态分配内存空间 |
void free(void * ptr) |
释放之前分配的内存空间 |
还有一些常用的内存控制函数位于
中。
常用函数有:
Name | Comment |
---|---|
int abs(int j) |
int类型数据绝对值 |
long labs(long j) |
long类型数据绝对值 |
int rand(void) |
产生一个随机数 |
void srand(unsigned int seed) |
初始化随机数种子 |
关于rand()
与srand()
的用法,之前写的这篇文章中进行了总结。
常用的有以下这3个函数:
Name | Comment |
---|---|
int atoi(const char * nptr) |
将字符串转换为整数(int) |
long atol(const char * nptr) |
将字符串转换为长整数(long) |
double atof(const char * nptr) |
将字符串转换为浮点型数(double) |
常用的函数有:
Name | Comment |
---|---|
int system(const char * string) |
执行Shell(或命令行)命令 |
char * getenv(const char * name) |
获取环境变量中的内容 |
int exit(int stauts) |
结束进程 |
Name | Comment |
---|---|
void qsort(void * base, size_t nmemb, size_t size, int (* compar)(const void *, const void *)) |
快速排序算法 |
void * bsearch(const void * key, const void * base, size_t nmemb, size_t size, int (* compar)(const void *, const void *)) |
在数组进行二分法查找某一元素,要求数组预先已排好序 |
在
中还有一些用于进行多字节字符处理的函数,此处没有列出。
中除了字符串处理函数,还有一些内存管理函数:
Name | Comment |
---|---|
void * memset(void * dest, int c, size_t n) |
将一段内存空间填上某值 |
void * memcpy(void * dest, const void * src, size_t n) |
复制一段内存内容 |
int memcmp(const void * s1, const void * s2, size_t n) |
比较两段内存内容 |
void * memchr(const void * s, int c, size_t n) |
在某一段内存范围中查找特定字节 |
常用的字符串操作函数有:
Name | Comment |
---|---|
char * strcat(char * deat, const char * src) |
连接两个字符串 |
char * strcpy(char * dest, const char * src) |
复制字符串 |
int strcmp(const char * s1, const char * s2) |
比较两个字符串 |
size_t strlen(const char * s) |
获取一个字符串的长度 |
char * strtok(char * s1, const char * s2) |
分割字符串 |
以下这些函数用于进行字符串查找:
Name | Comment |
---|---|
char * strchr(const char * s, int c) |
正向查找一个字符 |
char * strrchr(const char * s, int c) |
反向查找一个字符 |
char * strstr(const char * s1, const char * s2) |
查找一个字符串 |
char * strpbrk(const char * s1, const char * s2) |
查找一个字符集合 |
标准数学库,常用函数如下:
Name | Comment |
---|---|
double sin(double x) |
正弦 |
double cos(double x) |
余弦 |
double tan(double x) |
正切 |
double asin(double x) |
反正弦 |
double acos(double x) |
反余弦 |
double atan(double x) |
反正切 |
double atan2(double y, double x) |
计算y/x的反正切 |
Name | Comment |
---|---|
double sinh(double x) |
双曲正弦 |
double cosh(double x) |
双曲余弦 |
double tanh(double x) |
双曲正切 |
Name | Comment |
---|---|
double exp(double x) |
e的n次幂 |
double pow(double x, double y) |
x的y次幂 |
double sqrt(double x) |
开根号 |
double log(double x) |
e为底的对数 |
double log10(double x) |
10为底的对数 |
Name | Comment |
---|---|
double ceil(double x) |
向上取整 |
double floor(double x) |
向下取整 |
Name | Comment |
---|---|
double fabs(double x) |
计算绝对值 |
包含字符测试及大小写转换函数。
Name | Comment |
---|---|
isalpha(c) |
是否为字母 |
isupper(c) |
是否为大写字母 |
islower(c) |
是否为小写字母 |
isdigit(c) |
是否为数字 |
isxdigit(c) |
是否为16进制数字(数字 & A~F & a~f) |
isalnum(c) |
是否为字母及数字 |
ispunct(c) |
是否为标点符号 |
isspace(c) |
是否为空白字符(空格、\r(CR)、\n(LF)、\t(TAB)、\v(VT)、\f(FF)) |
iscntrl(c) |
是否为控制字符(ASCII 0 ~ 37(0x1F) & 177(0x7F)) |
isgraph(c) |
是否为可显示字符(字母 & 数字 & 标点) |
isprint(c) |
是否为可打印字符(字母 & 数字 & 标点 & 空白) |
Name | Comment |
---|---|
tolower(c) |
转换为小写 |
toupper(c) |
转换为大写 |
日期及时间操作。定义了time_t
、clock_t
及tm
这几种类型,常用函数有:
Name | Comment |
---|---|
time_t time(time_t * timer) |
获取UNIX时间戳,一般传入NULL |
clock_t clock(void) |
获取CPU时钟计数 |
double difftime(time_t time1, time_t time0) |
计算时间差,time1 - time0 |
struct tm * gmtime(const time_t * timer) |
GMT时间 |
struct tm * localtime(const time_t * timer) |
地方时时间 |
time_t mktime(struct tm * timeptr) |
地方时时间 |
Name | Comment |
---|---|
char * ctime(const time_t * timer) |
返回标准时间字符串,地方时时间,等价于asctime(localtime()) |
char * asctime(const struct tm * timeptr) |
返回标准时间字符串 |
size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *) |
返回自定义格式时间字符串 |
用于支持可变参数,定义了va_list
这个结构体,通过以下三个宏进行操作:
Name | Comment |
---|---|
void va_start(va_list ap, parmN) |
初始化va_list |
type va_arg(va_list ap, type) |
从va_list 中获取一个type 类型的参数 |
void va_end(va_list ap) |
释放va_list |
定义了信号(Signal)处理的相关宏及函数,这与Linux中的信号机制密切相关,包含下面两个函数:
Name | Comment |
---|---|
signal() |
设置处理特定Signal的Handler |
raise(int sig) |
产生一个Signal |
signal()
函数原型如下:
void (* signal(int sig, void (* handler)(int)))(int);
此头文件的唯一目的是提供assert(int x)
这个宏,如果断言非真,程序会在标准错误流输出错误信息,并调用abort()
函数使程序异常终止。
非局部跳转,用于从一个深层次嵌套中直接返回至最外层,通过这两个宏完成:
Name | Comment |
---|---|
int setjmp(jmp_buf env) |
设置跳转点 |
void longjmp(jmp_buf env, int val) |
进行跳转 |
声明了一个外部整形变量errno
用于表示错误,可用perror(const char * s)
输出错误原因,其中s
是错误提示前缀。
标准使用方法是:在一个库函数调用之前把它设为0,然后在下一个库函数调用前测试它,任何非零值均表示错误。示例代码:
1 2 3 4 5 6 7 8 |
#include |
定义了一些标准定义,如size_t
、wchar_t
、NULL
等,这些定义也会出现在其他的头文件里。还定义了以下这个宏:
Name | Comment |
---|---|
offsetof(type, member) |
返回结构体中某一成员相对于结构体起始地址的偏移量 |
国家、文化和语言规则集称为区域设置,主要影响字符串格式,通过以下函数进行设置:
Name | Comment |
---|---|
setlocale() |
设置或恢复本地化信息 |
用宏定义的方式定义了浮点数的最大值、最小值等信息。
定义了基本数据类型(int、char、short等)的最大值及最小值。常用宏定义有:
Name | Comment |
---|---|
CHAR_BIT |
一个字节的比特数 |
SCHAR_MIN |
带符号字符最小值 |
SCHAR_MAX |
带符号字符最大值 |
UCHAR_MAX |
无符号字符最大值 |
CHAR_MIN |
char 的最小值 |
CHAR_MAX |
char 的最大值 |
SHRT_MIN |
带符号短整型最小值 |
SHRT_MAX |
带符号短整型最大值 |
USHRT_MAX |
无符号短整型最大值 |
INT_MIN |
带符号整形最小值 |
INT_MAX |
带符号整形最大值 |
UINT_MAX |
无符号整形最大值 |
LONG_MIN |
带符号长整形最小值 |
LONG_MAX |
带符号长整形最大值 |
ULONG_MAX |
无符号长整形最大值 |