第15章 高级主题

系统函数库

系统函数库是操作系统提供的一组函数,用于访问系统资源和服务。在 C 语言中,这些函数通常通过标准头文件提供。

常用系统函数库

1. 标准输入/输出库 (<stdio.h>)

主要功能: 文件操作、标准输入/输出

常用函数详细说明:

1.1 printf() - 格式化输出到标准输出

函数原型:

1
int printf(const char *format, ...);

参数说明:

参数类型必填默认值描述
formatconst char *格式化字符串,包含普通字符和格式说明符
...可变参数要输出的数据,数量和类型必须与格式说明符匹配

返回值: 成功输出的字符数,失败返回负数

使用场景: 向标准输出(通常是终端)打印格式化信息

注意事项:

  • 格式说明符必须与参数类型匹配,否则会导致未定义行为
  • 格式化字符串中可以包含转义序列,如 \n(换行)、\t(制表符)等
  • 对于字符串参数,确保其以 null 字符结尾

示例:

1
2
3
printf("Hello, %s!\n", "World");  // 输出: Hello, World!
printf("The answer is %d\n", 42); // 输出: The answer is 42
printf("Pi is approximately %.2f\n", 3.14159); // 输出: Pi is approximately 3.14
1.2 scanf() - 从标准输入读取格式化数据

函数原型:

1
int scanf(const char *format, ...);

参数说明:

参数类型必填默认值描述
formatconst char *格式化字符串,包含格式说明符
...可变参数指向要存储读取数据的变量的指针,数量和类型必须与格式说明符匹配

返回值: 成功读取并赋值的参数个数,遇到文件结束返回 EOF

使用场景: 从标准输入(通常是键盘)读取用户输入的格式化数据

注意事项:

  • 参数必须是指针,否则会导致未定义行为
  • 格式说明符必须与参数类型匹配
  • 会跳过空白字符(空格、制表符、换行符等),直到遇到非空白字符
  • 对于字符串,会读取到空白字符为止

示例:

1
2
3
4
5
int age;
char name[50];
printf("请输入您的姓名和年龄:");
scanf("%s %d", name, &age);
printf("您好,%s!您的年龄是 %d 岁。\n", name, age);
1.3 fopen() - 打开文件

函数原型:

1
FILE *fopen(const char *filename, const char *mode);

参数说明:

参数类型必填默认值描述
filenameconst char *要打开的文件路径
modeconst char *文件打开模式

文件打开模式:

模式描述
"r"只读模式,文件必须存在
"w"只写模式,文件不存在则创建,存在则截断为空
"a"追加模式,文件不存在则创建,写入数据追加到文件末尾
"r+"读写模式,文件必须存在
"w+"读写模式,文件不存在则创建,存在则截断为空
"a+"读写模式,文件不存在则创建,写入数据追加到文件末尾
"rb", "wb", "ab", etc.二进制模式(在 Windows 上有区别)

返回值: 成功返回文件指针,失败返回 NULL

使用场景: 打开文件以进行读写操作

注意事项:

  • 必须检查返回值是否为 NULL,以处理文件打开失败的情况
  • 使用完毕后必须调用 fclose() 关闭文件
  • 二进制模式在 Windows 上会禁用换行符转换

示例:

1
2
3
4
5
6
7
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}
// 使用文件...
fclose(file);
1.4 fclose() - 关闭文件

函数原型:

1
int fclose(FILE *stream);

参数说明:

参数类型必填默认值描述
streamFILE *要关闭的文件指针

返回值: 成功返回 0,失败返回 EOF

使用场景: 关闭已打开的文件,释放资源

注意事项:

  • 关闭文件后,文件指针不再有效
  • 必须检查返回值,以处理关闭失败的情况(如磁盘满)
  • 程序结束前应关闭所有打开的文件

示例:

1
2
3
4
5
6
7
8
9
10
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}
// 使用文件...
if (fclose(file) != 0) {
perror("关闭文件失败");
return 1;
}
1.5 fread() - 从文件读取数据

函数原型:

1
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

参数说明:

参数类型必填默认值描述
ptrvoid *指向存储读取数据的缓冲区的指针
sizesize_t每个数据项的大小(以字节为单位)
countsize_t要读取的数据项数量
streamFILE *文件指针

返回值: 成功读取的数据项数量,可能小于 count(如果到达文件末尾或发生错误)

使用场景: 从文件(尤其是二进制文件)读取数据块

取值范围限制:

  • sizecount 参数的取值范围:0 到 SIZE_MAX
  • SIZE_MAX 是系统定义的 size_t 类型的最大值
  • sizecount 为 0 时,函数返回 0,不执行任何操作
  • 实际读取的字节数为返回值乘以 size

注意事项:

  • 通常用于读取二进制文件或结构化数据
  • 要检查是否到达文件末尾,应使用 feof() 函数
  • 要检查是否发生错误,应使用 ferror() 函数
  • 错误处理:
    • 当返回值小于 count 时,可能是到达文件末尾或发生错误
    • 应先检查 ferror(),再检查 feof()
  • 性能考虑:
    • 对于大文件,使用较大的缓冲区可以提高读取速度
    • 避免频繁调用 fread() 读取小数据块
  • 缓冲区要求:
    • ptr 指向的缓冲区必须足够大,至少能容纳 size * count 字节
    • 缓冲区的对齐方式可能影响读取性能
  • 文本文件与二进制文件:
    • 在文本模式下,某些系统可能会对换行符进行转换
    • 在二进制模式下,数据会被原样读取
  • 流状态:
    • 读取操作可能会更新流的文件位置指示器
    • 发生错误时,流的错误指示器会被设置

示例:

1
2
3
4
5
6
7
8
9
10
11
FILE *file = fopen("data.bin", "rb");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

int buffer[10];
size_t items_read = fread(buffer, sizeof(int), 10, file);
printf("成功读取 %zu 个整数\n", items_read);

fclose(file);
1.6 fwrite() - 向文件写入数据

函数原型:

1
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

参数说明:

参数类型必填默认值描述
ptrconst void *指向要写入的数据的指针
sizesize_t每个数据项的大小(以字节为单位)
countsize_t要写入的数据项数量
streamFILE *文件指针

返回值: 成功写入的数据项数量,可能小于 count(如果发生错误)

使用场景: 向文件(尤其是二进制文件)写入数据块

取值范围限制:

  • sizecount 参数的取值范围:0 到 SIZE_MAX
  • SIZE_MAX 是系统定义的 size_t 类型的最大值
  • sizecount 为 0 时,函数返回 0,不执行任何操作
  • 实际写入的字节数为返回值乘以 size

注意事项:

  • 通常用于写入二进制文件或结构化数据
  • 应检查返回值,以确保所有数据都已成功写入
  • 对于文本文件,建议使用 fprintf()fputs()
  • 错误处理:
    • 当返回值小于 count 时,表示发生了错误
    • 应使用 ferror() 函数检查具体的错误原因
  • 缓冲区刷新:
    • 数据可能会先写入文件流的缓冲区,而不是直接写入磁盘
    • 可以使用 fflush() 函数强制刷新缓冲区
    • 关闭文件时会自动刷新缓冲区
  • 性能考虑:
    • 对于大文件,使用较大的缓冲区可以提高写入速度
    • 避免频繁调用 fwrite() 写入小数据块
  • 文本文件与二进制文件:
    • 在文本模式下,某些系统可能会对换行符进行转换
    • 在二进制模式下,数据会被原样写入
  • 流状态:
    • 写入操作可能会更新流的文件位置指示器
    • 发生错误时,流的错误指示器会被设置
  • 磁盘空间:
    • 如果磁盘空间不足,fwrite() 会失败
    • 应始终检查返回值,即使之前的写入操作都成功了

示例:

1
2
3
4
5
6
7
8
9
10
11
FILE *file = fopen("data.bin", "wb");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

int data[] = {1, 2, 3, 4, 5};
size_t items_written = fwrite(data, sizeof(int), 5, file);
printf("成功写入 %zu 个整数\n", items_written);

fclose(file);
1.7 fgets() - 从文件读取一行

函数原型:

1
char *fgets(char *s, int size, FILE *stream);

参数说明:

参数类型必填默认值描述
schar *指向存储读取字符串的缓冲区的指针
sizeint缓冲区大小(包括 null 终止符)
streamFILE *文件指针

返回值: 成功返回 s,到达文件末尾或发生错误返回 NULL

使用场景: 从文件或标准输入读取一行文本

注意事项:

  • 最多读取 size-1 个字符,剩余空间用于存储 null 终止符
  • 会读取并包含换行符(如果行长度小于 size-1
  • 当读取到换行符或到达文件末尾时停止

示例:

1
2
3
4
5
6
7
8
9
10
11
12
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

char buffer[100];
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer); // 注意:buffer 已经包含换行符
}

fclose(file);
1.8 fputs() - 向文件写入字符串

函数原型:

1
int fputs(const char *s, FILE *stream);

参数说明:

参数类型必填默认值描述
sconst char *要写入的以 null 结尾的字符串
streamFILE *文件指针

返回值: 成功返回非负值,失败返回 EOF

使用场景: 向文件写入字符串

注意事项:

  • 不会自动添加换行符,需要手动添加
  • 字符串必须以 null 字符结尾
  • 对于格式化输出,建议使用 fprintf()

示例:

1
2
3
4
5
6
7
8
9
10
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

fputs("Hello, World!\n", file);
fputs("This is a test.\n", file);

fclose(file);
1.9 feof() - 检查文件是否到达末尾

函数原型:

1
int feof(FILE *stream);

参数说明:

参数类型必填默认值描述
streamFILE *文件指针

返回值: 如果文件到达末尾返回非零值,否则返回 0

使用场景: 检查文件读取操作是否因为到达文件末尾而停止

注意事项:

  • 只有在读取操作尝试读取超出文件末尾后,才会设置文件结束标志
  • 不会检测文件错误,应使用 ferror() 检查错误

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

char buffer[100];
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

if (feof(file)) {
printf("已到达文件末尾\n");
} else if (ferror(file)) {
perror("读取文件时发生错误");
}

fclose(file);
1.10 ferror() - 检查文件是否发生错误

函数原型:

1
int ferror(FILE *stream);

参数说明:

参数类型必填默认值描述
streamFILE *文件指针

返回值: 如果文件发生错误返回非零值,否则返回 0

使用场景: 检查文件操作是否发生错误

注意事项:

  • 错误标志会一直保持,直到调用 clearerr() 清除
  • 应在文件操作后检查错误状态

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

char buffer[100];
if (fgets(buffer, sizeof(buffer), file) == NULL) {
if (ferror(file)) {
perror("读取文件时发生错误");
} else if (feof(file)) {
printf("文件为空\n");
}
}

fclose(file);

使用示例:

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

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

// 打开文件
file = fopen("example.txt", "r");
if (file == NULL)
{
perror("无法打开文件");
return 1;
}

// 读取文件内容
while (fgets(buffer, sizeof(buffer), file) != NULL)
{
printf("%s", buffer);
}

// 检查文件状态
if (feof(file))
{
printf("\n已到达文件末尾\n");
}
else if (ferror(file))
{
perror("读取文件时发生错误");
}

// 关闭文件
if (fclose(file) != 0)
{
perror("关闭文件失败");
return 1;
}

return 0;
}

跨平台兼容性说明

在使用系统函数库时,需要注意不同操作系统之间的差异,以确保代码的可移植性。

主要平台差异

平台差异说明解决方案
Windows使用不同的头文件(如 <windows.h>)和函数名使用条件编译,为不同平台提供不同实现
Linux/Unix遵循 POSIX 标准,函数名和行为较为一致使用 POSIX 标准函数,确保兼容性
macOS基于 BSD,部分函数与 Linux 有差异参考 macOS 特定文档,注意函数参数和返回值的差异

头文件差异

  • Windows: 常用头文件包括 <windows.h>, <winsock2.h> 等
  • Linux/Unix: 常用头文件包括 <unistd.h>, <sys/socket.h>, <sys/stat.h> 等
  • macOS: 兼容大部分 BSD 和 POSIX 头文件

数据类型差异

  • time_t 类型在不同平台上的大小可能不同(32 位或 64 位)
  • pid_t, uid_t 等类型的大小也可能不同
  • 建议使用 <stdint.h> 中定义的固定大小类型,如 int32_t, int64_t

函数行为差异

  • signal() 函数在不同平台上的行为可能不同,建议使用 sigaction()
  • 文件路径分隔符不同:Windows 使用 \,Unix/Linux 使用 /
  • 换行符不同:Windows 使用 \r\n,Unix/Linux 使用 \n

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

1. 错误处理

  • 始终检查函数返回值,特别是可能失败的系统调用
  • 使用 errno 变量和 perror()strerror() 函数来获取详细的错误信息
  • 为错误处理编写清晰的代码,避免忽略错误

2. 资源管理

  • 对于分配的资源(如内存、文件描述符、网络连接等),确保在使用完毕后释放
  • 使用 RAII(资源获取即初始化)模式或封装资源管理函数
  • 避免资源泄漏,特别是在循环和错误处理路径中

3. 安全性

  • 避免使用不安全的函数,如 strcpy()gets()
  • 使用边界检查,防止缓冲区溢出
  • 对用户输入进行验证和 sanitization
  • 注意权限管理,避免权限提升漏洞

4. 性能优化

  • 避免频繁的系统调用,尽量批量处理操作
  • 使用适当的缓冲区大小,平衡内存使用和 I/O 性能
  • 对于计算密集型任务,考虑使用多线程或异步 I/O
  • 缓存常用的计算结果,避免重复计算

5. 代码风格和可读性

  • 为系统函数调用添加清晰的注释,说明其用途和可能的错误情况
  • 使用有意义的变量名和函数名
  • 遵循项目的代码风格指南
  • 编写单元测试,确保函数行为符合预期

6. 文档和维护

  • 记录系统函数的使用方式和注意事项
  • 定期更新依赖库和系统函数的使用方式
  • 监控系统函数的性能和错误率
  • 为复杂的系统函数调用编写封装函数,提高代码的可维护性

7. 调试技巧

  • 使用 strace(Linux)或 Process Explorer(Windows)等工具跟踪系统调用
  • 为系统函数调用添加日志,记录参数和返回值
  • 使用调试器逐步执行代码,观察系统函数的行为
  • 模拟错误情况,测试错误处理代码的正确性

总结

系统函数库是 C 语言编程中不可或缺的一部分,它们提供了与操作系统交互的能力。通过正确理解和使用这些函数,开发人员可以编写更加高效、可靠和安全的应用程序。

在使用系统函数时,应注意以下几点:

  1. 了解函数的参数和返回值:确保正确传递参数,并检查返回值以处理错误
  2. 注意平台差异:编写可移植的代码,处理不同操作系统之间的差异
  3. 遵循最佳实践:包括错误处理、资源管理、安全性和性能优化
  4. 持续学习:系统函数库不断发展,应关注新的函数和最佳实践

通过合理使用系统函数库,开发人员可以充分利用操作系统提供的功能,构建更加功能强大的应用程序。

1
2
3
4
5
6
7
8
9
10
11
12

#### 2. 字符串处理库 (`<string.h>`)

**主要功能:** 字符串操作

**常用函数详细说明:**

##### 2.1 `strlen()` - 获取字符串长度

**函数原型:**
```c
size_t strlen(const char *s);

参数说明:

参数类型必填默认值描述
sconst char *指向要计算长度的以 null 结尾的字符串

返回值: 字符串中字符的数量(不包括 null 终止符)

使用场景: 计算字符串的长度,用于字符串操作的边界检查

注意事项:

  • 字符串必须以 null 字符结尾,否则会导致未定义行为
  • 时间复杂度为 O(n),其中 n 是字符串长度

示例:

1
2
3
char str[] = "Hello, World!";
size_t length = strlen(str);
printf("字符串长度: %zu\n", length); // 输出: 字符串长度: 13
2.2 strcpy() - 复制字符串

函数原型:

1
char *strcpy(char *dest, const char *src);

参数说明:

参数类型必填默认值描述
destchar *指向目标缓冲区的指针
srcconst char *指向要复制的以 null 结尾的字符串

返回值: 指向目标缓冲区 dest 的指针

使用场景: 将一个字符串复制到另一个缓冲区

取值范围限制:

  • src 必须指向一个以 null 结尾的字符串
  • dest 必须指向一个足够大的缓冲区,能够容纳整个源字符串(包括 null 终止符)
  • 缓冲区大小没有明确的上限,但应根据实际需要合理分配

注意事项:

  • 目标缓冲区必须足够大,能够容纳源字符串(包括 null 终止符)
  • 会覆盖目标缓冲区中的原有内容
  • 源字符串和目标缓冲区不能重叠,否则会导致未定义行为
  • 由于可能导致缓冲区溢出,建议使用 strncpy() 代替
  • 安全性考虑:
    • strcpy() 是一个不安全的函数,因为它不检查目标缓冲区的大小
    • 可能导致缓冲区溢出漏洞,被攻击者利用
    • 在安全敏感的代码中,应使用更安全的替代函数
  • 替代函数推荐:
    • strncpy():指定最大复制长度
    • strlcpy():(某些系统提供)更安全的字符串复制函数
    • snprintf():格式化输出到缓冲区,可指定最大长度
  • 性能考虑:
    • strcpy() 的时间复杂度为 O(n),其中 n 是字符串长度
    • 对于短字符串,性能影响可以忽略
    • 对于长字符串,应确保目标缓冲区足够大
  • 空字符串处理:
    • 如果 src 是空字符串(只包含 null 终止符),strcpy() 只会复制 null 终止符到 dest
  • 内存重叠:
    • 如果 srcdest 指向的内存区域重叠,行为未定义
    • 应使用 memmove() 处理内存重叠的情况

示例:

1
2
3
4
char src[] = "Hello";
char dest[20]; // 确保目标缓冲区足够大
strcpy(dest, src);
printf("复制结果: %s\n", dest); // 输出: 复制结果: Hello
2.3 strncpy() - 复制指定长度的字符串

函数原型:

1
char *strncpy(char *dest, const char *src, size_t n);

参数说明:

参数类型必填默认值描述
destchar *指向目标缓冲区的指针
srcconst char *指向要复制的以 null 结尾的字符串
nsize_t要复制的最大字符数

返回值: 指向目标缓冲区 dest 的指针

使用场景: 安全地复制字符串,避免缓冲区溢出

取值范围限制:

  • n 参数的取值范围:0 到 SIZE_MAX
  • SIZE_MAX 是系统定义的 size_t 类型的最大值
  • dest 必须指向一个至少为 n 字节大小的缓冲区
  • src 必须指向一个以 null 结尾的字符串(除非 n 为 0)

注意事项:

  • 如果源字符串长度小于 n,会用 null 字符填充目标缓冲区直到 n 个字符
  • 如果源字符串长度大于或等于 n,目标缓冲区不会以 null 字符结尾
  • 源字符串和目标缓冲区不能重叠
  • null 终止符处理:
    • 当源字符串长度小于 n 时:会将剩余空间用 null 字符填充
    • 当源字符串长度大于或等于 n 时:不会添加 null 终止符,目标字符串不是以 null 结尾的
    • 建议在使用 strncpy() 后,手动添加 null 终止符:dest[n-1] = '\0';
  • 安全性考虑:
    • strncpy()strcpy() 更安全,但仍然存在安全隐患
    • 如果忘记手动添加 null 终止符,可能导致后续的字符串操作出现问题
    • 在安全敏感的代码中,应使用更安全的替代函数
  • 替代函数推荐:
    • strlcpy():(某些系统提供)更安全的字符串复制函数,总是添加 null 终止符
    • snprintf():格式化输出到缓冲区,可指定最大长度,总是添加 null 终止符
  • 性能考虑:
    • 当源字符串长度远小于 n 时,strncpy() 会填充大量 null 字符,可能影响性能
    • 对于短字符串,性能影响可以忽略
    • 对于长字符串,应合理设置 n 的值
  • 内存重叠:
    • 源字符串和目标缓冲区不能重叠,否则会导致未定义行为
    • 应使用 memmove() 处理内存重叠的情况

示例:

1
2
3
4
5
char src[] = "Hello, World!";
char dest[10];
strncpy(dest, src, sizeof(dest) - 1); // 留一个位置给 null 终止符
dest[sizeof(dest) - 1] = '\0'; // 确保 null 终止
printf("复制结果: %s\n", dest); // 输出: 复制结果: Hello, Wo
2.4 strcmp() - 比较字符串

函数原型:

1
int strcmp(const char *s1, const char *s2);

参数说明:

参数类型必填默认值描述
s1const char *指向第一个要比较的以 null 结尾的字符串
s2const char *指向第二个要比较的以 null 结尾的字符串

返回值:

  • 小于 0:s1 小于 s2
  • 等于 0:s1 等于 s2
  • 大于 0:s1 大于 s2

使用场景: 比较两个字符串的大小,用于排序或查找

注意事项:

  • 比较是基于字符的 ASCII 值
  • 字符串必须以 null 结尾
  • 比较会一直进行,直到遇到不同的字符或 null 终止符

示例:

1
2
3
4
5
6
7
8
9
10
11
char str1[] = "apple";
char str2[] = "banana";
int result = strcmp(str1, str2);
if (result < 0) {
printf("%s 小于 %s\n", str1, str2);
} else if (result > 0) {
printf("%s 大于 %s\n", str1, str2);
} else {
printf("%s 等于 %s\n", str1, str2);
}
// 输出: apple 小于 banana
2.5 strncmp() - 比较指定长度的字符串

函数原型:

1
int strncmp(const char *s1, const char *s2, size_t n);

参数说明:

参数类型必填默认值描述
s1const char *指向第一个要比较的以 null 结尾的字符串
s2const char *指向第二个要比较的以 null 结尾的字符串
nsize_t要比较的最大字符数

返回值:

  • 小于 0:s1 小于 s2
  • 等于 0:s1 等于 s2(前 n 个字符)
  • 大于 0:s1 大于 s2

使用场景: 比较两个字符串的前 n 个字符,用于前缀匹配

注意事项:

  • 比较是基于字符的 ASCII 值
  • 如果其中一个字符串长度小于 n,比较会在遇到 null 终止符时停止

示例:

1
2
3
4
5
6
7
8
9
char str1[] = "Hello, World!";
char str2[] = "Hello, C!";
int result = strncmp(str1, str2, 6); // 比较前 6 个字符
if (result == 0) {
printf("前 6 个字符相同\n");
} else {
printf("前 6 个字符不同\n");
}
// 输出: 前 6 个字符相同
2.6 strcat() - 连接字符串

函数原型:

1
char *strcat(char *dest, const char *src);

参数说明:

参数类型必填默认值描述
destchar *指向目标字符串的指针(必须以 null 结尾)
srcconst char *指向要追加的以 null 结尾的字符串

返回值: 指向目标字符串 dest 的指针

使用场景: 将一个字符串追加到另一个字符串的末尾

注意事项:

  • 目标缓冲区必须足够大,能够容纳原始字符串和追加的字符串(包括 null 终止符)
  • 源字符串会追加到目标字符串的 null 终止符位置
  • 源字符串和目标缓冲区不能重叠,否则会导致未定义行为
  • 由于可能导致缓冲区溢出,建议使用 strncat() 代替

示例:

1
2
3
4
char dest[50] = "Hello, ";  // 确保目标缓冲区足够大
char src[] = "World!";
strcat(dest, src);
printf("连接结果: %s\n", dest); // 输出: 连接结果: Hello, World!
2.7 strncat() - 连接指定长度的字符串

函数原型:

1
char *strncat(char *dest, const char *src, size_t n);

参数说明:

参数类型必填默认值描述
destchar *指向目标字符串的指针(必须以 null 结尾)
srcconst char *指向要追加的以 null 结尾的字符串
nsize_t要追加的最大字符数

返回值: 指向目标字符串 dest 的指针

使用场景: 安全地追加字符串,避免缓冲区溢出

注意事项:

  • 会在追加的字符串末尾添加 null 终止符
  • 目标缓冲区必须足够大,能够容纳原始字符串、追加的字符串和 null 终止符
  • 源字符串和目标缓冲区不能重叠

示例:

1
2
3
4
5
6
char dest[20] = "Hello, ";  // 确保目标缓冲区足够大
char src[] = "World! How are you?";
size_t dest_len = strlen(dest);
size_t available = sizeof(dest) - dest_len - 1; // 减去 null 终止符
strncat(dest, src, available);
printf("连接结果: %s\n", dest); // 输出: 连接结果: Hello, World! H
2.8 strchr() - 查找字符首次出现的位置

函数原型:

1
char *strchr(const char *s, int c);

参数说明:

参数类型必填默认值描述
sconst char *指向要搜索的以 null 结尾的字符串
cint要查找的字符(会被转换为 unsigned char

返回值: 指向字符串中首次出现字符 c 的位置的指针,如果未找到返回 NULL

使用场景: 在字符串中查找特定字符的位置

注意事项:

  • 会搜索到 null 终止符为止
  • 也会查找 null 终止符本身

示例:

1
2
3
4
5
6
7
8
9
10
char str[] = "Hello, World!";
char *pos = strchr(str, 'W');
if (pos != NULL) {
printf("找到 'W' 在位置: %zu\n", pos - str);
printf("从 'W' 开始的字符串: %s\n", pos);
} else {
printf("未找到字符 'W'\n");
}
// 输出: 找到 'W' 在位置: 7
// 输出: 从 'W' 开始的字符串: World!
2.9 strrchr() - 查找字符最后出现的位置

函数原型:

1
char *strrchr(const char *s, int c);

参数说明:

参数类型必填默认值描述
sconst char *指向要搜索的以 null 结尾的字符串
cint要查找的字符(会被转换为 unsigned char

返回值: 指向字符串中最后出现字符 c 的位置的指针,如果未找到返回 NULL

使用场景: 在字符串中查找特定字符的最后一个位置

注意事项:

  • 会搜索到字符串开头为止
  • 也会查找 null 终止符本身

示例:

1
2
3
4
5
6
7
8
9
10
char str[] = "Hello, World!";
char *pos = strrchr(str, 'o');
if (pos != NULL) {
printf("最后一个 'o' 在位置: %zu\n", pos - str);
printf("从最后一个 'o' 开始的字符串: %s\n", pos);
} else {
printf("未找到字符 'o'\n");
}
// 输出: 最后一个 'o' 在位置: 8
// 输出: 从最后一个 'o' 开始的字符串: orld!
2.10 strstr() - 查找子字符串

函数原型:

1
char *strstr(const char *haystack, const char *needle);

参数说明:

参数类型必填默认值描述
haystackconst char *指向要搜索的以 null 结尾的字符串
needleconst char *指向要查找的以 null 结尾的子字符串

返回值: 指向子字符串在原字符串中首次出现位置的指针,如果未找到返回 NULL

使用场景: 在字符串中查找子字符串,用于模式匹配

注意事项:

  • 如果 needle 是空字符串,返回 haystack
  • 时间复杂度为 O(m*n),其中 m 和 n 分别是两个字符串的长度

示例:

1
2
3
4
5
6
7
8
9
10
11
char haystack[] = "Hello, World!";
char needle[] = "World";
char *pos = strstr(haystack, needle);
if (pos != NULL) {
printf("找到子字符串在位置: %zu\n", pos - haystack);
printf("从子字符串开始的字符串: %s\n", pos);
} else {
printf("未找到子字符串\n");
}
// 输出: 找到子字符串在位置: 7
// 输出: 从子字符串开始的字符串: World!
2.11 memset() - 填充内存块

函数原型:

1
void *memset(void *s, int c, size_t n);

参数说明:

参数类型必填默认值描述
svoid *指向要填充的内存块的指针
cint要填充的值(会被转换为 unsigned char
nsize_t要填充的字节数

返回值: 指向内存块 s 的指针

使用场景: 将内存块设置为特定值,通常用于初始化或清空内存

注意事项:

  • 填充的是字节,不是多字节值
  • 对于非字符类型的数组,通常用于设置为 0 或 -1

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
char buffer[20];
// 清空缓冲区
memset(buffer, 0, sizeof(buffer));

int array[10];
// 将数组初始化为 0
memset(array, 0, sizeof(array));

// 将字符串设置为特定字符
char str[20];
memset(str, 'A', 5);
str[5] = '\0'; // 添加 null 终止符
printf("填充结果: %s\n", str); // 输出: 填充结果: AAAAA
2.12 memcpy() - 复制内存块

函数原型:

1
void *memcpy(void *dest, const void *src, size_t n);

参数说明:

参数类型必填默认值描述
destvoid *指向目标内存块的指针
srcconst void *指向源内存块的指针
nsize_t要复制的字节数

返回值: 指向目标内存块 dest 的指针

使用场景: 复制任意类型的内存块,包括非字符串数据

注意事项:

  • 目标内存块必须足够大,能够容纳要复制的数据
  • 源内存块和目标内存块不能重叠,否则会导致未定义行为(应使用 memmove() 代替)

示例:

1
2
3
4
5
6
7
8
9
10
int src[] = {1, 2, 3, 4, 5};
int dest[5];
// 复制整数数组
memcpy(dest, src, sizeof(src));
printf("复制结果: ");
for (int i = 0; i < 5; i++) {
printf("%d ", dest[i]);
}
printf("\n");
// 输出: 复制结果: 1 2 3 4 5
2.13 memcmp() - 比较内存块

函数原型:

1
int memcmp(const void *s1, const void *s2, size_t n);

参数说明:

参数类型必填默认值描述
s1const void *指向第一个内存块的指针
s2const void *指向第二个内存块的指针
nsize_t要比较的字节数

返回值:

  • 小于 0:s1 小于 s2
  • 等于 0:s1 等于 s2(前 n 个字节)
  • 大于 0:s1 大于 s2

使用场景: 比较任意类型的内存块,包括非字符串数据

注意事项:

  • 比较是基于字节的,逐字节比较
  • 对于多字节类型(如整数、浮点数),比较结果可能与预期不同,因为取决于字节序

示例:

1
2
3
4
5
6
7
8
9
10
11
int a[] = {1, 2, 3};
int b[] = {1, 2, 4};
int result = memcmp(a, b, sizeof(a));
if (result < 0) {
printf("a 小于 b\n");
} else if (result > 0) {
printf("a 大于 b\n");
} else {
printf("a 等于 b\n");
}
// 输出: a 小于 b

使用示例:

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 <string.h>

int main(void)
{
char str1[50] = "Hello";
char str2[50] = "World";
char buffer[100];

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

// 字符串复制
strcpy(buffer, str1);
printf("复制 str1 到 buffer: %s\n", buffer);

// 字符串连接
strcat(buffer, " ");
strcat(buffer, str2);
printf("连接后: %s\n", buffer);

// 字符串比较
int result = strcmp(str1, str2);
printf("str1 和 str2 比较结果: %d\n", result);

// 子字符串查找
char *sub = strstr(buffer, "World");
if (sub != NULL)
{
printf("找到子字符串: %s\n", sub);
}

// 内存操作
memset(buffer, 0, sizeof(buffer));
printf("memset 后 buffer: '%s'\n", buffer);

return 0;
}

3. 数学库 (<math.h>)

主要功能: 数学计算

常用函数详细说明:

3.1 sin() - 正弦函数

函数原型:

1
double sin(double x);

参数说明:

参数类型必填默认值描述
xdouble角度(弧度制)

返回值: x 的正弦值,范围在 [-1, 1] 之间

使用场景: 计算角度的正弦值,用于三角函数计算

注意事项:

  • 参数 x 必须是弧度制,不是角度制
  • 对于角度制,需要先转换为弧度:弧度 = 角度 * π / 180

示例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <math.h>

int main(void) {
double angle_deg = 90.0;
double angle_rad = angle_deg * M_PI / 180.0;
printf("sin(90°) = %f\n", sin(angle_rad)); // 输出: sin(90°) = 1.000000

return 0;
}
3.2 cos() - 余弦函数

函数原型:

1
double cos(double x);

参数说明:

参数类型必填默认值描述
xdouble角度(弧度制)

返回值: x 的余弦值,范围在 [-1, 1] 之间

使用场景: 计算角度的余弦值,用于三角函数计算

注意事项:

  • 参数 x 必须是弧度制,不是角度制
  • 对于角度制,需要先转换为弧度:弧度 = 角度 * π / 180

示例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <math.h>

int main(void) {
double angle_deg = 0.0;
double angle_rad = angle_deg * M_PI / 180.0;
printf("cos(0°) = %f\n", cos(angle_rad)); // 输出: cos(0°) = 1.000000

return 0;
}
3.3 tan() - 正切函数

函数原型:

1
double tan(double x);

参数说明:

参数类型必填默认值描述
xdouble角度(弧度制)

返回值: x 的正切值

使用场景: 计算角度的正切值,用于三角函数计算

注意事项:

  • 参数 x 必须是弧度制,不是角度制
  • x 接近 π/2 + kπ(k 为整数)时,结果会趋近于无穷大

示例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <math.h>

int main(void) {
double angle_deg = 45.0;
double angle_rad = angle_deg * M_PI / 180.0;
printf("tan(45°) = %f\n", tan(angle_rad)); // 输出: tan(45°) = 1.000000

return 0;
}
3.4 sqrt() - 平方根函数

函数原型:

1
double sqrt(double x);

参数说明:

参数类型必填默认值描述
xdouble要求平方根的值,必须非负

返回值: x 的平方根

使用场景: 计算非负数的平方根,用于几何计算、物理计算等

取值范围限制:

  • x 参数的取值范围:0.0 ≤ x ≤ +∞
  • x < 0.0 时,会产生域错误
  • x = 0.0 时,返回 0.0
  • x = +∞ 时,返回 +∞

注意事项:

  • 如果 x 为负数,会产生域错误(domain error),errno 会被设置为 EDOM
  • 如果 x 为 0 或正无穷大,返回相同的值
  • 精度考虑:
    • 返回值的精度取决于实现,但通常是双精度浮点数的最大精度
    • 对于完全平方数,返回值应该是精确的整数
  • 性能考虑:
    • sqrt() 是一个相对昂贵的操作,应避免在性能关键的代码中频繁调用
    • 某些处理器有硬件平方根指令,可以加速此操作
  • 替代方法:
    • 对于整数平方根,可以使用 sqrtl() 获取更高精度
    • 对于需要多次计算平方根的场景,可以考虑使用查表法或其他优化技术
  • 特殊值处理:
    • sqrt(NaN) 返回 NaN
    • sqrt(+0.0) 返回 +0.0
    • sqrt(-0.0) 返回 -0.0
    • sqrt(+∞) 返回 +∞

示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <math.h>

int main(void) {
double x = 16.0;
printf("sqrt(%f) = %f\n", x, sqrt(x)); // 输出: sqrt(16.000000) = 4.000000

return 0;
}
3.5 pow() - 幂运算函数

函数原型:

1
double pow(double x, double y);

参数说明:

参数类型必填默认值描述
xdouble底数
ydouble指数

返回值: xy 次幂,即 x^y

使用场景: 计算任意实数的幂,用于数学计算、科学计算等

取值范围限制:

  • xy 的取值范围受以下规则限制:
    • x < 0 时,y 必须是整数
    • x = 0 时,y 必须 > 0
    • x = +∞ 时,根据 y 的值返回不同结果
    • y = 0 时,返回 1.0(除非 x = 0,此时返回 NaN)
    • y = +∞ 时,根据 x 的值返回不同结果
    • y = -∞ 时,根据 x 的值返回不同结果

注意事项:

  • x 为负数且 y 不是整数时,会产生域错误,errno 会被设置为 EDOM
  • x 为 0 且 y 为负数时,会产生域错误,errno 会被设置为 EDOM
  • 当结果溢出时,会产生溢出错误,返回 ±HUGE_VAL,errno 会被设置为 ERANGE
  • 当结果下溢时,会产生下溢错误,返回 0.0,errno 会被设置为 ERANGE
  • 特殊值处理:
    • pow(1.0, y) 返回 1.0 对于任何 y
    • pow(x, 0.0) 返回 1.0 对于任何非零 x
    • pow(NaN, y) 返回 NaN
    • pow(x, NaN) 返回 NaN
    • pow(+0.0, y) 对于 y > 0 返回 +0.0
    • pow(-0.0, y) 对于 y > 0y 为奇数返回 -0.0
    • pow(+0.0, y) 对于 y < 0 返回 +∞
    • pow(-0.0, y) 对于 y < 0y 为奇数返回 -∞
  • 精度考虑:
    • 对于整数指数,使用循环乘法可能会更精确
    • 对于某些特殊情况(如平方、立方),使用专门的函数可能会更高效
  • 性能考虑:
    • pow() 是一个相对昂贵的操作,应避免在性能关键的代码中频繁调用
    • 对于整数指数,可以考虑使用快速幂算法

示例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <math.h>

int main(void) {
double x = 2.0;
double y = 3.0;
printf("pow(%f, %f) = %f\n", x, y, pow(x, y)); // 输出: pow(2.000000, 3.000000) = 8.000000

return 0;
}
3.6 exp() - 指数函数

函数原型:

1
double exp(double x);

参数说明:

参数类型必填默认值描述
xdouble指数值

返回值: 自然常数 e 的 x 次幂,即 e^x

使用场景: 计算指数增长、衰减等,用于科学计算、金融计算等

注意事项:

  • 当结果溢出时,会产生溢出错误
  • x 为负无穷大时,返回 0

示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <math.h>

int main(void) {
double x = 1.0;
printf("exp(%f) = %f\n", x, exp(x)); // 输出: exp(1.000000) = 2.718282

return 0;
}
3.7 log() - 自然对数函数

函数原型:

1
double log(double x);

参数说明:

参数类型必填默认值描述
xdouble要求对数的值,必须为正数

返回值: x 的自然对数(以 e 为底)

使用场景: 计算自然对数,用于科学计算、数学变换等

注意事项:

  • 如果 x 为负数或 0,会产生域错误
  • 如果 x 为 1,返回 0
  • 如果 x 为正无穷大,返回正无穷大

示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <math.h>

int main(void) {
double x = M_E; // M_E 是 math.h 中定义的自然常数 e
printf("log(%f) = %f\n", x, log(x)); // 输出: log(2.718282) = 1.000000

return 0;
}
3.8 log10() - 常用对数函数

函数原型:

1
double log10(double x);

参数说明:

参数类型必填默认值描述
xdouble要求对数的值,必须为正数

返回值: x 的常用对数(以 10 为底)

使用场景: 计算以 10 为底的对数,用于科学计数法、工程计算等

注意事项:

  • 如果 x 为负数或 0,会产生域错误
  • 如果 x 为 10 的整数次幂,返回整数

示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <math.h>

int main(void) {
double x = 100.0;
printf("log10(%f) = %f\n", x, log10(x)); // 输出: log10(100.000000) = 2.000000

return 0;
}
3.9 abs() - 整数绝对值函数

函数原型:

1
int abs(int x);

参数说明:

参数类型必填默认值描述
xint要求绝对值的整数

返回值: x 的绝对值

使用场景: 计算整数的绝对值,用于数学计算、比较操作等

注意事项:

  • 对于 int 类型的最小值(如 -2147483648),由于溢出,结果可能未定义
  • 对于其他整数类型,应使用相应的函数:labs()(长整型)、llabs()(长 long 整型)

示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h> // abs() 也可以在 stdlib.h 中找到

int main(void) {
int x = -5;
printf("abs(%d) = %d\n", x, abs(x)); // 输出: abs(-5) = 5

return 0;
}
3.10 fabs() - 浮点数绝对值函数

函数原型:

1
double fabs(double x);

参数说明:

参数类型必填默认值描述
xdouble要求绝对值的浮点数

返回值: x 的绝对值

使用场景: 计算浮点数的绝对值,用于数学计算、比较操作等

注意事项:

  • 对于 float 类型,应使用 fabsf() 函数
  • 对于 long double 类型,应使用 fabsl() 函数

示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <math.h>

int main(void) {
double x = -3.14;
printf("fabs(%f) = %f\n", x, fabs(x)); // 输出: fabs(-3.140000) = 3.140000

return 0;
}
3.11 ceil() - 向上取整函数

函数原型:

1
double ceil(double x);

参数说明:

参数类型必填默认值描述
xdouble要取整的值

返回值: 不小于 x 的最小整数

使用场景: 计算上界值,用于资源分配、容量计算等

注意事项:

  • 如果 x 已经是整数,返回相同的值
  • 对于 float 类型,应使用 ceilf() 函数
  • 对于 long double 类型,应使用 ceill() 函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <math.h>

int main(void) {
double x = 3.14;
printf("ceil(%f) = %f\n", x, ceil(x)); // 输出: ceil(3.140000) = 4.000000

x = -3.14;
printf("ceil(%f) = %f\n", x, ceil(x)); // 输出: ceil(-3.140000) = -3.000000

return 0;
}
3.12 floor() - 向下取整函数

函数原型:

1
double floor(double x);

参数说明:

参数类型必填默认值描述
xdouble要取整的值

返回值: 不大于 x 的最大整数

使用场景: 计算下界值,用于整数除法、区间划分等

注意事项:

  • 如果 x 已经是整数,返回相同的值
  • 对于 float 类型,应使用 floorf() 函数
  • 对于 long double 类型,应使用 floorl() 函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <math.h>

int main(void) {
double x = 3.14;
printf("floor(%f) = %f\n", x, floor(x)); // 输出: floor(3.140000) = 3.000000

x = -3.14;
printf("floor(%f) = %f\n", x, floor(x)); // 输出: floor(-3.140000) = -4.000000

return 0;
}
3.13 round() - 四舍五入函数

函数原型:

1
double round(double x);

参数说明:

参数类型必填默认值描述
xdouble要四舍五入的值

返回值: 最接近 x 的整数,如果 x 正好在两个整数中间,则返回偶数

使用场景: 进行四舍五入取整,用于数值计算、显示格式化等

注意事项:

  • 对于 float 类型,应使用 roundf() 函数
  • 对于 long double 类型,应使用 roundl() 函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <math.h>

int main(void) {
double x = 3.14;
printf("round(%f) = %f\n", x, round(x)); // 输出: round(3.140000) = 3.000000

x = 3.5;
printf("round(%f) = %f\n", x, round(x)); // 输出: round(3.500000) = 4.000000

x = -3.5;
printf("round(%f) = %f\n", x, round(x)); // 输出: round(-3.500000) = -4.000000

return 0;
}
3.14 rand() - 随机数生成函数

函数原型:

1
int rand(void);

参数说明:

返回值: 范围在 [0, RAND_MAX] 之间的伪随机整数

使用场景: 生成随机数,用于游戏、模拟、随机抽样等

注意事项:

  • rand() 生成的是伪随机数,需要先使用 srand() 初始化种子
  • 默认种子为 1,每次程序运行会生成相同的序列
  • 结果范围依赖于实现,RAND_MAX 至少为 32767

示例:

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

int main(void) {
// 初始化随机种子
srand(time(NULL));

// 生成 5 个随机数
for (int i = 0; i < 5; i++) {
printf("随机数 %d: %d\n", i+1, rand());
}

// 生成 0-99 之间的随机数
printf("0-99 之间的随机数: %d\n", rand() % 100);

return 0;
}
3.15 srand() - 随机数种子初始化函数

函数原型:

1
void srand(unsigned int seed);

参数说明:

参数类型必填默认值描述
seedunsigned int随机数种子值

返回值:

使用场景: 初始化随机数生成器的种子,确保每次运行生成不同的随机数序列

注意事项:

  • 通常使用 time(NULL) 作为种子,因为时间值每次运行都不同
  • 应在程序开始时只调用一次 srand(),多次调用会导致随机数序列质量下降

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
// 使用当前时间作为种子
srand(time(NULL));

// 生成随机数
printf("随机数: %d\n", rand());

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

int main(void)
{
double x = 2.0;
double y = 3.0;

// 数学计算
printf("sin(π/2) = %f\n", sin(M_PI / 2));
printf("sqrt(16) = %f\n", sqrt(16));
printf("pow(2, 3) = %f\n", pow(x, y));
printf("exp(1) = %f\n", exp(1));
printf("log(e) = %f\n", log(exp(1)));

// 取整函数
printf("ceil(3.14) = %f\n", ceil(3.14));
printf("floor(3.14) = %f\n", floor(3.14));
printf("round(3.14) = %f\n", round(3.14));

// 随机数生成
srand(time(NULL));
printf("随机数: %d\n", rand() % 100);

return 0;
}

4. 时间和日期库 (<time.h>)

主要功能: 时间和日期操作

常用函数详细说明:

4.1 time() - 获取当前时间戳

函数原型:

1
time_t time(time_t *timer);

参数说明:

参数类型必填默认值描述
timertime_t *NULL指向存储时间戳的变量的指针,如果为 NULL,则只返回时间戳

返回值: 当前时间戳(从 1970-01-01 00:00:00 UTC 开始经过的秒数),失败返回 (time_t)-1

使用场景: 获取当前系统时间,用于时间计算、时间戳生成等

取值范围限制:

  • timer 参数:可以是 NULL 或指向有效的 time_t 变量的指针
  • 返回值的取值范围:
    • 成功:从 1970-01-01 00:00:00 UTC 开始经过的秒数
    • 失败:(time_t)-1
  • time_t 类型的范围:
    • 通常为 32 位或 64 位整数
    • 32 位 time_t 的最大值对应于 2038-01-19 03:14:07 UTC(2038 年问题)
    • 64 位 time_t 可以表示到约 292 亿年后

注意事项:

  • time_t 类型通常是长整型,但具体实现可能不同
  • 时间戳是基于 UTC 的,不受时区影响
  • 错误处理:
    • 当函数失败时,返回 (time_t)-1,errno 会被设置为相应的错误代码
    • 常见的失败原因:系统时钟不可用
  • 2038 年问题:
    • 使用 32 位 time_t 的系统在 2038-01-19 03:14:07 UTC 后会出现溢出
    • 建议使用 64 位 time_t 的系统或库
  • 精度:
    • time() 函数的精度通常为秒级
    • 对于更高精度的时间测量,应使用 gettimeofday()clock_gettime() 函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <time.h>

int main(void) {
time_t now;

// 方式 1:通过返回值获取
now = time(NULL);
printf("当前时间戳: %lld\n", (long long)now);

// 方式 2:通过参数获取
time(&now);
printf("当前时间戳: %lld\n", (long long)now);

return 0;
}
4.2 ctime() - 转换时间戳为字符串

函数原型:

1
char *ctime(const time_t *timep);

参数说明:

参数类型必填默认值描述
timepconst time_t *指向要转换的时间戳的指针

返回值: 指向格式化时间字符串的指针,格式为 “Wed Jun 30 21:49:08 1993\n”

使用场景: 将时间戳转换为人类可读的字符串格式,用于日志记录、用户界面等

注意事项:

  • 返回的字符串存储在静态内存中,每次调用都会覆盖之前的内容
  • 线程不安全,多线程环境应使用 ctime_r()
  • 字符串包含换行符

示例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <time.h>

int main(void) {
time_t now = time(NULL);
char *time_str = ctime(&now);
printf("当前时间: %s", time_str); // 输出包含换行符

return 0;
}
4.3 localtime() - 转换时间戳为本地时间结构

函数原型:

1
struct tm *localtime(const time_t *timep);

参数说明:

参数类型必填默认值描述
timepconst time_t *指向要转换的时间戳的指针

返回值: 指向 struct tm 结构的指针,包含本地时间信息

使用场景: 将时间戳转换为本地时区的分解时间结构,用于日期时间的各个字段的访问

注意事项:

  • 返回的结构存储在静态内存中,每次调用都会覆盖之前的内容
  • 线程不安全,多线程环境应使用 localtime_r()
  • struct tm 结构包含年、月、日、时、分、秒等字段

示例:

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

int main(void) {
time_t now = time(NULL);
struct tm *local_time = localtime(&now);

printf("年: %d\n", local_time->tm_year + 1900); // tm_year 是从 1900 开始的年数
printf("月: %d\n", local_time->tm_mon + 1); // tm_mon 是从 0 开始的月数
printf("日: %d\n", local_time->tm_mday);
printf("时: %d\n", local_time->tm_hour);
printf("分: %d\n", local_time->tm_min);
printf("秒: %d\n", local_time->tm_sec);
printf("星期: %d\n", local_time->tm_wday); // 0-6,0 是周日
printf("年中第几天: %d\n", local_time->tm_yday); // 0-365

return 0;
}
4.4 gmtime() - 转换时间戳为 UTC 时间结构

函数原型:

1
struct tm *gmtime(const time_t *timep);

参数说明:

参数类型必填默认值描述
timepconst time_t *指向要转换的时间戳的指针

返回值: 指向 struct tm 结构的指针,包含 UTC 时间信息

使用场景: 将时间戳转换为 UTC 时区的分解时间结构,用于跨时区的时间处理

注意事项:

  • 返回的结构存储在静态内存中,每次调用都会覆盖之前的内容
  • 线程不安全,多线程环境应使用 gmtime_r()
  • struct tm 结构包含年、月、日、时、分、秒等字段

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <time.h>

int main(void) {
time_t now = time(NULL);
struct tm *utc_time = gmtime(&now);

printf("UTC 时间 - 年: %d\n", utc_time->tm_year + 1900);
printf("UTC 时间 - 月: %d\n", utc_time->tm_mon + 1);
printf("UTC 时间 - 日: %d\n", utc_time->tm_mday);
printf("UTC 时间 - 时: %d\n", utc_time->tm_hour);
printf("UTC 时间 - 分: %d\n", utc_time->tm_min);
printf("UTC 时间 - 秒: %d\n", utc_time->tm_sec);

return 0;
}
4.5 mktime() - 转换时间结构为时间戳

函数原型:

1
time_t mktime(struct tm *tm);

参数说明:

参数类型必填默认值描述
tmstruct tm *指向包含分解时间信息的 struct tm 结构的指针

返回值: 对应的时间戳,失败返回 (time_t)-1

使用场景: 将分解的时间结构转换回时间戳,用于时间计算、时间比较等

取值范围限制:

  • tm 参数的字段取值范围:
    • tm_sec:0-61(允许闰秒)
    • tm_min:0-59
    • tm_hour:0-23
    • tm_mday:1-31
    • tm_mon:0-11(0 表示一月)
    • tm_year:自 1900 年以来的年数
    • tm_isdst:-1(自动判断)、0(不使用夏令时)、1(使用夏令时)
  • 函数会自动调整超出范围的字段

注意事项:

  • 函数会自动调整 struct tm 中的字段,例如将 13 月调整为下一年的 1 月,将 60 秒调整为 1 分 0 秒等
  • 忽略 tm_wdaytm_yday 字段,根据其他字段计算
  • 基于本地时区进行转换
  • 错误处理:
    • 当函数失败时,返回 (time_t)-1,errno 会被设置为相应的错误代码
    • 常见的失败原因:时间值超出 time_t 类型的范围
  • 夏令时处理:
    • tm_isdst 为 -1 时,函数会尝试自动判断是否为夏令时
    • tm_isdst 为 0 或 1 时,函数会使用指定的值
  • 字段调整:
    • 函数执行后,会更新 tm 指向的结构中的所有字段,包括 tm_wdaytm_yday
  • 范围限制:
    • 转换结果必须在 time_t 类型的范围内
    • 对于 32 位 time_t,有效范围通常是 1901-12-13 20:45:52 UTC 到 2038-01-19 03:14:07 UTC
    • 对于 64 位 time_t,有效范围要大得多

示例:

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

int main(void) {
struct tm tm_time;
time_t timestamp;

// 设置时间为 2023 年 12 月 25 日 10:30:00
tm_time.tm_year = 2023 - 1900; // 从 1900 开始
tm_time.tm_mon = 12 - 1; // 从 0 开始
tm_time.tm_mday = 25;
tm_time.tm_hour = 10;
tm_time.tm_min = 30;
tm_time.tm_sec = 0;
tm_time.tm_isdst = -1; // 自动判断夏令时

timestamp = mktime(&tm_time);
printf("时间戳: %lld\n", (long long)timestamp);
printf("对应的时间: %s", ctime(&timestamp));

return 0;
}
4.6 difftime() - 计算时间差

函数原型:

1
double difftime(time_t time1, time_t time0);

参数说明:

参数类型必填默认值描述
time1time_t结束时间的时间戳
time0time_t开始时间的时间戳

返回值: time1time0 之间的时间差(以秒为单位),即 time1 - time0

使用场景: 计算两个时间点之间的时间差,用于性能测量、计时等

注意事项:

  • 返回值是双精度浮点数,可以表示小数秒
  • 时间差的符号表示时间顺序:正数表示 time1 在 time0 之后,负数表示 time1 在 time0 之前

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <time.h>
#include <unistd.h> // for sleep()

int main(void) {
time_t start, end;
double diff;

start = time(NULL);
printf("开始时间: %s", ctime(&start));

// 模拟耗时操作
sleep(2);

end = time(NULL);
printf("结束时间: %s", ctime(&end));

diff = difftime(end, start);
printf("时间差: %.2f 秒\n", diff);

return 0;
}
4.7 strftime() - 格式化时间字符串

函数原型:

1
size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *tm);

参数说明:

参数类型必填默认值描述
schar *指向存储结果字符串的缓冲区的指针
maxsizesize_t缓冲区的最大大小(包括 null 终止符)
formatconst char *格式化字符串,包含普通字符和格式说明符
tmconst struct tm *指向包含时间信息的 struct tm 结构的指针

返回值: 成功写入缓冲区的字符数(不包括 null 终止符),如果缓冲区太小返回 0

使用场景: 自定义格式的时间字符串,用于日志记录、用户界面、数据存储等

注意事项:

  • 格式化字符串中的格式说明符以 % 开头,如 %Y(年)、%m(月)、%d(日)等
  • 缓冲区必须足够大,能够容纳格式化后的字符串和 null 终止符
  • 支持的格式说明符可能因实现而异

常用格式说明符:

格式描述示例
%Y四位年份2023
%m两位月份 (01-12)12
%d两位日期 (01-31)25
%H24小时制小时 (00-23)14
%M分钟 (00-59)30
%S秒 (00-59)45
%A完整星期名Monday
%a缩写星期名Mon
%B完整月份名December
%b缩写月份名Dec
%x本地日期格式12/25/23
%X本地时间格式14:30:45

示例:

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

int main(void) {
time_t now = time(NULL);
struct tm *local_time = localtime(&now);
char buffer[100];

// 格式化为 YYYY-MM-DD HH:MM:SS
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", local_time);
printf("格式化时间: %s\n", buffer);

// 格式化为更详细的格式
strftime(buffer, sizeof(buffer), "%A, %B %d, %Y %H:%M:%S", local_time);
printf("详细格式: %s\n", buffer);

// 格式化为日期部分
strftime(buffer, sizeof(buffer), "%Y-%m-%d", local_time);
printf("日期: %s\n", buffer);

// 格式化为时间部分
strftime(buffer, sizeof(buffer), "%H:%M:%S", local_time);
printf("时间: %s\n", buffer);

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

int main(void)
{
time_t now;
struct tm *local_time;
char buffer[100];

// 获取当前时间
now = time(NULL);
printf("当前时间戳: %lld\n", (long long)now);

// 转换为字符串
printf("当前时间: %s", ctime(&now));

// 转换为本地时间结构
local_time = localtime(&now);

// 格式化时间
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", local_time);
printf("格式化时间: %s\n", buffer);

// 计算时间差
time_t later = now + 3600; // 1小时后
printf("时间差: %.2f 小时\n", difftime(later, now) / 3600);

return 0;
}

5. 内存分配库 (<stdlib.h>)

主要功能: 内存管理、程序控制

常用函数详细说明:

5.1 malloc() - 分配内存

函数原型:

1
void *malloc(size_t size);

参数说明:

参数类型必填默认值描述
sizesize_t要分配的内存大小(以字节为单位)

返回值: 指向分配的内存块的指针,失败返回 NULL

使用场景: 动态分配内存,用于不确定大小的数据结构、数组等

取值范围限制:

  • size 参数的取值范围:0 到 SIZE_MAX
  • SIZE_MAX 是系统定义的 size_t 类型的最大值
  • size 为 0 时,行为取决于实现:
    • 可能返回 NULL
    • 可能返回一个指向大小为 0 的内存块的指针,该指针后续应通过 free() 释放

注意事项:

  • 分配的内存未初始化,内容是不确定的
  • 应检查返回值是否为 NULL,以处理内存分配失败的情况
  • 分配的内存使用完毕后必须使用 free() 释放,否则会导致内存泄漏
  • 内存对齐:
    • 返回的指针保证对于任何内置类型都对齐
    • 通常对齐到 8 字节或 16 字节边界
  • 内存分配失败的原因:
    • 系统内存不足
    • 进程的内存使用达到 RLIMIT_AS 限制
    • 请求的内存大小超过系统可分配的最大值
  • 内存碎片:频繁的 malloc() 和 free() 可能导致内存碎片
  • 性能考虑:
    • 对于小内存分配,可能会分配比请求更大的内存块
    • 对于大内存分配,可能会使用不同的内存分配策略
  • 安全考虑:
    • 避免缓冲区溢出
    • 避免使用已释放的内存
    • 避免双重释放

示例:

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 <stdlib.h>

int main(void) {
int *array;
int size = 5;

// 分配内存
array = (int *)malloc(size * sizeof(int));
if (array == NULL) {
perror("内存分配失败");
return 1;
}

// 使用内存
for (int i = 0; i < size; i++) {
array[i] = i * 10;
}

// 打印数组
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
printf("\n");

// 释放内存
free(array);

return 0;
}
5.2 calloc() - 分配并初始化内存

函数原型:

1
void *calloc(size_t nmemb, size_t size);

参数说明:

参数类型必填默认值描述
nmembsize_t要分配的元素数量
sizesize_t每个元素的大小(以字节为单位)

返回值: 指向分配的内存块的指针,失败返回 NULL

使用场景: 动态分配内存并初始化为零,用于数组、结构体等需要初始化为零的数据

注意事项:

  • 分配的内存会被初始化为零
  • 总分配大小为 nmemb * size 字节
  • 应检查返回值是否为 NULL,以处理内存分配失败的情况
  • 分配的内存使用完毕后必须使用 free() 释放

示例:

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

int main(void) {
int *array;
int size = 5;

// 分配并初始化内存
array = (int *)calloc(size, sizeof(int));
if (array == NULL) {
perror("内存分配失败");
return 1;
}

// 打印数组(应该全为 0)
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
printf("\n");

// 使用内存
for (int i = 0; i < size; i++) {
array[i] = i * 10;
}

// 打印数组
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
printf("\n");

// 释放内存
free(array);

return 0;
}
5.3 realloc() - 重新分配内存

函数原型:

1
void *realloc(void *ptr, size_t size);

参数说明:

参数类型必填默认值描述
ptrvoid *指向之前分配的内存块的指针,如果为 NULL,则等同于 malloc(size)
sizesize_t新的内存大小(以字节为单位)

返回值: 指向重新分配的内存块的指针,失败返回 NULL(原内存块保持不变)

使用场景: 调整已分配内存的大小,用于动态增长或缩小数据结构

取值范围限制:

  • ptr 参数的要求:
    • 必须是之前通过 malloc(), calloc(), realloc() 分配的内存块的指针
    • 或者是 NULL
    • 不能是已释放的内存块的指针
    • 不能是栈内存或全局内存的指针
  • size 参数的取值范围:0 到 SIZE_MAX
  • SIZE_MAX 是系统定义的 size_t 类型的最大值

注意事项:

  • 如果 ptr 为 NULL,等同于 malloc(size)
  • 如果 size 为 0,等同于 free(ptr)
  • 新内存块的内容会被复制到新位置,直到新旧大小的较小值
  • 应检查返回值是否为 NULL,以处理内存分配失败的情况
  • 分配的内存使用完毕后必须使用 free() 释放
  • 内存重分配的行为:
    • 如果新大小小于等于旧大小,可能在原地调整大小
    • 如果新大小大于旧大小,可能需要分配新的内存块并复制数据
    • 复制数据的时间复杂度为 O(n),其中 n 是新旧大小的较小值
  • 内存分配失败的处理:
    • 如果 realloc() 失败,原内存块保持不变
    • 因此在调用 realloc() 时,应使用临时指针接收返回值,避免内存泄漏
  • 安全考虑:
    • 避免使用已释放的指针
    • 避免双重释放
    • 确保新大小足够容纳所有数据
  • 性能考虑:
    • 频繁的 realloc() 可能导致内存碎片
    • 对于需要频繁增长的数据结构,考虑使用指数增长策略

示例:

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>

int main(void) {
int *array;
int size = 5;

// 分配内存
array = (int *)malloc(size * sizeof(int));
if (array == NULL) {
perror("内存分配失败");
return 1;
}

// 初始化数组
for (int i = 0; i < size; i++) {
array[i] = i * 10;
}

// 打印数组
printf("原始数组: ");
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
printf("\n");

// 重新分配内存
size = 10;
array = (int *)realloc(array, size * sizeof(int));
if (array == NULL) {
perror("内存重新分配失败");
return 1;
}

// 初始化新分配的部分
for (int i = 5; i < size; i++) {
array[i] = i * 10;
}

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

// 释放内存
free(array);

return 0;
}
5.4 free() - 释放内存

函数原型:

1
void free(void *ptr);

参数说明:

参数类型必填默认值描述
ptrvoid *指向要释放的内存块的指针,如果为 NULL,则无操作

返回值:

使用场景: 释放之前通过 malloc(), calloc(), realloc() 分配的内存,避免内存泄漏

注意事项:

  • 只能释放通过动态内存分配函数分配的内存
  • 释放后,指针变为悬空指针,不应再使用
  • 不要重复释放同一块内存,否则会导致未定义行为
  • 如果 ptr 为 NULL,函数无操作

示例:

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

int main(void) {
int *array;
int size = 5;

// 分配内存
array = (int *)malloc(size * sizeof(int));
if (array == NULL) {
perror("内存分配失败");
return 1;
}

// 使用内存
for (int i = 0; i < size; i++) {
array[i] = i * 10;
}

// 打印数组
for (int i = 0; i < size; i++) {
printf("%d ", array[i]);
}
printf("\n");

// 释放内存
free(array);
// 注意:此时 array 变为悬空指针,不应再使用

return 0;
}
5.5 exit() - 终止程序

函数原型:

1
void exit(int status);

参数说明:

参数类型必填默认值描述
statusint程序退出状态码,0 表示正常退出,非 0 表示异常退出

返回值:

使用场景: 立即终止程序执行,用于错误处理、正常程序结束等

注意事项:

  • 会执行所有已注册的 atexit() 函数
  • 会刷新所有打开的文件流
  • 会关闭所有打开的文件
  • 不会执行 return 语句后的代码

示例:

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 <stdlib.h>

void cleanup(void) {
printf("执行清理操作\n");
}

int main(void) {
// 注册退出函数
atexit(cleanup);

printf("程序开始\n");

// 模拟错误
int error = 1;
if (error) {
fprintf(stderr, "发生错误,程序将退出\n");
exit(EXIT_FAILURE); // EXIT_FAILURE 通常定义为 1
}

printf("程序正常结束\n");
exit(EXIT_SUCCESS); // EXIT_SUCCESS 通常定义为 0

// 以下代码不会执行
printf("这行代码不会执行\n");

return 0;
}
5.6 abort() - 异常终止程序

函数原型:

1
void abort(void);

参数说明:

返回值:

使用场景: 异常终止程序,用于严重错误、不可恢复的情况

注意事项:

  • 不会执行 atexit() 注册的函数
  • 会产生 SIGABRT 信号
  • 可能会生成核心转储(core dump)
  • 通常用于调试目的

示例:

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

void cleanup(void) {
printf("执行清理操作\n");
}

int main(void) {
// 注册退出函数
atexit(cleanup);

printf("程序开始\n");

// 模拟严重错误
int critical_error = 1;
if (critical_error) {
fprintf(stderr, "发生严重错误,程序将异常终止\n");
abort(); // 异常终止,不会执行 cleanup()
}

printf("程序正常结束\n");
return 0;
}
5.7 system() - 执行系统命令

函数原型:

1
int system(const char *command);

参数说明:

参数类型必填默认值描述
commandconst char *要执行的系统命令字符串,如果为 NULL,则检查系统是否支持命令处理器

返回值: 命令执行的返回状态,具体值取决于系统

使用场景: 执行系统命令,用于调用外部程序、系统操作等

注意事项:

  • 存在安全风险,特别是当命令字符串包含用户输入时,可能导致命令注入攻击
  • 执行效率较低,应避免频繁调用
  • 依赖于系统环境,可能在不同平台上有差异
  • 应检查返回值,以处理命令执行失败的情况

示例:

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

int main(void) {
int status;

// 执行 dir 命令(Windows)或 ls 命令(Unix/Linux)
#ifdef _WIN32
status = system("dir");
#else
status = system("ls -l");
#endif

if (status != 0) {
fprintf(stderr, "命令执行失败\n");
return 1;
}

printf("命令执行成功\n");

return 0;
}
5.8 atoi() - 字符串转换为整数

函数原型:

1
int atoi(const char *nptr);

参数说明:

参数类型必填默认值描述
nptrconst char *指向要转换的字符串的指针

返回值: 转换后的整数

使用场景: 将字符串转换为整数,用于命令行参数处理、配置文件解析等

注意事项:

  • 跳过字符串开头的空白字符
  • 从第一个非空白字符开始转换,直到遇到非数字字符
  • 如果字符串不能转换为整数,返回 0
  • 不检查溢出,可能导致未定义行为

示例:

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

int main(void) {
char str1[] = "12345";
char str2[] = " -6789";
char str3[] = "123abc";
char str4[] = "abc123";

int num1 = atoi(str1);
int num2 = atoi(str2);
int num3 = atoi(str3);
int num4 = atoi(str4);

printf("atoi(\"%s\") = %d\n", str1, num1); // 输出: atoi("12345") = 12345
printf("atoi(\"%s\") = %d\n", str2, num2); // 输出: atoi(" -6789") = -6789
printf("atoi(\"%s\") = %d\n", str3, num3); // 输出: atoi("123abc") = 123
printf("atoi(\"%s\") = %d\n", str4, num4); // 输出: atoi("abc123") = 0

return 0;
}
5.9 atol() - 字符串转换为长整数

函数原型:

1
long atol(const char *nptr);

参数说明:

参数类型必填默认值描述
nptrconst char *指向要转换的字符串的指针

返回值: 转换后的长整数

使用场景: 将字符串转换为长整数,用于处理较大的整数值

注意事项:

  • 跳过字符串开头的空白字符
  • 从第一个非空白字符开始转换,直到遇到非数字字符
  • 如果字符串不能转换为长整数,返回 0
  • 不检查溢出,可能导致未定义行为

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int main(void) {
char str1[] = "1234567890";
char str2[] = " -987654321";

long num1 = atol(str1);
long num2 = atol(str2);

printf("atol(\"%s\") = %ld\n", str1, num1); // 输出: atol("1234567890") = 1234567890
printf("atol(\"%s\") = %ld\n", str2, num2); // 输出: atol(" -987654321") = -987654321

return 0;
}
5.10 atof() - 字符串转换为双精度浮点数

函数原型:

1
double atof(const char *nptr);

参数说明:

参数类型必填默认值描述
nptrconst char *指向要转换的字符串的指针

返回值: 转换后的双精度浮点数

使用场景: 将字符串转换为浮点数,用于处理小数、科学计数法等

注意事项:

  • 跳过字符串开头的空白字符
  • 从第一个非空白字符开始转换,直到遇到非数字、小数点或指数符号
  • 如果字符串不能转换为浮点数,返回 0.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
#include <stdio.h>
#include <stdlib.h>

int main(void) {
char str1[] = "123.45";
char str2[] = " -67.89";
char str3[] = "123e4";
char str4[] = "1.23E-2";
char str5[] = "abc123";

double num1 = atof(str1);
double num2 = atof(str2);
double num3 = atof(str3);
double num4 = atof(str4);
double num5 = atof(str5);

printf("atof(\"%s\") = %f\n", str1, num1); // 输出: atof("123.45") = 123.450000
printf("atof(\"%s\") = %f\n", str2, num2); // 输出: atof(" -67.89") = -67.890000
printf("atof(\"%s\") = %f\n", str3, num3); // 输出: atof("123e4") = 1230000.000000
printf("atof(\"%s\") = %f\n", str4, num4); // 输出: atof("1.23E-2") = 0.012300
printf("atof(\"%s\") = %f\n", str5, num5); // 输出: atof("abc123") = 0.000000

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
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int *array;
int size = 5;

// 分配内存
array = (int *)malloc(size * sizeof(int));
if (array == NULL)
{
perror("内存分配失败");
return 1;
}

// 初始化数组
for (int i = 0; i < size; i++)
{
array[i] = i * 10;
}

// 打印数组
printf("数组内容: ");
for (int i = 0; i < size; i++)
{
printf("%d ", array[i]);
}
printf("\n");

// 重新分配内存
size = 10;
array = (int *)realloc(array, size * sizeof(int));
if (array == NULL)
{
perror("内存重新分配失败");
return 1;
}

// 填充新元素
for (int i = 5; i < size; i++)
{
array[i] = i * 10;
}

// 打印数组
printf("重新分配后数组内容: ");
for (int i = 0; i < size; i++)
{
printf("%d ", array[i]);
}
printf("\n");

// 释放内存
free(array);

// 字符串转换为数值
char str[] = "12345";
int num = atoi(str);
printf("字符串 '%s' 转换为整数: %d\n", str, num);

return 0;
}

6. 进程控制库 (<unistd.h>)(Unix/Linux)

主要功能: 进程控制、系统调用

常用函数详细说明:

6.1 fork() - 创建子进程

函数原型:

1
pid_t fork(void);

参数说明:

返回值:

  • 成功:在父进程中返回子进程的 PID,在子进程中返回 0
  • 失败:返回 -1

使用场景: 创建新的子进程,用于并行处理任务、执行不同的程序等

取值范围限制:

  • 子进程的 PID 是一个非负整数,范围通常在 1 到 PID_MAX 之间
  • PID_MAX 的值取决于系统实现,通常为 32768 或更高

注意事项:

  • 子进程是父进程的副本,包括内存空间、文件描述符等
  • 父子进程的执行顺序是不确定的,取决于操作系统的调度
  • 子进程中应调用 exec 系列函数执行新程序,或在完成任务后退出
  • 父进程应调用 wait 系列函数等待子进程结束,避免产生僵尸进程
  • fork() 失败的常见原因:
    • 系统进程数达到上限
    • 内存不足
    • 非特权进程尝试创建进程数超过 RLIMIT_NPROC 限制
  • 子进程继承父进程的:
    • 内存空间(但有写时复制机制)
    • 文件描述符
    • 信号处理设置(除了 SIGCHLD 的处理方式)
    • 当前工作目录
    • 环境变量
    • 打开的文件状态
  • 子进程不继承父进程的:
    • PID
    • 父进程 ID
    • 资源使用统计
    • 未处理的信号
    • 定时器设置
    • 文件锁

示例:

1
2
3
4
5
6
7
8
9
10
11
pid_t pid = fork();
if (pid < 0) {
perror("fork 失败");
return 1;
} else if (pid == 0) {
// 子进程代码
printf("子进程 ID: %d\n", getpid());
} else {
// 父进程代码
printf("父进程 ID: %d, 子进程 ID: %d\n", getpid(), pid);
}
6.2 execvp() - 执行新程序

函数原型:

1
int execvp(const char *file, char *const argv[]);

参数说明:

参数类型必填默认值描述
fileconst char *要执行的程序文件名,如果包含路径则直接使用,否则在 PATH 环境变量中查找
argvchar *const []命令行参数数组,以 NULL 结尾

返回值: 成功不返回,失败返回 -1

使用场景: 在当前进程中执行新的程序,替换当前进程的代码和数据

注意事项:

  • 如果执行成功,函数不会返回,当前进程的代码会被新程序替换
  • 如果执行失败,函数返回 -1,应检查错误原因
  • argv 数组必须以 NULL 结尾
  • 新程序会继承当前进程的文件描述符、信号处理等

示例:

1
2
3
4
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
// 如果 execvp 返回,说明执行失败
perror("execvp 失败");
6.3 wait() - 等待子进程结束

函数原型:

1
pid_t wait(int *status);

参数说明:

参数类型必填默认值描述
statusint *NULL指向存储子进程退出状态的整数的指针,如果为 NULL,则不存储退出状态

返回值:

  • 成功:返回结束的子进程的 PID
  • 失败:返回 -1

使用场景: 父进程等待子进程结束,避免产生僵尸进程

取值范围限制:

  • status 指针指向的整数用于存储子进程的退出状态,其值的解释由系统定义
  • 退出状态的正常范围是 0 到 255

注意事项:

  • 函数会阻塞,直到有子进程结束
  • 如果 status 不为 NULL,可以使用以下宏来解析退出状态:
    • WIFEXITED(status):如果子进程正常退出,返回非零值
    • WEXITSTATUS(status):获取子进程的退出状态码(0-255)
    • WIFSIGNALED(status):如果子进程被信号终止,返回非零值
    • WTERMSIG(status):获取终止子进程的信号编号
    • WIFSTOPPED(status):如果子进程被暂停,返回非零值(仅用于 waitpid())
    • WSTOPSIG(status):获取暂停子进程的信号编号(仅用于 waitpid())
  • 如果没有子进程,函数会失败并设置 errno 为 ECHILD
  • 如果函数被信号中断,会返回 -1 并设置 errno 为 EINTR
  • 调用 wait() 会清理僵尸进程,释放其资源
  • 对于多个子进程,wait() 会按任意顺序等待它们结束

示例:

1
2
3
4
5
6
7
8
9
10
11
int status;
pid_t pid = wait(&status);
if (pid == -1) {
perror("wait 失败");
return 1;
}
if (WIFEXITED(status)) {
printf("子进程 %d 正常退出,退出状态: %d\n", pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程 %d 被信号终止,信号编号: %d\n", pid, WTERMSIG(status));
}
6.4 waitpid() - 等待指定子进程结束

函数原型:

1
pid_t waitpid(pid_t pid, int *status, int options);

参数说明:

参数类型必填默认值描述
pidpid_t要等待的子进程 PID,-1 表示等待任意子进程
statusint *NULL指向存储子进程退出状态的整数的指针
optionsint0控制等待行为的选项,如 WNOHANG(非阻塞)、WUNTRACED(等待停止的进程)等

返回值:

  • 成功:返回结束的子进程的 PID
  • 失败:返回 -1
  • 如果使用 WNOHANG 且没有子进程结束,返回 0

使用场景: 等待指定的子进程结束,或在非阻塞模式下检查子进程状态

注意事项:

  • pid 参数可以指定具体的子进程 PID,或使用特殊值:
    • pid > 0:等待 PID 为 pid 的子进程
    • pid == -1:等待任意子进程,等同于 wait()
    • pid == 0:等待同组的任意子进程
    • pid < -1:等待组 ID 为 -pid 的任意子进程
  • options 参数可以是多个选项的按位或

示例:

1
2
3
4
5
6
7
8
9
10
int status;
// 非阻塞等待任意子进程
pid_t pid = waitpid(-1, &status, WNOHANG);
if (pid == 0) {
printf("没有子进程结束\n");
} else if (pid > 0) {
printf("子进程 %d 结束\n", pid);
} else if (pid == -1) {
perror("waitpid 失败");
}
6.5 getpid() - 获取进程 ID

函数原型:

1
pid_t getpid(void);

参数说明:

返回值: 当前进程的 PID

使用场景: 获取当前进程的唯一标识符,用于日志记录、进程间通信等

注意事项: 无特殊注意事项

示例:

1
printf("当前进程 ID: %d\n", getpid());
6.6 getppid() - 获取父进程 ID

函数原型:

1
pid_t getppid(void);

参数说明:

返回值: 当前进程的父进程 PID

使用场景: 获取父进程的标识符,用于进程间通信、监控等

注意事项:

  • 如果父进程已经结束,当前进程会被 init 进程(PID 为 1)收养

示例:

1
printf("父进程 ID: %d\n", getppid());
6.7 sleep() - 进程休眠

函数原型:

1
unsigned int sleep(unsigned int seconds);

参数说明:

参数类型必填默认值描述
secondsunsigned int要休眠的秒数

返回值: 实际休眠的秒数,如果被信号中断,返回剩余的秒数

使用场景: 让进程暂停执行一段时间,用于定时任务、速率限制等

注意事项:

  • 函数可能会被信号中断,返回剩余的休眠时间
  • 实际休眠时间可能会比指定的时间长,取决于系统的调度

示例:

1
2
3
4
5
6
7
printf("开始休眠 5 秒...\n");
unsigned int remaining = sleep(5);
if (remaining > 0) {
printf("被信号中断,剩余 %u 秒\n", remaining);
} else {
printf("休眠完成\n");
}
6.8 getcwd() - 获取当前工作目录

函数原型:

1
char *getcwd(char *buf, size_t size);

参数说明:

参数类型必填默认值描述
bufchar *指向存储当前工作目录路径的缓冲区的指针
sizesize_t缓冲区的大小(以字节为单位)

返回值:

  • 成功:返回 buf,指向存储当前工作目录路径的缓冲区
  • 失败:返回 NULL

使用场景: 获取当前进程的工作目录路径,用于文件操作、日志记录等

注意事项:

  • 缓冲区必须足够大,能够容纳当前工作目录的路径和 null 终止符
  • 如果缓冲区太小,函数会失败并设置 errno 为 ERANGE
  • 可以使用 NULL 作为 buf,此时函数会自动分配内存,但需要在使用后调用 free() 释放

示例:

1
2
3
4
5
6
char buf[1024];
if (getcwd(buf, sizeof(buf)) != NULL) {
printf("当前工作目录: %s\n", buf);
} else {
perror("getcwd 失败");
}
6.9 chdir() - 更改工作目录

函数原型:

1
int chdir(const char *path);

参数说明:

参数类型必填默认值描述
pathconst char *要更改为的新工作目录的路径

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 更改当前进程的工作目录,用于文件操作、访问不同目录下的资源等

注意事项:

  • path 可以是绝对路径或相对路径
  • 如果指定的目录不存在或没有权限,函数会失败
  • 工作目录的更改只影响当前进程和其子进程,不会影响父进程

示例:

1
2
3
4
5
if (chdir("/home/user") == 0) {
printf("工作目录已更改为 /home/user\n");
} else {
perror("chdir 失败");
}
6.10 pipe() - 创建管道

函数原型:

1
int pipe(int pipefd[2]);

参数说明:

参数类型必填默认值描述
pipefdint[2]存储管道文件描述符的数组,pipefd[0] 是读端,pipefd[1] 是写端

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 创建管道,用于进程间通信,特别是父子进程之间

取值范围限制:

  • pipefd 数组必须至少有 2 个元素
  • 管道的容量通常在 4KB 到 64KB 之间,具体取决于系统实现

注意事项:

  • 管道是单向的,数据从写端流向读端
  • 管道的容量有限,当管道满时,写操作会阻塞
  • 当所有写端关闭时,读操作会返回 EOF
  • 应在不需要的文件描述符时关闭它们,避免资源泄漏
  • 管道的关闭顺序很重要:
    • 父进程在写入数据后应关闭写端
    • 子进程在读取数据后应关闭读端
    • 否则可能导致读操作阻塞
  • 管道只能用于具有亲缘关系的进程之间通信(如父子进程)
  • 对于无亲缘关系的进程间通信,应使用命名管道(FIFO)或套接字
  • 管道的错误处理:
    • 当管道的读端被关闭后,向写端写入数据会产生 SIGPIPE 信号
    • 如果忽略该信号,写操作会返回 -1 并设置 errno 为 EPIPE
  • 管道的性能考虑:
    • 管道是基于内核缓冲区的,数据在内核中复制,性能较好
    • 但对于大量数据传输,可能会受到缓冲区大小的限制

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe 失败");
return 1;
}

pid_t pid = fork();
if (pid == 0) {
// 子进程:读取数据
close(pipefd[1]); // 关闭写端
char buf[256];
ssize_t n = read(pipefd[0], buf, sizeof(buf));
if (n > 0) {
printf("子进程读取到: %.*s\n", (int)n, buf);
}
close(pipefd[0]); // 关闭读端
} else {
// 父进程:写入数据
close(pipefd[0]); // 关闭读端
const char *msg = "Hello from parent!";
write(pipefd[1], msg, strlen(msg));
close(pipefd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
}

使用示例:

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

int main(void)
{
pid_t pid;
int status;

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

if (pid < 0)
{
// 错误
perror("fork 失败");
return 1;
}
else if (pid == 0)
{
// 子进程
printf("子进程 ID: %d\n", getpid());
printf("子进程:父进程 ID: %d\n", getppid());

// 执行新程序
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);

// 如果 exec 失败
perror("exec 失败");
return 1;
}
else
{
// 父进程
printf("父进程 ID: %d\n", getpid());
printf("父进程:子进程 ID: %d\n", pid);

// 等待子进程结束
wait(&status);
printf("子进程已结束\n");
}

return 0;
}

7. 网络编程库 (<sys/socket.h>)(Unix/Linux)

主要功能: 网络通信

常用函数详细说明:

7.1 socket() - 创建套接字

函数原型:

1
int socket(int domain, int type, int protocol);

参数说明:

参数类型必填默认值描述
domainint套接字域,指定通信协议族,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地套接字)等
typeint套接字类型,如 SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW(原始套接字)等
protocolint0协议类型,通常为 0,表示使用默认协议

返回值:

  • 成功:返回套接字文件描述符
  • 失败:返回 -1

使用场景: 创建网络通信的端点,用于后续的网络操作

取值范围限制:

  • domain 参数的常见值:
    • AF_INET:IPv4 协议族
    • AF_INET6:IPv6 协议族
    • AF_UNIX(或 AF_LOCAL):本地套接字
    • AF_PACKET:链路层套接字
  • type 参数的常见值:
    • SOCK_STREAM:面向连接的可靠流(TCP)
    • SOCK_DGRAM:无连接的数据包(UDP)
    • SOCK_RAW:原始套接字
    • SOCK_SEQPACKET:面向连接的顺序数据包
  • protocol 参数通常为 0,表示使用默认协议:
    • 对于 AF_INET + SOCK_STREAM,默认协议是 IPPROTO_TCP
    • 对于 AF_INET + SOCK_DGRAM,默认协议是 IPPROTO_UDP

注意事项:

  • domain 参数决定了套接字的地址格式,如 AF_INET 使用 IPv4 地址格式
  • type 参数决定了套接字的通信方式,如 SOCK_STREAM 提供可靠的、面向连接的通信
  • protocol 参数通常为 0,让系统根据 domaintype 选择默认协议
  • socket() 失败的常见原因:
    • 不支持的 domaintype
    • 内存不足
    • 权限不足(如创建原始套接字需要特权)
  • 套接字文件描述符是一个非负整数,应在使用完毕后调用 close() 关闭
  • 对于原始套接字,需要 root 权限或 CAP_NET_RAW 能力

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建 TCP 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket 失败");
return 1;
}

// 创建 UDP 套接字
int udp_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_sockfd == -1) {
perror("socket 失败");
return 1;
}
7.2 bind() - 绑定地址

函数原型:

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

参数类型必填默认值描述
sockfdint套接字文件描述符
addrconst struct sockaddr *指向包含要绑定的地址信息的结构体的指针,需要根据套接字域进行类型转换
addrlensocklen_t地址结构体的大小(以字节为单位)

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 将套接字绑定到特定的 IP 地址和端口,通常用于服务器端

注意事项:

  • 对于服务器端,必须调用 bind() 来指定监听的地址和端口
  • 对于客户端,通常不需要调用 bind(),系统会自动分配地址和端口
  • 绑定的地址必须是主机上有效的地址,对于 IPv4,可以使用 INADDR_ANY 表示绑定到所有可用接口
  • 端口号必须是未被占用的,且非特权端口(大于 1023)除非以 root 权限运行

示例:

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有接口
addr.sin_port = htons(8080); // 端口号,需要转换为网络字节序

if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind 失败");
close(sockfd);
return 1;
}
7.3 listen() - 监听连接

函数原型:

1
int listen(int sockfd, int backlog);

参数说明:

参数类型必填默认值描述
sockfdint套接字文件描述符
backlogint监听队列的最大长度,指定可以排队的连接请求数

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 将套接字设置为监听模式,准备接受连接请求,仅用于服务器端的 TCP 套接字

注意事项:

  • 仅适用于 SOCK_STREAM 类型的套接字(TCP)
  • backlog 参数指定了连接请求队列的最大长度,超过此长度的连接请求会被拒绝
  • 实际的队列长度可能会被操作系统限制,不一定等于指定的值
  • 调用 listen() 后,套接字变为被动套接字,只能用于接受连接,不能用于发送或接收数据

示例:

1
2
3
4
5
6
if (listen(sockfd, 5) == -1) {  // 最大允许 5 个连接请求排队
perror("listen 失败");
close(sockfd);
return 1;
}
printf("服务器正在监听端口 8080...\n");
7.4 accept() - 接受连接

函数原型:

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明:

参数类型必填默认值描述
sockfdint监听套接字的文件描述符
addrstruct sockaddr *NULL指向存储客户端地址信息的结构体的指针,如果为 NULL,则不存储客户端地址
addrlensocklen_t *NULL指向存储地址结构体大小的变量的指针,传入时为 addr 结构体的大小,返回时为实际的地址大小

返回值:

  • 成功:返回新的套接字文件描述符,用于与客户端通信
  • 失败:返回 -1

使用场景: 接受客户端的连接请求,创建新的套接字用于与客户端通信,仅用于服务器端的 TCP 套接字

注意事项:

  • 函数会阻塞,直到有连接请求到达
  • 返回的新套接字与客户端一一对应,用于后续的通信
  • 原监听套接字仍然存在,继续用于接受其他连接请求
  • 如果 addraddrlen 不为 NULL,函数会填充客户端的地址信息

示例:

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr);

int client_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
if (client_sockfd == -1) {
perror("accept 失败");
close(sockfd);
return 1;
}
printf("客户端已连接,IP: %s, 端口: %d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
7.5 connect() - 连接到服务器

函数原型:

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

参数类型必填默认值描述
sockfdint套接字文件描述符
addrconst struct sockaddr *指向包含服务器地址信息的结构体的指针,需要根据套接字域进行类型转换
addrlensocklen_t地址结构体的大小(以字节为单位)

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 客户端连接到服务器,建立 TCP 连接

注意事项:

  • 对于 TCP 套接字,函数会阻塞直到连接建立或失败
  • 对于 UDP 套接字,函数不会建立真正的连接,只是设置默认的目标地址
  • 地址结构体必须包含有效的服务器 IP 地址和端口号
  • 端口号和 IP 地址需要转换为网络字节序

示例:

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器 IP 地址
server_addr.sin_port = htons(8080); // 服务器端口号

if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("connect 失败");
close(sockfd);
return 1;
}
printf("已连接到服务器\n");
7.6 send() - 发送数据

函数原型:

1
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

参数类型必填默认值描述
sockfdint套接字文件描述符
bufconst void *指向要发送的数据的指针
lensize_t要发送的数据长度(以字节为单位)
flagsint0控制发送行为的标志,通常为 0

返回值:

  • 成功:返回实际发送的字节数
  • 失败:返回 -1

使用场景: 通过套接字发送数据,适用于 TCP 和 UDP 套接字

注意事项:

  • 对于 TCP 套接字,send() 可能只发送部分数据,需要循环发送直到所有数据都被发送
  • 对于 UDP 套接字,send() 要么发送整个数据报,要么失败
  • flags 参数通常为 0,表示正常发送
  • 常见的 flags 值:
    • MSG_OOB:发送带外数据(仅 TCP)
    • MSG_DONTROUTE:不使用路由表查找
    • MSG_NOSIGNAL:发送失败时不产生 SIGPIPE 信号

示例:

1
2
3
4
5
6
7
8
9
10
const char *message = "Hello, Server!";
size_t message_len = strlen(message);

ssize_t bytes_sent = send(client_sockfd, message, message_len, 0);
if (bytes_sent == -1) {
perror("send 失败");
close(client_sockfd);
return 1;
}
printf("已发送 %zd 字节: %s\n", bytes_sent, message);
7.7 recv() - 接收数据

函数原型:

1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

参数类型必填默认值描述
sockfdint套接字文件描述符
bufvoid *指向存储接收数据的缓冲区的指针
lensize_t缓冲区的大小(以字节为单位)
flagsint0控制接收行为的标志,通常为 0

返回值:

  • 成功:返回实际接收的字节数
  • 失败:返回 -1
  • 连接关闭:返回 0(仅 TCP)

使用场景: 通过套接字接收数据,适用于 TCP 和 UDP 套接字

注意事项:

  • 对于 TCP 套接字,recv() 可能只接收部分数据,需要循环接收直到获取完整数据
  • 对于 UDP 套接字,recv() 接收整个数据报,如果数据报大于缓冲区,则会被截断
  • 函数会阻塞,直到有数据到达
  • 常见的 flags 值:
    • MSG_OOB:接收带外数据(仅 TCP)
    • MSG_PEEK:查看数据但不从缓冲区移除
    • MSG_DONTWAIT:非阻塞模式,没有数据时立即返回

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char buffer[1024];
ssize_t bytes_received = recv(client_sockfd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received == -1) {
perror("recv 失败");
close(client_sockfd);
return 1;
} else if (bytes_received == 0) {
printf("客户端已关闭连接\n");
close(client_sockfd);
return 0;
}

buffer[bytes_received] = '\0'; // 添加 null 终止符
printf("接收到 %zd 字节: %s\n", bytes_received, buffer);
7.8 close() - 关闭套接字

函数原型:

1
int close(int fd);

参数说明:

参数类型必填默认值描述
fdint要关闭的文件描述符,包括套接字文件描述符

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 关闭套接字,释放相关资源

注意事项:

  • 关闭套接字后,不能再使用该文件描述符进行通信
  • 对于 TCP 套接字,close() 会发送 FIN 报文,启动四次挥手过程
  • 对于 UDP 套接字,close() 只是释放套接字资源
  • 应在通信结束后及时关闭套接字,避免资源泄漏

示例:

1
2
close(client_sockfd);  // 关闭与客户端的连接
close(sockfd); // 关闭监听套接字
7.9 setsockopt() - 设置套接字选项

函数原型:

1
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数说明:

参数类型必填默认值描述
sockfdint套接字文件描述符
levelint选项所在的协议层,如 SOL_SOCKET(通用套接字选项)、IPPROTO_TCP(TCP 选项)等
optnameint要设置的选项名称,如 SO_REUSEADDR、SO_REUSEPORT 等
optvalconst void *指向包含选项值的缓冲区的指针
optlensocklen_t选项值的大小(以字节为单位)

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 设置套接字的各种选项,用于配置套接字的行为,如地址重用、端口重用、缓冲区大小等

取值范围限制:

  • level 参数的常见值:
    • SOL_SOCKET:通用套接字选项
    • IPPROTO_TCP:TCP 协议选项
    • IPPROTO_IP:IPv4 协议选项
    • IPPROTO_IPV6:IPv6 协议选项
  • optname 参数的取值取决于 level 参数

注意事项:

  • 不同的套接字选项需要不同类型的 optval 参数,应根据选项类型进行正确的类型转换
  • 对于布尔类型的选项,optval 通常是指向整数的指针,非零值表示启用,零表示禁用
  • 对于整数类型的选项,optval 是指向相应整数类型的指针
  • 对于时间类型的选项,optval 通常是指向 struct timeval 结构的指针
  • 常见的套接字选项:
    • SO_REUSEADDR:允许重用本地地址
    • SO_REUSEPORT:允许重用本地端口
    • SO_RCVBUF:设置接收缓冲区大小
    • SO_SNDBUF:设置发送缓冲区大小
    • SO_TIMEOUT:设置接收超时时间
    • TCP_NODELAY:禁用 Nagle 算法,减少小数据包的延迟
  • 套接字选项的设置通常在 bind() 之前进行,以确保选项生效
  • 某些选项只能在特定类型的套接字上设置,如 TCP_NODELAY 只能在 SOCK_STREAM 类型的套接字上设置

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 设置套接字选项,允许重用地址和端口
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
&opt, sizeof(opt)) == -1) {
perror("setsockopt 失败");
close(server_fd);
return 1;
}

// 设置接收超时时间为 5 秒
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
if (setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO,
&timeout, sizeof(timeout)) == -1) {
perror("setsockopt 失败");
close(server_fd);
return 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
66
67
68
69
70
71
72
73
74
75
76
#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(void)
{
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 失败");
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 失败");
exit(EXIT_FAILURE);
}

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

printf("服务器正在监听端口 %d...\n", PORT);

// 接受连接
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("客户端消息: %s\n", buffer);

// 发送消息给客户端
send(new_socket, hello, strlen(hello), 0);
printf("消息已发送\n");

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

return 0;
}

8. 信号处理库 (<signal.h>)

主要功能: 信号处理

常用信号说明:

信号编号描述默认处理方式
SIGINT2中断信号(Ctrl+C)终止进程
SIGTERM15终止信号终止进程
SIGKILL9杀死信号终止进程(不可捕获)
SIGSTOP19停止信号暂停进程(不可捕获)
SIGCONT18继续信号继续进程
SIGSEGV11段错误信号终止进程并产生核心转储
SIGFPE8浮点异常信号终止进程并产生核心转储
SIGABRT6中止信号终止进程并产生核心转储
SIGCHLD17子进程状态改变信号忽略
SIGALRM14闹钟信号终止进程
SIGUSR110用户自定义信号 1终止进程
SIGUSR212用户自定义信号 2终止进程

常用函数详细说明:

8.1 signal() - 设置信号处理函数

函数原型:

1
2
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数说明:

参数类型必填默认值描述
signumint要处理的信号编号,如 SIGINT(中断信号)、SIGTERM(终止信号)等
handlersighandler_t信号处理函数的指针,或以下特殊值:
- SIG_DFL:使用默认处理方式
- SIG_IGN:忽略该信号

返回值:

  • 成功:返回之前的信号处理函数指针
  • 失败:返回 SIG_ERR

使用场景: 设置信号的处理方式,用于捕获和处理各种信号,如用户中断、程序错误等

取值范围限制:

  • signum 参数的取值范围:1 到 NSIG-1
  • NSIG 是系统定义的最大信号编号,通常为 32 或 64
  • 某些信号(如 SIGKILL 和 SIGSTOP)不能被捕获或忽略

注意事项:

  • signal() 函数的行为在不同系统上可能有所不同,建议使用更可移植的 sigaction() 函数
  • 信号处理函数应该保持简短,避免调用可能不安全的函数(如 printf())
  • 某些信号(如 SIGKILL 和 SIGSTOP)不能被捕获或忽略
  • 在信号处理函数中,应尽量只做简单的操作,如设置标志位
  • 信号处理函数的安全性:
    • 应使用 volatile sig_atomic_t 类型的变量进行通信
    • 避免调用不可重入函数(如 malloc()、free()、printf() 等)
    • 避免持有锁
    • 避免进行长时间的计算
  • 信号处理函数的返回值:信号处理函数没有返回值,它的作用是处理信号并返回
  • 信号的重启:某些系统调用可能会被信号中断,需要考虑是否使用 SA_RESTART 标志
  • 信号的嵌套:默认情况下,信号处理期间会阻塞相同的信号,但可能会接收其他信号
  • 推荐使用 sigaction() 函数:它提供了更详细的控制选项,行为更加可预测

示例:

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
// 信号处理函数
void signal_handler(int signum) {
printf("收到信号: %d\n", signum);
if (signum == SIGINT) {
printf("用户按下了 Ctrl+C\n");
}
}

int main(void) {
// 设置信号处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("无法设置 SIGINT 处理函数");
return 1;
}
if (signal(SIGTERM, signal_handler) == SIG_ERR) {
perror("无法设置 SIGTERM 处理函数");
return 1;
}

printf("程序运行中,按 Ctrl+C 测试信号处理...\n");

// 无限循环
while (1) {
printf("正在运行...\n");
sleep(1);
}

return 0;
}
8.2 raise() - 发送信号

函数原型:

1
int raise(int signum);

参数说明:

参数类型必填默认值描述
signumint要发送的信号编号

返回值:

  • 成功:返回 0
  • 失败:返回非 0

使用场景: 向当前进程发送信号,用于测试信号处理函数、触发进程自我终止等

注意事项:

  • raise(signum) 等价于 kill(getpid(), signum)
  • 如果信号被忽略,函数仍然返回成功
  • 如果信号导致进程终止,函数不会返回

示例:

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

void signal_handler(int signum) {
printf("收到信号: %d\n", signum);
}

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

printf("程序开始运行\n");

// 向自身发送 SIGINT 信号
printf("向自身发送 SIGINT 信号...\n");
if (raise(SIGINT) != 0) {
perror("raise 失败");
return 1;
}

printf("程序继续运行\n");

return 0;
}
8.3 kill() - 向进程发送信号

函数原型:

1
int kill(pid_t pid, int signum);

参数说明:

参数类型必填默认值描述
pidpid_t目标进程的 PID,或以下特殊值:
- pid > 0:发送信号给指定 PID 的进程
- pid == 0:发送信号给与当前进程同组的所有进程
- pid == -1:发送信号给所有有权限发送的进程(除了 init 进程)
- pid < -1:发送信号给组 ID 为 -pid 的所有进程
signumint要发送的信号编号

返回值:

  • 成功:返回 0
  • 失败:返回 -1

使用场景: 向指定进程或进程组发送信号,用于进程间通信、控制其他进程等

取值范围限制:

  • pid 参数的取值范围:
    • 正数:有效的进程 ID
    • 0:当前进程组
    • -1:所有有权限的进程
    • 负数(除 -1 外):进程组 ID
  • signum 参数的取值范围:1 到 NSIG-1
  • NSIG 是系统定义的最大信号编号,通常为 32 或 64

注意事项:

  • 需要有向目标进程发送信号的权限,通常只有进程的所有者或 root 用户可以发送信号
  • 权限检查:
    • 普通用户可以向自己的进程发送任何信号
    • 普通用户可以向其他用户的进程发送 SIGCONT 信号
    • 普通用户只能向其他用户的进程发送终止信号,如果目标进程设置了相应的权限
    • root 用户可以向任何进程发送任何信号(除了不可捕获的信号)
  • 某些信号(如 SIGKILL)不能被捕获或忽略,会强制终止进程
  • 发送信号 0 可以用于检查进程是否存在,而不实际发送信号
  • kill() 失败的常见原因:
    • 目标进程不存在(errno = ESRCH)
    • 没有权限发送信号(errno = EPERM)
    • 信号编号无效(errno = EINVAL)
  • 对于进程组操作:
    • pid == 0 时,信号会发送给当前进程组的所有进程,包括调用进程本身
    • pid < -1 时,信号会发送给组 ID 为 -pid 的所有进程
  • 信号的传递是异步的,kill() 成功返回只表示信号已被发送,不表示信号已被处理

示例:

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

int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "用法: %s <pid> <signal>\n", argv[0]);
return 1;
}

pid_t pid = atoi(argv[1]);
int signum = atoi(argv[2]);

printf("向进程 %d 发送信号 %d...\n", pid, signum);
if (kill(pid, signum) == -1) {
perror("kill 失败");
return 1;
}

printf("信号发送成功\n");

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

// 信号处理函数
void signal_handler(int signum)
{
printf("收到信号: %d\n", signum);
if (signum == SIGINT)
{
printf("用户按下了 Ctrl+C\n");
}
}

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

printf("程序运行中,按 Ctrl+C 测试信号处理...\n");

// 无限循环
while (1)
{
printf("正在运行...\n");
sleep(1);
}

return 0;
}

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

  1. 包含正确的头文件 - 每个系统函数库都有对应的头文件,必须正确包含
  2. 检查函数返回值 - 大多数系统函数会返回错误码,必须检查
  3. 了解平台差异 - 某些系统函数在不同平台上可能有差异
  4. 使用 perror()strerror() - 用于打印系统错误信息
  5. 资源管理 - 确保释放所有分配的资源(内存、文件描述符等)
  6. 错误处理 - 实现健壮的错误处理机制
  7. 安全编程 - 避免缓冲区溢出、使用安全的函数变体
  8. 性能考虑 - 对于频繁调用的函数,考虑缓存结果或使用更高效的实现

示例:综合使用多个系统函数库

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

int main(void)
{
// 使用时间库
time_t now = time(NULL);
struct tm *local_time = localtime(&now);
char time_str[100];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", local_time);
printf("当前时间: %s\n", time_str);

// 使用数学库
double radius = 5.0;
double area = M_PI * pow(radius, 2);
printf("半径为 %.2f 的圆面积: %.2f\n", radius, area);

// 使用内存分配库
int *numbers;
int count = 10;
numbers = (int *)malloc(count * sizeof(int));
if (numbers == NULL)
{
perror("内存分配失败");
return 1;
}

// 使用随机数
srand(time(NULL));
printf("随机数列表: ");
for (int i = 0; i < count; i++)
{
numbers[i] = rand() % 100;
printf("%d ", numbers[i]);
}
printf("\n");

// 使用字符串库
char message[200];
sprintf(message, "生成了 %d 个随机数,范围 0-99", count);
printf("消息: %s\n", message);

// 释放内存
free(numbers);

return 0;
}