# 常用库

  • ANSI C 标准,制定了一些标准库,提供了一些常用的函数、常量、宏等。
    • 一款 C 语言编译器,如果遵守 ANSI C 标准,则必须内置这些标准库,方便用户调用。
    • 用户 include 这些标准库的头文件,就可以调用它们。
  • 用户也可以下载第三方编写的库,例如:
    • windows.h :提供了 Windows 系统的很多 API 。比如 Sleep() 函数,用于暂停程序一定毫秒。
    • unistd.h :提供了类 Unix 系统的很多 API 。比如 sleep() 函数,用于暂停程序一定秒。
    • curl/curl.h :提供了 libcurl 库的一些函数,用于 HTTP、HTTPS、FTP 等网络协议的通信。
    • cJSON.h :用于处理 JSON 格式的字符串。

# assert.h

  • assert.h 提供了 assert() 函数。
  • 源代码:
    # undef assert
    # ifdef NDEBUG
    # define assert(x)    ((void)0)
    # else
    # define assert(e)    ((e) ? (void)0 : _assert(# e, __FILE__, __LINE__))
    # endif
    
  • assert() 称为断言,用于检查一个条件表达式是否成立,方便用户进行调试。
    • 如果取值为真,则无操作。
    • 如果取值为假,则调用 _assert() ,打印报错信息,并调用 abort() 来终止程序。
    • 在生产环境,用户可以定义一个名为 NDEBUG 的宏,使 assert() 失效。
  • 例:
    int x = 1;
    assert(x > 1);
    
    执行该程序,输出如下:
    test.c:8: main: Assertion `x > 1' failed.
    Aborted
    

# pthread.h

  • C 语言如何创建多线程?

    • 在 Linux 系统上,可调用 pthread.h 库的 API 创建线程。比如 pthread_create() 。
      • pthread.h 符合 POSIX 标准,适用于类 Unix 系统。也有兼容 Windows 系统的版本:pthreads-w32 。
      • 编译时,需要链接 pthread.h 库,例如:gcc test.c -o test -l pthread
    • 在 Windows 系统上,可调用 windows.h 库的 API 创建线程。比如 CreateProcess() 。
    • C11 制定了一个标准的线程库 threads.h ,但有的编译器不支持 C11 。
  • int pthread_create(pthread_t *id,
                      const pthread_attr_t *attr,
                      void *(*) (void *),
                      void *arg);
    
    • 用途:创建一个线程,来运行一个函数。
      • 需要传入一个 id 变量,用于记录线程 ID 。
      • 需要传入一个 attr 变量,用于配置线程的属性。如果传入 NULL ,则采用默认属性。
      • 待运行的函数,应该定义成 void *fun(void *arg) 的格式。
      • 需要传入一个 arg 变量 ,作为待运行的函数的实参。如果不需要传参,则传入 NULL 。如果有多个参数,则需要封装成一个结构体。
      • 如果线程创建成功,则返回 0 。
      • 如果线程创建成功,则返回非零值。
  • void pthread_exit(void *retval);
    
    • 用途:终止当前线程的运行。
      • 需要传入一个变量 retval ,用于存储当前线程的返回值。
  • int pthread_cancel(pthread_t id);
    
    • 用途:向指定 id 的那个线程,发送 cancel 请求。
      • 目标线程可能立即终止,也可能稍后终止,也可能忽略 cancel 请求。
  • int pthread_join(pthread_t id, void **retval);
    
    • 用途:暂停当前线程的运行,等待指定 id 的那个的线程终止。
      • 当目标线程终止之后,其返回值会存储在 retval 变量中。
  • 例:

    #include <stdio.h>
    #include <pthread.h>
    
    void *fun1(){
        printf("fun1() end.\n");
        return 0;
    }
    
    typedef struct{
        char name[10];
        int age;
    } Horse;
    
    void *fun2(void *p){
        Horse h = *(Horse *)p;
        printf("name: %s , age: %d\n", h.name, h.age);
        printf("fun2() end.\n");
        return 0;
    }
    
    int main(){
        int rc;
        pthread_t id;
    
        // 创建第一个线程
        rc = pthread_create(&id, NULL, fun1, NULL);
        if (rc)
            printf("Failed to create the thread fun1().\n");
    
        // 创建第二个线程
        Horse horse = {"Jack", 5};
        rc = pthread_create(&id, NULL, fun2, &horse);
        if (rc)
            printf("Failed to create the thread fun2().\n");
    
        // 阻塞主线程的运行,以免提前终止子线程
        pthread_join(id, NULL);
        printf("main() end.\n");
        return 0;
    }
    

# stdafx.h

  • Visual Studio 2010 中,将 stdio.hstdlib.hwindows.h 等头文件都包含到了 stdafx.h(标准应用程序框架的扩展)中。
    • 它属于预编译头文件,将一些经常调用的代码预先编译好了。
    • 导入该头文件时,需要使用双引号,即 #include "stdafx.h"

# stdarg.h

  • stdarg.h 用于定义变参函数。
    • 变参函数,没有声明具体的形参列表。因此调用该函数时,可以传入类型不定、数量不定的实参。
    • 例如 printf()scanf() 就属于变参函数。
  • 例:定义一个变参函数
    #include <stdio.h>
    #include <stdarg.h>                      // 导入头文件 stdarg.h
    
    void print_int(int argc, ...)            // 定义函数时,形参只有一个 argc 参数(表示参数个数),然后是省略号
    {
        va_list ap;                          // 创建一个 va_list 类型的变量,用于存储参数列表
        va_start(ap, argc);                  // 初始化参数列表,存储 argc 个参数
        int i;
        for(i = 0; i < argc; i++)
            printf("%d\n", va_arg(ap, int)); // 读取下一个参数,预期为 int 类型
        va_end(ap);                          // 销毁参数列表
    }
    
    int main(){
        print_int(3, 10, 11, 12);   // 调用函数,第一个实参为 3 ,表示后面还有 3 个实参
        return 0;
    }
    

# stddef.h

  • stddef.h 定义了一些变量类型、宏。其它标准库头文件,大多导入了这个头文件。
  • stddef.h 定义了 size_t 数据类型,通常用于接收 sizeof 的返回值。
    • 在 32 位 CPU 上,size_t 被定义为 unsigned int
    • 在 64 位 CPU 上,size_t 被定义为 unsigned long long

# stdio.h

  • stdio.h 用于处理标准输入和输出(standard input and output),也就是如何在终端输入、输出字符串。
  • stdio.h 定义了三个特殊的文件,表示在电脑终端显示的三种内容:
    • stdin :标准输入。用户通过键盘在终端输入的字符串,通常会写入 stdin 文件,程序可通过 stdin 读取该字符串。
    • stdout :标准输出。程序通过 print() 函数打印的字符串,通常会写入 stdout 文件,然后显示在终端。
    • stderr :标准错误。程序的报错信息,通常会写入 stderr 文件,然后显示在终端。

# stdout

  • 以下几个函数,用于输出字符串:

    int putchar(int char);
        // 将一个字符 char 输出到 stdout
        // 例如:putchar('H');
    
    int puts(const char *str);
        // 将一个字符串 str 输出到 stdout ,并在末尾加上 \n
        // 例如:puts("Hello World!");
    
    int printf(const char *format, ...);
        // 按照 format 格式生成一个字符串,输出到 stdout
        // 省略号 ... 可以填入任意个变量,用于嵌入任意个值,到 format 字符串中
        // 该函数的返回值是一个数字,表示成功输出了几个字符
    
    int fprintf(FILE *stream, const char *format, ...);
        // 按照 format 格式生成一个字符串,输出到 stream 文件流
    
    int sprintf(char *str, const char *format, ...);
        // 按照 format 格式生成一个字符串,输出到 str
        // 如果函数执行成功,则返回输出的字符串长度(不包括末尾的空字符 \0 )
        // 如果函数执行失败,则返回一个负数
    
    int snprintf(char *str, size_t n, const char *format, ...);
        // 与 sprintf 类似,但限制生成的字符串最大长度为 n bytes (包括末尾的空字符 \0 )
        // 如果生成的字符串长度大于 n ,则只取前 n-1 个字符,然后加上一个空字符 \0 结尾,输出到 str
        // 如果函数执行成功,则不管生成的字符串长度是否大于 n ,总是返回输出到 str 的字符串长度
    
  • 与 sprintf() 函数相比,snprintf() 更安全,可避免输出的字符串,超过 str 容量。

    char *buffer = malloc(sizeof(char) * 10);
    int len = 0;
    len = snprintf(buffer, 6, "%d.1234", 0);
    printf("buffer=%s\n", buffer);  // 输出为:buffer=0.123
    printf("len=%d\n", len);        // 输出为:len=6
    

# format

  • printf()、scanf() 等函数,函数名都带 f 后缀,表示采用特定格式(format)来处理字符串。

    • format 是一个字符串,用于控制格式,它可以包含任意个普通字符、格式控制符。
  • 格式控制符,表示此处存在一个值,并声明其数据类型、显示格式。

    • 存在多种格式控制符:
      %d    // 表示 decimal 十进制数字,对应 int 或 short 数据类型
      %i    // 在 printf() 中,%d 与 %i 效果相同。在 scanf() 中,%i 能识别八进制前缀 0 、十六进制前缀 0x
      %u    // unsigned int 或 unsigned short
      %ld   // long
      %lu   // unsigned long
      %lx   // long 型数值,但采用十六进制格式
      
      %f    // float
      %lf   // double
      %Lf   // long double
      
      %c    // char 数据类型
      %s    // 字符串
      
      %e    // 按科学计数法显示的值
      %o    // 八进制格式的值
      %#o   // 八进制格式的值,并且加上 0 前缀
      %x    // 十六进制格式的值,并且每个字母小写
      %X    // 十六进制格式的值,并且每个字母大写
      %#x   // 十六进制格式的值,并且加上 0x 前缀
      %#X   // 十六进制格式的值,并且加上 0X 前缀
      %p    // 指针指向的内存地址,是一个十六进制格式的值,加上 0x 前缀
      
    • 各个格式控制符,都以百分号 % 开头。如果用户想在 format 字符串中包含一个普通的百分号,则需要使用 %%
    • 例:
      #include <stdio.h>
      
      int main(){
          int x = 10;
          printf("%d\n", x);    // 输出为:10
          printf("%f\n", 10.2); // 输出为:10.200000
          printf("%d\n", 10.2); // 用 %d 处理浮点数会出错,这里输出为:2147483638
          printf("%f\n", 3);    // 用 %f 处理整数会出错,这里会输出最近一个浮点数:10.200000
      
          printf("%e\n", 10.2); // 输出为:1.020000e+01
      
          int octal_num = 075;
          int hex_num = 0x1F;
          printf("octal_num=%o, hex_num=%X\n", octal_num, hex_num); // 输出为:octal_num=75, hex_num=1F
          printf("octal_num=%o, hex_num=%X\n", 10, 10);   // 输出为:octal_num=12, hex_num=A
      
          printf("%d\n", &x);   // 输出为:681891340
          printf("%x\n", &x);   // 输出为:28a4d60c
          printf("%p\n", &x);   // 输出为:0x7ffc28a4d60c
      
          return 0;
      }
      
  • 输出一个值时,可以用 %m.n 的格式,使得至少为 m ,使得小数点后的位数为 n 。

    • m 用于控制输出的最少字符数(包括小数点)。
      printf("--%5d--", 1);
          // 如果实际总位数小于 m ,则在左侧加上空格(因为排版为右对齐),补齐位数
          // 输出为:--    1--
      printf("--%05d--", 1);
          // 可以改用 0 补齐位数
          // 输出为:--00001--
      printf("--%5s--", "test");
          // 同理,可以控制 %s 输出的最少字符数
          // 输出为:-- test--
      
    • n 用于控制小数点后的保留位数。
      printf("--%5.2f--", 1.0);
          // 输出为:-- 1.00--
      printf("--%05.2f--", 1.0);
          // 输出为:--01.00--
      printf("--%5f--", 1.0);
          // 如果不指定 n ,则默认小数点后默认保留 6 位
          // 输出为:--1.000000--
      printf("--%5.2f--", 1.056);
          // 如果小数点后实际位数大于 n ,则进行 5 舍 6 入,不是四舍五入
          // 输出为:-- 1.06--
      
    • 输出负数时,默认包含负号。而输出正数时,默认不包含正号,除非使用 %+
      printf("--%5d--", 1);
          // 输出为:--    1--
      printf("--%+5d--", 1);
          // 输出为:--   +1--
      
    • 默认排版为右对齐,如果使用 %- ,则改为左对齐。
      printf("--%5d--", 1);
          // 输出为:--    1--
      printf("--%-5d--", 1);
          // 输出为:--1    --
      

# stdin

  • 以下几个函数,用于读取字符串:

    int getchar(void);
        // 从 stdin 读取一个字符,作为函数的返回值
        // 例如:char c = getchar();
    
    char *gets(char *str);
        // 从 stdin 读取一个字符串,存储到 str 中
        // 例如:char str[10]; gets(str);
    
    char *fgets(char *str, int n, FILE *stream);
        // 从 stream 文件读取一个字符串,存储到 str 中
        // 最多读取 n-1 个字符,并在 str 末尾写入一个空字符
    
    int scanf(const char *format, ...);
        // 从 stdin 读取一个字符串,并按照 format 格式,从中提取值
        // 省略号 ... 可以填入任意个变量,用于存储提取的各个值
        // 并且,这里不能直接填入变量的名称,而需要填入变量的地址,比如用 & 取地址运算符
        // 该函数的返回值是一个数字,表示成功解析了几个值
    
    int sscanf(const char *str, const char *format, ...);
        // 读取 str 这个字符串,并按照 format 格式,从中提取输入值
    
  • 例:从 stdin 读取一个值

    int x = 0;
    printf("Please enter: ");
    scanf("%d", &x);
        // 假设用户输入 10 ,则 x 会被赋值为 10
        // 假设用户输入 10.2 ,则 x 会被赋值为 10 ,因为 %d 只会识别整数
        // 假设用户输入 a ,则 x 依然取值为 0 ,因此 %d 只会识别整数
    printf("You entered: %d\n", x);
    
    scanf("x=%d", &x);
        // 假设用户输入 x=10 ,则 x 会被赋值为 10
        // 假设用户输入 10 ,则 x 依然取值为 0 ,因为输入必须符合 x=%d 格式,才能被解析
    
    scanf("%3d", &x);
        // 这是只提取数字的前 3 位
        // 假设用户输入 123456 ,则 x 会被赋值为 123
    
    scanf("%*d.%d", &x);
        // 这是只提取小数部分
        // %* 表示解析一个值之后,不存储到变量
        // 假设用户输入 3.14 ,则 x 会被赋值为 14
    
  • 例:从 stdin 读取多个值

    int a=0, b=0 , c=0;
    printf("Please enter: ");
    int ret = scanf("%d %d %d", &a, &b, &c);
        // 假设用户输入 1 2 3 ,则结果为 ret=3, a=1, b=2, c=3
        // 假设用户输入 1  2   3 ,插入多个空字符,则依然能解析成功,结果为 ret=3, a=1, b=2, c=3
        // 假设用户输入 1  b ,包含非数字,则从二个变量开始解析失败,结果为 ret=1, a=1, b=0, c=0
    printf("ret=%d, a=%d, b=%d, c=%d\n", ret, a, b, c);
    if(ret != 3)
        printf("Failed to read all values.\n");
    
  • 例:从 stdin 读取一个字符串

    char str[10];
    printf("Please enter: ");
    scanf("%s", str);
    printf("You entered: %s\n", str);
    
  • 使用 %[字符组]s 的格式解析输入字符串时,只会提取在字符组中存在的字符,其它字符则当作定界符。

    • 例:
      scanf("%3[0-9]s", str);
          // 假设用户输入 123456 ,则结果为 str=123
          // 假设用户输入 12a3 ,则结果为 str=12
      
    • a-z0-9 的格式可匹配一组 ASCII 编码连续的字符。
    • 如果想直接匹配负号 - ,则必须将它放在字符组的末尾。
    • 使用 %[^字符组] 的格式读取输入时,只会提取在字符组中不存在的字符。
    • 当 scanf() 读取到定界符时,会停止解析当前这个值,然后解析下一个值。
      • scanf() 中,默认的定界符是空字符。gets() 中,默认的定界符是换行符。
      • 假设用户用键盘输入 Hello World! 然后按下 Enter 键。用 scanf("%s", str); 会解析得到 Hello ,用 gets(str); 则会解析得到 Hello World!

# stderr

  • 大部分情况下,用户只会用到 stdin、stdout 。如果需要使用 stderr ,则可通过以下方式,输出字符串作为报错:

    fprintf(stderr, "test\n");
    fprintf(stderr, "%s\n", "test");
    
  • errno.h 定义了一个全局变量 errno ,用于记录 C 语言程序,最近调用的一个函数的报错信息。

    • 如果调用一个函数出错,则通常返回值为负数,并且给 errno 赋值一个数字,作为错误代码。
    • 如果调用一个函数成功,则通常返回值为 0 ,并且给 errno 赋值为 0 。
    • 例:
      #include <stdio.h>
      #include <errno.h>
      
      int main(){
          printf("errno=%d\n", errno);  // 此时没有函数出错,因此输出为: errno=0
          return 0;
      }
      
    • POSIX 定义了一些错误代码,及其描述。
      • 例如 errno=1 对应的错误描述为 Operation not permitted 。
      • 例如 errno=2 对应的错误描述为 No such file or directory 。
  • stdio.h 提供了 perror() 函数,用于查找 errno 对应的错误描述,打印到 stderr 。

    • 例:
      #include <stdio.h>
      
      int main(){
          perror("result"); // 此时没有函数出错,因此输出为: result: Success
          return 0;
      }
      

# 读写文件

  • FILE *fopen(const char *filename, const char *mode);
    
    • 用途:打开磁盘中一个文件。
      • 如果函数执行成功,则返回一个 FILE * 类型的文件指针 ,代表一个文件流,用户可以从中读、写数据。
      • 如果函数执行失败,则返回 NULL 。
    • 打开文件时,有多种 mode 模式:
      r     # 只读模式。文件指针指向文件开头,如果文件不存在则报错
      w     # 只写模式。文件指针指向文件开头(因此写入时会覆盖原数据),如果文件不存在则自动创建它
      a     # 追加(只写)模式。文件指针指向文件末尾,如果文件不存在则报错
      r+    # r+w 模式。如果文件不存在则报错
      w+    # w+r 模式。如果文件不存在则自动创建它
      a+    # a+r 模式。如果文件不存在则报错
      
      • 以上模式,都会将文件中每个字节的数据,当作 ASCII 字符处理,称为字符类型的数据流,简称为字符流。
      • 如果在模式中加入 b 字母,则会将每个字节的数据,当作二进制值处理,称为二进制流。比如 "rb"、"wb"、"ab+"
    • FILE 结构体的定义如下:
      typedef struct{
        int fd;			    // 记录文件描述符
        unsigned char *buf;	 // 每打开一个文件,会创建一个缓冲区。这样每次写入数据到文件时,先将数据放到缓冲区,然后批量写入磁盘,从而减少写磁盘的次数
        size_t size;		// 记录该文件的体积,单位为 bytes
        size_t pos;		  // 记录当前读写的位置,是文件中第几个字节
      } FILE;
      
      • 使用 r 模式打开文件时,指针 FILE.pos 最初指向文件中的第 0 个字节。
      • 每读取一个 char 字符,指针 FILE.pos 就会向后移动 1 个字节。
      • 移动到文件末尾时,会读取到文件结束符 EOF (宏定义为 -1 )。
  • int fseek(FILE *stream, long int offset, int whence);
    
    • 用途:修改 FILE.pos ,赋值为 whence + offset 。
      • offset 表示字节偏移量。
      • whence 表示定位的起点,可以取值为:
        #define SEEK_SET 0  // 表示文件开头
        #define SEEK_CUR 1  // 表示当前位置
        #define SEEK_END 2  // 表示文件末尾
        
    • 例:
      fseek(f, 5L, SEEK_SET);
      
  • long ftell(FILE *stream);
    
    • 用途:返回 FILE.pos 的值。
      • 如果函数执行失败,则返回 -1L 。
    • 例:
      long pos = ftell(f);
      if(pos == -1L)
          return -1;
      
  • 例:读取文件

    // 打开文件
    FILE *f = fopen("/tmp/1.txt", "r");
    
    // 检查文件是否成功打开
    if(f == NULL){
        printf("Failed to open the file.\n");
        return -1;
    }
    
    int c;
    while (c != EOF){
        c = fgetc(f); // fgetc() 函数用于从文件读取一个 char 字符
        putchar(c);   // 将一个 char 字符打印到终端
    }
    
    // 读写完一个文件之后,应该调用 fclose() 函数关闭该文件,从而释放文件描述符,允许其它进程修改该文件。
    fclose(f);
    
  • 例:使用 fprintf() 和 fscanf() 读写字符流

    f = fopen(filename, "w");
    int x = 1, y = 2;
    fprintf(f, "%d%d", x, y);
    fclose(f);
    
    f = fopen(filename, "r"); // 关闭文件,再以读取模式打开文件,从而让文件指针重新指向第 0 个字节
    fscanf(f, "%d%d", &x, &y);
    fclose(f);
    
  • 以下函数,用于读写二进制流

    size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
      // 从文件流 stream 读取二进制数据,每次读取 size 个字节,读取 count 次,写入到 buffer 中
      // 如果函数运行成功,则返回实际的 count
    
    size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
      // 从 buffer 读取二进制数据,每次读取 size 个字节,读取 count 次,写入到文件流 stream 中
      // 如果函数运行成功,则返回实际的 count
    

    例:

    #define COUNT 3
    
    typedef struct{
        char name[10];
        int age;
    } Horse;
    Horse array[COUNT] = {{"AA", 1}, {"BB", 2}, {"CC", 3}};
    
    FILE *f = NULL;
    char *filename = "/tmp/1.txt";
    
    // 写入
    f = fopen(filename, "wb");
    if (f == NULL)
    {
        printf("Failed to open the file.\n");
        return -1;
    }
    if (fwrite(array, sizeof(Horse), COUNT, f) != COUNT)
    {
        printf("Error writing the file.\n");
        return -1;
    }
    fclose(f);
    
    // 读取
    f = fopen(filename, "rb");
    if (f == NULL)
    {
        printf("Failed to open the file.\n");
        return -1;
    }
    if (fread(array, sizeof(Horse), COUNT, f) != COUNT)
    {
        printf("Error reading the file.\n");
        return -1;
    }
    fclose(f);
    
    int i;
    for (i = 0; i < COUNT; i++) {
        printf("name: %s, age: %d\n", array[i].name, array[i].age);
    }
    

# stdlib.h

  • stdlib.h 是标准函数库(standard library),提供了一些常用的函数。

# 关于数字

  • int abs(int x);
    
    • 用途:输入一个整数,返回它的绝对值。
    • 例:
      printf("%d\n", abs(-1));  // 输出为:1
      
  • int atoi(const char *str);
    
    • 用途:输入一个字符串,将它转换成整数并返回。如果转换失败,则返回 0 。
    • 例:
      printf("%d\n", atoi("Hello"));  // 输出为:0
      printf("%d\n", atoi("3"));      // 输出为:3
      printf("%d\n", atoi("3.14"));   // 输出为:3
      
  • double atof(const char *str);
    
    • 用途:输入一个字符串,将它转换成浮点数并返回。如果转换失败,则返回 0.0 。
    • 例:
      printf("%lf\n", atof("Hello"));     // 输出为:0.000000
      printf("%lf\n", atof("3"));         // 输出为:3.000000
      printf("%lf\n", atof("3.14"));      // 输出为:3.140000
      printf("%lf\n", atof("-31.4e-1f")); // 输出为:-3.140000
      
  • int rand(void);
    
    • 用途:返回一个伪随机数。
    • 原理:
      • 根据 seed ,随机生成一组整数,组成一个序列。这些整数的取值范围为 0 至 RAND_MAX=0x7FFFFFF
      • 每次调用该函数,就会返回序列中下一个整数。
      • 只要 seed 相同,每次重启程序,都会得到相同的随机数。
    • 例:
      int max = 10, min = 1;
      int x = rand()%(max-min) + min;   //生成介于 max 与 min 之间的随机数
      printf("%d\n", x);
      
  • void srand(unsigned int seed);            //用于设置随机数的种子,使得函数产生不同的随机数序列
    
    • 用途:设置随机数算法的种子,不同的种子会让 rand() 函数产生不同的随机数序列。
    • 如果别人不知道种子,就不能预测随机数的取值,相当于真随机数。
    • 例:
      srand(123);
      printf("%d\n", rand(););
      

# 关于内存

  • void *malloc(size_t size);
    
    • 用途:请求分配一块内存空间,体积为 size 个字节。
      • 如果请求成功,则返回一个指向该内存地址的指针。
      • 如果请求失败,则返回 NULL 。
    • 什么情况下使用 malloc() ?
      • 当用户需要存储一些数据时,如果创建变量,则该变量会一直占用内存空间,直到变量的作用域结束。
      • 为了节省内存空间,可以在需要存储数据时,通过 malloc() 临时分配一块内存空间。在不需要存储数据时,调用 free() 释放内存空间。
    • 例:
      char *str = malloc(10*sizeof(char));  // 分配一块内存空间,可以存储 10 个 char 字符
      if(str == NULL)       // 申请分配内存之后,应该先检查返回的指针是否有效,再进行下一步操作
          return -1;
      printf("%s\n", str);  // 输出为乱码,因为这块内存空间,此时每个字节的取值是未知的
      strcpy(str, "Hello");
      printf("%s\n", str);  // 输出为:Hello
      free(str);            // 释放这块内存空间
      
    • malloc() 的返回值是 void * 类型的指针,即通用指针。
      • C 语言中,将通用指针,赋值给其它类型的指针时,会发生自动类型转换。
      • C++ 中,通用指针不会自动类型转换,只能强制类型转换:
        char *str = (char *)malloc(10*sizeof(char));
        
        不过 C++ 分配内存空间时,很少使用 malloc() ,而是使用关键字 new 。
  • void free(void *p);
    
    • 用途:假设用户请求分配了一块内存空间,地址记在指针 p 中。用户可以调用 free() ,释放这块内存空间。
    • 每块内存空间,只能被释放一次。如果重复释放,则会导致程序崩溃:
      free(p);
      free(p);
      
      为了避免这种情况,建议每次调用 free() 之后,就将指针存储的内存地址抹除:
      free(p);
      p = NULL;   // 此后如果再调用 free(p) ,则相当于 free(NULL) ,不会引发问题
      
  • void *calloc(size_t num, size_t size);
    
    • 用途:请求分配一块内存空间,体积为 num*size 个字节,并将每个字节赋值为 0 。
      • 如果请求成功,则返回一个指向该内存地址的指针。
      • 如果请求失败,则返回 NULL 。
    • 例:
      char *str = calloc(10, sizeof(char));
      if(str == NULL)
          return -1;
      printf("%s\n", str);  // 输出为空
      strcpy(str, "Hello");
      printf("%s\n", str);  // 输出为: Hello
      free(str);
      
  • void *realloc(void *p, size_t size);
    
    • 用途:假设用户请求分配了一块内存空间,地址记在指针 p 中。用户可以调用 realloc() ,将这块内存空间的体积,改为 size 个字节。
      • 如果请求成功,则返回一个指向该内存地址的指针。
      • 如果请求失败,则返回 NULL 。
      • 如果 p 为空指针,则 realloc() 会分配一块新的内存空间。
    • 例:
      char *str = malloc(10);
      strcpy(str, "Hello");
      
      str = realloc(str, 20); // 可以扩大内存空间
      str = realloc(str, 10); // 也可以缩小内存空间
      
      printf("%s\n", str);    // 输出为: Hello
      

# 管理内存

  • 程序运行时,会占用一定容量的内存空间,并将它们分为 stack、heap 等几个用途不同的区域。

  • 程序运行时,需要分配多块内存空间来存储数据,主要有两种分配方式:

    • 静态的内存分配方式
      • :在程序编译时,就决定了如何分配这些内存空间。在程序启动时,会立即申请这些内存空间,然后一直占用。直到程序终止,才释放这些内存空间。
      • 比如程序代码、全局变量、静态变量,采用静态的内存分配方式。
    • 动态的内存分配方式
      • :在程序运行时,可以随时申请这些内存空间,随时释放它们。
      • 比如局部变量、malloc() 分配的内存,采用动态的内存分配方式。
  • C 语言的主要特色之一是,可以调用 malloc() 动态分配内存空间,可以通过指针直接访问内存地址。

    • 而 Java、Python 等语言中,不允许用户直接分配指定容量的内存空间,也不允许直接访问内存地址。
      • 用户只能以创建变量(实际上是创建对象)的方式,来存储数据。当变量被创建时,会自动分配内存空间。当对象停止使用时,会自动释放内存空间。
      • 优点:用户不必直接管理内存空间,工作量大幅减少。
      • 缺点:用户的操作权限变少,不能精打细算地分配内存空间,不能随意访问任意内存地址。
  • C 语言允许用户直接管理内存,容易引发一些内存问题:

    • 访问无效的内存地址

      • 可能该内存地址的格式不正确,比如空指针。
      • 可能该内存地址的格式正确,但没有分配给当前进程使用,比如被 free() 的指针。
      • 可能该内存地址已分配给当前进程使用,但存储着未知的数据,比如野指针。
      • 影响:可能导致程序故障,甚至崩溃。
    • 内存越界访问

      • :访问一块内存空间时,使用的内存地址,超出了这块内存空间的地址范围。
      • 例:程序用 malloc() 申请一块 2 bytes 的内存空间,得到一个指针。然后往该指针指向的内存地址,写入 3 bytes 的数据。此时第 3 个 byte ,就会写入当前内存的地址范围之外。
        • 为了避免这种问题,建议用户不要使用 gets、sprintf、strcpy、strcat、 等函数,因为它们没有限制字符串的长度,可能导致内存越界访问。建议改用 fgets、snprintf、strncpy、strncat 等函数。
      • 越界访问的内存地址,分为两种情况:
        • 该内存地址,已分配给当前进程使用,但属于另外一块内存空间(比如被另外一个 mallo() 分配)。读取它,可能导致程序故障。写入它,可能修改程序存储的其它数据,导致程序故障。
        • 该内存地址,没有分配给当前进程使用。读取它、写入它,都会导致程序崩溃。
      • Linux 内核会为每个进程维护一个页表,记录该进程申请使用了哪些内存地址。当进程访问一个内存地址时:
        • 如果该内存地址,记录在当前页表中,则说明已分配给该进程。因此 Linux 会允许访问。
        • 如果该内存地址,不记录在当前页表中,则说明未分配给该进程。因此 Linux 会拒绝访问,发出 segment fault 信号,导致进程崩溃。
        • 除了堆内存,如果进程越界访问内核内存空间、只读存储区、代码段,Linux 也会拒绝访问,发出 segment fault 信号,导致进程崩溃。
    • 缓冲区溢出攻击(Buffer Overflow Attack)

      • :指用户利用内存越界访问,对程序进行恶意攻击。
      • 通常是程序的开发者,将程序交给其他人使用,但有人故意引发内存越界访问。比如向程序的一个变量(通常用作缓冲区,允许用户修改)中,故意写入过长的数据,超出该变量的内存地址范围,写入到该变量隔壁的内存地址,从而修改隔壁的数据(比如变量、函数返回地址)。
    • 栈溢出(stack overflow)

      • :发生在 stack 区域的缓冲区溢出。
      • 比如读写一个数组时,下标越界,这可能读写隔壁变量的内存地址。
    • 内存溢出(out of memory)

      • :指程序占用的内存,超出了允许的最大容量。
      • 程序的 stack 区域,容量较小,容易达到上限。
        • 比如 Linux 默认设置了每个线程的 stack 容量最大为 8MB 。
        • 程序运行时,会将所有函数参数、局部变量存储在 stack 区域。如果创建一个很大的数组类型局部变量,可能超出栈区的最大容量,导致程序崩溃。
        • 每次调用一个函数,都会在栈区分配一块内存来存储函数参数、局部变量,当函数执行完毕时才释放内存。如果一个函数调用另一个函数,后者再调用另一个函数,调用层数太深,则占用内存可能超出栈区的最大容量,导致程序崩溃。
      • 程序的 heap 区域,容量没有上限。如果不断增长,则主机可用内存会越来越少。最终主机内存耗尽,触发 OOM 杀死进程。
    • 内存泄漏(memory leak)

      • :指程序 heap 区域的内存容量不断增长,未来可能发生内存溢出。
      • 这通常是因为程序不断调用 malloc() ,占用越来越多的内存,没有用 free() 全部释放。

# 关于系统

  • void exit(int status);
    
    • 用途:使当前进程终止运行,并返回一个整数,作为退出码。
    • C 语言程序启动时,会执行 main() 函数。等到 main() 函数执行完毕,程序才终止。不过调用 exit() ,可以让程序立即终止。
  • void abort(void);
    
    • 用途:发送 SIGABRT 信号,使当前进程终止运行。
  • char *getenv(const char *name);
    
    • 用途:从系统读取一个名为 *name 的环境变量,返回它的值。如果没找到该环境变量,则返回 NULL 。
    • 例:
      const char *value = getenv("PWD");
      if(value != NULL)
          printf("%s\n", value);
      
  • int system(const char *string);
    
    • 用途:输入一个字符串,作为一条终端命令,让本机操作系统执行。
    • 例:
      system("pause");  // 在 Windows 系统执行 pause 命令,会暂停终端
      system("cls");    // 在 Windows 系统执行 cls 命令,会清空终端
      system("exit");   // 在 Windows 系统执行 cls 命令,会退出终端
      

# string.h

string.h 提供了一些处理字符串的函数。如下:

# 读取字符串

  • size_t strlen(const char *str);
    
    • 用途:返回字符串的长度,也就是包含多少个字符,不考虑末尾的空字符。
    • 例:
      char str[] = "Hello";
      printf("%d\n", strlen(str));  // 输出为:5
      printf("%d\n", sizeof(str));  // 输出为:6 。因为包含末尾的空字符
      
  • char *strchr(const char *str, int chr);
    
    • 用途:在字符串中,查找指定一个字符,返回第一次找到该字符的内存地址。如果没有找到,则返回 NULL 。
    • 例:
      char *pstr = strchr("Hello", 'e');
      if(pstr != NULL)
          printf("%s\n", pstr);   // 输出为:ello
      
  • int strcmp(const char *str1, const char *str2);
    
    • 用途:逐个字符地比较两个字符串。如果字符数相同、同位字符也相同,则返回 0 ,否则返回非零。
  • int strncmp(const char *, const char *, size_t n);
    
    • 用途:与 strcmp() 相似,但最多比较 n 个字符。
    • 例:
      printf("%d\n", strcmp("Hello", "Hello!"));      // 输出为:-1
      printf("%d\n", strncmp("Hello", "Hello!", 5));  // 输出为:0
      

# 拷贝字符串

  • char *strcpy(char *dst, const char *src);
    
    • 用途:拷贝 src 字符串,写入 dst 字符串的内存空间,这会覆盖 dst 字符串的原有字符。函数执行成功后,会返回 dst 字符串的内存地址。
    • 源码:
      char *strcpy(char *dst, const char *src){
          char *r = dst;
          assert((dst != NULL) && (src != NULL));
          while (*r++ = *src++);  // 逐个字符地赋值,当赋值到空字符(ASCII 码为 0 )时,结束 while 循环
          return dst;
      }
      
    • 例:
      char *pstr = malloc(10*sizeof(char));
      strcpy(pstr, "Hello");
      printf("%s\n", pstr);   // 输出为:Hello
      
      strcpy(pstr, "Hi");     // 拷贝 "Hi" ,这会覆盖 pstr 的第 0、1 个字符,并将第 2 个字符改为空字符
      printf("%s\n", pstr);   // 输出为:Hi
      printf("%s\n", pstr+3); // 输出为:lo 。此时 pstr 的第 3、4 个字符,没有被覆盖
      
  • int strncpy(char *dst, const char *src, size_t n);
    
    • 用途:与 strcpy() 相似,但最多拷贝 n 个字符(包括空字符)。
      • 如果 strlen(src) + 1 < n ,则 strncpy() 会将缺少的几位字符,用空字符补齐。
      • 如果 strlen(src) + 1 > n ,则 strncpy() 不会完整拷贝 src 字符串。而且,不会拷贝 src 末尾的空字符,导致 dst 缺少空字符,读取 dst 字符串时可能内存越界。
      • 因此,要谨慎考虑字符串的长度。
    • 例:
      char *src = "Hello";
      int len = (strlen(src) + 1);
      char *dst = malloc(sizeof(char) * len);
      strncpy(dst, src, len);
      printf("%s\n", dst);  // 输出为:Hello
      
  • char *strcat(char *dst, const char *src);
    
    • 用途:拷贝 src 字符串,写入 dst 字符串的末尾。使得 src 的第一个字符,覆盖 dst 原本的空字符。
    • 例:
      char *pstr = malloc(10*sizeof(char));
      strcat(pstr, "Hello");
      strcat(pstr, "!");
      printf("%s\n", pstr); // 输出为:Hello!
      
  • char *strncat(char *dst, const char *src, size_t n);
    
    • 用途:与 strcat() 相似,但最多拷贝 n 个字符(包括空字符)。
  • void *memcpy(void *dst, const void *src, size_t n);
    
    • 用途:从 src 内存地址,拷贝 n 个字节的数据,写入 dst 内存地址。
    • strcpy() 用于拷贝字符串,遇到空字符就会停止拷贝。而 memcpy() 用于拷贝任意字节数据,遇到空字符也不会停止拷贝。
  • void *memset(void *src, int x, size_t n);
    
    • 用途:将从 src 内存地址开始的 n 个字节,都赋值为 x 。
    • 动态分配一块内存空间之后,它每个字节的取值是未知的,称为垃圾值。通常用 memset() 初始化每个字节,以免读取到垃圾值。例:
      char *pstr = malloc(10*sizeof(char));
      memset(pstr, 0, sizeof(10));
      

# time.h

  • time.h 提供了获取时间、日期的函数。
  • 例:
    #include <stdio.h>
    #include <time.h>
    
    int main(){
      // 从操作系统获取当前时间。取值为 1970 年至今的秒数,称为 Unix timestamp
      time_t timestamp;
      time(&timestamp);
      printf("timestamp: %ld\n", timestamp);
    
      // 将 timestamp 时间戳,转换成年月日时分秒的格式,采用本地时区
      struct tm *time;
      time = localtime(&timestamp);
    
      printf("Year: %d\n", 1900 + time->tm_year);
      printf("Month: %d\n", 1 + time->tm_mon);
      printf("Day: %d\n", time->tm_mday);
      printf("Hour: %d\n", time->tm_hour);
      printf("Minute: %d\n", time->tm_min);
      printf("Second: %d\n", time->tm_sec);
      printf("Weekday: %d\n", time->tm_wday);
    
      // 输出 Www Mmm dd hh:mm:ss yyyy 格式的时间字符串
      printf("Time String: %s", asctime(time));
    
      return 0;
    }