C Primer Plus 第12章 12.7 ANSI C的類型限定詞

您已經知道一個變量是以它的類型與存儲類表徵的。C90增長了兩個屬性:不變性和易變性。這些屬性是經過關鍵字const和volatile聲明的,這樣就建立了受限類型(qualified type)。C99標準添加了第三個限定詞resrict,用以方便編譯器優化。數組

C99授予類型限定詞一個新屬性:它們如今是冪等的(idempotent)!這聽起來像一個強大的功能,其實只意味着能夠在一個聲明 中不止一次地使用同一限定詞,多餘的將被忽略掉:緩存

const const const int n = 6;  //至關於const int n = 6;安全

例如,這使下列序列能夠被接受:ide

typedef const int zip;函數

const zip q=8;優化

12.7.1  類型限定詞constspa

回顧一下,若是變量聲明中帶有關鍵字const,則不能經過賦值、增量或減量運算來修改該變量的值。在與ANSI 編譯器中,下面的代碼將產生一個錯誤信息:代理

const int nochange;  //把m限定爲常量指針

nochange = 12;   //不容許rest

然而,能夠初始化一個const變量。所以,下面的代碼是正確的:

const int nochange = 12; //能夠

上面的聲明使nochange成爲一個只讀變量。在初始化之後,不能夠再改變它。

例如,能夠用關鍵字const建立一組程序不能夠改變的數據:

const int days1[12] = {31,28,31,30,31,30,31,31,30,31,30,31};

1、在指針和參量聲明中const

指針要複雜一些,由於不得不把讓指針成爲const與讓指針指向的值成爲const區分開來。下面的聲明代表pf指向的值必須是不變的:

const float * pf;  /*pf指向一個常量浮點數值*/

但pf自己的值能夠改變。例如,它能夠指向另外一個const值。相反,下面的聲明代表指針pt自己的值不能夠改變:

float * const pt;  /*pt是一個常量指針*/

它必須老是指向同一個地址,但所指向的值能夠改變。最後,下面的聲明:

const float * const ptr;

意味着ptr必須老是指向同一個位置,而且它所指位置存儲的值也不能改變。

還有第三种放置const關鍵字的方法:

float const * pfc;  //等同於const float * pfc;

總而言之,一個位於*左邊任意位置的const使得數據成爲常量,而一個位於*右邊的const使得指針自身成爲常量。

這個新關鍵字的一個常見用法是聲明做爲函數形式參量的指針。例如,假定一個名爲display()的函數顯示一個數組的內容。爲了使用它,您可能會把數組名做爲實際參數傳送,但數組名是一個地址,這樣作將容許函數改變調用函數中的數據。下面的原型防止了這樣的狀況發生:

void display(const int array[],int limit);

在函數原型和函數頭部,參量聲明const int  array[]與const int * array相同,所以該聲明代表array指向的數據是不可變的。

ANSI C庫遵循這一慣例。若是指針只是用來記函數訪問值,將把它聲明爲const受限指針。若是指針被用來改變調用函數中的數據,則不使用關鍵字const。例如,ANSI C 中的strcat()聲明以下:

char * strcat(char *,const char *);

回憶下,函數strcat()在第一個字符串的末尾處添加第二個字符串的一份拷貝。這改變了第一個字符串,但不改變第二個字符串。該聲明也體現了這一點。

2、對全局數據使用const

回憶一下,使用全局變量被認爲是一個冒險的方法,由於它暴露了數據,使程序的任何部分均可以錯誤地修改數據。若是數據是const的,這種危險就不存在了,所以對全局數據使用const限定詞是很合理的。

然而,文件之間共享const數據時要當心。可使用兩個策略首先是遵循外部變量的慣用規則:在一個文件中進行定義聲明,在其餘文件中進行引用聲明(關鍵字extern):

/*file 1.c  --定義一些全局常量*/
const double PI = 3.14159;
const char * MONTHS[12] = 
{"January","February","March","April","May","June","July",
"August","September","October","November","December"};

/*file 2.c --使用在其餘文件中定義的全局常量*/
extern const double PI;
extern const char * MONTHS[];

其次是將常量放在一個include文件中。這時還必須使用靜態外部存儲類:

/*constant.h  --定義一些全局常量*/
static const double PI = 3.14159;
static const char * [12] = 
{...};

/*file1.c --使用在其餘文件中定義的全局常量*/
#include "constant.h"

/*file2.c --使用在其餘文件中定義的全局常量*/
#include "constant.h"

若是不使用關鍵字static,在文件file1.c和file2.c中包含constant.h將致使每一個文件都有同一標識符的定義聲明,ANSI C 不支持這樣作。經過使每一個標識符成爲靜態的外部的,實際上給了每一個文件一個獨立的數據拷貝。若是文件想使用該數據與另外一文件通話,這樣作就不行了,由於每一個文件都只能看到它本身的拷貝。然而,因爲數據是不變的(const)和相同的(經過使兩個文件都包含一樣的頭文件),這就不是問題了。

使用頭文件的好處是不用惦記着在一個文件中進行定義聲明,在下一個文件中進行引用聲明;所有文件都包含同一個頭文件。缺點在於複製了數據。在前述的例子中,這不構成一個問題,但若是常量數據包含着巨大的數組,它可能就是一個問題了。

12.7.2  類型限定詞

限定詞volatile告訴編譯器該變量除了可被程序改變外還能夠被其餘代理改變。典型地,它被用於硬件地址和其餘並行運行的程序共享的數據 。例如,一個地址中可能保存着當前的時鐘時間。無論程序作什麼,該地址的值都會隨着時間而改變。另外一種狀況是,一個地址被用來接收來自其餘計算機的信息。

語法同const

volatile int locl;    /*  locl是一個易變的位置*/

volatile int ploc;  /*ploc指向一個易變的位置*/

這些語句聲明locl是一個volatile值,而且ploc指向一個volatile值。

您可能認爲volatile是一個有趣的概念,但您也可能奇怪爲何ANSI C 以爲有必要把volatile做爲一個關鍵字。緣由是它能夠方便編譯器優化。例如,假定有以下代碼:

val1=x;

    /*一些不使用x的代碼x*/

val2=x;

一個聰明的(優化的)編譯器可能注意到您兩次使用了x,而沒有改變它的值。它將把x臨時存儲在一個寄存器中。接着當VAL2須要x時,可能經過寄存器而非初始的內存位置中讀取該值以節省時間。這個過程被稱爲緩存(caching)。一般,緩存是一個好的優化方式,但若是在兩個語句間其餘代理改變了x的話就不是這樣了。若是沒有規定volatile關鍵字,編譯器將無從得知這種改變是否可能發生。所以,爲了安全起見,編譯器不使用緩存。那是在ANSI之前的情形。然而如今,若是在聲明中沒有使用volatile,編譯器就能夠假定一個值在使用過程當中沒有被修改,它就能夠試着優化代碼 。

一個值能夠同時是const和volatile。例如,硬件時鐘通常設定爲不能由程序改變,這一點使它成爲const;但它被程序之外的代理改變,這使它成爲volatile的。只需在聲明中同時使用這兩個限定詞,以下所示:順序並不重要:

volatile  const  int loc;

const volatile int * ploc;

12.7.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 += 5; 
    ar[n] *= 2;
    par[n] += 3;
    restar[n] += 3;
}

知道了restar是訪問它所指向數據塊的唯一初始方式,編譯器就能夠用具備一樣效果的一條語句來代替包含restar的兩個語句:

restar[n] += 8;    /*能夠進行替換*/

然而,將兩個包含par的語句精簡爲一個語句將致使計算錯誤:

par[n] += 8;    /*給出錯誤的結果*/

出現錯誤結果的緣由是循環在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()要求兩個位置之間不重疊,但memmove()沒有這個要求。把s1和s2聲明爲restrict意味着每一個指針都是相應數據的唯一訪問方式,所以它們不能訪問同一數據塊。這知足了不能有重疊 的要求。函數memmove()容許重疊,它不得不在複製數據時更加當心,以防在使用數據前就覆蓋了數據。

關鍵字restrict有兩個讀者,一個是編譯器,它告訴編譯器能夠自由地作一些有關優化的假定。另外一個讀者是用戶,它告訴用戶僅使用知足restrict要求的參數。通常,編譯器沒法假定您是否遵循了這一限制,若是您蔑視它也就是在讓本身冒險。

12.7.4  舊關鍵字的新位置

C99容許將類型限定詞和存儲類限定詞static放在函數原型和函數頭部的形式參量所屬的初始方括號內。對於類型限定詞的情形,這樣作爲已有功能提供了一個可選語法。例如,下面是一個使用舊語法的聲明:

void ofmouth( int * const a1, int * restrict a2, int n)  //之前的風格

它代表a1是一個指向int的const指針。回憶一下,這意味着該指針是不變的,而不是它所指向的數據不變。還代表a2是一個受限指針,如上一節所述。等價的新語法以下:

void ofmouth( int a1[const], int a2 [restrict], int n);  //C99容許

static的情形是不一樣的,由於它引起了一些新問題。例如,考慮以下原型:

double stick (double ar[static 20]);

使用static代表在函數調用中,實際參數將是一個指向數組首元素的指針,該數組至少具備20個元素。這樣的目的是容許編譯器使用這個信息來優化函數的代碼。

與restrict相同,關鍵字static有兩個讀者,一個是編譯器,它告訴編譯器能夠自由地作一些有關優化的假定。另外一個是用戶,它告訴用戶僅使用知足static要求的參數。

相關文章
相關標籤/搜索