3.8 随机数产生器
下面要介绍一个在模拟事件和游戏的程序中常用的组件。本节和下节开发一个结构良好、包括多个函数的游戏程序。程序中要使用前面介绍的大多数控制结构。
在赌场上,人人都关心的一个问题就是机会元素(element of chance),也就是赢钱的运气。这个机会元素可以用标准库中的rand函数引入计算机应用程序中。
考虑下列语句:
i=rand();
rand 函数产生O到RAND_MAX之间的整数(这是<stdlib.h>头文件中定义的符号常量)。RAND_MAX的值至少应为32767,也就是两个字节(即16位)所能表示的最大整数值。如果rand函数真的可以随机产生整数,则每次调用rand函数时,o到RAND_MAX之间的每个数出现的机会(chance)或概率(probability)是相等的。
rand函数产生的数值范围可能与特定应用中所要求的数值范围不同。例如,模拟掷硬币的程序只要0(正面)和1(反面),模拟投骰子的程序只要1到6之间的随机整数,视频游戏中预测飞船(有四种类型)下一个类型的程序只要1到4之间的随机整数。
要演示rand函数,我们开发一个程序,模拟投骰子20次并打印每次的值。rand函数的函数原型见<stdlib.h>。我们使用求模运算符(%)和rand函数,如下所示:
rand( ) % 6
产生0到5的整数,称为比例缩放(scaling)。数字6称为比例因子(scalingfactor)。然后我们将产生的数值范围加1。得到所要结果。图3.7确认了产生的结果在1到6之间。
要显示这些数值的出现机会近似均等.图3.8模拟投骰子6000次。1到6的每个数值大约都出现1000次。
从程序输出可见,通过比例缩放和移动,可以利用rand函数真实地模拟投骰子。注意程序用不到switch结构中提供的default case,但我们还是加上default case,这是个良好的编程习惯。第4章数组将介绍如何把整个switch结构转换成简单的单行语句。
// Fig. 3.7: figO307.cpp // Shifted, scaled integers produced by 1 + rand( ) % 6 #include #include #include int main(){ for( int i = 1; i <= 20; i ++){ cout << setw( 10 ) << ( 1 + rand() % 6 ); if( i % 5 == 0 ) cout << endl; } return O; } }
输出结果:
5 5 3 5 5 2 4 2 5 5 5 3 2 2 1 5 1 4 6 4
图3.7 比例缩放和移动“rand()%6”产生的整数
// Fig. 3.8:fig03 08.cpp // Roll a six-sided die 6000 times #include #include #include int main(){ int frequencyl = 0, frequency2 = 0, frequency3 = 0, frequency4 = 0, frequency5 = 0, frequency6 = 0, face; forf( int roll = 1; roll <= 6000; roll++ ) { ace = 1 + rand( ) % 6; switch ( face ) { case 1: ++frequencyl; break; case 2: ++frequency2; break; case 3: ++frequency3; break; case 4: ++frequency4; break; case 5: ++frequeneyS; break; case 6: ++frequency6; break; default: cout << "should never get here!"; } } cout << "Face" << setw( 13 ) << "Frequency" << "\n 1" << setw( I3 ) << frequencyl << "\n 2" << setw( 13 ) << frequency2 << "\n 3" << setw( 13 ) << frequency3 << "\n 4" << setw( 13 ) << frequency4 << "\n 5" << setw( 13 ) << frequency5 << "\n 6" << setw( 13 ) << frequency6 << endl; return 0; }
输出结果:
Face Frequency 1 987 2 984 3 1029 4 974 5 1004 6 1022
图3.8模拟投骰子6000次
测试与调试提示3.1
即使能确切保证程序没有缺陷,也应在switch结 构中用一个default case来找到错误。
再次执行图3.7的程序得到:
5 5 3 5 5 2 4 2 5 5 5 3 2 2 1 5 1 4 6 4
注意打印的数值顺序与上一次完全相同,怎么能算是随机数呢,其实,这种可重复性是rand函数的一个重要特性。调试程序时,这种可重复性提供了证明修改后程序能正确工作的关键。
rand函数实际上产生的是伪随机数(pseudo-randomnumber)。重复调用rand函数产生一系列看上去是随机的数值,但每次执行程序时,这组数值本身是可重复的。一旦程序进行彻底调试之后,就可以调整为在每次执行程序时产生不同的随机数系列。这个过程称为随机化(randomizing),是用标准库函数srand完成的。srand函数取一个unsigned类型的整数参数并内嵌rand函数(即种子),
就可以在每次执行程序时产生不同的随机数系列。
srand函数的使用如图3.9所示。在这个程序中,我们使用数据类型unsigned(unsigned int的缩写)。int值至少占内存的两个字节,可以存正值或负值。unsignediht变量值也至少占内存的两个字节。2字节的unsigned int只能取O到65535的非负值,4字节的unsignediht只能取O到4294967295的非负值,srand函数取unsigned int值作参数。srand函数的函数原型在头文件<stdlibh>新C++标准的cstdlib)中。
下面将程序运行几次并观察结果。注意,只要提供不同的种子,即可在每次执行程序时产生不同的随机数系列。
1 // Fig. 3.9: fig0309.cpp // Randomizing die-rolling program #include <iostream.h> #include <iomanip.h> #include <stdlib.h> int main(){ unsigned seed; cout << "Enter seed: "; cin >> seed; srand( seed ); for ( int i = 1; i <= 10; i++ ) { cout << setw( 10 ) << 1 + rand( ) % 6; if ( i % 5 == 0 ) cout << endl; } return 0; }
输出结果:
Enter seed: 67 1 6 5 1 4 5 6 3 1 2
Enter seed:432 4 2 6 4 3 2 5 1 4 4
Enter seed: 67 1 6 5 1 4 5 6 3 1 2
图3.9投骰子随机化程序
如果不想每次输入种子值而随机化,则要用如下语句:
srand( time ( 0 ) );
使计算机通过时钟值自动取得种子值。time函数(上述语句中该函数的参数为0)返回当前“日历时间”的秒数,将这个值转换为无符号整数值,作为随机数产生器的种子。time函数的函数原型在<time>(新C++标准的ctime)中。
性能提示 3.2
srand函数只要在程序中调用一次即可得到所需的随机化结果,多次调用是多余的,会降低程序性能。
由rand函数直接产生的值总是取值为:
O≤rand()≤ RAND_MAX
前面介绍了如何用一个语句模拟投骰子,该语句如下所示:
face=1+rand() % 6;
总是对变量face指定1≤face≤6的整数(随机)。注意这个范围的宽度(即构成该范围的连续整数的个数)为6并从1开始。从上述语句可以看出,范围宽度是由求模运算符比例缩放rand的数值(6)确定的,开始值等于rand%6中加进的数值(即1)。可以将这个结果一般化,如下所示:
n = a + rand() %b;
其中a是位移值(等于所要的连续整数范围的开始值),b是比例因子(即由连续整数构成的该范围的宽度)。练习中将介绍如何从一组非连续整数中随机选择整数。
常见编程错误3.16
用srnd函数代替rand函数产生随机数是个语法错误,因为srnd函数不返回值。