天天看點

深入淺出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() 函數産生的随機數是僞随機數,是根據一個數值按照某個公式推算出來的,這個數值我們稱之為“種子”。種子和随機數之間的關系是一種正态分布,如下圖所示:

深入淺出C語言:(九)存儲類别、連結和記憶體管理

種子在每次啟動計算機時是随機的,但是一旦計算機啟動以後它就不再變化了;也就是說,每次啟動計算機以後,種子就是定值了,是以根據公式推算出來的結果(也就是生成的随機數)就是固定的。

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;
}      
深入淺出C語言:(九)存儲類别、連結和記憶體管理
深入淺出C語言:(九)存儲類别、連結和記憶體管理

多次運作程式,會發現每次生成的随機數都不一樣了。這些随機數會有逐漸增大或者逐漸減小的趨勢,這是因為我們以時間為種子,時間是逐漸增大的,結合上面的正态分布圖,很容易推斷出随機數也會逐漸增大或者減小。

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;
}      
深入淺出C語言:(九)存儲類别、連結和記憶體管理
深入淺出C語言:(九)存儲類别、連結和記憶體管理

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()允許重疊,它在拷貝資料時不得不更小心,以防止在使用資料之前就先覆寫了資料。

繼續閱讀