46、C标准库(二): 常用函数详解、 常用函数详解、 时间处理

好,咱们接着聊C标准库。上一章我们把 <stdio.h><stdlib.h> 啃了一遍,今天继续深入另外三个高频库:<string.h><math.h><time.h>

说实话,这三个库我几乎每天都在用。字符串处理、数学计算、时间记录——任何一个正经项目都绕不开它们。我刚开始写C的时候,总觉得这些函数背一背就行,后来发现光背没用,得知道它们背后有哪些坑。

嗯,咱们一个一个来。

C标准库核心三剑客 <string.h> 字符串处理 strlen / strcpy strcat / strcmp strchr / strstr memcpy / memset memmove / memcmp ⚠️ 注意缓冲区溢出 <math.h> 数学计算 sin / cos / tan sqrt / pow / fabs floor / ceil / round exp / log / log10 fmod / fmax / fmin ⚠️ 链接 -lm 选项 <time.h> 时间处理 time / clock localtime / gmtime strftime / asctime difftime / mktime clock_gettime ⚠️ 32位时间溢出 三者协同:字符串解析 → 数学计算 → 时间记录

一、:字符串操作,别踩缓冲区的地雷

字符串处理是C语言里最基础也最容易出bug的地方。我个人习惯是:能用 strncpy 就不用 strcpy,能用 strncat 就不用 strcat。为什么?你想想看,strcpy 只管复制,不管目标缓冲区够不够大——这就是缓冲区溢出的温床。

1.1 长度与复制

函数作用注意事项
strlen(s)返回字符串长度(不含'\0')O(n)复杂度,不要在循环里反复调用
strcpy(dst, src)复制字符串不检查缓冲区大小,危险
strncpy(dst, src, n)复制最多n个字符如果src长度≥n,不会自动加'\0'
memcpy(dst, src, n)内存复制(不处理重叠)效率高,但源和目标不能重叠
memmove(dst, src, n)安全内存复制(处理重叠)比memcpy稍慢,但更安全

核心原则:永远假设用户输入可能超出预期。用 strncpy 并手动加 '\0',或者直接用 snprintf

// 安全复制示例
char dst[32];
const char* src = "Hello, this is a long string that might overflow!";

// ❌ 危险写法
// strcpy(dst, src);  // 缓冲区溢出!

// ✅ 安全写法
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';  // 手动保证以'\0'结尾

// 或者用 snprintf(我个人更推荐)
snprintf(dst, sizeof(dst), "%s", src);

我曾经在一个嵌入式项目里,因为用了 strcpy 没检查长度,导致堆栈被破坏,程序跑飞了三天才定位到问题。从那以后,我写字符串复制必加长度检查。

1.2 拼接与比较

strcatstrncat 的区别跟复制函数一样——一个不检查长度,一个检查。但 strncat 有个细节:它会从目标字符串的末尾开始追加,并且最多追加 n 个字符,然后自动加 '\0'。

char buf[16] = "Hello";
// strcat(buf, " World!!!");  // 溢出!buf只有16字节

strncat(buf, " World!!!", sizeof(buf) - strlen(buf) - 1);
// 结果: "Hello World" (截断了)

比较函数 strcmpstrncmp 也很常用。返回值是 <00>0,分别对应小于、等于、大于。注意:strcmp 比较的是字典序,不是长度。

1.3 查找与内存操作

  • strchr(s, c):在字符串中查找字符 c 第一次出现的位置,返回指针。没找到返回 NULL。
  • strstr(haystack, needle):查找子串,返回首次匹配的指针。这个我经常用来做简单的协议解析。
  • memset(ptr, value, n):把内存块设置为指定值。初始化结构体时特别好用。
  • memcmp(ptr1, ptr2, n):比较两块内存是否相等。注意它比较的是字节,不是字符串。

小技巧:memset 初始化结构体时,建议用 memset(&st, 0, sizeof(st)),而不是手动逐个字段赋值。这样能避免漏掉某个字段。

二、:数学计算,别忘了链接 -lm

数学库的函数很多,但常用的就那么十几个。我刚开始用 math.h 时犯过一个低级错误:编译时没加 -lm,结果链接报错,我还以为是代码写错了……

注意:在 Linux 上用 gcc 编译时,必须加 -lm 选项链接数学库:gcc main.c -o main -lm。Windows 上 Visual Studio 默认已经链接了,不需要手动加。

2.1 三角函数与基础运算

函数说明注意
sin(x), cos(x), tan(x)三角函数,x 是弧度角度转弧度:rad = deg * M_PI / 180.0
asin(x), acos(x), atan(x)反三角函数返回值是弧度
sqrt(x)平方根x 不能为负数,否则返回 NaN
pow(x, y)计算 x 的 y 次方效率比手动循环乘法低,小整数次幂建议用乘法
fabs(x)浮点数绝对值整数用 abs(),在 <stdlib.h> 里
#include <math.h>
#include <stdio.h>

int main() {
    double angle = 45.0;
    double rad = angle * M_PI / 180.0;  // 角度转弧度
    printf("sin(45°) = %f\n", sin(rad));
    
    // 计算两点距离
    double x1=0, y1=0, x2=3, y2=4;
    double dist = sqrt(pow(x2-x1, 2) + pow(y2-y1, 2));
    printf("距离 = %f\n", dist);  // 5.0
    
    return 0;
}

2.2 取整与余数

取整函数有好几个,容易搞混。我列个表帮你理清楚:

函数行为示例(x=3.7)示例(x=-3.7)
floor(x)向下取整(往负无穷)3.0-4.0
ceil(x)向上取整(往正无穷)4.0-3.0
round(x)四舍五入4.0-4.0
trunc(x)截断取整(往零方向)3.0-3.0
fmod(x, y)浮点数取余fmod(5.5, 2.0) = 1.5

避坑指南:我在做金融计算时,用 floorceil 处理金额,结果发现负数时行为跟预期不一样。后来统一改用 trunc 加手动判断,才搞定。

2.3 指数与对数

  • exp(x):计算 e^x
  • log(x):自然对数(以 e 为底),x 必须大于 0
  • log10(x):常用对数(以 10 为底)
  • log2(x):以 2 为底的对数(C99 标准)

这些函数在科学计算、信号处理里很常见。我记得有一次做音频算法,用 log10 把幅度转成 dB 单位,省了不少事。

三、:时间处理,别被32位溢出坑了

时间处理看似简单,其实坑不少。尤其是 2038 年问题——32 位 time_t 会在 2038 年 1 月 19 日溢出。嗯,虽然现在大部分系统已经用 64 位了,但嵌入式设备上还是有可能遇到。

3.1 获取时间

函数说明返回值
time(NULL)获取当前日历时间(秒数)time_t 类型
clock()获取程序运行的处理器时间clock_t 类型,单位是 CLOCKS_PER_SEC
difftime(t1, t2)计算 t1 - t2 的秒数差double 类型
#include <time.h>
#include <stdio.h>

int main() {
    time_t now = time(NULL);
    printf("当前时间戳: %ld\n", (long)now);
    
    // 测量代码执行时间
    clock_t start = clock();
    // 做一些计算...
    for (int i = 0; i < 1000000; i++);
    clock_t end = clock();
    
    double cpu_time = (double)(end - start) / CLOCKS_PER_SEC;
    printf("耗时: %f 秒\n", cpu_time);
    
    return 0;
}

3.2 时间转换与格式化

time_t 是一个整数,人类看不懂。我们需要把它转成可读的格式:

  • localtime(&t):把时间戳转成本地时间(struct tm
  • gmtime(&t):转成 UTC 时间
  • mktime(&tm):把 struct tm 转回 time_t
  • strftime(buf, size, format, &tm):格式化输出时间字符串
time_t now = time(NULL);
struct tm *local = localtime(&now);

char buf[64];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local);
printf("当前时间: %s\n", buf);
// 输出类似: 2025-01-15 14:30:22

我个人习惯strftime 而不是 asctime,因为 asctime 的格式固定且包含换行符,不够灵活。strftime 可以自定义格式,比如生成日志文件名时特别好用。

3.3 高精度计时

如果 clock() 的精度不够(通常只有毫秒级),可以用 POSIX 的 clock_gettime

#include <time.h>

struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
// ts.tv_sec 是秒,ts.tv_nsec 是纳秒
double elapsed = ts.tv_sec + ts.tv_nsec / 1e9;

注意:clock_gettime 不是标准 C 函数,是 POSIX 扩展。在 Linux 上编译需要加 -lrt(老版本)或直接可用(新版本 glibc)。Windows 上需要用 QueryPerformanceCounter

小结

这三个库是 C 语言日常开发的基石。<string.h> 管字符串和内存,<math.h> 管数学计算,<time.h> 管时间处理。记住几个关键点:

  • 字符串操作永远考虑缓冲区大小,用 strncpysnprintf
  • 数学库编译加 -lm,角度转弧度用 M_PI
  • 时间处理注意 2038 年问题,高精度计时用 clock_gettime

这些函数看着简单,但每个都有它的脾气。多写几次,踩过坑,自然就记住了。


专注资料整理