C语言标准库梳理

概述

C89 标准库总共划分为 15 个部分,每个部分用一个头文件描述,C99 标准新增了 9 个(为了简化学习,这里暂不提 C11 标准),总共有 24 个头文件。

头文件描述
assert.h于验证程序做出的假设,并在假设为假时输出诊断消息
ctype.h字符判断和转换
errno.h定义了一系列表示不同错误代码的宏
float.h包含了一组与浮点值相关的依赖于平台的常量
limits.h决定了各种变量类型的各种属性,例如范围
locale.h定义了特定地域的设置,比如日期格式和货币符号
math.h定义了各种数学函数和一个宏
setjmp.h定义了宏 setjmp()、函数 longjmp() 和变量类型 jmp_buf
signal.h定义了一个变量类型 sig_atomic_t、两个函数调用和一些宏来处理程序执行期间报告的不同信号。
stdarg.h定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数
stddef.h定义了各种变量类型和宏。这些定义中的大部分也出现在其它头文件中。
stdio.h定义了三个变量类型、一些宏和各种函数来执行输入和输出。
stdlib.h定义了四个变量类型、一些宏和各种通用工具函数。
string.h定义了一个变量类型、一个宏和各种操作字符数组的函数。
time.h定义了四个变量类型、两个宏和各种操作日期和时间的函数。
--新增-----下面是C99新增---
complex.h复数算术
fenv.h浮点环境
inttypes.h整数类型格式转换
iso646.h拼写转换
stdbool.h布尔类型支持
stdint.h整数类型
tgmath泛型数学
wchar.h扩展的多字节和宽字符实用工具
wctype.h宽字符分类和映射使用工具

显示8进制和16进制

如下,%x 十六进制格式输出, %#x 十六进制带格式符输出。同样的还有 %c 打印字符, %e, %Le 打印浮点值。还有一些特别的,例如 %zd 强制转换为整型打印。

#include <stdio.h>

int main(void)
{
    int x = 10;

    printf("十进制:%d, 八进制:%o, 十六进制:%x\n", x, x, x);
    printf("十进制:%d, 八进制:%#o, 十六进制:%#x\n", x, x, x);

    return 0;
}

Console Out

十进制:10, 八进制:12, 十六进制:a 十进制:10, 八进制:012, 十六进制:0xa

常用的格式转换说明符如下:

格式转换符解释
%a(%A)浮点数、十六进制数字和p-(P-)记数法(C99)
%c字符
%d有符号十进制整数
%f浮点数(包括float和doulbe)
%e(%E)浮点数指数输出[e-(E-)记数法]
%g(%G)浮点数不显无意义的零”0”
%i有符号十进制整数(与%d相同)
%u无符号十进制整数
%o八进制整数 e.g. 0123
%x(%X)十六进制整数0f(0F) e.g. 0x1234
%p指针
%s字符串
%%”%”

同样输入函数 scanf() 也使用上面的格式转换符, 例如 scanf("%s", name);.

可移植类型

C语言中有很多数据类型,但是在不同的设备和系统中每个数据类型所占的内存可能不同,C99新增了两个头文件 stdintinttypes.h 来确保在各个系统中的功能相同。

精确宽度类型

stdint 中定义了很多类型名,例如 int32_t 作为 int 的别名,这样一来在 int 为 16 位, long 为 32 位的系统会把 int32_t 作为long的别名。

最小宽度类型

上面的 int32_t 类型可能在有的系统不支持32位整数,最大支持8位。 我们可以使用 int_least8_t ,如果此时某个系统最小整数类型是16位,则会把该类型变为16位。

最快最小宽度类型

这种就很好理解了,会自动根据系统此时最小整数类型选择更小的宽度来提高速度。例如 int_fast8_t 定义系统中对8位有符号值而言运算最快的整数类型别名。

另外还有最大整数类型 intmax_t,无符号

#include <stdio.h>
#include <inttypes.h>

int main(void)
{
    int32_t me32;
    me32 = 45934334;
    printf("me32 = %d\n", me32);
    printf("me32 = %" PRID32 "\n", me32);
    return 0;
}

参数 PRID32 被定义在 inttypes.h 中,用于替代 d , 这条语句等价于 printf("me32 = %" "d" "\n", me32);, 这里可以看出C语言另一个特点,可以把连续的字符串拼接为一个字符串。

char数组和字符串

数组是同类型数据元素的有序序列,字符串是末尾添加 \0 结束符的字符(char)数组。

#define STRING "x"

char a = 'x';

注意上面字符串和字符的区别, 在 string.h 头文件中包含多个与字符串相关的原型函数,比如 strlen() 获取字符串长度。

上面的 #define STRING "x" 是预处理,也就是说在编译时期就会将 STRING 替换成字符串 x ,通常用这种方式定义一些常量。另外我们对一些不可改变的常量使用 const 限定符。

#define PI 3.14159 // 常量宏  

const doulbe Pi=3.14159; // 常量

两个常量之间的区别:

  1. define宏是在预处理阶段展开,const常量是编译运行阶段使用。
  2. define宏没有类型,不做任何类型检查,仅仅是展开,const常量有具体的类型,在编译阶段会执行类型检查。
  3. define宏在定义时不会分配内存;define宏仅仅是展开,有多少地方使用,就展开多少次,const常量在定义时会在内存中分配(可以是堆中也可以是栈中)。

I/O和缓冲

单字符输入输出使用 stdio.h 中的 getchar()putchar():

#include <stdio.h>

int main(void)
{
    char ch;
    //while((ch = getchar()) != '#'){
    while((ch = getchar()) != EOF) //按 Enter 或者 Ctrl + D 结束输入
    {
        putchar(ch);
    }
    return 0;
}


//Input: i am shuihan, this is my blog # haha
//Output: i am shuihan, this is my blog 

你会发现并不是你每输入一个字符就会打印到屏幕,而是你按回车(Enter)的时候读取缓冲区的字符。 上面的 EOF 是在 stdio.h 中的预处理 #define EOF (-1), 在 Unix 系统中一般采用文件字符长度来判断文件结束,当检测到文件结尾就会返回 EOF.

输入输出流缓冲原理

打开文件流并读出文件内容示例代码如下:

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

int main(void)
{
    int ch;
    FILE * fp;
    char fname[50];  //存储文件名

    printf("输入文件名:");
    scanf("%s", fname);

    fp = fopen(fname, "r");

    if(fp == NULL)
    {
        printf("打开文件失败");
        exit(1);  //退出程序
    }

    while((ch = getc(fp)) != EOF)
    {
        putchar(ch);
    }
    fclose(fp);         //关闭文件

    return 0;
}

使用 FILE *fopen(const char *path, const char *mode); 函数来打开文件:

//以输入方式打开文本/二进制文件,只读。前提是文件必须存在                    
  fp =fopen( "txtFileName", "r" );  
  fp =fopen( "binFileName", "rb" );  
//建立输出方式文本/二进制文件,只写。如存在此名字文件,则清除原有内容  
  fp =fopen( "txtFileName", "w" );  
  fp =fopen( "binFileName", "wb" );  
//以输入输出方式打开文本/二进制文件,可读可写,指针指向文件头  
  fp =fopen( "txtFileName", "r+" );  
  fp =fopen( "binFileName", "rb+" );  
//以输入输出方式打开文本/二进制文件,可读可写,指针指向文件尾  
  fp =fopen( "txtFileName", "a+" );  
  fp =fopen( "binFileName", "ab+" ); 

使用 int fclose(FILE *fp); 来关闭文件, 不关闭文件有可能会丢失数据,调用fclose之后,系统会刷新缓存,将缓存区域中的数据全部刷新到文件中去。然后再去释放文件。

字符串I/O

定义字符串:

char tc[] = "Hello""Old""Are you";

//等价于

char tcl[] = "HelloOldAre you";


//字符串常量
const char ml[] = "Test String const.";

//等价于(值得注意的是字符串和字符数组的区别就在末尾是否有 \0 )
const char mls[] = {'T', 'e', 's', 't', ' ', 'S', 't', 'r', 'i', 'n', 'g', ' ', 'c', 'o', 's', 't', '.', '\0'}

数组和指针的区别

char arry1[] = "Test Test";
const char *arry2[] = "TTTTT";

arry1arry2 的区别有, arry1 是常量而 arry2 是变量。 arry1 和 arry2 都指向字符数组的首地址。

char * arry3 = "frame";

当然上面的 arry3 指针也指向该字符串的首地址,那么 arry3[1] = '1' 这样修改是否正确呢?有些编译器是允许这么做的,这样容易造成一些问题,所以通常需要给添加 const 修饰符。

const char * arry4 = "it's right';

字符串有它专有的输入/输出函数 puts(str)gets(str), 等价于 printf("%s\n", str)scanf("%s\n", str), 会在末尾自动添加换行符。

#include <stdio.h>
#define STLEN 81

int main(void)
{
    char words[STLEN];

    puts("输入一串字符串:");
    gets(words);

    printf("输出内容:\n");
    printf("%s\n", words);
    puts(words);

    return 0;
}

不幸的是上面代码你可能会执行失败,因为在 C11 中 gets() 方法已经被去掉,因为 gets() 函数是不安全的,替代函数有两个:

fgets() : 第二参数指明了读入字符串的最长量,如果该参数为n,那么最多将会读入n-1个字符,或者读到换行符为止。与 gets()不同的是,fgets()会将读到的换行符存储在数组中,而gets()会丢弃换行符。fgets()的第三个参数必须声明要读入的文件,如果从键盘读入,则声明为stdin作为参数,该标识符的定义在sdtio中。

char *fgets(char *str, int n, FILE *stream);

gets_s() :只从标准输入中读数据,因此它不需要第三个参数。gets_s()和gets()是非常相似的,一旦超出了存储长度,gets()函数就会不安全,因为它会修改超出部分的内存,擦写现存的数据,而gets_s是安全的,一旦超出,就会自动调用“处理函数”,中止或退出程序。

char *gets_s( char *str, rsize_t n);

所以上面的 gets(words); 在C11中可以换成 fgets(words, STLEN, stdin); 当然 puts() 函数也有对应的 fputs() 替代品。

字符串函数

string.h 中提供了很多处理字符串的函数,例如 strlen() , strcat() , strcmp() , strncmp() , strcpy() 等。

strlen()函数: 统计字符串长度。

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

void fit(char *, unsigned int);

int main(void)
{
    char str1[] = "abcdefghijklmnopqrstu";
    puts(str1);
    fit(str1, 10);
    puts(str1);
    puts(str1 + 11);

    return 0;
}


/*缩短字符串长度*/

void fit(char * string, unsigned int size)
{
    if(strlen(string) > size)
        string[size] = '\0';
}

你会发现上面将字符串截成了两个部分,原理如下:

字符串截断原理

来思考一个问题,如果将上面的字符串定义换成 char * str1 = "abcdefghijklmnopqrstu"; 程序能正常执行吗?其实这个问题上面已经提到过了这种指针形式的字符串大多数编译器是不允许修改其每个字符的值的。

strcat()函数:拼接两个字符串,如下会将 str2 拼接到 str1 后面, str2 不变。

/* 字符串拼接 */
void testStrcat(){

    char str1 [] = "str1";
    char str2 [] = "str2";

    strcat(str1, str2);

    puts(str1);
    puts(str2);
}

上面程序看似没有任何问题,但是假设我们给 str1 数组设定了长度,那么就不能保证拼接后的字符串能存放到 str1 中了。所以要注意数组长度问题。

试想一下,将上面的 str1 改为 char * str1 = "str1"; 这个代码是执行失败的,原因同上。如果将 str2 改为 char * str2 = "str2"; 也不能执行成功,但是我们可以将 str2 修饰为 constconst char str2[] = "str2";

strncat()函数:也是拼接字符串,只不过和 strcat() 不同的是遇到空字符或长度限制自动停止,不会存在上面的 str1 长度空间不够用情况。和 gets() 函数类似 strcat() 可能会导致缓冲区溢出,而 strncat() 可以设置限制长度来避免这个问题。

strcmp()函数:两个字符串比较,类似于Java中的 equals() 方法,比较的不是地址,相等返回 0 ,字典排序 str1 < str2 返回 -1 , str1 > str2 返回 1.