深入浅出C语言:(九)存储类别、链接和内存管理

目录

一、存储类别---暂不更新

二、随机数函数和静态变量

1、rand() 函数的用法

2、rand() 函数---随机数的本质

3、srand() 函数---重新播种

4、生成一定范围内的随机数

5、连续生成随机数

三、分配内存:malloc()和free()

1、malloc()函数

2、free()函数

3、calloc()函数

四、ANSI C类型限定符

1、const类型限定符:

2、volatile类型限定符

3、restrict类型限定符

一、存储类别---暂不更新

二、随机数函数和静态变量

1、rand() 函数的用法

在 C 语言中,在 <stdlib.h> 头文件中的 rand() 函数来生成随机数,它的用法为:

int rand (void);

rand() 会随机生成一个位于 0 ~ RAND_MAX 之间的整数。
RAND_MAX 是 <stdlib.h> 头文件中的一个宏,它用来指明 rand() 所能返回的随机数的最大值

#include <stdio.h>
#include <stdlib.h>

int main()
{
int a = rand();
printf("%d\n",a);
return 0;
}

运行结果举例:
41

2、rand() 函数---随机数的本质

多次运行上面的代码,你会发现每次产生的随机数都一样。实际上, rand() 函数产生的随机数是伪随机数,是根据一个数值按照某个公式推算出来的,这个数值我们称之为“种子”。种子和随机数之间的关系是一种正态分布,如下图所示:

种子在每次启动计算机时是随机的,但是一旦计算机启动以后它就不再变化了;也就是说,每次启动计算机以后,种子就是定值了,所以根据公式推算出来的结果(也就是生成的随机数)就是固定的。

3、srand() 函数---重新播种

void srand (unsigned int seed);

它需要一个 unsigned int 类型的参数。在实际开发中,我们可以用时间作为参数,只要每次播种的时间不同,那么生成的种子就不同,最终的随机数也就不同。
使用 <time.h> 头文件中的 time() 函数即可得到当前的时间(精确到秒)

srand((unsigned)time(NULL));
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() 
{
int a;
srand((unsigned)time(NULL));
a = rand();
printf("%d\n", a);
return 0;
}

多次运行程序,会发现每次生成的随机数都不一样了。这些随机数会有逐渐增大或者逐渐减小的趋势,这是因为我们以时间为种子,时间是逐渐增大的,结合上面的正态分布图,很容易推断出随机数也会逐渐增大或者减小。

4、生成一定范围内的随机数

如果要规定上下限:

int a = rand() % m + n;  产生 n~n+m 的随机数

分析:取模即取余, rand()%m+n我们可以看成两部分: rand()%m是产生 0~m的随机数,后面+n保证 a 最小只能是 n,最大就是 n+m。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()
{
int a;
srand((unsigned)time(NULL));
a = rand() % 2+ 5;
printf("%d\n",a);
return 0;
}

5、连续生成随机数

       有时候我们需要一组随机数(多个随机数),该怎么生成呢?很容易想到的一种解决方案是使用循环,每次循环都重新播种,请看下面的代码:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h> //Sleep(1000);延时1s的头文件 

int main() 
{
int a, i;
//使用for循环生成10个随机数
for (i = 0; i < 10; i++) 
{	
srand((unsigned)time(NULL));
a = rand();
printf("%d ", a);
Sleep(1000);

}
return 0;
}

for 循环运行速度非常快,在一秒之内就运行完成了,而 time() 函数得到的时间只能精确到秒,所以每次循环得到的时间都是一样的,这样一来,种子也就是一样的,随机数也就一样了。所以我们必须加一个1s的延时

三、分配内存:malloc()和free()

1、malloc()函数

       其函数原型为void *malloc(unsigned int size);其作用是在内存的动态存储区中分配一个长度为size的连续空间。此函数的返回值是分配区域的起始地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的开头位置。

       如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。

函数的使用模型

int * ptd;
ptd = (int *)malloc(max * sizeof(int))

说明:
1、(int *) 表示强制转换,如果是char类型,就变成了(char *)。
2、sizeof(int)求出int类型的所占的内存,max表示要存max个数据。
3、max * sizeof(int)表示要存max个int型数据所占内存。
4、ptd =  返回的内存的首地址

判断是否请求内存成功

int * ptd;
ptd = (int *)malloc(max * sizeof(int))

if(ptd == NULL)
{
  puts("内存申请失败!")
  exit(EXIT_FAILURE);   在头文件 stdlib.h里,表示程序异常终止
}

2、free()函数

经常与malloc搭配使用,使用方式如下

int * ptd;
ptd = (int *)malloc(max * sizeof(int))

if(ptd == NULL)
{
  puts("内存申请失败!")
  exit(EXIT_FAILURE);   在头文件 stdlib.h里,表示程序异常终止
}

当不用这块内存时

free(ptd);  释放内存

3、calloc()函数

free()函数也可以用于calloc()申请的内存的释放。

int * ptd;
ptd = (int *)calloc(max ,sizeof(int))

if(ptd == NULL)
{
  puts("内存申请失败!")
  exit(EXIT_FAILURE);   在头文件 stdlib.h里,表示程序异常终止
}

当不用这块内存时

free(ptd);  释放内存

第一个参数是所需存储单元的数量;第二个参数是存储单元的大小(以字节为单位)。

四、ANSI C类型限定符

1、const类型限定符:

① const定义变量,它的值不能被改变,在整个作用域中都保持固定。

② const 和指针

const int *p1;
int const *p2;
int * const p3;

        在最后一种情况下,指针是只读的,也就是 p3 本身的值(:p3只能指向那一个地址,不可改变其他地址)不能被修改;在前面两种情况下,指针所指向的数据:指针指向的某个地址的值,不可以改变,但是指针可以指向别的地址)只读的,也就是 p1、 p2 本身的值可以修改(指向不同的数据),但它们指向的数据不能被修改。
        指针本身它指向的数据都是只读的:

const int * const p4;
int const * const p5;

        const 离变量名近就是用来修饰指针变量的离变量名远就是用来修饰指针指向的数据,如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据。

注意:const修饰的变量不允许这里修改不代表不允许别处修改,比如

#include <stdio.h>

int main() 
{
int n = 5;
const int* p = &n;
*p = 6;           不可以;
n = 7;            完全可以,而且那个“const”的“*p”也跟着变成了7。
printf("%d",i);
return 0;
}

③ const 和函数形参

const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。

int strcmp ( const char * str1, const char * str2 );
char * strcat ( char * destination, const char * source );

       不能将 const char *类型的数据赋值给 char *类型的变量。但反过来是可以的,编译器允许将 char *类型的数据赋值给 const char *类型的变量。这种限制很容易理解, char *指向的数据有读取和写入权限,而 const char *指向的数据只有读取权限降低数据的权限不会带来任何问题,但提升数据的权限就有可能发生危险。
 

2、volatile类型限定符

① volatile存在的意义:

val = x ;

一些不使用x的代码

val1 = x;

智能的编译器会注意到以上代码使用了两次x,但并未改变他们的值,于是编译器会把x的值临时存在寄存器中,第二次用到的时候,再从寄存器读出。而此时在其他代理可能改变了x的值,就会造成错误(简单来说,可能是单片机自己改变的),而使用volatile就禁止了编译器对这个变量这么做。

② 可能是毁三观的认识:

   const和volatile放在一起的意义在于:

  (1)本程序段中不能对a作修改,任何修改都是非法的,或者至少是粗心,编译器应该报错,防止这种粗心;

  (2)另一个程序段则完全有可能修改,因此编译器最好不要做太激进的优化。

        “const”含义是“请做为常量使用”,而并非“放心吧,那肯定是个常量”

        “volatile”的含义是“请不要做没谱的优化,这个值可能变掉的”,而并非“你可以修改这个值”

         因此,它们本来就不是矛盾的。const修饰的变量不允许这里修改不代表不允许别处修改,比如:

#include <stdio.h>

int main() 
{
int n = 5;
const int* p = &n;
*p = 6;           不可以;
n = 7;            完全可以,而且那个“const”的“*p”也跟着变成了7。
printf("%d",i);
return 0;
}

对于非指针非引用的变量,const volatile同时修饰的意义确实不大。

两者同时修饰一个对象的典型情况,是用于驱动中访问外部设备的只读寄存器

留一个问题:const volatile int i=10;这行代码有没有问题?如果没有,那 i 到底是什么 属性?

回答一:没有问题,例如只读的状态寄存器。它是volatile,因为它可能被意想不到地改变;它是const,因为程序不应该试图去修改它。volatile和const并不矛盾,只是控制的范围不一样,一个在程序本身之外,另一个是程序本身。

回答二:没问题,const和volatile这两个类型限定符不矛盾。const表示(运行时)常量语义:被const修饰的对象在所在的作用域无法进行修改操作,编译器对于试图直接修改const对象的表达式会产生编译错误。volatile表示“易变的”,即在运行期对象可能在当前程序上下文的控制流以外被修改(例如多线程中被其它线程修改;对象所在的存储器可能被多个硬件设备随机修改等情况):被volatile修饰的对象,编译器不会对这个对象的操作进行优化。一个对象可以同时被const和volatile修饰,表明这个对象体现常量语义,但同时可能被当前对象所在程序上下文意外的情况修改。另外,LS错误,const可以修饰左值,修饰的对象本身也可以作为左值(例如数组)。

只读寄存器:我们只能编写程序读出只读寄存器中值,但是我们无法改变它,这就是const在起作用,可是,只读寄存器读出来的值可能每次都不一样,这就是volatile在起作用。

3、restrict类型限定符

       restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问该对象唯一且初始的方式。要弄明白为什么这样做有用,先看几个例子。考虑下面的代码:

int ar[10];
int * restrict restar= (int *) malloc(10 * sizeof(int));
int * par= ar;

       这里,指针restar是访问由malloc()所分配的内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不用把它设置成restrict。

       现在考虑下面稍微复杂的例子,其中n是int类型:

for (n=0; n<10; n++)
  {
    par[n]+=5;
    restar[n] +=5;
    ar[n] *=2;
    par[n] +=3;
    restar[n] +=3;
  }

       由于之前声明了restar是访问它所指向的数据块的唯一且初始的方式,编译器可以把涉及restar的两条语句替换成下面的语句,效果相同:

restar[n] +=8;/*可以进行替换*/

       但是,如果把与par相关的两条语句替换成下面的语句,将导致计算错误:

par[n] +=8;/*将给出错误的结果*/

       这是因为for循环在par两次访问相同的数据之间,用ar改变了该数据的值。

       在本例中,如果未使用restrict关键字,编译器必须假定最坏的情况(即,两次使用指针之间,其他的标识符可能已经改变了该数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。

       restrict限定符还可以用于函数形参中的指针。这意味着编译器可以假定该函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。例如,C库有两个函数用于把一个位置上的字节拷贝到另一个位置。在C99中,这两个函数的原型是:

void * memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void * s1, const void * s2,size_t n);

       这两个函数都从位置s2把n个字节拷贝到位置s1。memcpy()函数要求两个位置不重叠,但是memove()没有这样的要求。声明s1和s2为restrict说明这两个指针是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足memcpy()函数无重叠的要去。memmove()允许重叠,它在拷贝数据时不得不更小心,以防止在使用数据之前就先覆盖了数据。

总结

       restrict关键字有两个读者。一个是编译器,该关键字告诉编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数,总而言之,编译器不会检查用户是否遵循这一限制,但是无是它可能产生严重后果。

发布了350 篇原创文章 · 获赞 684 · 访问量 38万+

猜你喜欢

转载自blog.csdn.net/qq_38351824/article/details/102131355