为什么 rand() + rand() 会产生负数?

307
我观察到当在循环中仅调用一次rand()库函数时,它几乎总是会产生正数。

我观察到当在循环中仅调用一次rand()库函数时,它几乎总是会产生正数。

for (i = 0; i < 100; i++) {
    printf("%d\n", rand());
}

但是当我添加两个 rand() 调用时,生成的数字现在有更多的负数。

for (i = 0; i < 100; i++) {
    printf("%d = %d\n", rand(), (rand() + rand()));
}
能有人解释一下为什么在第二种情况下我看到负数吗?
PS:在循环之前,我用srand(time(NULL))初始化了种子。

13
rand() 无法产生负数。 - twentylemon
296
rand() + rand() 可能会溢出。 - maskacovnik
15
你的编译器中的RAND_MAX是多少?你通常可以在stdlib.h中找到它。(有趣的是,检查man 3 rand,它只有一行描述:“不好的随机数生成器”。) - Jongware
6
做任何明智的程序员都会做的事情:abs(rand()+rand())。我宁愿有一个正向未定义行为(UB)也不要有负向的! ;) - Vinicius Kamakura
12
@hexa:这对于未定义行为并不是解决方案,因为这已经在加法中发生过了。你不能使未定义行为成为“定义行为”。一个“明智”的程序员会尽量避免未定义行为。 - too honest for this site
显示剩余15条评论
9个回答

547

rand()函数被定义为返回一个介于0RAND_MAX之间的整数。

rand() + rand()

可能会发生溢出。您观察到的很可能是整数溢出导致的未定义行为的结果。


4
不同的编程语言对于溢出行为的规定是不同的。例如,Python中没有明确规定(好吧,只要内存足够大),整数会自动增长以避免溢出。 - too honest for this site
2
@Olaf 这取决于语言如何表示有符号整数。Java 在 Java 8 之前没有检测整数溢出的机制,并将其定义为环绕,而 Go 仅使用二进制补码表示,并将有符号整数溢出定义为合法。C 显然支持超过二进制补码的表示方式。 - P.P
2
@EvanCarslake 不,这不是普遍行为。你所说的是关于二进制补码表示法的。但是C语言也允许其他表示法。C语言规范指出,有符号整数溢出是未定义的。因此,一般情况下,没有程序应该依赖这种行为,并且需要小心编写代码以避免有符号整数溢出。但是对于无符号整数来说,它们会以明确定义的方式“环绕”(模2减少)。[续...] - P.P
12
这是关于有符号整数溢出的C标准引用:“如果在表达式求值期间发生异常情况(也就是说,如果结果在其类型所能表示的范围之外或者不是数学上被定义的),行为是未定义的。” - P.P
3
@EvanCarslake 偏离问题一点,C编译器确实使用标准,对于有符号整数,如果编译器知道 b > 0,则可以假设 a + b > a。此外,如果后面执行了语句 a + 5,则当前值比 INT_MAX - 5 要低。因此,即使在没有陷阱的 2's 补码处理器/解释器上,程序的行为也可能不像使用带陷阱的 int 类型那样被视为 2's 补码。 - Maciej Piechotka
显示剩余7条评论

91
这个问题出在加法上。rand() 返回一个 0... RAND_MAXint 值。所以,如果你将两个值相加,最大可能得到的值为 RAND_MAX * 2。如果这个结果超过了 INT_MAX,那么加法的结果会超出 int 可以保存的有效范围。有符号整数的溢出是未定义行为,并可能导致你的键盘用外语与你交流。
既然在两个随机结果相加没有什么收益,那么简单的方法就是不要相加。或者,在相加之前将每个结果强制转换为 unsigned int(如果该类型可以保持总和)。或者使用更大的类型。请注意,long 不一定比 int 更宽,如果 int 至少有 64 位,则同样适用于 long long
结论:避免使用加法。它不会提供更多的“随机性”。如果需要更多位数,可以连接这些值sum = a + b * (RAND_MAX + 1),但是这很可能需要比int更大的数据类型。
由于您所述的原因是要避免零结果:通过添加两个rand()调用的结果无法避免零。相反,您可以进行递增。如果RAND_MAX == INT_MAX,则无法在int中完成此操作。但是,(unsigned int)rand() + 1 很有可能做到。可能(并非确定),因为它确实需要UINT_MAX > INT_MAX,这在我所知道的所有实现中都是正确的(涵盖了相当多的嵌入式架构、DSP和过去30年的所有桌面、移动和服务器平台)。
警告:

虽然在评论中已经提到,但需要注意的是,将两个随机值相加并不会得到均匀分布,而是类似于掷两个骰子的三角形分布:为了得到12(两个骰子),两个骰子都必须显示6。对于11,已经有两种可能的变体:6 + 55 + 6等。

因此,从这个方面来看,加法也是不好的。

还请注意,rand()生成的结果彼此不独立,因为它们是由一个伪随机数生成器生成的。同时,请注意标准并未指定计算值的质量或均匀分布。


14
如果两个函数调用都返回0,那又怎样呢? - too honest for this site
3
@badmad:我只是想知道UINT_MAX > INT_MAX != false是否被标准保证了。(听起来很可能,但不确定是否必需)。如果是这样,你可以先将单个结果转换为整数,然后再递增(按此顺序!)。 - too honest for this site
3
当你需要得到非均匀分布时,增加多个随机数会带来收益。原文链接:https://dev59.com/KV0a5IYBdhLWcg3wTHFt - Cœur
6
为避免出现0,可以简单地使用“当结果为0时重新投掷”的方法。 - Olivier Dulac
2
不仅添加它们是避免0的不好方法,而且还会导致非均匀分布。你得到的分布就像掷骰子的结果一样:7比2或12可能性高6倍。 - Barmar
显示剩余11条评论

36

这是对于这个回答中的评论做出的澄清回答:

我之前添加的原因是为了避免在我的代码中随机生成 '0'。rand()+rand()是我能想到的快速脏解决方案。

问题是要避免生成0。建议不要使用 rand()+rand(),因为像其他答案所指出的那样,它可能导致未定义的行为。另一个问题是无法保证 rand() 不会连续两次生成0。

以下代码可以避免生成0,并且避免未定义行为,而且在大多数情况下比调用两次rand()要快:

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);

9
rand() + 1 是什么意思? - askvictor
4
@askvictor这可能会溢出(尽管可能性不大)。 - gerrit
3
@gerrit - 这取决于 MAX_INT 和 RAND_MAX。 - askvictor
3
@gerrit,如果它们不是相同的,我会感到惊讶,但我认为这是一个追求严谨的地方 :) - askvictor
12
如果RAND_MAX等于MAX_INT,那么rand() + 1的溢出概率与rand()返回0的概率完全相同,这使得该解决方案完全没有意义。如果你愿意冒险并忽略溢出的可能性,那么你可以直接使用rand()并忽略它返回0的可能性。 - Emil Jeřábek
显示剩余7条评论

3
基本上,rand() 生成介于 0RAND_MAX 之间的数字,在您的情况下,2 RAND_MAX > INT_MAX。您可以使用数据类型的最大值进行模数运算,以防止溢出。当然,这会破坏随机数的分布,但是 rand 只是获取快速随机数的一种方式。
#include <stdio.h>
#include <limits.h>

int main(void)
{
    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;
}

2

也许你可以尝试一种比较巧妙的方法,确保由两个rand()求和返回的值不会超过RAND_MAX的值。一种可能的方法是sum = rand() / 2 + rand() / 2; 这将确保对于一个具有32767的RAND_MAX值的16位编译器,即使rand都返回32767,结果仍不会是负数,因为(32767/2 = 16383),16383 + 16383 = 32766。


1
OP希望在结果中排除0。此外,加法也不能提供随机值的均匀分布。 - too honest for this site
@Olaf:不能保证连续两次调用rand()不会都产生零,因此希望避免零并不是将两个值相加的好理由。另一方面,如果确保没有溢出发生,希望具有非均匀分布则是将两个随机值相加的好理由。 - supercat

1
为了避免0,请尝试这样做:

int rnumb = rand()%(INT_MAX-1)+1;

你需要包含limits.h


4
这将使得获得1的概率翻倍。基本上,它与条件性地在rand()产生0时加1是相同的(但可能会更慢)。 - too honest for this site
是的,Olaf,你说得对。如果rand() = 0或INT_MAX-1,则rnumb将为1。 - Doni
更糟糕的是,当我考虑它时,它实际上会使“1”和“2”的概率加倍(假设所有值都为“RAND_MAX == INT_MAX”)。我忘记了“-1”。 - too honest for this site
1
这里的“-1”没有用处。rand()%INT_MAX + 1;仍然只会生成[1 ... INT_MAX]范围内的值。 - chux - Reinstate Monica

1
我添加的原因是为了避免在我的代码中出现'0'作为随机数。rand()+rand()是我想到的快速脏解决方案。
一个简单的解决方案(好吧,称之为“Hack”),它永远不会产生零结果,也永远不会溢出:
x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

这会限制您的最大值,但如果您不在意这一点,那么这应该对您很有效。

1
附注:对于带符号变量的右移操作要小心。它仅对非负值有明确定义,对于负数则是实现定义的。(幸运的是,rand()始终返回非负值)。然而,我会将优化留给编译器处理。 - too honest for this site
@Olaf:一般来说,有符号整数除以2的效率要低于移位操作。除非编译器已经知道rand是非负数,否则移位操作比有符号整数除以2更有效率。除以2u可能可行,但如果x是int类型,则可能会出现从无符号转换为有符号的隐式转换警告。 - supercat
@supercat:请仔细再次阅读我的评论。你应该非常清楚,任何合理的编译器都会对“/2”使用移位操作(我甚至在像“-O0”这样没有明确请求优化的情况下看到过这种情况)。这可能是C代码中最琐碎和最成熟的优化之一。关键是标准为整数范围内的除法定义得很好,而不仅仅是非负值。再次强调:将优化留给编译器,在第一时间编写正确和清晰的代码更加重要,尤其是对于初学者。 - too honest for this site
@Olaf:我测试过的每个编译器都会在将rand()向右移动一位或除以2u时生成更高效的代码,即使使用了-O3。有人可能会合理地说这样的优化不太重要,但是说“让编译器处理这样的优化”会暗示编译器可能会执行它们。你知道任何实际会执行这些优化的编译器吗? - supercat
@supercat:是的,它似乎这样做是为了正确处理负面情况。 但它并没有使用除法,就像你提出的那样。 另外两个指令每个时钟周期至多为1个(它们可能可以在非合成程序中折叠,但即使不能,在这里完全无关紧要),顺便说一句。 如果您想没有区别,请使用unsigned,对于rand()来说,这也很好,并且在这里更好的“优化”方式。 哦,而不同于x64(我使用的)的架构可能会导致两者都具有相同的代码。 顺便说一下:这就是我在这里讨论的所有内容。 你正在提出糟糕的做法。 - too honest for this site
显示剩余2条评论

0
谢谢。我添加的原因是为了避免在我的代码中得到“0”作为随机数。rand()+rand() 是我的快速脏解决方案。
对我来说,这听起来像一个 XY 问题,为了不从 rand() 中得到 0,您调用 rand() 两次,使程序变慢,面临新的挫折,并且仍有可能得到 0。
另一个解决方案是使用 uniform_int_distribution,它创建在定义的区间内随机且均匀分布的数字。

https://wandbox.org/permlink/QKIHG4ghwJf1b7ZN

#include <random>
#include <array>
#include <iostream>
 
int main()
{
    const int MAX_VALUE=50;
    const int MIN_VALUE=1;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> distrib(MIN_VALUE, MAX_VALUE);
    std::array<int,MAX_VALUE-MIN_VALUE> weight={0};

    for(int i=0; i<50000; i++) {
        weight[distrib(gen)-MIN_VALUE]++;
    }
    
    for(int i=0;i<(int)weight.size();i++) {
        std::cout << "value: " << MIN_VALUE+i << " times: " << weight[i] << std::endl; 
    }
}

-2

虽然其他人所说的可能会造成溢出的原因很可能是负的,即使你使用了无符号整数。但真正的问题实际上是使用时间/日期功能作为种子。如果你真正熟悉这个功能,你就会知道我为什么这么说。它真正做的是给出一个距离(经过的时间)自从某个日期/时间以来。虽然将日期/时间功能用作rand()的种子是一种非常常见的做法,但它确实不是最好的选择。你应该寻找更好的替代方案,因为关于这个主题有许多理论,我不可能涵盖所有的理论。如果你再加上溢出的可能性,那么这种方法从一开始就注定要失败。

那些发布rand()+1的人正在使用大多数人使用的解决方案,以确保他们不会得到负数。但是,这种方法也不是最好的方式。

你能做的最好的事情就是花费额外的时间编写和使用适当的异常处理,并且只在出现零结果时添加rand()数字。并且,正确处理负数。rand()功能并不完美,因此需要与异常处理一起使用,以确保你最终获得预期的结果。

花费额外的时间和精力来调查、研究和正确实现rand()功能是值得的。这只是我的个人意见。祝你在编程中好运……

2
rand() 函数没有指定使用什么种子。标准确实规定它使用一个伪随机生成器,而不是与任何时间相关的关系。它也没有说明生成器的质量。实际问题显然是溢出。请注意,rand() + 1 用于避免 0rand() 不返回负值。很抱歉,但您在这里误解了重点。它并不涉及 PRNG 的质量问题。... - too honest for this site
在GNU/Linux下的良好实践是从/dev/random中获取种子,然后使用一个好的伪随机数生成器(不确定glibc中rand()的质量),或者继续使用该设备 - 如果熵不足,则会使您的应用程序阻塞。尝试在应用程序中获取熵可能很容易成为漏洞,因为这可能更容易受到攻击。现在涉及到加固问题 - 这里不再讨论。 - too honest for this site

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接