9、随机数与通用工具(stdlib.h):rand与srand、RAND_MAX、随机数范围控制、atoi/atof/atol、strtol/strtod
说到 stdlib.h,很多人第一反应就是「哦,那个 malloc 和 free 的头文件」。没错,内存管理确实是它的重头戏,但今天我想聊聊这个头文件里另一组非常实用的工具——随机数生成和字符串转换函数。
这两类功能看起来不搭边,但在实际项目中经常成对出现。比如你写一个模拟程序,需要生成随机数,然后把这些数字转成字符串输出;或者反过来,从配置文件中读出一堆字符串,转成数值去控制随机数种子。嗯,我当年在写一个网络仿真器时就遇到过这种场景。
9.1 随机数生成:rand 与 srand
C 标准库提供的随机数生成器其实挺朴素的。它用的是线性同余算法,说白了就是一个数学公式不断迭代,产生一串看起来随机的数列。
#include <stdlib.h>
#include <stdio.h>
int main() {
// 不设置种子,直接调用 rand()
printf("第一次运行:%d\n", rand());
printf("第二次运行:%d\n", rand());
return 0;
}
你试试看,每次运行这个程序,输出的数字是不是一模一样?这就是问题所在——如果不调用 srand() 设置种子,rand() 默认种子是 1,所以每次生成的序列完全一样。
srand() 播种。
我个人习惯这样初始化种子:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int main() {
srand((unsigned int)time(NULL)); // 用当前时间做种子
for (int i = 0; i < 5; i++) {
printf("随机数 %d: %d\n", i+1, rand());
}
return 0;
}
用 time(NULL) 返回的秒数作为种子,每次运行程序时种子不同,生成的随机数序列也就不同了。不过这里有个坑——如果在一秒内多次调用 srand(),种子是一样的,生成的随机数也会一样。我曾经在循环里不小心写了 srand(time(NULL)),结果每次循环生成的随机数一模一样,排查了半天才发现。
9.2 RAND_MAX 与随机数范围控制
rand() 返回的值在 0 到 RAND_MAX 之间。RAND_MAX 是个宏,在大多数实现中是 32767(也就是 2^15 - 1)。
但实际项目中,我们很少需要 0 到 32767 的随机数。更多时候,我们需要指定范围的随机数,比如 1 到 100,或者 -50 到 50。
控制范围的标准做法:
// 生成 [0, n-1] 范围的随机数
int rand_range(int n) {
return rand() % n;
}
// 生成 [min, max] 范围的随机数
int rand_between(int min, int max) {
return rand() % (max - min + 1) + min;
}
举个例子:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int main() {
srand((unsigned int)time(NULL));
// 模拟掷骰子:1 到 6
for (int i = 0; i < 10; i++) {
int dice = rand() % 6 + 1;
printf("第 %d 次掷骰子:%d\n", i+1, dice);
}
return 0;
}
% 取模的方式虽然简单,但如果你对随机数的「均匀性」要求很高,这种方法会有微小偏差。因为 RAND_MAX + 1 不一定能被 n 整除,导致前面几个数出现的概率略高。对于大多数应用来说这点偏差可以忽略,但如果你写的是蒙特卡洛模拟或者加密相关代码,建议用更严谨的算法。
9.3 字符串转数值:atoi / atof / atol
这三兄弟是 C 语言里最简单的字符串转数值函数。它们的原型是:
int atoi(const char *str);
double atof(const char *str);
long atol(const char *str);
用法很简单:
#include <stdlib.h>
#include <stdio.h>
int main() {
char str1[] = "42";
char str2[] = "3.14159";
char str3[] = "1234567890";
int a = atoi(str1);
double b = atof(str2);
long c = atol(str3);
printf("atoi: %d\n", a);
printf("atof: %.5f\n", b);
printf("atol: %ld\n", c);
return 0;
}
输出:
atoi: 42
atof: 3.14159
atol: 1234567890
atoi 系列函数有一个严重的问题——没有错误处理机制。如果传入的字符串不是合法的数字,比如 "abc",它会返回 0。但问题来了,如果字符串本身就是 "0",它也返回 0。你根本分不清是转换成功还是失败。
我记得有一次在项目中用 atoi 解析用户输入的配置参数,用户不小心输了个字母进去,结果程序静默地用了 0 作为参数值,导致整个系统行为异常。从那以后,我给自己定了个规矩:生产代码中尽量不用 atoi 系列函数。
9.4 更安全的替代方案:strtol / strtod
如果你想要更健壮的字符串转换,strtol 和 strtod 是更好的选择。它们不仅能处理错误,还能告诉你转换停在了哪里。
先看 strtol:
long strtol(const char *str, char **endptr, int base);
参数说明:
str:要转换的字符串endptr:指向一个指针,转换结束后,这个指针会指向字符串中第一个未被转换的字符base:进制,可以是 0 或 2-36。如果为 0,函数会自动根据前缀判断(0x 开头是十六进制,0 开头是八进制,否则是十进制)
来看个实际例子:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
int main() {
const char *input = " 123abc 456";
char *endptr;
errno = 0; // 先清空错误标志
long val = strtol(input, &endptr, 10);
if (errno == ERANGE) {
printf("数值超出 long 范围\n");
} else if (endptr == input) {
printf("没有找到任何有效数字\n");
} else {
printf("转换结果:%ld\n", val);
printf("未转换的部分:%s\n", endptr);
}
return 0;
}
输出:
转换结果:123
未转换的部分:abc 456
看到了吗?strtol 会跳过开头的空白字符,转换到第一个非数字字符停下,然后通过 endptr 告诉你停在了哪里。这样你就能精确控制解析过程。
strtod 的用法类似:
double strtod(const char *str, char **endptr);
它没有 base 参数,因为浮点数就是十进制。但它能识别 NaN、Infinity 这些特殊值。
#include <stdlib.h>
#include <stdio.h>
int main() {
const char *inputs[] = {"3.14159", " -2.5e-3", "NaN", "Infinity", "abc"};
char *endptr;
for (int i = 0; i < 5; i++) {
double val = strtod(inputs[i], &endptr);
printf("输入:\"%s\" → 结果:%f,剩余:\"%s\"\n",
inputs[i], val, endptr);
}
return 0;
}
输出:
输入:"3.14159" → 结果:3.141590,剩余:""
输入:" -2.5e-3" → 结果:-0.002500,剩余:""
输入:"NaN" → 结果:nan,剩余:""
输入:"Infinity" → 结果:inf,剩余:""
输入:"abc" → 结果:0.000000,剩余:"abc"
strtol 和 strtod 替代 atoi 系列。虽然代码会多几行,但换来的是健壮性和可调试性。你想想看,一个能告诉你「哪里出错了」的函数,比一个默默返回 0 的函数强太多了。
9.5 知识体系总览
下面这张图把本章的核心知识点串了起来:
9.6 综合示例:随机数 + 字符串转换
最后,我把今天讲的内容串成一个完整的例子。这个程序生成随机数,然后从字符串中解析数值,两者结合起来模拟一个简单的「抽奖系统」:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
int main() {
// 用当前时间初始化随机数种子
srand((unsigned int)time(NULL));
// 模拟从配置文件读取的抽奖人数
const char *config = " 100 ";
char *endptr;
errno = 0;
long total = strtol(config, &endptr, 10);
if (errno == ERANGE || endptr == config) {
printf("配置解析失败,使用默认值 50\n");
total = 50;
}
// 生成中奖号码(1 到 total 之间)
int winner = rand() % total + 1;
printf("抽奖人数:%ld\n", total);
printf("中奖号码:%d\n", winner);
return 0;
}
这个例子虽然简单,但涵盖了今天讲的核心内容:srand 播种、rand 生成随机数、取模控制范围、strtol 安全解析字符串。你在实际项目中遇到类似需求时,可以直接拿这个框架去改。
- 用
srand()播种,用rand()取随机数 - 取模运算控制范围,注意微小偏差
atoi系列简单但危险,生产代码慎用strtol和strtod更安全,支持错误检测- 检查
errno和endptr是健壮编程的好习惯
好了,关于 stdlib.h 中的随机数和字符串转换工具就聊到这里。这些函数看着不起眼,但用好了能让你的代码更健壮、更专业。下次写代码时,不妨试试用 strtol 替换 atoi,你会发现多写的那几行代码,真的很值。
公众号:蓝海资料掘金营,微信deep3321