第27章 标准库函数:数学、时间、随机数与字符串转换
说实话,标准库函数这块内容,很多初学者觉得就是查查手册的事。但我在实际项目中吃过不少亏——比如时间函数用错了导致日志时间错乱,随机数没初始化结果每次运行结果都一样。这些坑,今天咱们一个一个说清楚。
27.1 数学函数(math.h)
数学库提供了常用的数学运算函数。我个人习惯在涉及浮点计算时,一定先确认编译器的数学库是否完整——嵌入式环境里有些库是裁剪过的。
| 函数 | 说明 | 注意点 |
|---|---|---|
| sin(), cos(), tan() | 三角函数,参数为弧度 | 角度转弧度:rad = deg * M_PI / 180.0 |
| asin(), acos(), atan() | 反三角函数 | 返回值范围有限,注意输入域 |
| sqrt() | 平方根 | 参数不能为负数 |
| pow() | 幂运算 | 底数为负且指数非整数时出错 |
| fabs() | 浮点绝对值 | 整型绝对值用 abs() |
| floor(), ceil() | 向下/向上取整 | 返回 double 类型 |
| fmod() | 浮点数取余 | 与 % 运算符不同,支持小数 |
避坑指南:我曾经在STM32上写了一个三角函数计算,结果发现运行时间长达几百微秒。后来查资料才知道,嵌入式MCU的数学库如果没有开启硬件浮点单元(FPU),纯软件模拟会非常慢。所以,记得在编译器选项中启用FPU。
#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));
printf("sqrt(144) = %f\n", sqrt(144.0));
printf("pow(2, 10) = %f\n", pow(2.0, 10.0));
printf("floor(3.7) = %f, ceil(3.7) = %f\n", floor(3.7), ceil(3.7));
return 0;
}
27.2 时间函数(time.h)
时间函数主要用来获取系统时间、计算程序运行时长。我建议你在做日志系统或性能测试时,一定要用好这些函数。
| 函数 | 说明 | 典型用途 |
|---|---|---|
| time() | 获取当前时间戳(秒) | 记录事件发生时间 |
| localtime() | 将时间戳转为本地时间结构体 | 格式化输出日期 |
| strftime() | 格式化时间字符串 | 生成日志时间戳 |
| clock() | 获取程序运行的CPU时钟数 | 测量代码段执行时间 |
| difftime() | 计算两个时间戳的差值(秒) | 统计耗时 |
小技巧:clock() 返回的是时钟周期数,要换算成秒需要除以 CLOCKS_PER_SEC。这个宏在 time.h 里定义,通常是 1000000。
#include <time.h>
#include <stdio.h>
int main() {
// 获取当前时间
time_t now = time(NULL);
printf("当前时间戳: %ld\n", now);
// 转为本地时间
struct tm *local = localtime(&now);
printf("本地时间: %s", asctime(local));
// 格式化输出
char buf[64];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local);
printf("格式化时间: %s\n", buf);
// 测量代码执行时间
clock_t start = clock();
for (long i = 0; i < 100000000; i++); // 空循环
clock_t end = clock();
double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
printf("循环耗时: %f 秒\n", elapsed);
return 0;
}
27.3 随机数函数(stdlib.h)
随机数在游戏、测试、加密场景中很常用。但很多人不知道,C语言的随机数是伪随机——如果不设置种子,每次运行结果都一样。
重要提醒:我曾经在写一个抽奖程序时,忘了调用 srand() 设置种子,结果每次重启程序抽奖结果都一样。测试同事还以为我作弊了……所以,一定要用 srand() 初始化随机数种子。
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int main() {
// 用当前时间作为种子
srand((unsigned int)time(NULL));
// 生成 0 到 RAND_MAX 之间的随机数
int r1 = rand();
printf("随机数: %d\n", r1);
// 生成 1 到 100 之间的随机数
int r2 = rand() % 100 + 1;
printf("1~100 随机数: %d\n", r2);
// 生成 0.0 到 1.0 之间的浮点随机数
double r3 = (double)rand() / RAND_MAX;
printf("0~1 随机浮点数: %f\n", r3);
return 0;
}
注意:rand() 的随机质量其实不高,不适合用于加密场景。如果你需要高质量的随机数,建议使用操作系统提供的随机数接口(如 Linux 的 /dev/urandom)。
27.4 字符串转换函数(stdlib.h)
字符串和数字之间的转换,是日常开发中高频使用的功能。我建议你优先使用带安全检测的函数,比如 strtol() 而不是 atoi()。
| 函数 | 功能 | 返回值 | 错误处理 |
|---|---|---|---|
| atoi() | 字符串转 int | int | 无法检测错误 |
| atol() | 字符串转 long | long | 无法检测错误 |
| atof() | 字符串转 double | double | 无法检测错误 |
| strtol() | 字符串转 long(带错误检测) | long | 可检测非法字符 |
| strtod() | 字符串转 double(带错误检测) | double | 可检测非法字符 |
| sprintf() | 格式化输出到字符串 | int(写入字符数) | 缓冲区溢出风险 |
| snprintf() | 安全版 sprintf | int(应写入字符数) | 指定缓冲区大小 |
我的建议:在嵌入式开发中,我几乎不用 atoi() 和 atof()。因为一旦输入字符串格式不对,它们会静默返回 0,你根本不知道转换失败了。用 strtol() 和 strtod() 配合 endptr 参数,可以精确判断哪里出错了。
#include <stdlib.h>
#include <stdio.h>
int main() {
// 安全的字符串转整数
const char *str = "123abc";
char *endptr;
long val = strtol(str, &endptr, 10);
if (*endptr != '\0') {
printf("转换部分成功: %ld, 非法字符: %s\n", val, endptr);
} else {
printf("完全转换: %ld\n", val);
}
// 安全的字符串转浮点数
const char *fstr = "3.14xyz";
double fval = strtod(fstr, &endptr);
if (*endptr != '\0') {
printf("浮点转换部分成功: %f, 非法字符: %s\n", fval, endptr);
}
// 使用 snprintf 安全格式化
char buf[32];
int n = snprintf(buf, sizeof(buf), "温度: %.1f°C", 25.5);
if (n >= sizeof(buf)) {
printf("缓冲区太小,需要 %d 字节\n", n + 1);
} else {
printf("格式化结果: %s\n", buf);
}
return 0;
}
27.5 知识体系总览
下面这张图,把本章涉及的标准库函数之间的关系梳理清楚了。你想想看,这些函数虽然分属不同头文件,但在实际项目中经常配合使用——比如用 time() 获取种子,用 rand() 生成随机数,再用 snprintf() 格式化成字符串。
核心总结:标准库函数是C语言的基石。数学函数注意精度和性能,时间函数注意跨平台差异,随机数函数一定要初始化种子,字符串转换优先用带错误检测的版本。这些经验,都是我一行一行代码踩坑踩出来的。