C 類型限定符

C 類型限定符

我們通常用類型和存儲類別來描述一個變量。C90還新增了兩個屬性:恆常性(constancy)和易變性(volatility)。這兩個屬性可以分別使用關鍵字constvolatile來聲明,以這兩個關鍵字創建的類型是限定類型(qualified type)。C99標準新增了第3個限定符:restrict,用於提高編譯器優化。C11標準新增了第四個限定符:_Atomic。C11提供一個可選庫,有stdatomic.h管理,以支持併發程序設計,而且_Atomic是可選支持項。

C99爲類型限定符增加了一個新屬性:冪等性(idempotent),表示可以再一條聲明中多次使用同一個限定符,多餘的限定符將被忽略:

const const const int n = 6; // 等同於 const int n = 6;

const 類型限定符(C90)

const關鍵字聲明的對象,其值不能通過賦值或遞增、遞減來修改。在ANSI兼容的編譯器中,以下代碼

const int nochange;  // 限定nochange的值不能被修改
nochange = 12;       // 不允許

編譯器會報錯。但是可以初始化const變量,即下面代碼沒問題

const int nochage = 12; // 編譯通過

該聲明讓nochange成爲只讀變量。初始化後,就不能在改變它的值。

在指針和形參聲明中使用const

const限定符可以用來修飾普通變量、數組和指針,但修飾指針時要區分是限定指針本身爲const還是限定指針指向的值爲const

const float *pf; // pf指向一個float類型的const值。 pf指向的值不能被改變,pf本身的值可以改變。
float const *pfc; // 等同於 const float *pfc;
float * const pt; // pt是一個const指針。 pt本身的值不能改變,但其所指向的值可以改變。
const float * const ptr; // 表明ptr本身不能改變,其指向的值也不能改變。

如註釋所示,把const放在類型名之後、*之前,表明該指針不能用於改變它所指向的值。簡而言之,const放在*左側任意位置,限定了指針指向的數據不能改變;const放在*的右側,限定了指針本身不能改變。

const關鍵字的常見用法是聲明爲函數形參的指針。

void display(const int array[], int limit); // array數組不會被修改。const int array[] 等同於 const int *array

對全局數據使用const

全局變量會暴露數據,導致程序的任何部分都能更改數據。如果把數據設置爲const,就可避免這樣的危險。因此,用const限定符聲明全局數據很合理,可以創建const變量、const數據和const結構

文件間共享const數據有兩個策略

遵循外部變量的常用規則

即在一個文件中使用定義式聲明,在其他文件使用引用式聲明(用extern關鍵字)

const變量放在都文件,然後其他文件包含頭文件

這種方案必須在頭文件中用關鍵字static聲明全局const變量,如果去掉static,那麼包含頭文件的每個文件都會有一個相同標識符的定義式聲明,即每個文件都有一個單獨的數據副本,由於每個副本只對該文件可見,故無法用這些數據和其他文件通信。

頭文件的好處是,方便你偷懶,不用惦記着在一個文件中使用定義式聲明,在其他文件中使用引用式聲明。所有文件都只需包含同一個頭文件。但是它的缺點是,數據是重複的。對於簡單的數據而言,這沒有問題,但是如果const數據包含龐大的數組,就不能視而不見了。

C中的constC++中的const有何異同?

未完待續

Cdefineconst的區別?

  • 編譯器處理方式不同

    define宏定義在預處理階段展開,而const常量是在編譯運行階段使用

  • 類型和安全檢查不同

    define宏定義沒有類型,不做任何類型檢查,僅僅是替換展開

    const常量有具體的類型,在編譯階段會執行類型檢查

  • 內存分配不同

    define宏僅僅是展開,不會分配內存

    const常量會在內存中分配空間,可以是堆或是棧

  • 常量和只讀變量

    C中定義常量是用enum或者define宏,常量不需要分配內存空間

    const修飾的是隻讀變量,不是常量,不可以作爲數組維數,也不能放在case關鍵字後面。

volatile 類型限定符(C90)

volatile本意爲易變的,表明代理(非變量所在程序)可以改變該變量的值。通常編譯器爲了優化減少讀取內存會將變量由內存讀入寄存器。但如果使用volatile聲明變量時,系統總是重新內存讀取數據,即不進行優化。如果不適用volatile,則編譯器可能會聲明語句進行優化。簡單說就是volatile關鍵字影響編譯器編譯結果,volatile聲明的變量表明該變量隨時可能變化,相關運算不要優化,以免出錯。

假設有以下代碼

val1 = x;
/* 一些不適應x的代碼 */
val2 = x;

智能的(進行優化的)編譯器會注意到以上代碼使用了兩次x,但並未改變它的值。於是編譯器把x的值臨時儲存在寄存器中,然後val2使用x時,從寄存器中(而不是原始內存位置上)讀取x的值,以節約時間。這個過程被稱爲高速緩存(caching)。通常,高速緩存是個不錯的優化方案,但是如果一些其他代理在以上兩條語句之間改變了x的值,就不能這樣優化了。如果沒有volatile關鍵字,編譯器會假定變量的值在使用過程中不變,嘗試優化代碼。

可以同時是有constvolatile限定一個值。例如,通常用const把硬件時鐘設置爲程序不能更改的變量,但是可以通過代理改變,這時用1volatile。只能在聲明中同時使用這兩個限定符,順序不重要

volatile const int loc;
const volatile int * ploc;

restrict 類型限定符(C99)

restrict關鍵字允許編譯器優化某部分代碼以更好的支持計算。它只能用於指針,表明該指針是訪問數據對象的唯一且初始的方法。假設下列代碼

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

這裏,指針restar是訪問由malloc()所分配內存的唯一且初始的方式。因此,可以用restrict關鍵字限定它。而指針par既不是訪問ar數組中數據的初始方式,也不是唯一方式。所以不用把它設置爲restrict

現在考慮下面稍複雜的例子,其中nint類型

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);

這兩個函數都從位置s2n個字節拷貝到位置s1memcpy()函數要求兩個位置不重疊,但是memove()沒有這樣的要求。聲明s1s2restrict說明這兩個指針是訪問相應數據的唯一方式,所以它們不能訪問相同塊的數據。這滿足memcpy()函數無重疊的要求。memmove()允許重疊,它在拷貝數據時不得不更小心,以防止在使用數據之前就先覆蓋了數據。

restrict關鍵字有兩個讀者。一個是編譯器,該關鍵字告知編譯器可以自由假定一些優化方案。另一個讀者是用戶,該關鍵字告知用戶要使用滿足restrict要求的參數。總而言之,編譯器不會檢查用戶是否遵循這一限制,但是無視它後果自負。

_Atomic 類型限定符(C11)

併發程序設計把程序執行分成可以同時執行的多個線程。這個程序設計帶來了新的挑戰,包括如何管理訪問相同數據的不同線程。C11通過包含可選的頭文件stdatomic.hthreads.h ,提供了一些可選的(不是必須實現的)管理方法。值得注意的是,要通過各種宏函數來訪問院子類型。當一個線程對一個原子類型的對象執行原子操作時,其他線程不能訪問該隊形。例如,下面的代碼:

int hogs; // 普通聲明
hogs = 12; // 普通賦值
// 替換爲
_Atomic int hogs; // hogs是一個原子類型的變量
atomic_store(&hogs, 12); // stdatomic.h 中的宏

這裏,在hogs中儲存12是一個原子過程,其他線程不能訪問hogs

編寫這種代碼的前提是,編譯器支持這一新特性。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章