第16章 系统函数库

1. 系统函数库概述

1.1 什么是系统函数库

系统函数库是操作系统提供的一组函数集合,用于访问系统功能和资源。这些函数封装了底层的系统调用,为应用程序提供了一个统一的接口,使应用程序能够与操作系统进行交互。系统函数库在C语言编程中扮演着至关重要的角色,它们不仅提供了对底层硬件和操作系统服务的访问,还通过抽象和封装简化了编程复杂度。

1.2 系统函数库的分类

C语言中的系统函数库主要包括:

  • 标准I/O库:用于文件输入/输出操作,提供缓冲机制和格式化I/O
  • 字符串处理库:用于字符串操作,包括复制、连接、比较、查找等功能
  • 数学库:用于数学计算,提供三角函数、指数对数函数、取整函数等
  • 时间和日期库:用于时间和日期操作,包括获取当前时间、时间转换、格式化等
  • 内存分配库:用于动态内存分配,包括malloc、calloc、realloc、free等函数
  • 进程控制库:用于进程管理,包括创建、终止、等待进程等功能
  • 网络编程库:用于网络通信,包括套接字操作、TCP/IP协议实现等
  • 信号处理库:用于信号处理,包括信号注册、捕获、忽略等功能
  • 线程编程库:用于多线程编程,包括线程创建、同步、互斥等功能
  • 系统调用库:用于直接调用系统服务,提供对操作系统底层功能的访问
  • 环境变量库:用于环境变量操作,包括获取、设置、删除环境变量等
  • 错误处理库:用于错误处理,包括错误码转换、错误信息获取等功能

1.3 系统函数库的使用

使用系统函数库需要:

  1. 包含相应的头文件:如#include <stdio.h>,头文件包含了函数声明、宏定义和类型定义
  2. 链接相应的库:如使用-lm链接数学库,确保编译器能找到函数的实现
  3. 遵循函数的调用约定:正确传递参数,处理返回值,遵循函数的使用规范
  4. 了解函数的实现细节:理解函数的底层实现,以便更好地使用和优化
  5. 注意函数的安全性:避免使用不安全的函数,如gets、strcpy等
  6. 处理函数的错误返回:检查函数返回值,妥善处理错误情况

1.4 系统函数库的底层实现

系统函数库的底层实现通常包括:

  • 封装系统调用:系统函数库通过封装底层的系统调用,为应用程序提供更高级、更易用的接口
  • 缓冲区管理:如标准I/O库的缓冲区机制,减少系统调用次数,提高性能
  • 错误处理:提供统一的错误处理机制,简化应用程序的错误处理逻辑
  • 平台兼容性:处理不同平台之间的差异,提供跨平台的统一接口
  • 性能优化:实现各种性能优化技术,如缓存、预读、写回等
  • 线程安全:在多线程环境下保证函数的安全性,使用锁、线程本地存储等技术

1.5 系统函数库的性能考量

使用系统函数库时的性能考量:

  • 函数调用开销:系统函数调用比普通函数调用开销更大,应避免频繁调用
  • 系统调用次数:减少系统调用次数,利用缓冲区和批处理技术
  • 内存使用:合理使用内存,避免内存泄漏和过度分配
  • 缓存利用:提高缓存命中率,减少缓存未命中带来的性能损失
  • 并发性能:在多线程环境下,注意函数的线程安全性和并发性能
  • I/O操作:优化I/O操作,使用适当的缓冲区大小和I/O模式

1.6 系统函数库的安全使用

安全使用系统函数库的最佳实践:

  • 使用安全的函数:优先使用安全的函数,如snprintf、strncpy等
  • 参数验证:验证函数参数的有效性,避免缓冲区溢出等安全漏洞
  • 错误处理:妥善处理函数返回的错误,避免错误传播
  • 资源管理:正确管理系统资源,如文件描述符、内存等,避免资源泄漏
  • 权限控制:注意函数的权限要求,避免权限提升漏洞
  • 输入验证:验证用户输入,避免注入攻击等安全问题

2. 标准I/O库

2.1 标准I/O库概述

标准I/O库(<stdio.h>)提供了文件输入/输出操作的函数,是C语言中最常用的库之一。它通过缓冲区机制和文件流抽象,为应用程序提供了高效、统一的I/O操作接口,屏蔽了底层系统调用的复杂性。标准I/O库的设计目标是在易用性和性能之间取得平衡,为不同类型的I/O操作提供最佳的实现。

2.1.1 标准I/O库的底层实现

标准I/O库的核心是文件流(FILE)和缓冲区管理:

  1. 文件流结构

    • FILE 是一个包含文件描述符、缓冲区状态、定位信息和锁信息的复杂结构体
    • 每个文件流都有一个关联的文件描述符,用于底层系统调用
    • 流状态包括错误标志、文件结束标志、缓冲区状态和读写位置
    • 现代实现通常包含线程安全锁,支持多线程环境下的并发访问
    • FILE结构体的具体字段:文件描述符(_fileno)、缓冲区指针(_buf)、缓冲区大小(_bufsiz)、当前位置(_ptr)、剩余字节数(_cnt)、错误标志(_err)、文件结束标志(_eof)、锁信息(_lock)等
    • 不同平台的FILE结构体实现可能有所不同,但核心功能一致
  2. 缓冲区管理

    • 全缓冲:只有当缓冲区满或显式刷新时才执行I/O操作(如普通文件)
    • 行缓冲:当遇到换行符或缓冲区满时执行I/O操作(如标准输入/输出)
    • 无缓冲:直接执行I/O操作,无缓冲区(如标准错误)
    • 缓冲区分配策略:首次I/O操作时动态分配,或通过 setvbuf 预分配
    • 缓冲区刷新时机:缓冲区满、显式调用fflush、文件关闭、程序正常退出
    • 缓冲区管理算法:环形缓冲区、双缓冲等技术
  3. 缓冲区大小

    • 默认缓冲区大小通常为8192字节(8KB),与系统页大小匹配
    • 可通过 setvbuf 函数自定义缓冲区大小和类型
    • 缓冲区大小对性能的影响:过小会增加系统调用次数,过大则浪费内存
    • 最佳缓冲区大小:通常为系统页大小的整数倍(4KB、8KB、16KB)
    • 大文件操作的缓冲区优化:使用更大的缓冲区(如64KB、128KB)
  4. 文件操作的原子性

    • 某些I/O操作在特定条件下保证原子性,如 fwrite 对连续内存块的写入
    • 多线程环境下的并发I/O需要额外的同步机制
    • 原子操作的实现:通过锁机制或原子指令保证操作的不可中断性
    • 非原子操作的同步:使用互斥锁、读写锁等同步原语
  5. 与底层系统调用的映射

    • fopenopen + 初始化FILE结构体
    • fclosefflush + close + 释放FILE结构体
    • fread → 检查缓冲区 → 不足时调用 read 填充 → 从缓冲区复制
    • fwrite → 写入缓冲区 → 缓冲区满时调用 write 刷新
    • fseekfflush + lseek + 更新内部位置指针

2.1.2 标准I/O库的性能考量

标准I/O库的性能优势主要来自于:

  1. 减少系统调用:通过缓冲区减少了系统调用次数,降低了用户态到内核态的切换开销
  2. 统一接口:为不同类型的设备(文件、终端、管道等)提供了统一的操作接口
  3. 错误处理:提供了更详细的错误信息和处理机制,简化了错误处理逻辑
  4. 格式化功能:支持复杂的数据格式化和解析,减少了手动格式化的工作量
  5. 预读和写回:某些实现会预测性地读取数据(预读)和延迟写入(写回),提高了连续I/O的性能
  6. 批处理:将多个小的I/O操作合并为一个大的I/O操作,减少系统调用开销
  7. 缓存优化:缓冲区大小与CPU缓存行大小和系统页大小匹配,提高缓存命中率

性能优化策略

  1. 选择合适的缓冲区大小:根据I/O操作的特点选择最佳的缓冲区大小
  2. 使用适当的缓冲模式:对于不同类型的设备使用不同的缓冲模式
  3. 减少缓冲区刷新:避免频繁调用fflush,让缓冲区自然刷新
  4. 批量I/O操作:将多个小的I/O操作合并为一个大的I/O操作
  5. 避免混合读写:在同一个文件流上混合读写会导致缓冲区频繁刷新
  6. 使用二进制模式:对于非文本文件,使用二进制模式可以避免行尾转换开销
  7. 关闭不必要的文件:及时关闭不再使用的文件,释放系统资源

性能对比

操作类型标准I/O库直接系统调用性能差异适用场景
小文件读写标准I/O库系统调用标准I/O库更快频繁的小文件操作
大文件顺序读写标准I/O库系统调用性能相近大文件的顺序处理
随机读写标准I/O库系统调用系统调用更快频繁的随机I/O操作
终端I/O标准I/O库系统调用标准I/O库更合适交互式终端操作

2.1.3 标准I/O库的高级特性

  1. 宽字符支持:通过 <wchar.h> 提供了宽字符I/O函数,如 fwprintffwscanf

    • 宽字符流(FILE *)与普通流的区别:处理的字符大小不同
    • 宽字符编码:UTF-16、UTF-32等编码的支持
    • 宽字符I/O的性能考量:字符转换开销
  2. 定位函数扩展:除了 fseekftell,还提供了 fseekoftello(支持大文件)和 fgetposfsetpos(支持任意大小文件)

    • 大文件支持:处理超过2GB的文件
    • 64位文件操作:使用off_t类型存储文件偏移量
    • 定位操作的性能:避免频繁的定位操作,减少缓冲区刷新
  3. 格式化输出扩展vsnprintf 等函数支持可变参数和安全的格式化输出

    • 可变参数处理:使用va_list、va_start、va_end等宏
    • 安全的格式化输出:避免缓冲区溢出
    • 自定义格式化函数:基于vsnprintf实现自定义的格式化函数
  4. 二进制模式和文本模式:在不同操作系统下处理行尾字符的差异

    • 文本模式:自动处理行尾字符(\n ↔ \r\n)
    • 二进制模式:直接读写字节,不进行任何转换
    • 跨平台兼容性:注意不同平台的文件模式差异
  5. 文件锁定:某些实现提供了文件锁定功能,如 flockfileftrylockfilefunlockfile

    • 线程级锁定:保证多线程环境下的文件操作安全
    • 进程级锁定:通过系统调用实现的文件锁定
    • 锁定粒度:字节级锁定、记录级锁定、文件级锁定
  6. 临时文件操作:提供了 tmpfiletmpnam 等函数用于创建临时文件

    • 临时文件的安全创建:避免命名冲突和安全漏洞
    • 临时文件的自动删除:程序退出时自动删除临时文件
    • 临时文件的使用场景:需要临时存储数据的情况
  7. 错误处理增强:提供了 perrorstrerror 等函数用于获取错误信息

    • 错误码转换:将errno转换为人类可读的错误信息
    • 错误信息的国际化:支持不同语言的错误信息
    • 自定义错误处理:基于标准错误处理机制实现自定义错误处理

2.1.4 标准I/O库的线程安全性

标准I/O库的线程安全性是一个重要的考量因素:

  1. 线程安全的实现

    • 现代标准I/O库实现(如glibc)通常是线程安全的
    • 线程安全的实现通过在FILE结构体中嵌入锁来实现
    • 锁定粒度:每个FILE流一个锁,保证流级别的线程安全
  2. 线程安全的限制

    • 标准I/O库的线程安全仅保证函数调用本身的安全,不保证复杂操作的原子性
    • 多线程环境下的并发读写可能导致数据交错
    • 需要额外的同步机制来保证复杂操作的原子性
  3. 线程安全的优化

    • 减少锁竞争:避免多个线程同时访问同一个FILE流
    • 使用线程本地存储:为每个线程创建独立的FILE流
    • 批量操作:减少函数调用次数,降低锁竞争
  4. 非线程安全的函数

    • 某些标准I/O库函数不是线程安全的,如 tmpnam
    • 应使用线程安全的替代函数,如 tmpnam_r

2.1.5 标准I/O库的最佳实践

  1. 选择合适的I/O函数:根据操作类型选择最合适的函数
  2. 正确处理错误:检查函数返回值,妥善处理错误情况
  3. 合理使用缓冲区:根据操作特点调整缓冲区大小和类型
  4. 避免资源泄漏:及时关闭文件,释放系统资源
  5. 注意线程安全性:在多线程环境下正确同步文件操作
  6. 优化I/O模式:根据文件类型选择合适的打开模式
  7. 使用安全的函数:优先使用安全的函数,如snprintf、fgets等
  8. 考虑跨平台兼容性:注意不同平台的差异,编写可移植的代码

2.2 常用函数

2.2.1 文件操作函数

函数名功能原型底层实现性能考量安全考量
fopen打开文件FILE *fopen(const char *filename, const char *mode);调用 open 系统调用,初始化 FILE 结构体和缓冲区开销较大,应避免频繁打开/关闭文件文件名参数应进行安全检查,避免路径遍历攻击
fclose关闭文件int fclose(FILE *stream);刷新缓冲区,调用 close 系统调用,释放 FILE 结构体确保所有数据被写入,避免资源泄漏应检查返回值,处理关闭失败的情况
fread读取文件size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);从缓冲区读取数据,必要时调用 read 系统调用填充缓冲区大块读取更高效,避免频繁调用目标缓冲区应足够大,避免缓冲区溢出
fwrite写入文件size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);写入缓冲区,必要时调用 write 系统调用刷新缓冲区大块写入更高效,考虑使用缓冲区优化源数据应有效,避免写入无效数据
fseek设置文件位置int fseek(FILE *stream, long offset, int whence);刷新缓冲区,调用 lseek 系统调用,更新文件位置指针可能触发缓冲区刷新,影响性能应检查返回值,处理定位失败的情况
ftell获取文件位置long ftell(FILE *stream);返回当前文件位置指针,无需系统调用开销很小,可频繁使用对于大文件,应使用 ftello 替代
rewind重置文件位置void rewind(FILE *stream);等同于 fseek(stream, 0, SEEK_SET)可能触发缓冲区刷新不返回错误码,无法检查操作是否成功
feof检查文件结束int feof(FILE *stream);检查文件结束标志,无需系统调用开销很小,可频繁使用应在读取操作失败后调用,不能作为读取循环的条件
ferror检查文件错误int ferror(FILE *stream);检查错误标志,无需系统调用开销很小,可频繁使用应在读取操作失败后调用,获取错误原因
clearerr清除文件错误void clearerr(FILE *stream);清除错误标志和文件结束标志,无需系统调用开销很小,可频繁使用应在需要重新尝试操作时调用
setvbuf设置缓冲区int setvbuf(FILE *stream, char *buf, int mode, size_t size);设置缓冲区类型和大小,影响I/O性能应在打开文件后立即调用自定义缓冲区应确保有效且足够大
fflush刷新缓冲区int fflush(FILE *stream);将缓冲区数据写入文件,调用 write 系统调用可能触发系统调用,影响性能应检查返回值,处理刷新失败的情况
fgetpos获取文件位置int fgetpos(FILE *stream, fpos_t *pos);保存当前文件位置到 pos 结构体开销很小,支持大文件应检查返回值,处理获取失败的情况
fsetpos设置文件位置int fsetpos(FILE *stream, const fpos_t *pos);恢复文件位置从 pos 结构体可能触发缓冲区刷新应检查返回值,处理设置失败的情况
tmpfile创建临时文件FILE *tmpfile(void);创建匿名临时文件,自动删除安全,避免命名冲突应检查返回值,处理创建失败的情况
fileno获取文件描述符int fileno(FILE *stream);返回 FILE 结构体中的文件描述符开销很小,无系统调用应检查返回值,处理无效流的情况
freopen重定向文件流FILE *freopen(const char *filename, const char *mode, FILE *stream);关闭原有流,打开新文件开销较大,应谨慎使用应检查返回值,处理重定向失败的情况

2.2.2 格式化输入/输出函数

函数名功能原型安全考量性能考量
printf格式化输出到标准输出int printf(const char *format, ...);安全,无缓冲区溢出风险频繁调用可能影响性能,考虑使用 putsfputs
scanf格式化输入到标准输入int scanf(const char *format, ...);不安全,可能导致缓冲区溢出输入验证不足,建议使用 fgets + sscanf
fprintf格式化输出到文件int fprintf(FILE *stream, const char *format, ...);安全,无缓冲区溢出风险频繁调用可能影响性能,考虑批量写入
fscanf格式化输入到文件int fscanf(FILE *stream, const char *format, ...);不安全,可能导致缓冲区溢出输入验证不足,建议使用 fgets + sscanf
sprintf格式化输出到字符串int sprintf(char *str, const char *format, ...);不安全,可能导致缓冲区溢出应使用 snprintf 替代
sscanf格式化输入到字符串int sscanf(const char *str, const char *format, ...);相对安全,输入受限于源字符串性能较好,适合解析已知格式的字符串
snprintf安全的格式化输出到字符串int snprintf(char *str, size_t size, const char *format, ...);安全,可防止缓冲区溢出性能略低于 sprintf,但安全性更高
vsnprintf可变参数的安全格式化输出int vsnprintf(char *str, size_t size, const char *format, va_list ap);安全,可防止缓冲区溢出适合实现自定义格式化函数

2.2.3 字符输入/输出函数

函数名功能原型性能考量使用场景
getchar从标准输入读取字符int getchar(void);行缓冲,适合交互式输入逐字符读取标准输入
putchar向标准输出写入字符int putchar(int c);行缓冲,适合交互式输出逐字符写入标准输出
fgetc从文件读取字符int fgetc(FILE *stream);可能触发缓冲区填充,频繁调用影响性能逐字符读取文件
fputc向文件写入字符int fputc(int c, FILE *stream);可能触发缓冲区刷新,频繁调用影响性能逐字符写入文件
ungetc放回字符到输入流int ungetc(int c, FILE *stream);操作流内部缓冲区,开销很小实现词法分析器、解析器等

2.2.4 字符串输入/输出函数

函数名功能原型安全考量性能考量
gets从标准输入读取字符串(不安全)char *gets(char *s);极不安全,已从C11标准中移除应使用 fgets 替代
puts向标准输出写入字符串int puts(const char *s);安全,自动添加换行符性能较好,适合输出字符串
fgets从文件读取字符串char *fgets(char *s, int size, FILE *stream);安全,可防止缓冲区溢出性能较好,适合读取文本行
fputs向文件写入字符串int fputs(const char *s, FILE *stream);安全,不自动添加换行符性能较好,适合写入字符串

2.3 高级文件操作

2.3.1 二进制文件操作

二进制文件操作与文本文件操作的主要区别在于:

  1. 打开模式:使用 "rb""wb""ab" 等模式打开二进制文件
  2. 数据处理:二进制文件直接读写字节,不进行行尾转换
  3. 性能考量:二进制文件操作通常比文本文件操作更高效

示例:二进制文件操作

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
42
43
44
45
46
47
#include <stdio.h>

typedef struct {
int id;
char name[50];
float salary;
} Employee;

int main() {
FILE *fp;
Employee emp1 = {1, "John Doe", 5000.0};
Employee emp2;

// 写入二进制文件
fp = fopen("employees.dat", "wb");
if (fp == NULL) {
perror("fopen");
return 1;
}

if (fwrite(&emp1, sizeof(Employee), 1, fp) != 1) {
perror("fwrite");
fclose(fp);
return 1;
}

fclose(fp);

// 读取二进制文件
fp = fopen("employees.dat", "rb");
if (fp == NULL) {
perror("fopen");
return 1;
}

if (fread(&emp2, sizeof(Employee), 1, fp) != 1) {
perror("fread");
fclose(fp);
return 1;
}

printf("ID: %d\nName: %s\nSalary: %.2f\n", emp2.id, emp2.name, emp2.salary);

fclose(fp);

return 0;
}

2.3.2 文件定位与随机访问

文件定位是实现随机访问文件的基础,通过 fseekftellrewind 函数实现:

  1. 文件位置指示器:每个文件流都有一个文件位置指示器,指向当前读写位置
  2. 定位模式
    • SEEK_SET:从文件开头计算偏移量
    • SEEK_CUR:从当前位置计算偏移量
    • SEEK_END:从文件末尾计算偏移量
  3. 大文件支持:对于超过 2GB 的文件,应使用 fseekoftello 函数,它们使用 off_t 类型

示例:随机访问文件

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>

int main() {
FILE *fp;
int data[] = {1, 2, 3, 4, 5};
int value;
long pos;

// 写入数据
fp = fopen("data.bin", "wb");
if (fp == NULL) {
perror("fopen");
return 1;
}

fwrite(data, sizeof(int), 5, fp);
fclose(fp);

// 随机读取数据
fp = fopen("data.bin", "rb");
if (fp == NULL) {
perror("fopen");
return 1;
}

// 读取第三个元素(索引为2)
pos = 2 * sizeof(int);
if (fseek(fp, pos, SEEK_SET) != 0) {
perror("fseek");
fclose(fp);
return 1;
}

if (fread(&value, sizeof(int), 1, fp) != 1) {
perror("fread");
fclose(fp);
return 1;
}

printf("Third element: %d\n", value);

// 读取最后一个元素
if (fseek(fp, -sizeof(int), SEEK_END) != 0) {
perror("fseek");
fclose(fp);
return 1;
}

if (fread(&value, sizeof(int), 1, fp) != 1) {
perror("fread");
fclose(fp);
return 1;
}

printf("Last element: %d\n", value);

fclose(fp);

return 0;
}

2.3.3 临时文件操作

临时文件用于存储临时数据,通常在程序结束后自动删除:

  1. 临时文件创建

    • tmpfile():创建一个临时文件,程序结束时自动删除
    • tmpnam():生成一个唯一的临时文件名
    • mkstemp():创建一个唯一的临时文件,更安全
  2. 临时文件安全

    • 避免使用固定的临时文件名,防止文件覆盖攻击
    • 使用 mkstemp()tmpfile() 生成唯一的临时文件名
    • 临时文件应设置适当的权限,防止未授权访问

示例:使用临时文件

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
#include <stdio.h>

int main() {
FILE *fp;
char buffer[100];

// 创建临时文件
fp = tmpfile();
if (fp == NULL) {
perror("tmpfile");
return 1;
}

// 写入数据
fprintf(fp, "Temporary data\n");
fprintf(fp, "More temporary data\n");

// 重置文件位置
rewind(fp);

// 读取数据
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}

// 关闭文件(自动删除)
fclose(fp);

return 0;
}

2.4 性能优化技巧

2.4.1 缓冲区优化

  1. 选择合适的缓冲区大小

    • 过小的缓冲区会增加系统调用次数
    • 过大的缓冲区会浪费内存
    • 建议缓冲区大小为4KB或8KB,与系统页大小匹配
  2. 使用自定义缓冲区

    1
    2
    3
    char buffer[8192];
    FILE *fp = fopen("file.txt", "r");
    setvbuf(fp, buffer, _IOFBF, sizeof(buffer));
  3. 批量读写

    • 避免逐字符或逐行读写大文件
    • 使用 freadfwrite 进行批量读写
    • 合理设置读写块大小,通常为8KB或16KB

2.4.2 错误处理优化

  1. 集中错误处理

    • 定义统一的错误处理函数
    • 避免在每个I/O操作后都检查错误
  2. 使用 perrorstrerror

    • 提供详细的错误信息
    • 有助于快速定位问题
  3. 检查返回值

    • 始终检查I/O函数的返回值
    • 不要依赖文件结束标志作为唯一的错误指示

2.4.3 文件操作优化

  1. 减少文件打开/关闭次数

    • 避免在循环中频繁打开和关闭文件
    • 一次性打开文件,完成所有操作后关闭
  2. 使用二进制模式

    • 对于非文本文件,使用二进制模式读写
    • 避免行尾转换开销
  3. 合理使用文件定位

    • 避免频繁的文件定位操作
    • 尽量顺序访问文件,利用预读机制
  4. 使用内存映射

    • 对于大文件,考虑使用 mmap 进行内存映射
    • 可以显著提高大文件的访问性能

2.5 安全编程实践

2.5.1 防止缓冲区溢出

  1. 使用安全的输入函数

    • 使用 fgets 替代 gets
    • 使用 snprintf 替代 sprintf
    • 使用 strncpy 替代 strcpy
  2. 输入验证

    • 始终验证输入数据的长度和格式
    • 对用户输入进行严格检查
  3. 缓冲区大小计算

    • 使用 sizeof 计算缓冲区大小
    • 避免硬编码缓冲区大小

2.5.2 资源管理

  1. 文件句柄管理

    • 始终关闭打开的文件
    • 使用 fclose 确保所有数据被写入
  2. 异常处理

    • 在函数返回前确保所有资源被释放
    • 考虑使用 goto 语句进行统一的资源清理
  3. 内存管理

    • 避免内存泄漏
    • 及时释放不再使用的内存

2.6 使用示例

2.6.1 高效文件复制

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <stdio.h>
#define BUFFER_SIZE 8192

int main(int argc, char *argv[]) {
FILE *src, *dst;
char buffer[BUFFER_SIZE];
size_t bytes_read;

if (argc != 3) {
fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
return 1;
}

// 打开源文件
src = fopen(argv[1], "rb");
if (src == NULL) {
perror("fopen (source)");
return 1;
}

// 打开目标文件
dst = fopen(argv[2], "wb");
if (dst == NULL) {
perror("fopen (destination)");
fclose(src);
return 1;
}

// 设置缓冲区
setvbuf(src, NULL, _IOFBF, BUFFER_SIZE);
setvbuf(dst, NULL, _IOFBF, BUFFER_SIZE);

// 复制文件
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src)) > 0) {
if (fwrite(buffer, 1, bytes_read, dst) != bytes_read) {
perror("fwrite");
fclose(src);
fclose(dst);
return 1;
}
}

if (ferror(src)) {
perror("fread");
fclose(src);
fclose(dst);
return 1;
}

// 关闭文件
if (fclose(src) != 0) {
perror("fclose (source)");
fclose(dst);
return 1;
}

if (fclose(dst) != 0) {
perror("fclose (destination)");
return 1;
}

printf("File copied successfully\n");
return 0;
}

2.6.2 大文件读取与处理

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
42
43
44
45
46
#include <stdio.h>
#define BUFFER_SIZE 16384

int main() {
FILE *fp;
char buffer[BUFFER_SIZE];
size_t bytes_read;
long long total_bytes = 0;
int line_count = 0;

// 打开文件
fp = fopen("large_file.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}

// 设置缓冲区
setvbuf(fp, NULL, _IOFBF, BUFFER_SIZE);

// 读取文件
while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, fp)) > 0) {
total_bytes += bytes_read;

// 统计行数(简单实现)
for (size_t i = 0; i < bytes_read; i++) {
if (buffer[i] == '\n') {
line_count++;
}
}
}

if (ferror(fp)) {
perror("fread");
fclose(fp);
return 1;
}

// 关闭文件
fclose(fp);

printf("Total bytes: %lld\n", total_bytes);
printf("Line count: %d\n", line_count);

return 0;
}

2.7 常见问题与解决方案

2.7.1 缓冲区问题

问题:程序输出不显示,或者文件内容未写入

解决方案

  • 对于标准输出,添加换行符或使用 fflush(stdout)
  • 对于文件,使用 fflush(fp) 或确保调用 fclose(fp)

2.7.2 文件权限问题

问题fopen 失败,错误信息为 “Permission denied”

解决方案

  • 检查文件权限
  • 确保文件路径正确
  • 避免在只读文件系统中写入文件

2.7.3 文件锁定问题

问题:多个进程同时访问同一个文件,导致数据损坏

解决方案

  • 使用文件锁(flockfcntl
  • 实现互斥访问机制
  • 避免多个进程同时写入同一个文件

2.7.4 大文件处理问题

问题:无法处理超过2GB的文件

解决方案

  • 使用 fseekoftello 替代 fseekftell
  • 编译时定义 _FILE_OFFSET_BITS=64
  • 考虑使用内存映射(mmap)处理大文件

2.8 总结

标准I/O库是C语言中最常用的库之一,它提供了丰富的文件操作函数和格式化输入/输出函数。通过合理使用标准I/O库,可以:

  1. 提高I/O性能:通过缓冲区机制减少系统调用次数
  2. 简化编程:提供统一的接口处理不同类型的I/O设备
  3. 增强安全性:使用安全的输入/输出函数防止缓冲区溢出
  4. 提高可移植性:标准I/O库在不同平台上的实现基本一致

在实际编程中,应根据具体需求选择合适的I/O函数,并注意性能优化和安全编程实践,以编写高效、可靠的I/O操作代码。

3. 字符串处理库

3.1 字符串处理库概述

字符串处理库(<string.h>)提供了字符串操作和内存操作的函数,是C语言中最常用的库之一。它包含了丰富的函数用于字符串的复制、连接、比较、查找等操作,以及内存块的操作函数。

3.2 常用函数

3.2.1 字符串操作函数

函数名功能原型实现细节性能考量
strlen获取字符串长度size_t strlen(const char *s);线性扫描,直到遇到空字符时间复杂度O(n),现代编译器会优化短字符串
strcpy复制字符串(不安全)char *strcpy(char *dest, const char *src);逐字节复制,直到遇到空字符不安全,可能导致缓冲区溢出
strncpy安全的复制字符串char *strncpy(char *dest, const char *src, size_t n);最多复制n个字节,可能不添加空终止符安全但需要手动添加空终止符
strcat连接字符串(不安全)char *strcat(char *dest, const char *src);先查找dest的末尾,再复制src不安全,可能导致缓冲区溢出,性能较差
strncat安全的连接字符串char *strncat(char *dest, const char *src, size_t n);最多连接n个字节,自动添加空终止符安全,性能比strcat好
strcmp比较字符串int strcmp(const char *s1, const char *s2);逐字节比较,直到遇到差异或空字符时间复杂度O(n),通常很快因为大多数字符串在前几个字符就不同
strncmp比较字符串前n个字符int strncmp(const char *s1, const char *s2, size_t n);最多比较n个字节适合比较前缀,性能较好
strchr查找字符char *strchr(const char *s, int c);线性扫描,直到遇到指定字符或空字符时间复杂度O(n)
strrchr反向查找字符char *strrchr(const char *s, int c);从末尾开始线性扫描时间复杂度O(n)
strstr查找子字符串char *strstr(const char *haystack, const char *needle);朴素字符串匹配算法时间复杂度O(m*n),对于长字符串效率较低
strtok分割字符串char *strtok(char *str, const char *delim);修改原始字符串,插入空字符非线程安全,不可重入
memset填充内存void *memset(void *s, int c, size_t n);按字节填充内存块高度优化,对于大内存块使用SIMD指令
memcpy复制内存void *memcpy(void *dest, const void *src, size_t n);按字节复制内存块,不处理重叠高度优化,对于大内存块使用SIMD指令
memmove安全的复制内存void *memmove(void *dest, const void *src, size_t n);处理内存重叠的情况比memcpy稍慢,但更安全
memcmp比较内存int memcmp(const void *s1, const void *s2, size_t n);逐字节比较内存块高度优化,对于大内存块使用SIMD指令

3.2.2 扩展字符串函数(C11及以上)

函数名功能原型适用场景
strcpy_s安全的字符串复制errno_t strcpy_s(char *dest, rsize_t destsz, const char *src);需要安全保证的场景
strcat_s安全的字符串连接errno_t strcat_s(char *dest, rsize_t destsz, const char *src);需要安全保证的场景
strncpy_s安全的有限长度字符串复制errno_t strncpy_s(char *dest, rsize_t destsz, const char *src, rsize_t count);需要安全保证的场景
strncat_s安全的有限长度字符串连接errno_t strncat_s(char *dest, rsize_t destsz, const char *src, rsize_t count);需要安全保证的场景
strtok_s可重入的字符串分割char *strtok_s(char *str, const char *delim, char **saveptr);线程安全的场景

3.3 字符串处理的性能优化

3.3.1 内存访问模式优化

  1. 减少内存分配

    • 预分配足够大的缓冲区,避免频繁的动态内存分配
    • 使用栈上的固定大小缓冲区处理短字符串
  2. 减少字符串扫描

    • 避免多次扫描同一字符串,如先strlenstrcpy
    • 使用memcpy替代strcpy当已知字符串长度时
  3. 利用CPU缓存

    • 尽量顺序访问内存,避免随机访问
    • 小字符串优先放在栈上,利用L1缓存
  4. SIMD优化

    • 现代编译器会自动对memsetmemcpy等函数使用SIMD指令
    • 对于大量字符串处理,可以考虑使用SIMD指令集手动优化

3.3.2 字符串操作的最佳实践

  1. 字符串复制

    • 优先使用strncpymemcpy替代strcpy
    • 对于已知长度的字符串,使用memcpy性能更好
  2. 字符串连接

    • 避免使用strcat,尤其是在循环中
    • 预先计算总长度,一次性分配内存后复制
  3. 字符串比较

    • 对于固定长度的字符串,使用memcmp性能更好
    • 对于前缀比较,使用strncmp
  4. 字符串查找

    • 对于短字符串,strstr足够高效
    • 对于长字符串或频繁查找,考虑使用更高级的算法如KMP、Boyer-Moore

3.4 字符串处理的安全实践

3.4.1 缓冲区溢出防护

  1. 使用安全函数

    • 优先使用带长度限制的函数,如strncpystrncat
    • 在支持的平台上,使用C11的安全函数如strcpy_s
  2. 输入验证

    • 始终验证输入字符串的长度
    • 对用户输入进行严格的长度检查
  3. 缓冲区大小计算

    • 使用sizeof计算缓冲区大小,避免硬编码
    • 考虑字符串结束符的空间

3.4.2 线程安全

  1. 避免使用非线程安全函数

    • 替代strtok使用strtok_sstrsep
    • 避免在多线程环境下修改共享字符串
  2. 同步机制

    • 对于共享字符串的访问,使用互斥锁保护
    • 考虑使用线程本地存储存储临时字符串

3.5 高级字符串处理技术

3.5.1 字符串池化

字符串池化是一种优化技术,用于减少重复字符串的内存使用:

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
// 简单的字符串池实现
struct string_pool {
char **strings;
size_t count;
size_t capacity;
};

// 查找或添加字符串到池
const char *string_pool_intern(struct string_pool *pool, const char *str) {
// 查找现有字符串
for (size_t i = 0; i < pool->count; i++) {
if (strcmp(pool->strings[i], str) == 0) {
return pool->strings[i];
}
}

// 添加新字符串
if (pool->count >= pool->capacity) {
// 扩容
size_t new_cap = pool->capacity ? pool->capacity * 2 : 16;
char **new_strings = realloc(pool->strings, new_cap * sizeof(char *));
if (!new_strings) return str;
pool->strings = new_strings;
pool->capacity = new_cap;
}

pool->strings[pool->count] = strdup(str);
return pool->strings[pool->count++];
}

3.5.2 零拷贝字符串操作

零拷贝技术可以减少字符串操作中的内存复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 字符串视图,避免复制
struct string_view {
const char *data;
size_t length;
};

// 从字符串创建视图
struct string_view sv_from_string(const char *str) {
return (struct string_view){
.data = str,
.length = strlen(str)
};
}

// 从字符串的一部分创建视图
struct string_view sv_from_substring(const char *str, size_t start, size_t length) {
return (struct string_view){
.data = str + start,
.length = length
};
}

3.6 使用示例

3.6.1 高效字符串连接

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
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 高效的字符串连接函数
char *concat_strings(const char *str1, const char *str2) {
size_t len1 = strlen(str1);
size_t len2 = strlen(str2);
char *result = malloc(len1 + len2 + 1);
if (result) {
memcpy(result, str1, len1);
memcpy(result + len1, str2, len2 + 1); // 包含空终止符
}
return result;
}

int main() {
char str1[50] = "Hello, ";
char str2[] = "world!";
char str3[50];

// 连接字符串(高效方式)
char *combined = concat_strings(str1, str2);
if (combined) {
printf("Combined: %s\n", combined);
free(combined);
}

// 复制字符串(安全方式)
strncpy(str3, str1, sizeof(str3) - 1);
str3[sizeof(str3) - 1] = '\0'; // 确保空终止
printf("str3: %s\n", str3);

// 比较字符串
if (strcmp(str1, str3) == 0) {
printf("str1 and str3 are equal\n");
}

// 获取字符串长度
printf("Length of str1: %zu\n", strlen(str1));

// 查找子字符串
char *ptr = strstr(str1, "world");
if (ptr != NULL) {
printf("Found 'world' at position: %td\n", ptr - str1);
} else {
printf("'world' not found in str1\n");
}

return 0;
}

3.6.2 字符串令牌化(线程安全)

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
42
43
44
#include <stdio.h>
#include <string.h>

// 线程安全的字符串分割函数
char *strtok_r(char *str, const char *delim, char **saveptr) {
char *token;

if (str == NULL) {
str = *saveptr;
}

// 跳过前导分隔符
str += strspn(str, delim);
if (*str == '\0') {
*saveptr = str;
return NULL;
}

// 找到令牌结束
token = str;
str = strpbrk(token, delim);
if (str == NULL) {
*saveptr = token + strlen(token);
} else {
*str = '\0';
*saveptr = str + 1;
}

return token;
}

int main() {
char str[] = "apple,banana,orange,grape";
char *saveptr;
char *token;

printf("Fruits: ");
for (token = strtok_r(str, ",", &saveptr); token != NULL; token = strtok_r(NULL, ",", &saveptr)) {
printf("%s ", token);
}
printf("\n");

return 0;
}

4. 数学库

4.1 数学库概述

数学库(<math.h>)提供了丰富的数学计算函数,包括三角函数、指数对数函数、取整函数等。这些函数在底层通常由高度优化的汇编代码实现,利用硬件特性(如FPU、SIMD指令)来提高性能。使用时需要链接数学库(-lm)。

4.2 常用函数

4.2.1 基本数学函数

函数名功能原型实现原理性能考量
sin正弦函数double sin(double x);使用泰勒级数或CORDIC算法,利用角度归约提高精度现代CPU有硬件指令支持,性能优异
cos余弦函数double cos(double x);与sin类似,或利用cos(x) = sin(x + π/2)硬件加速,性能优异
tan正切函数double tan(double x);通常实现为sin(x)/cos(x)性能略低于sin和cos
asin反正弦函数double asin(double x);使用多项式近似或迭代算法比正弦函数计算量大
acos反余弦函数double acos(double x);通常实现为π/2 - asin(x)与asin性能相近
atan反正切函数double atan(double x);使用多项式近似或CORDIC算法比反正弦函数计算量小
atan2反正切函数(两个参数)double atan2(double y, double x);处理象限信息,返回正确的角度性能比atan略低,但提供更完整的角度信息
sinh双曲正弦函数double sinh(double x);实现为(e^x - e^-x)/2计算量较大,涉及指数运算
cosh双曲余弦函数double cosh(double x);实现为(e^x + e^-x)/2与sinh性能相近
tanh双曲正切函数double tanh(double x);实现为sinh(x)/cosh(x)或优化公式性能比sinh和cosh好

4.2.2 指数和对数函数

函数名功能原型实现原理性能考量
exp指数函数double exp(double x);使用泰勒级数或有理逼近,利用硬件指令计算量大,但现代CPU有硬件支持
log自然对数double log(double x);使用多项式近似或对数变换计算量大,性能较低
log10以10为底的对数double log10(double x);通常实现为log(x)/log(10)性能与log相近
log2以2为底的对数double log2(double x);通常实现为log(x)/log(2)或硬件指令某些CPU有硬件支持,性能较好
pow幂函数double pow(double x, double y);实现为exp(y * log(x))计算量很大,性能较低
sqrt平方根double sqrt(double x);使用牛顿-拉夫逊迭代或硬件指令现代CPU有硬件指令,性能优异
cbrt立方根double cbrt(double x);使用牛顿-拉夫逊迭代性能比sqrt低,但仍较快
hypot直角三角形斜边长度double hypot(double x, double y);实现为sqrt(x² + y²),但避免溢出性能与sqrt相近

4.2.3 取整和绝对值函数

函数名功能原型实现原理性能考量
ceil向上取整double ceil(double x);利用浮点数表示或硬件指令性能优异,通常为单周期操作
floor向下取整double floor(double x);与ceil类似性能优异
round四舍五入double round(double x);利用浮点数表示或加法技巧性能优异
trunc截断小数部分double trunc(double x);直接操作浮点数指数部分性能优异
fmod浮点取模double fmod(double x, double y);实现为x - y * trunc(x/y)性能中等
remainder带符号余数double remainder(double x, double y);实现为IEEE 754标准定义的余数性能比fmod低
fabs绝对值double fabs(double x);清除符号位性能优异,通常为单周期操作
abs整数绝对值int abs(int x);利用位运算或条件判断性能优异
labs长整数绝对值long labs(long x);与abs类似性能优异
llabs长 long 整数绝对值long long llabs(long long x);与abs类似性能优异

4.2.4 特殊函数(C99及以上)

函数名功能原型适用场景
erf误差函数double erf(double x);概率统计、信号处理
erfc互补误差函数double erfc(double x);概率统计、尾部分布
gamma伽马函数double tgamma(double x);数学分析、统计
lgamma伽马函数的自然对数double lgamma(double x);大数值计算,避免溢出
exp22的幂double exp2(double x);计算机科学、信息论
expm1exp(x) - 1double expm1(double x);小x值的精确计算
log1plog(1 + x)double log1p(double x);小x值的精确计算
fdim正差值double fdim(double x, double y);优化计算max(x-y, 0)
fmax最大值double fmax(double x, double y);比条件判断更高效
fmin最小值double fmin(double x, double y);比条件判断更高效

4.3 数学库的性能优化

4.3.1 算法选择

  1. 利用硬件特性

    • 现代CPU包含专门的数学指令(如SSE、AVX),编译器会自动生成这些指令
    • 对于大量数据的数学计算,考虑使用SIMD指令集手动优化
  2. 避免重复计算

    • 缓存计算结果,避免在循环中重复计算相同的值
    • 使用查找表替代计算密集型函数(对于有限范围的输入)
  3. 选择合适的函数

    • 优先使用硬件支持的函数(如sqrt、sin、cos)
    • 对于近似计算,考虑使用快速但精度较低的实现

4.3.2 数值稳定性

  1. 避免数值溢出

    • 使用hypot替代直接计算sqrt(x*x + y*y)
    • 使用expm1log1p处理小数值
  2. 避免精度损失

    • 注意浮点数精度限制,避免累积误差
    • 使用合适的算法减少舍入误差
  3. 处理特殊情况

    • 正确处理无穷大、NaN等特殊值
    • 检查输入参数的有效性

4.4 高级数学计算技术

4.4.1 向量和矩阵运算

虽然标准C数学库不直接提供向量和矩阵运算,但可以利用SIMD指令和优化技术实现高效的数值计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用SIMD指令的向量加法示例(伪代码)
#include <immintrin.h>

void vector_add(double *a, double *b, double *result, size_t n) {
size_t i;
// 处理可以向量化的部分
for (i = 0; i <= n - 4; i += 4) {
__m256d va = _mm256_loadu_pd(&a[i]);
__m256d vb = _mm256_loadu_pd(&b[i]);
__m256d vresult = _mm256_add_pd(va, vb);
_mm256_storeu_pd(&result[i], vresult);
}
// 处理剩余部分
for (; i < n; i++) {
result[i] = a[i] + b[i];
}
}

4.4.2 随机数生成

标准库提供了基本的随机数生成函数,但对于需要高质量随机数的应用,可以使用更高级的算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 简单的线性同余生成器
unsigned int lcg_rand(unsigned int *seed) {
*seed = (*seed * 1103515245 + 12345) % ((unsigned int)1 << 31);
return *seed;
}

// 使用标准库的随机数生成
#include <stdlib.h>
#include <time.h>

void init_random() {
srand(time(NULL)); // 设置种子
}

int get_random_int(int min, int max) {
return min + rand() % (max - min + 1);
}

4.5 使用示例

4.5.1 科学计算示例

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
#include <stdio.h>
#include <math.h>

int main() {
double x = 1.0;
double y = 2.0;
double pi = M_PI; // 数学常数

// 基本数学函数
printf("sin(π/2) = %f\n", sin(pi / 2));
printf("cos(π) = %f\n", cos(pi));
printf("tan(0) = %f\n", tan(0));

// 指数和对数函数
printf("exp(1.0) = %f\n", exp(x));
printf("log(e) = %f\n", log(exp(1.0)));
printf("pow(2.0, 3.0) = %f\n", pow(y, 3.0));
printf("sqrt(4.0) = %f\n", sqrt(y * y));

// 取整和绝对值函数
printf("ceil(1.5) = %f\n", ceil(1.5));
printf("floor(1.5) = %f\n", floor(1.5));
printf("round(1.5) = %f\n", round(1.5));
printf("fabs(-1.5) = %f\n", fabs(-1.5));

// 双曲函数
printf("sinh(1.0) = %f\n", sinh(x));
printf("cosh(1.0) = %f\n", cosh(x));
printf("tanh(1.0) = %f\n", tanh(x));

// 特殊函数(如果支持)
#ifdef __USE_ISOC99
printf("erf(1.0) = %f\n", erf(x));
printf("gamma(0.5) = %f\n", tgamma(0.5));
#endif

return 0;
}

4.5.2 数值积分示例

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
#include <stdio.h>
#include <math.h>

// 数值积分(梯形法则)
double integrate(double (*f)(double), double a, double b, int n) {
double h = (b - a) / n;
double sum = 0.5 * (f(a) + f(b));

for (int i = 1; i < n; i++) {
double x = a + i * h;
sum += f(x);
}

return sum * h;
}

// 被积函数
double func(double x) {
return sin(x);
}

int main() {
double result = integrate(func, 0, M_PI, 100000);
printf("Integral of sin(x) from 0 to π: %f\n", result);
printf("Expected value: 2.0\n");

return 0;
}

5. 时间和日期库

5.1 时间和日期库概述

时间和日期库(<time.h>)提供了时间和日期操作的函数,是C语言中处理时间相关任务的基础。它定义了多种时间表示形式和转换函数,支持从简单的时间获取到复杂的日期计算等多种操作。

5.2 时间表示形式

C语言中的时间表示主要有以下几种:

  1. time_t

    • 整数类型,通常是64位整数
    • 表示从1970年1月1日00:00:00 UTC开始的秒数(Unix时间戳)
    • 适合存储和计算时间间隔
  2. struct tm

    • 分解的时间结构,包含年、月、日、时、分、秒等字段
    • 适合格式化输出和日期计算
    • 字段包括:tm_sec(秒)、tm_min(分)、tm_hour(时)、tm_mday(日)、tm_mon(月,0-11)、tm_year(年,从1900开始)、tm_wday(星期,0-6)、tm_yday(年内天数,0-365)、tm_isdst(夏令时标志)
  3. struct timespec(C11及以上):

    • 高精度时间结构,包含秒和纳秒
    • 用于需要纳秒精度的场景

5.3 常用函数

5.3.1 时间获取和转换函数

函数名功能原型实现细节性能考量
time获取当前时间time_t time(time_t *t);调用系统时间服务,返回Unix时间戳性能优异,系统调用开销小
clock_gettime获取高精度时间int clock_gettime(clockid_t clk_id, struct timespec *tp);支持多种时钟源(实时时钟、单调时钟等)性能优异,精度高达纳秒
clock获取处理器时间clock_t clock(void);测量程序使用的CPU时间性能优异,但精度较低
localtime转换为本地时间struct tm *localtime(const time_t *t);考虑本地时区和夏令时线程不安全,返回静态缓冲区
localtime_r线程安全的本地时间转换struct tm *localtime_r(const time_t *t, struct tm *result);线程安全版本,使用用户提供的缓冲区线程安全,推荐使用
gmtime转换为UTC时间struct tm *gmtime(const time_t *t);转换为格林威治标准时间线程不安全,返回静态缓冲区
gmtime_r线程安全的UTC时间转换struct tm *gmtime_r(const time_t *t, struct tm *result);线程安全版本,使用用户提供的缓冲区线程安全,推荐使用
mktime转换为time_ttime_t mktime(struct tm *tm);考虑本地时区,反向转换计算量较大,需要处理时区和夏令时
difftime计算时间差double difftime(time_t time1, time_t time0);简单的减法运算性能优异,几乎无开销

5.3.2 时间格式化函数

函数名功能原型实现细节性能考量
ctime转换为字符串char *ctime(const time_t *t);生成固定格式的时间字符串线程不安全,返回静态缓冲区
ctime_r线程安全的时间字符串转换char *ctime_r(const time_t *t, char *buf);线程安全版本,使用用户提供的缓冲区线程安全,推荐使用
asctime转换结构体为字符串char *asctime(const struct tm *tm);生成固定格式的时间字符串线程不安全,返回静态缓冲区
asctime_r线程安全的结构体字符串转换char *asctime_r(const struct tm *tm, char *buf);线程安全版本,使用用户提供的缓冲区线程安全,推荐使用
strftime格式化时间为字符串size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *tm);支持丰富的格式化选项计算量较大,但功能强大
strftime_l特定区域设置的时间格式化size_t strftime_l(char *s, size_t maxsize, const char *format, const struct tm *tm, locale_t loc);支持特定区域设置的格式化计算量较大,支持国际化

5.4 时区处理

5.4.1 时区概念

  1. UTC(协调世界时):

    • 标准时间基准,以前称为GMT
    • 不受夏令时影响
  2. 本地时间

    • 基于本地时区的时间
    • 可能受到夏令时的影响
  3. 时区偏移

    • 本地时间与UTC的差值,以小时为单位
    • 例如,北京时间为UTC+8

5.4.2 时区设置和获取

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
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main() {
time_t t = time(NULL);
struct tm *tm_gmt, *tm_local;
char buf[256];

// 获取UTC时间
tm_gmt = gmtime(&t);
strftime(buf, sizeof(buf), "UTC: %Y-%m-%d %H:%M:%S", tm_gmt);
printf("%s\n", buf);

// 获取本地时间
tm_local = localtime(&t);
strftime(buf, sizeof(buf), "Local: %Y-%m-%d %H:%M:%S", tm_local);
printf("%s\n", buf);

// 获取时区环境变量
char *tz = getenv("TZ");
printf("Current TZ: %s\n", tz ? tz : "(not set)");

// 设置时区
setenv("TZ", "America/New_York", 1);
tzset(); // 重新初始化时区信息

// 重新获取本地时间(纽约时间)
tm_local = localtime(&t);
strftime(buf, sizeof(buf), "New York: %Y-%m-%d %H:%M:%S", tm_local);
printf("%s\n", buf);

return 0;
}

5.5 时间库的高级应用

5.5.1 定时器实现

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <time.h>

// 简单的定时器
typedef struct {
struct timespec start;
struct timespec interval;
} Timer;

// 初始化定时器
void timer_init(Timer *timer, double seconds) {
clock_gettime(CLOCK_MONOTONIC, &timer->start);
timer->interval.tv_sec = (time_t)seconds;
timer->interval.tv_nsec = (long)((seconds - timer->interval.tv_sec) * 1e9);
}

// 检查定时器是否到期
int timer_expired(Timer *timer) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);

// 计算时间差
long sec_diff = now.tv_sec - timer->start.tv_sec;
long nsec_diff = now.tv_nsec - timer->start.tv_nsec;

// 转换为纳秒
long long total_diff = (long long)sec_diff * 1e9 + nsec_diff;
long long interval_nsec = (long long)timer->interval.tv_sec * 1e9 + timer->interval.tv_nsec;

return total_diff >= interval_nsec;
}

// 重置定时器
void timer_reset(Timer *timer) {
clock_gettime(CLOCK_MONOTONIC, &timer->start);
}

int main() {
Timer timer;
int count = 0;

// 初始化1秒定时器
timer_init(&timer, 1.0);

printf("Counting seconds...\n");

while (count < 5) {
if (timer_expired(&timer)) {
count++;
printf("%d seconds\n", count);
timer_reset(&timer);
}
}

return 0;
}

5.5.2 时间戳的持久化和解析

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
42
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

// 时间戳转换为字符串(ISO 8601格式)
char *timestamp_to_string(time_t timestamp, char *buf, size_t bufsize) {
struct tm *tm = gmtime(&timestamp);
if (tm) {
strftime(buf, bufsize, "%Y-%m-%dT%H:%M:%SZ", tm);
return buf;
}
return NULL;
}

// 字符串转换为时间戳
int string_to_timestamp(const char *str, time_t *timestamp) {
struct tm tm;
if (strptime(str, "%Y-%m-%dT%H:%M:%SZ", &tm)) {
*timestamp = mktime(&tm);
return 1;
}
return 0;
}

int main() {
time_t now = time(NULL);
char buf[32];

// 时间戳转换为字符串
if (timestamp_to_string(now, buf, sizeof(buf))) {
printf("Current timestamp: %ld\n", now);
printf("ISO 8601 format: %s\n", buf);
}

// 字符串转换为时间戳
time_t parsed;
if (string_to_timestamp(buf, &parsed)) {
printf("Parsed timestamp: %ld\n", parsed);
}

return 0;
}

5.6 使用示例

5.6.1 基本时间操作

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
#include <stdio.h>
#include <time.h>

int main() {
time_t current_time;
struct tm *local_time, *gmt_time;
char time_string[50];

// 获取当前时间
current_time = time(NULL);
printf("Current timestamp: %ld\n", current_time);

// 转换为本地时间
local_time = localtime(&current_time);

// 格式化为字符串
strftime(time_string, sizeof(time_string), "%Y-%m-%d %H:%M:%S %Z", local_time);
printf("Local time: %s\n", time_string);

// 转换为GMT时间
gmt_time = gmtime(&current_time);
strftime(time_string, sizeof(time_string), "%Y-%m-%d %H:%M:%S %Z", gmt_time);
printf("GMT time: %s\n", time_string);

// 计算时间差
time_t future_time = current_time + 3600; // 1小时后
double diff = difftime(future_time, current_time);
printf("Time difference: %.0f seconds\n", diff);

// 使用ctime
printf("ctime: %s", ctime(&current_time));

return 0;
}

5.6.2 高精度时间测量

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
#include <stdio.h>
#include <time.h>

int main() {
struct timespec start, end;
double elapsed;
int i, sum = 0;

// 开始计时
clock_gettime(CLOCK_MONOTONIC, &start);

// 执行一些计算
for (i = 0; i < 100000000; i++) {
sum += i;
}

// 结束计时
clock_gettime(CLOCK_MONOTONIC, &end);

// 计算经过的时间
elapsed = (end.tv_sec - start.tv_sec) +
(end.tv_nsec - start.tv_nsec) / 1e9;

printf("Sum: %d\n", sum);
printf("Elapsed time: %.6f seconds\n", elapsed);

return 0;
}

6. 内存分配库

6.1 内存分配库概述

内存分配库(<stdlib.h>)提供了动态内存分配的函数,是C语言中管理内存的重要工具。它封装了底层的内存管理机制,为应用程序提供了简洁的内存分配和释放接口。

6.2 内存分配函数

6.2.1 基本内存分配函数

函数名功能原型实现细节性能考量
malloc分配内存void *malloc(size_t size);从堆中分配指定大小的内存块,不初始化性能取决于分配器实现,通常很快
calloc分配并清零内存void *calloc(size_t nmemb, size_t size);分配内存并将所有字节初始化为0比malloc慢,因为需要清零内存
realloc重新分配内存void *realloc(void *ptr, size_t size);调整已分配内存块的大小,可能需要复制数据如果在原内存块后扩展,性能较好;否则需要复制数据,性能较差
free释放内存void free(void *ptr);将内存块返回给分配器,可能合并相邻空闲块性能通常很好,是O(1)操作
aligned_alloc分配对齐内存void *aligned_alloc(size_t alignment, size_t size);分配指定对齐方式的内存块性能与malloc相近,但可能有额外开销
posix_memalign分配对齐内存(POSIX)int posix_memalign(void **memptr, size_t alignment, size_t size);分配指定对齐方式的内存块,返回错误码性能与aligned_alloc相近

6.2.2 内存分配器的工作原理

  1. 内存池管理

    • 分配器维护一个内存池,从操作系统获取大块内存
    • 将大块内存分割成不同大小的小块,满足不同的分配请求
    • 管理空闲内存块,通过链表或位图等数据结构跟踪
  2. 分配策略

    • 首次适应:从内存池开始查找,找到第一个足够大的空闲块
    • 最佳适应:查找最小的足够大的空闲块
    • 最坏适应:查找最大的空闲块
    • 快速适应:为不同大小的请求维护不同的空闲块链表
  3. 内存碎片

    • 内部碎片:分配的内存块大于请求的大小
    • 外部碎片:空闲内存分散成小块,无法满足大的分配请求
    • 分配器通过合并相邻空闲块来减少外部碎片
  4. 线程安全

    • 现代分配器通常是线程安全的,使用锁或无锁算法
    • 线程本地缓存可以减少线程间的竞争,提高性能

6.3 内存分配的性能优化

6.3.1 内存分配策略

  1. 减少内存分配次数

    • 预分配足够大的内存,避免频繁的小分配
    • 使用对象池管理频繁创建和销毁的对象
    • 对于固定大小的对象,使用专用的内存池
  2. 选择合适的分配函数

    • 对于需要清零的内存,使用calloc
    • 对于需要调整大小的内存,使用realloc
    • 对于需要特定对齐的内存,使用aligned_allocposix_memalign
  3. 内存分配大小优化

    • 避免分配非常小的内存块(如几个字节),因为分配器的开销可能超过实际使用的内存
    • 对于大内存分配,考虑使用内存映射(mmap

6.3.2 内存使用模式优化

  1. 内存局部性

    • 尽量让相关的数据在内存中相邻,提高缓存命中率
    • 避免随机访问内存,尽量顺序访问
  2. 内存释放策略

    • 及时释放不再使用的内存,避免内存泄漏
    • 对于长期运行的程序,定期检查内存使用情况
    • 避免频繁的分配和释放,考虑使用内存池
  3. 内存对齐

    • 对于需要高效访问的数据,确保其内存对齐
    • 对于SIMD指令,需要特定的内存对齐要求

6.4 内存分配的高级技术

6.4.1 自定义内存分配器

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
// 简单的内存池实现
#include <stddef.h>
#include <stdint.h>

#define POOL_SIZE 4096

struct memory_pool {
uint8_t buffer[POOL_SIZE];
size_t used;
};

// 初始化内存池
void pool_init(struct memory_pool *pool) {
pool->used = 0;
}

// 从内存池分配内存
void *pool_alloc(struct memory_pool *pool, size_t size) {
if (pool->used + size > POOL_SIZE) {
return NULL; // 内存池已满
}

void *ptr = &pool->buffer[pool->used];
pool->used += size;
return ptr;
}

// 重置内存池
void pool_reset(struct memory_pool *pool) {
pool->used = 0;
}

// 计算内存池使用率
size_t pool_usage(const struct memory_pool *pool) {
return pool->used;
}

6.4.2 内存分配的调试和分析

  1. 内存泄漏检测

    • 使用工具如Valgrind、AddressSanitizer检测内存泄漏
    • 实现自定义的内存分配包装器,跟踪分配和释放
  2. 内存使用分析

    • 使用工具如Massif(Valgrind的一部分)分析内存使用情况
    • 监控内存分配的大小和频率,识别内存使用热点
  3. 内存错误检测

    • 使用AddressSanitizer检测缓冲区溢出、使用已释放内存等错误
    • 使用UndefinedBehaviorSanitizer检测未定义行为

6.5 其他常用函数

6.5.1 字符串转换函数

函数名功能原型实现细节性能考量
atoi字符串转整数int atoi(const char *nptr);简单的字符串到整数转换,错误处理有限性能很好,但错误处理有限
atol字符串转长整数long atol(const char *nptr);类似atoi,但返回长整数性能很好,但错误处理有限
atoll字符串转长 long 整数long long atoll(const char *nptr);类似atoi,但返回长 long 整数性能很好,但错误处理有限
strtod字符串转双精度浮点数double strtod(const char *nptr, char **endptr);功能强大的浮点数转换,支持科学记数法性能较好,错误处理完善
strtol字符串转长整数long strtol(const char *nptr, char **endptr, int base);功能强大的整数转换,支持不同进制性能较好,错误处理完善
strtoll字符串转长 long 整数long long strtoll(const char *nptr, char **endptr, int base);类似strtol,但返回长 long 整数性能较好,错误处理完善
strtoull字符串转无符号长 long 整数unsigned long long strtoull(const char *nptr, char **endptr, int base);类似strtol,但返回无符号长 long 整数性能较好,错误处理完善

6.5.2 随机数生成函数

函数名功能原型实现细节性能考量
rand生成随机数int rand(void);通常使用线性同余生成器(LCG),周期较短性能很好,但随机性较差
srand设置随机数种子void srand(unsigned int seed);设置随机数生成器的初始状态性能很好
rand_r线程安全的随机数生成int rand_r(unsigned int *seed);线程安全版本的rand,使用用户提供的种子性能很好,线程安全
arc4random生成高质量随机数(BSD)uint32_t arc4random(void);使用ARC4算法,提供更高质量的随机性性能较好,随机性好
random生成高质量随机数long random(void);提供比rand更好的随机性性能较好,随机性好
srandom设置random的种子void srandom(unsigned int seed);设置random生成器的初始状态性能很好

6.5.3 排序和查找函数

函数名功能原型实现细节性能考量
qsort快速排序void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));通常使用快速排序算法,平均时间复杂度O(n log n)性能很好,适合大多数排序场景
bsearch二分查找void *bsearch(const void *key, const void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));二分查找算法,时间复杂度O(log n)性能很好,但要求数组已排序

6.6 使用示例

6.6.1 内存分配的最佳实践

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
int *arr;
int n = 5;
int i;

// 分配内存(安全方式)
arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
perror("malloc");
return 1;
}

// 初始化数组
srand(time(NULL));
for (i = 0; i < n; i++) {
arr[i] = rand() % 100;
printf("arr[%d] = %d\n", i, arr[i]);
}

// 排序数组
qsort(arr, n, sizeof(int), compare);

// 打印排序后的数组
printf("Sorted array:\n");
for (i = 0; i < n; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}

// 重新分配内存
int *new_arr = (int *)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
perror("realloc");
free(arr);
return 1;
}
arr = new_arr;

// 初始化新分配的内存
for (i = 5; i < 10; i++) {
arr[i] = rand() % 100;
}

// 打印扩展后的数组
printf("Extended array:\n");
for (i = 0; i < 10; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}

// 释放内存
free(arr);

return 0;
}

// 比较函数
int compare(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}

6.6.2 自定义内存池的使用

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
#include <stdio.h>
#include "memory_pool.h" // 假设包含上面的内存池实现

int main() {
struct memory_pool pool;

// 初始化内存池
pool_init(&pool);

// 从内存池分配内存
int *arr = (int *)pool_alloc(&pool, 10 * sizeof(int));
if (arr) {
// 使用内存
for (int i = 0; i < 10; i++) {
arr[i] = i;
printf("arr[%d] = %d\n", i, arr[i]);
}
}

// 分配更多内存
char *str = (char *)pool_alloc(&pool, 20 * sizeof(char));
if (str) {
strcpy(str, "Hello, memory pool!");
printf("String: %s\n", str);
}

// 检查内存使用情况
printf("Memory used: %zu bytes\n", pool_usage(&pool));

// 重置内存池
pool_reset(&pool);
printf("Memory used after reset: %zu bytes\n", pool_usage(&pool));

return 0;
}

7. 进程控制库

7.1 进程控制库概述

进程控制库(<unistd.h>)提供了进程管理的函数,主要用于Unix-like系统。

7.2 常用函数

函数名功能原型
fork创建子进程pid_t fork(void);
exec 系列执行程序int execl(const char *path, const char *arg, ...);
wait等待子进程结束pid_t wait(int *status);
waitpid等待指定子进程结束pid_t waitpid(pid_t pid, int *status, int options);
getpid获取进程IDpid_t getpid(void);
getppid获取父进程IDpid_t getppid(void);
sleep睡眠指定秒数unsigned int sleep(unsigned int seconds);
usleep睡眠指定微秒数int usleep(useconds_t usec);
nanosleep睡眠指定纳秒数int nanosleep(const struct timespec *req, struct timespec *rem);
getuid获取用户IDuid_t getuid(void);
geteuid获取有效用户IDuid_t geteuid(void);
getgid获取组IDgid_t getgid(void);
getegid获取有效组IDgid_t getegid(void);
chdir改变当前目录int chdir(const char *path);
getcwd获取当前目录char *getcwd(char *buf, size_t size);
unlink删除文件int unlink(const char *pathname);
rename重命名文件int rename(const char *oldpath, const char *newpath);

7.3 使用示例

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
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
pid_t pid;

// 创建子进程
pid = fork();

if (pid < 0) {
// 错误
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process: PID = %d, PPID = %d\n", getpid(), getppid());
sleep(1);
printf("Child process exiting\n");
return 0;
} else {
// 父进程
int status;
printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
// 等待子进程结束
wait(&status);
printf("Parent process: Child exited with status %d\n", WEXITSTATUS(status));
return 0;
}
}

8. 网络编程库

8.1 网络编程库概述

网络编程库(<sys/socket.h>)提供了网络通信的函数,是C语言中进行网络编程的基础。

8.2 常用函数

8.2.1 套接字函数

函数名功能原型
socket创建套接字int socket(int domain, int type, int protocol);
bind绑定地址int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen监听连接int listen(int sockfd, int backlog);
accept接受连接int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
connect连接服务器int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
send发送数据ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv接收数据ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sendto发送数据(UDP)ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
recvfrom接收数据(UDP)ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
close关闭套接字int close(int fd);
getaddrinfo获取地址信息int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
freeaddrinfo释放地址信息void freeaddrinfo(struct addrinfo *res);
inet_ntop网络地址转字符串const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_pton字符串转网络地址int inet_pton(int af, const char *src, void *dst);

8.3 使用示例

服务器端

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
char *hello = "Hello from server";

// 创建套接字文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

// 绑定套接字到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}

// 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}

// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}

// 读取客户端消息
read(new_socket, buffer, BUFFER_SIZE);
printf("Client: %s\n", buffer);

// 发送响应
send(new_socket, hello, strlen(hello), 0);
printf("Hello message sent\n");

// 关闭套接字
close(new_socket);
close(server_fd);

return 0;
}

客户端

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
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
char *hello = "Hello from client";

// 创建套接字文件描述符
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("Socket creation error\n");
return -1;
}

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 转换IPv4地址
if(inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr)<=0) {
printf("Invalid address\n");
return -1;
}

// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("Connection Failed\n");
return -1;
}

// 发送消息
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");

// 读取响应
read(sock, buffer, BUFFER_SIZE);
printf("Server: %s\n", buffer);

// 关闭套接字
close(sock);

return 0;
}

9. 信号处理库

9.1 信号处理库概述

信号处理库(<signal.h>)提供了信号处理的函数,用于处理系统发送的各种信号。

9.2 常用函数

函数名功能原型
signal设置信号处理函数void (*signal(int signum, void (*handler)(int)))(int);
sigaction设置信号处理函数(更灵活)int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
kill发送信号int kill(pid_t pid, int sig);
raise向自身发送信号int raise(int sig);
alarm设置闹钟unsigned int alarm(unsigned int seconds);
pause暂停进程直到信号到达int pause(void);
sigprocmask阻塞/解除阻塞信号int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigemptyset初始化信号集为空int sigemptyset(sigset_t *set);
sigfillset初始化信号集为所有信号int sigfillset(sigset_t *set);
sigaddset向信号集添加信号int sigaddset(sigset_t *set, int signum);
sigdelset从信号集删除信号int sigdelset(sigset_t *set, int signum);
sigismember检查信号是否在信号集中int sigismember(const sigset_t *set, int signum);

9.3 使用示例

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
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int signum) {
printf("Received signal %d\n", signum);
if (signum == SIGINT) {
printf("Ctrl+C pressed, exiting...\n");
_exit(0);
}
}

int main() {
// 设置信号处理函数
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);

printf("Waiting for signals... (Press Ctrl+C to exit)\n");

// 无限循环
while (1) {
sleep(1);
printf("Still waiting...\n");
}

return 0;
}

10. 线程编程库

10.1 线程编程库概述

线程编程库(<pthread.h>)提供了多线程编程的函数,使用时需要链接线程库(-lpthread)。

10.2 常用函数

函数名功能原型
pthread_create创建线程int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
pthread_join等待线程结束int pthread_join(pthread_t thread, void **retval);
pthread_detach分离线程int pthread_detach(pthread_t thread);
pthread_exit终止线程void pthread_exit(void *retval);
pthread_self获取线程IDpthread_t pthread_self(void);
pthread_equal比较线程IDint pthread_equal(pthread_t t1, pthread_t t2);
pthread_mutex_init初始化互斥锁int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
pthread_mutex_lock加锁int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_trylock尝试加锁int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_unlock解锁int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_destroy销毁互斥锁int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_cond_init初始化条件变量int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
pthread_cond_wait等待条件变量int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_signal唤醒一个等待的线程int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast唤醒所有等待的线程int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_destroy销毁条件变量int pthread_cond_destroy(pthread_cond_t *cond);

10.3 使用示例

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define NUM_THREADS 5

pthread_mutex_t mutex;
int counter = 0;

void *thread_function(void *arg) {
int thread_id = *((int *)arg);

// 加锁
pthread_mutex_lock(&mutex);

// 临界区
counter++;
printf("Thread %d: counter = %d\n", thread_id, counter);

// 解锁
pthread_mutex_unlock(&mutex);

pthread_exit(NULL);
}

int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
int i;

// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);

// 创建线程
for (i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
perror("pthread_create");
return 1;
}
}

// 等待所有线程结束
for (i = 0; i < NUM_THREADS; i++) {
if (pthread_join(threads[i], NULL) != 0) {
perror("pthread_join");
return 1;
}
}

// 销毁互斥锁
pthread_mutex_destroy(&mutex);

printf("Final counter value: %d\n", counter);

return 0;
}

11. 系统调用库

11.1 系统调用库概述

系统调用库(<sys/syscall.h>)提供了直接调用系统服务的函数,是操作系统与应用程序之间的接口。

11.2 常用函数

函数名功能原型
syscall直接调用系统调用long syscall(long number, ...);
read读取文件ssize_t read(int fd, void *buf, size_t count);
write写入文件ssize_t write(int fd, const void *buf, size_t count);
open打开文件int open(const char *pathname, int flags, mode_t mode);
close关闭文件int close(int fd);
lseek设置文件位置off_t lseek(int fd, off_t offset, int whence);
mkdir创建目录int mkdir(const char *pathname, mode_t mode);
rmdir删除目录int rmdir(const char *pathname);
chmod修改文件权限int chmod(const char *pathname, mode_t mode);
chown修改文件所有者int chown(const char *pathname, uid_t owner, gid_t group);
link创建硬链接int link(const char *oldpath, const char *newpath);
symlink创建符号链接int symlink(const char *target, const char *linkpath);
readlink读取符号链接ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
unlink删除文件int unlink(const char *pathname);
rename重命名文件int rename(const char *oldpath, const char *newpath);

11.3 使用示例

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
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main() {
int fd;
char buffer[100];
ssize_t n;

// 打开文件
fd = open("example.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}

// 写入文件
n = write(fd, "Hello, world!\n", 13);
if (n < 0) {
perror("write");
close(fd);
return 1;
}

// 关闭文件
close(fd);

// 重新打开文件读取
fd = open("example.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}

// 读取文件
n = read(fd, buffer, sizeof(buffer));
if (n < 0) {
perror("read");
close(fd);
return 1;
}

// 打印读取的内容
write(STDOUT_FILENO, buffer, n);

// 关闭文件
close(fd);

return 0;
}

12. 系统函数库的最佳实践

12.1 一般原则

  1. 包含正确的头文件:确保包含了所有需要的头文件
  2. 链接必要的库:如数学库(-lm)、线程库(-lpthread)等
  3. 检查函数返回值:始终检查函数的返回值,处理错误情况
  4. 释放资源:确保所有分配的资源都被释放,如文件描述符、内存等
  5. 使用安全的函数:避免使用不安全的函数,如getsstrcpy
  6. 遵循函数的调用约定:正确传递参数,处理返回值
  7. 了解函数的限制:如缓冲区大小、参数范围等
  8. 使用适当的错误处理:如perrorstrerror
  9. 测试边界情况:测试函数在边界情况下的行为
  10. 参考文档:查阅系统函数库的文档,了解函数的详细用法

12.2 性能优化

  1. 减少函数调用:对于频繁调用的函数,考虑内联或使用宏
  2. 缓存计算结果:对于计算密集型函数,缓存结果
  3. 使用适当的数据结构:选择高效的数据结构
  4. 避免不必要的转换:如字符串和数字之间的转换
  5. 使用编译器优化:如-O2-O3等优化选项

12.3 安全性

  1. 检查参数:验证所有函数参数的有效性
  2. 防止缓冲区溢出:使用安全的字符串处理函数
  3. 释放资源:确保所有分配的资源都被释放
  4. 避免使用不安全的函数:如getsstrcpy
  5. 使用地址随机化:编译时启用地址随机化
  6. 限制权限:最小化程序的权限
  7. 加密敏感数据:对敏感数据进行加密
  8. 防止注入攻击:如SQL注入、命令注入等

13. 系统函数库的跨平台兼容性

13.1 跨平台兼容性问题

  • 头文件差异:不同平台的头文件可能不同
  • 函数差异:不同平台的函数可能有不同的实现或行为
  • 类型差异:不同平台的类型大小可能不同
  • 系统调用差异:不同平台的系统调用可能不同
  • 路径分隔符:不同平台的路径分隔符可能不同(/ vs \

13.2 解决方案

  1. 使用条件编译:根据不同平台使用不同的代码
  2. 使用宏定义:定义平台相关的宏
  3. 使用抽象层:为平台相关的功能创建抽象层
  4. 使用跨平台库:如SDL、Boost等
  5. 测试多个平台:确保代码在多个平台上都能正常工作
  6. 参考标准:遵循C语言标准,避免使用非标准功能

13.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

#ifdef _WIN32
#include <windows.h>
#define SLEEP(ms) Sleep(ms)
#elif defined(__linux__)
#include <unistd.h>
#define SLEEP(ms) usleep((ms) * 1000)
#elif defined(__APPLE__)
#include <unistd.h>
#define SLEEP(ms) usleep((ms) * 1000)
#else
#error Unsupported platform
#endif

int main() {
printf("Sleeping for 1 second...\n");
SLEEP(1000);
printf("Woke up!\n");
return 0;
}

14. 系统函数库的调试

14.1 调试工具

  • gdb:GNU调试器
  • valgrind:内存分析工具
  • strace:系统调用跟踪工具
  • ltrace:库函数调用跟踪工具
  • gprof:性能分析工具
  • addr2line:地址转换工具

14.2 调试技巧

  1. 使用printf:在关键位置添加打印语句
  2. 使用assert:在关键位置添加断言
  3. 检查返回值:始终检查函数的返回值
  4. 使用调试器:使用gdb等调试器逐步执行代码
  5. 使用内存分析工具:使用valgrind等工具检查内存问题
  6. 使用系统调用跟踪:使用strace等工具跟踪系统调用
  7. 查看核心转储:分析程序崩溃时生成的核心转储文件
  8. 使用日志:使用日志记录程序的执行情况

14.3 常见问题

14.3.1 段错误

原因:访问了无效的内存地址
解决:使用gdb或valgrind检查内存访问

14.3.2 内存泄漏

原因:分配的内存未被释放
解决:使用valgrind检查内存泄漏

14.3.3 文件操作错误

原因:文件不存在、权限不足等
解决:检查文件路径、权限,使用perror查看错误信息

14.3.4 网络连接错误

原因:网络不可达、端口未开放等
解决:检查网络连接、防火墙设置,使用perror查看错误信息

15. 示例代码

15.1 综合示例:文件复制程序

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 4096

int main(int argc, char *argv[]) {
FILE *src, *dest;
char buffer[BUFFER_SIZE];
size_t n;

// 检查参数
if (argc != 3) {
fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
return 1;
}

// 打开源文件
src = fopen(argv[1], "rb");
if (src == NULL) {
perror("fopen source");
return 1;
}

// 打开目标文件
dest = fopen(argv[2], "wb");
if (dest == NULL) {
perror("fopen destination");
fclose(src);
return 1;
}

// 复制文件
while ((n = fread(buffer, 1, BUFFER_SIZE, src)) > 0) {
if (fwrite(buffer, 1, n, dest) != n) {
perror("fwrite");
fclose(src);
fclose(dest);
return 1;
}
}

// 检查读取错误
if (ferror(src)) {
perror("fread");
fclose(src);
fclose(dest);
return 1;
}

// 关闭文件
if (fclose(src) != 0) {
perror("fclose source");
fclose(dest);
return 1;
}

if (fclose(dest) != 0) {
perror("fclose destination");
return 1;
}

printf("File copied successfully\n");

return 0;
}

15.2 综合示例:简单的HTTP服务器

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 4096

int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\n\r\nHello World!";

// 创建套接字文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

// 绑定套接字到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}

// 开始监听
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}

printf("Server listening on port %d...\n", PORT);

while (1) {
// 接受连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}

// 读取请求
read(new_socket, buffer, BUFFER_SIZE);
printf("Request:\n%s\n", buffer);

// 发送响应
send(new_socket, response, strlen(response), 0);
printf("Response sent\n");

// 关闭连接
close(new_socket);
}

return 0;
}