如何生成随机数+原理详细分析
文章目录
- 如何生成随机数+原理详细分析
- 原理
- 如果使用相同的随机数种子,得到的随机数序列会是相同的吗
- 示例
- 为什么需要随机数种子
- 动态内存管理
- 前言
- malloc函数
- calloc函数
- realloc函数
- free函数 - 避免内存泄漏
- 常见的动态内存错误
原理
说到如何生成一个随机数,可能当你百度后会看到这样一段代码。
srand((unsigned int)time(NULL));
int ret = rand();
12
那么一个随机数到底是如何生成的呢?我相信善于探索的你一定想知道这其中的原理,那么话不多说,进入正题把!
一个随机数到底是如何生成的:
说到生成随机数我们都知道要用到一个rand函数,那么这个函数究竟是如何运用的呢,我们可以打开MSDN看看这个函数的用法
这里的第一句说到rand函数会返回一个从0到RAND_MAX的整型,那么RAND_MAX的值是多少呢,我们可以将它复制到编译器中然后选中它右击鼠标点击转到定义,就可以看到这句话
其实RAND_MAX的值也就是0x7fff,转换为十进制也就是32767,所以说rand函数可以随机生成一个0到32767的整型,当你在编译器中尝试时,你会看到
当你天真的以为你已经成功的可以生成随机数的时候,你会发现当年再次运行该代码时,它生成的还是这些随机数,也就是第一次运行代码时代码生成了随机数,但是第二次运行时会生成相同的随机数。
这时我们应该想起对rand函数的描述中还有第二句话:在调用rand函数之前,我们要使用srand函数设置生成随机数的起点。我们又在MSDN中查查srand函数:
我们可以看到srand函数的参数是一个无符号整型并且无返回值,那么这时我们可以来测试一下,就随便给一个无符号整型传给srand函数
但是当我们再次执行程序时照样还是这些随机数,当我们把传入srand函数的改变时,发现所给随机数便改变了:
所以我们只要在每次执行程序的时候给srand函数传入一个与上一次不同的数即可,但是我们就是要生成一个随机数,现在又需要一个随机数,这不成死循环了吗?
这时我们想到在电脑上有一个东西是时刻在发生着变化的,那就是时间,这时我们需要介绍一个概念,那就是时间戳。
时间戳: 当前时间与计算机起始时间的差值,单位是秒。
计算机的起始时间:1970-01-01 08:00:00
每一秒的时间戳都不一样,所以我们只要把时间戳传入srand函数即可,这时我们就需要用到time函数,因为time函数的返回值就是时间戳。
这里我们可以看到time函数的参数是time_t型指针,返回值是time_t型,这里的time_t我们也可以把它放到编译器中右击鼠标,点击转到定义:
这里我们可以看到,其实time_t就是int型被typedef重定义了(也就是起了个别名)而已。
而我们也不需要向time函数传入什么指针,于是我们就向time函数传入一个空指针( NULL)即可,也就是time( NULL),但是srand函数的参数是unsigned int型,所以我们如果要将time函数的返回值传入srand函数,那么我们就需要将time函数的返回值强制性转化会unsigned int型,也就是( unsigned int )time( NULL),所以我们最终将代码写为:
这样,每次运行代码时所得到的就是真正意义上的随机数了。
如何生成规定位数的随机数:
生成两位随机数:
我们只需要将所得随机数对90取余数,那么我们得到的数就是0-89的数字,这时再加上10便是10-99的数字了。
生成三位随机数:
道理与生成两位随机数相同我就不再阐述了。
如果使用相同的随机数种子,得到的随机数序列会是相同的吗
是的,如果使用相同的随机数种子,并且使用相同的伪随机数生成器(PRNG)算法,在相同的情况下运行,将得到相同的随机数序列。
这是因为伪随机数生成器的工作方式是基于确定性算法。给定一个种子值,这些算法会按照预定的方式计算出一系列看似随机的数字。虽然这些数字在统计上满足随机性的一些性质,但它们实际上是可预测的。只要输入相同的种子,就会产生相同的输出。
这种特性对于调试和复现问题非常有用,因为如果你知道使用了什么种子,就可以再次生成相同的随机数序列来帮助诊断问题。然而,这也意味着为了保证每次运行程序时都能获得不同的随机数,通常需要使用当前时间或其他难以预测的值作为种子。
示例
在C语言中,可以使用rand()
函数生成随机数。以下是一个简单的示例:
#include <stdio.h>
#include <stdlib.h> // 包含 rand() 和 srand()
#include <time.h> // 包含 time()
int main() {
// 使用当前时间作为随机数种子,确保每次运行时的随机性
unsigned int seed = (unsigned) time(NULL);
srand(seed);
// 生成一个0到RAND_MAX之间的随机整数
int random_number = rand();
printf("Random number: %d\n", random_number);
return 0;
}
这个程序首先包含必要的头文件:stdio.h
用于输入输出操作,stdlib.h
包含了rand()
和srand()
函数,而time.h
则包含了获取系统时间的time()
函数。
接着,它使用time(NULL)
来获取当前的时间(以秒为单位),并将这个值作为seed
传递给srand()
函数。这样,每次运行程序时,由于时间总是不同的,所以seed
也会不同,从而使得rand()
生成的随机数序列也不同。
然后,程序调用rand()
函数生成一个随机数,并将其存储在random_number
变量中。最后,它打印出这个随机数。
注意,rand()
生成的随机数是介于0和RAND_MAX
之间的整数,其中RAND_MAX
是一个宏定义,其典型值为32767。如果你需要生成特定范围内的随机数,你可以通过取模运算和位移等方法进行调整。
例如,如果你想生成1到10之间的随机数,你可以这样做:
// 生成1到10之间的随机数
int random_number_in_range = (rand() % 10) + 1;
这里,我们先使用rand() % 10
得到一个0到9的随机数,然后再加1,使其变为1到10的随机数。
为什么需要随机数种子
设置随机数种子的主要目的是为了保证程序每次运行时产生相同的随机数序列1。这在复现实验结果和调试代码时非常有用2。例如,在深度学习中,我们往往会用到随机向量,随机矩阵,这使得我们每次运行算法计算出来的结果是不一致的,会为我们调试算法带来麻烦1。基于随机种子来实现代码中的随机方法,能够保证多次运行此段代码能够得到完全一样的结果,即保证结果的可复现性1。
此外,随机种子的工作原理是这样的:所有的随机数算法在初始化阶段都需要一个随机“种子”(random seed),完全相同的种子每次将产生相同的“随机”数序列1。如果我们没有手动进行显式设置,系统则默认根据时间来选择这个值,此时每次生成的随机数因时间差异而不同1。所以,只要我们的运行环境一致(保证伪随机数生成程序一样),而我们设定的随机种子一样的话,那么我们就可以复现结果3。这样别人跑你的代码的时候也能够很好地复现出你的结果1。这就是为什么需要设置随机数种子的原因。希望这个解释对你有所帮助!
动态内存管理
前言
如果我们被问道:如何创建一个可以根据用户需求来开辟大小的数组?
可能有些博友会写出如下代码:
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int arr[n];
return 0;
}
12345678
这个代码在C99标准下是可以运行的,但大多数编译器并不支持C99标准,所以这种代码缺乏了跨平台性(可移植性),那么我们有没有办法写出一个既可以满足题目要求,又可以在任何一个编译器下都编译得过去的代码呢?
答案是肯定的。这就和C语言中的动态内存的开辟有关了,动态开辟,即可以按照需求开辟内存的大小。
下面,我向博友们介绍几个操作动态内存的常用函数。
注:下面介绍的几个函数的操作对象都是堆区的内存。
扩展:局部变量存放在内存中的栈区;全局变量、静态变量(static修饰的变量)存放在内存中的静态区(也叫数据段)。
malloc函数
void *malloc( size_t size );
1
malloc函数的功能是开辟指定字节大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。传参时只需传入需要开辟的字节个数。
假设我们要开辟一个可以存放10个整型的空间:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
//因为malloc函数的返回值为void*,所以需要强制类型转换为对应类型。
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
//使用...
//使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
}
1234567891011121314151617181920
注:malloc函数开辟好空间后,不对空间内容做任何初始化,所以空间内的数据为随机值。
calloc函数
void *calloc( size_t num, size_t size );
1
calloc函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。但calloc函数传参时需要传入两个参数(开辟的内存用于存放的元素个数和每个元素的大小)。
calloc函数与malloc函数的用法也是大同小异:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10 , sizeof(int));
//开辟一个可以存放10个整型的内存空间
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
//使用...
//使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
}
1234567891011121314151617181920
注:calloc函数与malloc函数的最大区别在于:calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0。
realloc函数
void *realloc( void *memblock, size_t size );
1
realloc函数可以调整已经开辟好的动态内存的大小,第一个参数是需要调整大小的动态内存的首地址,第二个参数是动态内存调整后的新大小。
realloc函数与上面两个函数一样,如果开辟成功便返回开辟好的内存的首地址,开辟失败则返回NULL。
如果我们要将已开辟的动态内存大小做一些调整,我们会这样做:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("内存开辟失败\n");
}
else
{
printf("内存开辟成功\n");
//使用...
//扩展容量
int* ptr = (int*)realloc(p, 100);
//将空间扩展为100个字节大小
if (ptr != NULL)
{
p = ptr;//开辟成功时
//使用...
}
//使用结束,释放内存(后面介绍)
free(p);
p = NULL;
}
return 0;
}
123456789101112131415161718192021222324252627
realloc函数调整动态内存大小的时候会有三种情况:
假如我们要将一个大小为50个字节的空间调整为100个字节。
情况一:
此时,realloc函数直接在原空间后方进行扩展,并返回该内存空间首地址(即原来的首地址)。
情况二:
此时,realloc函数会在堆区中重新找一块满足要求的内存空间,把原空间内的数据拷贝到新空间中,并主动将原空间内存释放(即还给操作系统),返回新内存空间的首地址。
情况三:
此时,需扩展的空间后方没有足够的空间可供扩展,并且堆区中也没有符合需要开辟的内存大小的空间。结果就是开辟内存失败,返回一个NULL。
free函数 - 避免内存泄漏
void free( void *memblock );
1
free函数的作用就是将malloc、calloc以及realloc函数申请的动态内存空间释放,其释放空间的大小取决于之前申请的内存空间的大小。
其使用方式非常简单,就是在使用完后加上两个语句:
free(p);//p为要释放的代码块的首地址
p = NULL;//必不可少
12
我们也已经看到了,上面每一个开辟了动态内存的代码,在使用完该动态内存后,都将该内存空间释放了(即还给操作系统)。
如果在使用完动态内存后忘记将其空间释放,便会造成内存泄漏的问题:
- 内存泄漏(MemoryLeak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
- 内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。
注意:
- 在释放代码块后,必须将该代码块的首地址改为NULL,否则该指针将变为野指针,我们都知道,野指针非常危险。
- 如果传入free函数的为空指针(NULL),则free函数什么也不做。
常见的动态内存错误
一、对NULL指针进行解引用操作
我们都知道,不能对NULL指针进行解引用操作,但是如果我们在调用了malloc、calloc以及realloc函数之后,没有检测返回的指针的有效性(即是否为NULL指针),那我们在后面使用该指针的时候就可能会导致对NULL指针进行解引用操作。
二、对动态开辟空间的越界访问
我们需要时刻注意,不能访问未申请的动态内存空间。比如你向动态内存申请了10个字节,那就绝不能访问第11个字节。
三、对非动态开辟的内存使用free释放
free函数只能释放动态开辟的内存空间。
四、使用free释放动态开辟内存的一部分
free函数只能从开辟好的动态内存空间的起始位置开始释放,所以使用free函数释放动态内存时,传入的指针必须是当时开辟内存时返回的指针。
五、对同一块内存多次释放
对同一块动态内存空间只能释放一次。避免这个问题的出现也很简单,我们只要记住在第一次释放完空间后立即将该指针置为NULL即可,因为当传入free函数的指针为NULL指针时,free函数什么也不做(也就不会出现对同一内存多次释放的问题)。
六、动态开辟内存忘记释放(内存泄漏)
一定要做到自己开辟的动态内存自己记得释放,也许你觉得这件事没什么,但当你需要从几十万甚至几百万行代码中找出一个因忘记释放动态内存而造成的内存泄漏问题时,你就会真正知道这件事的重要性。
free函数只能从开辟好的动态内存空间的起始位置开始释放,所以使用free函数释放动态内存时,传入的指针必须是当时开辟内存时返回的指针。
五、对同一块内存多次释放
对同一块动态内存空间只能释放一次。避免这个问题的出现也很简单,我们只要记住在第一次释放完空间后立即将该指针置为NULL即可,因为当传入free函数的指针为NULL指针时,free函数什么也不做(也就不会出现对同一内存多次释放的问题)。
六、动态开辟内存忘记释放(内存泄漏)
一定要做到自己开辟的动态内存自己记得释放,也许你觉得这件事没什么,但当你需要从几十万甚至几百万行代码中找出一个因忘记释放动态内存而造成的内存泄漏问题时,你就会真正知道这件事的重要性。