本章中,你將學習物件、函式和型別,我們將研究如何宣告變數(有識別符號的物件)和函式,獲取物件的地址,并對這些物件指標的解參考,你已經看到了C語言程式員可用的一些型別, C語言中的型別不是物件就是函式,
物件、函式、型別和指標
物件是你可以表示數值的存盤,準確地說,C標準(ISO/IEC 9899:2018)將物件定義為 "執行環境中的資料存盤區域,其內容可以代表數值",并補充說明,"當被參考時,物件可以被解釋為具有特定型別",變數是物件的例子,
變數會宣告的型別,告訴你它的值代表哪種物件,例如型別為int的物件包含整數值,型別很重要,因為代表一種型別的物件的位元集合,如果被解釋為不同型別的物件,可能會有不同的值,例如,數字1在IEEE 754(IEEE浮點運算標準)中由位元模式0x3f800000(IEEE 754-2008)表示,但是,如果你把這個相同的位元模式解釋為一個整數,你會得到1,065,353,216的值,而不是1,
函式不是物件,但確實有型別,
C語言也有指標,它在地址--記憶體中存盤物件或函式的位置,指標型別是由參考型別的函式或物件型別派生出來的,從被參考型別T派生出來的指標型別被稱為對T的指標,
宣告變數
宣告變數時,需要指定型別,并提供名稱用來參考該變數,
可以一行宣告多個變數,但如果變數是指標或陣列,或者變數是不同的型別,這樣做就會引起混亂,下面的宣告都是正確的,
char *src, c;
int x, y[5];
int m[12], n[15][3], o[21];
第一行宣告了兩個變數,src和c,它們的型別不同,src變數的型別為char *,而c的型別為char,第二行再次宣告了兩個型別不同的變數,x和y,變數x的型別是int,y是由五個元素組成的陣列,型別是int,第三行宣告了三個陣列-m、n和o-具有不同的尺寸和元素數,
一行一種型別的宣告可讀性會更好:
char *src; // src has a type of char *
char c; // c has a type of char
int x; // x has a type int
int y[5]; // y is an array of 5 elements of type int
int m[12]; // m is an array of 12 elements of type int
int n[15][3]; // n is an array of 15 arrays of 3 elements of type int
int o[21]; // o is an array of 21 elements of type int
實體:交換值1
在{ }字符之間有代碼塊稱為復合陳述句,我們在主函式中定義了兩個變數,a和b,我們宣告這些變數的型別為int,并將它們分別初始化為21和17,每個變數都必須有一個宣告,然后主函式呼叫swap函式來嘗試交換這兩個整數的值,
#include <stdio.h>
void swap(int a, int b) {
int t = a;
a = b;
b = t;
printf("swap: a = %d, b = %d\n", a, b);
}
int main(void) {
int a = 21;
int b = 17;
swap(a, b);
printf("main: a = %d, b = %d\n", a, b);
return 0;
}
區域變數,如清單2-1中的a和b,具有自動存盤期限,這意味著它們一直存在,直到執行離開它們被定義的塊,我們將嘗試交換存盤在這兩個變數中的值,
swap函式宣告了兩個引數,a和b,你用它們來向這個函式傳遞引數,我們還在交換函式中宣告了int型別的臨時變數t,并將其初始化為a的值,這個變數用于臨時保存a中存盤的值,以便在交換程序中不會丟失,
執行結果
$ ./a.out
swap: a = 17, b = 21
main: a = 21, b = 17
變數a和b分別被初始化為21和17,在swap函式中對printf的第一次呼叫顯示這兩個值被交換了,但在main中對printf的第二次呼叫顯示原始值沒有變化,
C語言是傳值的語言,傳參時引數的值被復制到一個單獨的變數中,以便在函式中使用,swap函式將你作為引數傳遞的物件的值分配給各自的引數,當函式中的引數值發生變化時,呼叫方的值不會受到影響,因為它們是不同的物件,因此,在第二次呼叫printf時,變數a和b在main中保留了它們的原始值,
實體:交換值2
我們使用指示符(*)來宣告指標
#include <stdio.h>
int swap(int *_a, int *_b) {
int tmp = *_a;
*_a = *_b;
*_b = tmp;
}
int main(void) {
int a = 21;
int b = 17;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
清單2-3:修改后的使用指標的交換函式
當在函式宣告或定義中使用時,作為指標宣告器的一部分,表示引數是指向特定型別的物件或函式的指標,注意_a表示指標,_a表示指標指向的值,&獲取運算子的地址,
執行結果
$ ./a.out
a = 17, b = 21
變數a和b分別被初始化為21和17,然后代碼將這些物件的地址作為引數傳遞給交換函式,
在swap函式中,引數_a和_b現在都被宣告為int的指標型別,并且包含了從呼叫函式(在本例中,main)傳遞給swap的引數的副本,這些地址副本仍然指向完全相同的物件,所以當它們所參考的物件的值在交換函式中被交換時,在main中宣告的原始物件的內容也被訪問并被交換,這種方法通過生成物件地址,通過值傳遞這些地址,然后通過地址來訪問原始物件,即傳址,
范圍
物件、函式、宏和其他C語言識別符號都有范圍,它限定了它們可以被訪問的連續區域,C語言有四種型別的范圍:檔案、塊、函式原型和函式,
物件或函式識別符號的范圍是由它的宣告位置決定的,如果宣告在任何塊或引數串列之外為檔案范圍;如果宣告出現在塊內或引數內,只能在該塊內訪問,
如果宣告出現在函式原型的引數宣告串列中(不是函式定義的一部分),那么識別符號具有函式原型作用域,它終止于函式宣告末端; 函式范圍是指函式定義的開頭{和結尾}之間的區域,標簽名是唯一一種函式作用域的識別符號,標簽是識別符號,后面有一個冒號,用來標識函式中的一個陳述句,控制權可以被轉移到這個陳述句中,
作用域可以被嵌套,有內部和外部作用域,內層作用域可以訪問外層作用域,但反之不行,如果你在內層作用域和外層作用域中都宣告了同一個識別符號,那么在外層作用域中宣告的識別符號會被內層作用域中的識別符號所隱藏,后者具有優先權,
存盤期限
有四種存盤期限可供選擇:自動、靜態、執行緒和分配,你已經看到,自動存盤期限的物件被宣告在塊中或作為函式引數,這些物件的生命周期從它們被宣告的塊開始執行時開始,到塊的執行結束時結束,
范圍和壽命是完全不同的概念,范圍適用于識別符號,而壽命適用于物件,識別符號的范圍是代碼區域,在這個區域中,識別符號所表示的物件可以通過它的名字被訪問,物件的生命周期是該物件存在的時間段,
在檔案作用域中宣告的物件具有靜態存盤期限,這些物件的生命期是程式的整個執行程序,它們的存盤值在程式啟動前被初始化,你也可以通過使用存盤類指定符static,將塊作用域中的變數宣告為具有靜態存盤期限,如清單2-6中的計數例子所示,這些物件在函式退出后持續存在,
#include <stdio.h>
void increment(void) {
static unsigned int counter = 0;
counter++;
printf("%d ", counter);
}
int main(void) {
for (int i = 0; i < 5; i++) {
increment();
}
return 0;
}
這個程式輸出1 2 3 4 5,我們在程式啟動時將靜態變數counter初始化為0,并在每次呼叫increment函式時將其遞增,計數器的生命周期是程式的整個執行程序,它將在整個生命周期內保留其最后存盤的值,你可以通過用檔案范圍宣告計數器來實作同樣的行為,然而在可能的情況下,限制物件的范圍是一種良好的軟體工程實踐,
靜態物件必須用常量值而不是變數來初始化,
int *func(int i) {
const int j = i; // ok
static int k = j; // error
return &k;
}
常量值指的是字面常量(例如,1、'a'或0xFF)、列舉成員以及alignof或sizeof等運算子的結果,
執行緒存盤持續時間用于并發編程,動態分配的記憶體,
對齊
物件型別有對齊要求,物件可能被分配的地址進行限制,對齊代表了給定物件可以被分配的連續地址之間的位元組數,CPU在訪問對齊的資料(例如,資料地址是資料大小的倍數)和未對齊的資料時可能有不同的行為,
一些機器指令可以在非字的邊界上執行多位元組訪問,但可能會有性能上的損失,字是自然的、固定大小的資料單位,由指令集或處理器的硬體處理,一些平臺不能訪問未對齊的記憶體,對齊要求可能取決于CPU字的大小(通常為16、32或64位),
一般來說,C語言程式員不需要關心對齊要求,因為編譯器為其各種型別選擇合適的對齊方式,對于所有的標準型別,包括陣列和結構,從malloc動態分配的記憶體都需要充分對齊,然而,在極少數情況下,你可能需要覆寫編譯器的默認選擇;例如,在必須從二冪地址邊界開始的記憶體快取行邊界上對齊資料,或者滿足其他系統特定的要求,傳統上,這些要求是通過linker命令來滿足的,或者通過malloc對記憶體進行整體分配,然后將用戶地址向上舍入,或者通過涉及其他非標準設施的類似操作,
C11引入了一種簡單的、向前兼容的機制來指定對齊方式,對齊是以size_t型別的值表示的,每個有效的對齊值都是的2整數次方,每個物件都有默認的對齊要求:更嚴格的對齊(更大的2次方)可以通過對齊指定器(_Alignas)來請求,你可以在宣告的指定器中包個對齊方式的指定器,清單2-7使用對齊指定符來確保good_buff是正確對齊的(bad_buff對于成員訪問運算式可能有不正確的對齊),
struct S {
int i; double d; char c;
};
int main(void) {
unsigned char bad_buff[sizeof(struct S)];
_Alignas(struct S) unsigned char good_buff[sizeof(struct S)];
struct S *bad_s_ptr = (struct S *)bad_buff; // wrong pointer alignment
struct S *good_s_ptr = (struct S *)good_buff; // correct pointer alignment
}
對齊是按從弱到強(也叫嚴格)的順序排列的,
物件型別
我們將介紹布爾型別、字符型別和數字型別(包括整數和浮點型別),
布爾型別
宣告為_Bool的物件只能存盤0和1的值,這種布爾型別是在C99中引入的,并以下劃線開始,以便在已經宣告了自己的識別符號名為bool或boolean的現有程式中加以區分,以下劃線和大寫字母或另一個下劃線開頭的識別符號總是被保留,
如果你包含頭檔案<stdbool.h>,你也可以把這個型別拼成bool,并給它賦值為true(擴展為整數常數1)和false(擴展為整數常數0),在這里,我們使用兩種型別名稱的拼寫來宣告兩個布爾變數:
#include <stdbool.h>
_Bool flag1 = 0;
bool flag2 = false;
兩種拼法都可以使用,但最好使用bool,因為這是語言的長期方向,
字符型別
C語言定義了三種字符型別:char、signed char和unsigned char,每個編譯器的實作都會將char定義為具有相同的對齊方式、大小、范圍、表示方式和行為,即signed char或nsigned char,無論做出什么樣的選擇,char都是獨立的型別,與其他兩種型別都不兼容,
char型別通常用于表示C語言程式中的字符資料,特別是,char型別的物件必須能夠表示執行環境中所需要的最小字符集(稱為基本執行字符集),包括大寫和小寫字母、10位小數、空格字符以及各種標點符號和控制字符,char型別不適合整數資料;使用signed char來表示小的有符號整數值,使用unsigned char來表示小的無符號值,是比較安全的,
基本的執行字符集適合許多傳統的資料處理應用的需要,但它缺乏非英文字母是國際用戶接受的障礙,為了解決這一需要,C標準委員會指定了一種新的、寬的型別,以允許大型字符集,你可以通過使用wchar_t型別將大字符集的字符表示為寬字符,它通常比基本字符占用更多空間,通常情況下,實作者選擇16或32位來表示一個寬字符,C標準庫提供了同時支持窄字符和寬字符型別的函式,
數值型別
C提供了幾種數字型別,可以用來表示整數、列舉器和浮點值,第3章更詳細地介紹了其中的一些型別,但這里是簡單的介紹,
- 整數型別
有符號的整數型別可以用來表示負數、正數和零,有符號的整數型別包括signed char、short int、int、long int和long long int,
除了int本身,在這些型別的宣告中可以省略關鍵字int,所以你可以,例如,用long long而不是long long int來宣告一個型別,
對于每個有符號的整數型別,都有相應的無符號整數型別,使用相同的存盤量:unsigned char、unsigned short int、unsigned int、unsigned long int和unsigned long long int,無符號型別只能用來表示正數和零,
有符號和無符號整數型別用于表示各種大小的整數,每個平臺(當前或歷史)都決定了這些型別的大小,給定了一些約束條件,每個型別都有最小的可表示范圍,這些型別按寬度排序,保證較寬的型別至少和較窄的型別一樣大,這樣,long long int型別的物件可以表示long int型別的物件可以表示的所有值,long int型別的物件可以表示int型別的物件可以表示的所有值,等等,各種整數型別的實際大小可以從<limits.h>頭檔案中指定的各種整數型別的最小和最大可表示值推斷出來,
int型別通常具有執行環境的架構所建議的自然大小,因此在16位架構上,其大小為16位寬,在32位架構上為32位寬,你可以通過使用<stdint.h>或<inttypes.h>頭檔案的型別定義來指定實際寬度的整數,如uint32_t,這些頭檔案還為最寬的可用整數型別提供了型別定義:uintmax_t和intmax_t,
第3章詳細介紹了整數型別,
- 列舉型別
列舉,或稱enum,允許你定義一個型別,在具有可列舉的常量值集的情況下,為整數值分配名稱(列舉器),下面是列舉的例子:
enum day { sun, mon, tue, wed, thu, fri, sat };
enum cardinal_points { north = 0, east = 90, south = 180, west = 270 };
enum months { jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec };
如果你沒有用=運算子給第一個列舉器指定值,那么它的列舉常量的值就是0,而后面每個沒有=的列舉器都會在前面的列舉常量的值上加1,因此,day列舉中sun的值是0,mon是1,以此類推,
你也可以給每個列舉器分配特定的值,如cardinal_points列舉所示,與列舉器一起使用=可能會產生具有重復值的列舉常數,如果你錯誤地認為所有的值都是唯一的,這可能是一個問題,months列舉將第一個列舉器設定為1,每個后續的列舉器如果沒有被特別指定一個值,將被遞增1,
列舉常量的實際值必須可以作為int來表示,但是它的型別是由實作定義的,例如,Visual C++使用有符號的int,而GCC使用無符號的int,
- 浮點型別
C語言支持三種浮點型別:float、double和long double,浮點運算類似于實數運算,并經常被用作實數運算的模型,C語言支持多種浮點表示法,包括在大多數系統上支持IEEE浮點算術標準(IEEE 754-2008),浮點表示法的選擇取決于實作,第3章詳細介紹了浮點型別,
- void型別
關鍵字void(本身)的意思是 "不能容納任何值",例如,你可以用它來表示函式不回傳值,或者作為函式的唯一引數來表示該函式不接受引數,另一方面,派生型別void *意味著指標可以參考任何物件,我將在本章后面討論派生型別,
函式型別
函式型別是派生型別,在這種情況下,該型別是由回傳型別和其引數的數量和型別衍生出來的,函式的回傳型別不能是陣列型別,
當你宣告函式時,你使用函式宣告器來指定函式的名稱和回傳型別,如果宣告器包括引數型別串列和定義,每個引數的宣告必須包括識別符號,除了只有void型別引數的引數串列,它不需要識別符號,
下面是幾個函式型別的宣告:
int f(void);
int *fip();
void g(int i, int j);
void h(int, int);
首先,我們宣告沒有引數的函式f,回傳int,接下來,我們宣告一個沒有指定引數的函式fip,它回傳指向int的指標,最后,我們宣告兩個函式,g和h,每個函式都回傳void,并接受兩個int型別的引數,
如果識別符號是宏,用識別符號來指定引數(如這里的g)可能會有問題,然而,提供引數名稱是自我記錄代碼的良好做法,所以通常不建議省略識別符號(如對h的做法),
在函式宣告中,指定引數是可選的,然而,不這樣做偶爾也會有問題,如果你用C++寫fip的函式宣告,它將宣告不接受任何引數的函式,并回傳int *,在C語言中,fip宣告的是接受任何型別引數的函式,并回傳一個int *,在C語言中,你不應該用空引數串列來宣告函式,首先,這是語言的廢棄功能,將來可能會被洗掉,其次,這段代碼可能會被移植到C++中,所以要明確地列出引數型別,在沒有引數的時候使用void,
帶有引數型別串列的函式型別被稱為函式原型,函式原型告知編譯器一個函式所接受的引數的數量和型別,編譯器使用這些資訊來驗證在函式定義和對函式的任何呼叫中是否使用了正確的引數數量和型別,
函式定義提供了該函式的實際實作,請看下面的函式定義:
int max(int a, int b)
{ return a > b ? a : b; }
回傳型別指定為int;函式宣告器為max(int a, int b);而函式主體為{ return a > b ? a : b; },函式型別的指定不能包括任何型別限定符(參見第32頁 "型別限定符"),函式體本身使用了條件運算子(???,這將在第4章進一步解釋,這個運算式指出,如果a大于b,回傳a;否則,回傳b,
派生型別
派生型別是由其他型別構建的型別,這些型別包括指標、陣列、型別定義、結構和聯合,我們將在這里介紹所有這些型別,
指標型別
指標型別是從它所指向的函式或物件型別派生出來的,稱為被引型別,指標提供了對被參考型別的物體的參考,
下面的三個宣告宣告了指向int的指標,指向char的指標,以及指向void的指標:
int *ip;
char *cp;
void *vp;
在本章的前面,我介紹了address-of(&)和indirection(*)運算子,你使用&運算子來獲取物件或函式的地址,例如,如果物件是一個int,運算子的結果就有int的指標型別:
int i = 17;
int *ip = &i;
我們將變數ip宣告為int的指標,并將i的地址分配給它,你也可以對*運算子的結果使用&運算子:
ip = &*ip;
通過使用間接運算子解除對ip的參考,可以決議到實際的物件i,通過使用&運算子獲取ip的地址,可以檢索到指標,所以這兩個操作是相互抵消的,
單元的運算子將型別的指標轉換為該型別的值,它對指標進行操作,如果運算元指向函式,使用*運算子的結果是函式的代號,如果指向物件,結果是指定物件的值,例如,如果運算元是指向int的指標,那么轉折運算子的結果就有int型別,如果指標沒有指向一個有效的物件或函式,可能會發生不好的事情,
陣列
陣列是連續分配的物件序列,它們都具有相同的元素型別,陣列型別的特點是其元素型別和陣列中的元素數量,這里我們宣告了由11個元素組成的陣列,其型別為int,標識為ia,還有由17個元素組成的陣列,其型別為指標浮點,標識為afp:
int ia[11];
float *afp[17];
你可以使用方括號([])來標識一個陣列的元素,例如,下面的代碼片段創建了字串 "0123456789 "來演示如何為陣列中的元素賦值:
char str[11];
for (unsigned int i = 0; i < 10; ++i) {
str[i] = '0' + i;
}
str[10] = '\0';
第一行宣告了大小為11的char陣列,這分配了足夠的存盤空間來創建10個字符和空字符的字串,for回圈迭代了10次,i的值從0到9不等,每次迭代都將運算式'0'+i的結果分配給str[i],在回圈結束后,空字符被復制到陣列str[10],
str被自動轉換為指向陣列第一個成員的指標(char型別的物件),而i具有無符號整數型別,下標([])運算子和加法(+)運算子被定義,因此str[i]與*(str + i)相同,當str是一個陣列物件時(如這里),運算式str[i]指定陣列的第i個元素(從0開始計算),因為陣列的索引是從0開始的,所以陣列char str[11]的索引是從0到10,10是最后一個元素,如本例最后一行所參考,
如果單數&運算子的運算元是[]運算子的結果,其結果就像去掉&運算子并將[]運算子改為+運算子一樣,例如,&str[10]與str + 10相同,
你也可以宣告多維陣列,清單2-8在函式main中宣告arr是int型別的二維5×3陣列,也被稱為矩陣,
void func(int arr[5]);
int main(void) {
unsigned int i = 0;
unsigned int j = 0;
int arr[3][5];
func(arr[i]);
int x = arr[i][j];
return 0;
}
清單2-8:矩陣操作
更確切地說,arr是由三個元素組成的陣列,每個元素都是由五個int型別的元素組成的陣列,
型別定義
你使用typedef來宣告現有型別的別名;它從不創建新的型別,例如,下面的每個宣告都創建了新的型別別名:
typedef unsigned int uint_type;
typedef signed char schar_type, *schar_p, (*fp)(void);
在第一行,我們宣告uint_type是無符號int型別的別名,在第二行,我們宣告schar_type是有符號char的別名,schar_p是有符號char 的別名,fp是有符號char()(void)的別名,在標準頭檔案中以_t結尾的識別符號是型別定義(現有型別的別名),一般來說,你不應該在自己的代碼中遵循這個慣例,因為C標準保留了符合int[0-9a-z_]t和uint[0-9a-z]_t模式的識別符號,而便攜式作業系統介面(POSIX)保留了所有以_t結尾的識別符號,如果你定義了使用這些名稱的識別符號,它們可能會與實作所使用的名稱發生沖突,這可能會導致難以除錯的問題,
結構體
結構體包含按順序分配的成員物件,每個物件都有自己的名字,并且可以有不同的型別--與陣列不同,陣列必須都是相同的型別,結構類似于其他編程語言中的記錄型別,清單2-9宣告了由sigline標識的物件,其型別為struct sigrecord,并有指向由sigline_p標識的sigline物件的指標,
struct sigrecord {
int signum;
char signame[20];
char sigdesc[100];
} sigline, *sigline_p;
結構體對于宣告相關物件的集合很有用,可以用來表示諸如日期、客戶或人事記錄等,它們對于將經常一起作為引數傳遞給函式的物件分組特別有用,因此你不需要重復地分別傳遞單個物件,
一旦你定義了一個結構,你可能想參考它的成員,你可以通過使用結構成員(.)運算子來參考結構型別的物件的成員,如果你有一個指向結構的指標,你可以用結構指標(->)運算子來參考其成員,清單2-10演示了每個運算子的使用,
sigline.signum = 5;
strcpy(sigline.signame, "SIGINT");
strcpy(sigline.sigdesc, "Interrupt from keyboard");
sigline_p = &sigline;
sigline_p->signum = 5;
strcpy(sigline_p->signame, "SIGINT");
strcpy(sigline_p->sigdesc, "Interrupt from keyboard");
聯合體
聯盟型別與結構類似,只是成員物件使用的記憶體是重疊的,聯合型別可以在某一時刻包含一個型別的物件,在另一時刻包含一個不同型別的物件,但絕不會同時包含兩個物件,它主要用于節省記憶體,清單2-11顯示了包含三個結構的聯盟u:n、ni和nf,這個聯合體可能用在樹、圖或其他資料結構中,這些結構的一些節點包含整數值(ni),其他節點包含浮點值(nf),
union {
struct {
int type;
} n;
struct {
int type;
int intnode;
} ni;
struct {
int type;
double doublenode;
} nf;
} u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
和結構一樣,你可以通過.運算子來訪問聯盟成員,使用指向聯合體的指標,你可以用->運算子來參考它的成員,在清單 2-11 中,聯盟的 nf 結構中的 type 成員被參考為 u.nf.type,而 doublenode 成員被參考為 u.nf .doublenode,使用這個聯盟的代碼通常會通過檢查存盤在u.n.type中的值來檢查節點的型別,然后根據型別來訪問intnode或doublenode結構,如果這被實作為結構,每個節點將包含intnode和doublenode成員的存盤,使用聯合體可以為兩個成員使用相同的存盤,
Tags
標簽是結構體、聯合體和列舉的一種特殊命名機制,例如,出現在下面結構中的識別符號s就是一個標簽:
struct s {
//---snip---
};
標簽本身不是一個型別名,不能用來宣告變數(Saks 2002),相反,你必須按以下方式宣告該型別的變數:
struct s v; // instance of struct s
struct s *p; // pointer to struct s
聯合體和列舉的名稱也是標記,而不是型別,這意味著它們不能單獨用來宣告一個變數,比如說
enum day { sun, mon, tue, wed, thu, fri, sat };
day today; // error
enum day tomorrow; // OK
結構、聯合體和列舉的標記被定義在與普通識別符號不同的命名空間中,這允許C程式在同一范圍內同時擁有一個標簽和另一個拼寫相同的識別符號:
enum status { ok, fail }; // enumeration
enum status status(void); // function
你甚至可以宣告型別為struct s的物件s:
struct s s;
這可能不是好的做法,但在C語言中是有效的,你可以把結構標簽看作是型別名,并通過使用typedef為標簽定義別名,下面是一個例子:
typedef struct s { int x; } t;
現在您可以宣告型別為t的變數,而不是結構為s的變數,結構、聯合和列舉中的標記名稱是可選的,所以您可以完全省略它:
typedef struct { int x; } t;
除了包含指向自身的指標的自指結構外,這樣做效果不錯:
struct tnode {
int count;
struct tnode *left;
struct tnode *right;
};
如果你省略了第一行的標記,編譯器可能會抱怨,因為第3行和第4行的參考結構還沒有被宣告,或者因為整個結構沒有被聯合體和列舉的名稱也是標記,而不是型別,這意味著它們不能單獨用來宣告一個變數,比如說
typedef struct tnode {
int count;
struct tnode *left;
struct tnode *right;
} tnode;
大多數C程式員為標簽和typedef使用了不同的名字,但相同的名字也可以使用,你也可以在結構之前定義這個型別,這樣你就可以用它來宣告參考其他tnode型別物件的左右成員:
typedef struct tnode tnode;
struct tnode {
int count;
tnode *left
tnode *right;
} tnode;
型別定義可以提高代碼的可讀性,不僅僅是用于結構,例如,下面三個信號函式的宣告都指定了相同的型別:
typedef void fv(int), (*pfv)(int);
void (*signal(int, void (*)(int)))(int);
fv *signal(int, fv *);
pfv signal(int, pfv);
型別限定詞
到目前為止所研究的所有型別都是未限定的型別,型別可以通過使用以下一個或多個限定符來限定:const, volatile, 和 restrict,當訪問限定型別的物件時,這些限定符都會改變行為,
型別的限定和非限定版本可以互換使用,作為函式的引數、函式的回傳值和聯合體的成員,
注意:從C11開始使用的_Atomic型別限定符,支持并發程式,
- const
用const修飾符宣告的物件(const-qualified型別)是不可修改的,特別是,它們不能被分配,但可以有常量初始化器,這意味著具有const-qualified型別的物件可以被編譯器放在只讀記憶體中,任何試圖寫到它們的行為都會導致運行時錯誤:
const int i = 1; // const-qualified int
i = 2; // error: i is const-qualified
有可能意外地說服你的編譯器為你改變一個const-qualified的物件,在下面的例子中,我們取了常量限定的物件i的地址,并告訴編譯器這實際上是一個指向int的指標:
const int i = 1; // object of const-qualified type
int *ip = (int *)&i;
*ip = 2; // undefined behavior
如果原來的物件被宣告為常數限定的物件,C語言不允許你拋開常數,這段代碼可能看起來是有效的,但它是有缺陷的,以后可能會失敗,例如,編譯器可能會把const-qualified物件放在只讀記憶體中,當運行時試圖在該物件中存盤一個值時,會引起記憶體故揮發性障,
C語言允許你修改一個由常量限定的指標指向的物件,只要原物件沒有被宣告為常量:
int i = 12;
const int j = 12;
const int *ip = &i;
const int *jp = &j;
*(int *)ip = 42; // ok
*(int *)jp = 42; // undefined behavior
- volatile
volatile-qualified型別的物件有一個特殊的用途,靜態volatile-qualified物件被用來模擬記憶體映射的輸入/輸出(I/O)埠,靜態常量volatile-qualified物件被用來模擬記憶體映射的輸入埠,如實時時鐘,
存盤在這些物件中的值可能在編譯器不知情的情況下發生變化,例如,每次讀取實時時鐘的值時,它可能會改變,即使該值沒有被C語言程式寫入,使用 volatile-qualified 型別讓編譯器知道值可能會改變,并確保對實時時鐘的每次訪問都會發生(否則,對實時時鐘的訪問可能會被優化掉或被先前讀取和快取的值取代),用在任何地方,因此,你別無選擇,只能為該結構宣告一個標簽,但你也可以宣告一個型別定義:
volatile int port;
port = port;
如果沒有volatile限定,編譯器會把它看作是no-op(什么都不做的編程陳述句),并有可能消除讀和寫,
另外,volatile限定的型別用于與信號處理程式和setjmp/longjmp的通信(關于信號處理程式和setjmp/longjmp的資訊請參考C標準),與Java和其他編程語言不同,C語言中的volatile-qualified型別不應該用于執行緒之間的同步,
- restrict
限制性限定的指標用于促進優化,通過指標間接訪問的物件經常不能被完全優化,因為潛在的別名,當一個以上的指標指向同一個物件時就會發生,別名可以抑制優化,因為編譯器無法判斷一個物件的部分在另一個明顯不相關的物件被修改時是否可以改變值,例如,
下面的函式從q參考的存盤空間復制了n個位元組到p參考的存盤空間,函式引數p和q都是限制性限定的指標:
void f(unsigned int n, int * restrict p, int * restrict q) {
while (n-- > 0) {
*p++ = *q++;
}
}
因為p和q都是限制性限定的指標,編譯器可以假定通過其中一個指標引數訪問的物件不會同時通過另一個指標引數訪問,編譯器可以只根據引數宣告來做這個評估,而不分析函式體,盡管使用限制性限定的指標可以產生更有效的代碼,但你必須確保指標不指向重疊的記憶體,以防止未定義的行為,
釘釘或微信號: pythontesting 微信公眾號:pythontesting轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552177.html
標籤:C++
上一篇:C++ 入門
下一篇:返回列表