46、C标准库(二): 常用函数详解、 常用函数详解、 时间处理
好,咱们接着聊C标准库。上一章我们把 <stdio.h> 和 <stdlib.h> 啃了一遍,今天继续深入另外三个高频库:<string.h>、<math.h> 和 <time.h>。
说实话,这三个库我几乎每天都在用。字符串处理、数学计算、时间记录——任何一个正经项目都绕不开它们。我刚开始写C的时候,总觉得这些函数背一背就行,后来发现光背没用,得知道它们背后有哪些坑。
嗯,咱们一个一个来。
一、:字符串操作,别踩缓冲区的地雷
字符串处理是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 拼接与比较
strcat 和 strncat 的区别跟复制函数一样——一个不检查长度,一个检查。但 strncat 有个细节:它会从目标字符串的末尾开始追加,并且最多追加 n 个字符,然后自动加 '\0'。
char buf[16] = "Hello";
// strcat(buf, " World!!!"); // 溢出!buf只有16字节
strncat(buf, " World!!!", sizeof(buf) - strlen(buf) - 1);
// 结果: "Hello World" (截断了)
比较函数 strcmp 和 strncmp 也很常用。返回值是 <0、0、>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 | — |
避坑指南:我在做金融计算时,用 floor 和 ceil 处理金额,结果发现负数时行为跟预期不一样。后来统一改用 trunc 加手动判断,才搞定。
2.3 指数与对数
exp(x):计算 e^xlog(x):自然对数(以 e 为底),x 必须大于 0log10(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_tstrftime(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> 管时间处理。记住几个关键点:
- 字符串操作永远考虑缓冲区大小,用
strncpy或snprintf - 数学库编译加
-lm,角度转弧度用M_PI - 时间处理注意 2038 年问题,高精度计时用
clock_gettime
这些函数看着简单,但每个都有它的脾气。多写几次,踩过坑,自然就记住了。