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,所以每次生成的序列完全一样。

注意: 这不是 bug,是特性。有些场景(比如调试、回归测试)反而需要可重复的随机序列。但如果你想要真正的「随机」,就必须用 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

如果你想要更健壮的字符串转换,strtolstrtod 是更好的选择。它们不仅能处理错误,还能告诉你转换停在了哪里。

先看 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 参数,因为浮点数就是十进制。但它能识别 NaNInfinity 这些特殊值。

#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"
我的建议: 在新项目中,直接用 strtolstrtod 替代 atoi 系列。虽然代码会多几行,但换来的是健壮性和可调试性。你想想看,一个能告诉你「哪里出错了」的函数,比一个默默返回 0 的函数强太多了。

9.5 知识体系总览

下面这张图把本章的核心知识点串了起来:

stdlib.h 随机数与通用工具 随机数生成 srand(seed) → 设置种子 常用种子:time(NULL) rand() → 返回 [0, RAND_MAX] RAND_MAX 通常为 32767 范围控制:取模运算 rand() % n → [0, n-1] ⚠ 注意:取模有微小偏差 高精度场景需用更严谨算法 字符串转数值 简单但危险:atoi / atof / atol 无错误处理,非法输入返回 0 不推荐用于生产代码 安全可靠:strtol / strtod 通过 endptr 定位错误位置 支持进制选择、特殊值识别 使用建议 • 新项目优先用 strtol/strtod • 检查 errno 判断溢出 • 比较 endptr 与输入判断是否有效 实际项目中常配合使用

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 系列简单但危险,生产代码慎用
  • strtolstrtod 更安全,支持错误检测
  • 检查 errnoendptr 是健壮编程的好习惯

好了,关于 stdlib.h 中的随机数和字符串转换工具就聊到这里。这些函数看着不起眼,但用好了能让你的代码更健壮、更专业。下次写代码时,不妨试试用 strtol 替换 atoi,你会发现多写的那几行代码,真的很值。


公众号:蓝海资料掘金营,微信deep3321