C语言标准库(1)

姓名:吕彬 学号:1613014035

【嵌牛导读】标准库(Standard Library)是C语言重要的一部分,不过学习C语言这么长时间,都没有细致的了解过标准库到底中包含哪些内容,这几天打算来仔细看看这部分内容。

【嵌牛鼻子】C语言标准库有各种不同的实现,比如最著名的glibc, 用于嵌入式Linux的uClibc,还有ARM公司的自己的C语言标准库及精简版的MicroLib等。不同标准库的实现并不相同,而且提供的函数也不完全相同,不过有一个它们都支持的最小子集,这也就是最典型的C语言标准库。

【嵌牛提问】C语言标准库包含哪些内容呢?

【嵌牛正文】这个C语言标准库中一共包含15个头文件,粗略的按常用程度排序列举如下:

C语言标准库(1)_第1张图片
图片发自App


本文总结的是不完整的C标准库,仅列举一些常用且最重要的部分。ime.h日期和时间操作。需要特别注意的是,书中使用的time_t时间戳标准是从1900年1月1日午夜开始的,这与目前广泛使用的UNIX时间戳不一样,也和Glibc的实现不一样,书中是通过_TBIAS这个宏定义偏置量来解决这个问题的,为了简单起见,此处对此进行了改写,忽略了偏置问题,直接将其修改为与UNIX时间戳一样。使用方法通常使用time(NULL)获取一个time_t类型的UNIX时间戳,这一般是一个32位整数(signed int),指的是从1970年1月1日午夜至今的秒数,大约可以表示到2038年。如果要获取更精确的时间,可使用clock()函数。其余函数用于在几种不同数据结构间进行转换,根据需要选取即可,其中tm类型的定义一般是这样的:1234567891011 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可能会被定义为无符号整数,故需要先比较二者的大小:123 double difftime(time_t t1, time_t t0) {    return (t0 <= t1 ? (double)(t1 - t0) : -(double)(t0 - t1));}tm与time_t间的转换函数是中的重点,这里主要来看一下gmtime()和mktime()的实现方法。下列代码在书中给出的代码基础上进行了些改写,主要是做了些精简,没有考虑夏令时等问题。虽然以下两段代码比Glibc中的实现要简单得多,不过经测试完全可以正常使用。1234567891011121314151617181920212223242526272829303132333435363738394041 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;}123456789101112131415161718192021222324252627282930 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()等函数大同小异,在此不作分析。ctype.h包含字符测试及大小写转换函数。使用方法提供了若干isxxxx()函数用于判断字符类型,并提供了大小写转换函数,具体函数列表见:C语言标准库总结需要说明的是,字符集的具体定义和区域设置有关,不过常用的就是英文的情况,因为这些函数也无法处理中文编码。另外,函数接受的参数是一个int类型的整数,不过只有unsigned char类型所能表示的值加上EOF宏定义的值(一般为-1)是有效的,传入其它值的行为是未定义的。实现方法出于效率考虑,标准库中的实现方法是基于转换表的,这里不列举具体使用的转换表了,仅描述一下设计思路。首先将整个字符集合划分为若干个合理设计的子集,如数字(0~9)、小写字母(a~z)、大写字母(A~Z)等,每一类用一个比特位来表示,这样就可以得到如下宏定义:1234 #define _XD  0x01  /* '0'-'9', 'A'-'F', 'a'-'f' */#define _UP  0x02  /* 'A'-'Z' */#define _SP  0x04  /* space */// ......任何一个字符都属于某一子集(或某几个子集)中,这样就可以根据以上宏定义得到这个字符的编码了,将全体字符编码构成一个数组,这就是所谓的转换表,书中这个数组的名字叫做_Ctype。这样一来,要判断某个字符是否属于某个子集就很简单了,只要检查这个字符在转换表中对应值的特定位是否被置位了就可以了,比如检查一个字符是否是大写字母:123 int isupper(int c) {    return (_Ctype[c] & _UP);}大小写间的转换也是基于转换表的,这个转换表相当于在原始ACSII表的基础上将大写字母替换为小写字母(或相反)得到的。关于区域编码的问题此处从略。stdarg.h用于处理可变参数。使用方法可变参数函数的定义类似这样:1234567891011 #include void fun(int parmN,...) {    va_list ap;    va_start(ap, parmN);    //......    int a = va_arg(ap, int);    double b = va_arg(ap, double);    //......    va_end(ap);}必须要有至少一个固定参数,习惯上把最后一个固定参数叫做parmN。在函数中先调用va_start()初始化va_list,之后就可以通过va_arg()依次获取各参数,最后调用va_end()即可。需要注意的是,在可变参数中,应用的是“加宽”原则,也就是float会被扩展成double,char、short等会被扩展成int,也就是说,函数中只该使用以下这些表达式:123 va_arg(ap, double);va_arg(ap, int);va_arg(ap, unsigned int);实现方法12345 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.h提供断言。使用方法在需要使用断言的地方加入assert(x)即可,x是一个int,若x为零断言成立,此时程序会向标准错误流输出一条包含出错行号等的错误信息并调用abort()函数终止程序的运行。assert(x)返回void。一般只有在程序调试时才需要终止程序运行,发布时应该去掉这个功能,为实现这一目的,可通过定义NDEBUG这个宏来实现,一般使用编译器预定义。实现方法为了对NDEBUG作出正确回应,头文件的基本结构如下:123456 #undef assert    /* remove existing definition */#ifdef NDEBUG#define assert (test) ((void) 0)  /* passive form */#else#define assert (test) ...        /* active form */#endif其中active form的定义如下:1234 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__"#就是把宏参数进行字符串处理。

你可能感兴趣的:(C语言标准库(1))