結構體
聲明結構體時其實並未創建真實的數據,也並未給數據分配空間,只是描述了它由哪些成員(member)組成
struct tag {
...
};
也忽略tag可以直接聲明一個結構體變量,但這個結構體只能使用一次:
struct {
...
} var_name;
結構體的初始化,注意結構體中不同值之間用逗號(,)隔開,有兩種方法:
1. 按照順序初始化:
struct book b1 {
"The Hours",
"Michael Cunningham",
3.99
};
2. 通過變量名初始化,可以只初始化某幾個變量:
struct book b1 = {
.value = 3.99,
.author = "Michael Cunningham",
.title = "The Hours"};
也可以一次初始化多個結構體,不同結構體之間用逗號隔開
如果是static storage duration,變量只能用常量初始化(其實所有的static storage duration變量都是這樣)
結構體可以直接互相賦值,如struct book b2 = b1;
結構體可以作爲函數的返回值
結構體可以嵌套,如:
struct name {
char first[20];
char last[20];
};
struct person {
int id;
struct name pn;
};
struct person tom = { 123132, {"Tom", "Hanks"}};
結構體指針:
struct book* p; p = &b1;
注意,結構體的名字並不是結構體的地址(不像數組)。(*p).title == p->title。這裏因爲title是一個array,所以其實p->title的結果其實是數組名,也就是這個數組的地址
結構體參與函數運算有兩種方式:
1. 使用結構體指針,如:double sum(const struct* book); 這種方式在調用結構體時要使用結構體的地址,不能使用結構體的名字!如:sum(&b1);
2. 將結構體作爲參數,如double sum(struct book);
兩種方式的比較:
1. 結構體指針
優點:a) 能夠在舊的環境下運行 b) 速度快(因爲不用複製結構體本身,只需要傳遞地址)
缺點:對數據的保護不夠,可能會改動數據。但是這個問題可以通過將函數形參定義成const來解決
2. 傳送結構體作爲參數
優點:a) 函數處理的是結構體的副本,可以保護數據 b) 程序style更清晰
缺點:a) 舊的環境可能不支持 b) 速度慢,佔用更多的空間,特別是將一個大的結構體傳給函數卻只用到了其中的一兩個member
所以,這種方法通常只用於比較小的結構體
字符數組和結構體中的字符指針
struct names{
char first[20];
char last[20];
};
struct pnames{
char* first;
char* last;
};
對於names來說,字符串被存儲在結構體中;而對panmes來說,字符串被存放在compiler存放字符串常量的位置。pnames沒有給字符串分配存儲空間,所以它只能處理那些在別處已經被分配了空間的字符串(如字符串常量或字符串數組)。同時,如果給結構體中未初始化的字符指針賦值,如scanf("%s", pn1.last); 程序可能將輸入的字符串存儲在任何位置,而這可能會導致嚴重的問題。
如果使用malloc()來存儲結構體中的字符串,則可以在結構體中使用char型指針。
複合文字用於結構體(compound literal)
C99開始支持的複合文字(compound literal)除了數組外還支持結構體,如果只需要一個臨時結構體的值的話可以使用,如作爲實參傳入函數(如果函數的形參是一個地址,則需要將compound literal的地址傳入,在前面加上"&"即可)或給另一個結構體賦值。語法是將具體的初始值用大括號({})括起,在前面放置用小括號括起的值的類型,例:(struct book) {"The Hours", "Michael Cunningham", 3.99}(參考"C的數組與指針")
Flexible Array Members(C99)
C99支持一個叫做flexible array member的特性,它允許聲明一個結構體,這個結構體的最後一個member是一個數組,這個數組有以下特性:
1. 這個數組並不存在,至少不是立刻存在
2. 可以像這個數組存在一樣使用它,並且它的元素個數是任意的
聲明flexible array member要遵守以下規則:
1. flexible array member必須是結構體的最後一個member
2. 除它本身之外結構體必須要有其他的member
3. flexible array member的聲明方式與普通數組相同,但是括號內必須是空的
例:
struct flex {
int count;
double average;
double scores[]; // flexible array member
};
儘管定義了結構體flex,但不能使用數組scores,因爲並未給它分配空間。要想使用這個結構體必須聲明一個指向它的指針並且使用malloc()爲flex的普通內容和flexible array member分配空間。例:
struct flex* pf;
pf = (struct flex*)malloc(sizeof(struct flex) + 5*sizeof(double));
有flexible array member的結構體一些特殊的處理要求:
1. 例:struct flex* pf1, pf2;
...
*pf2 = * pf1;
這樣只能將除flexible array member外其他的普通member賦給*pf2,所以請使用memcpy()函數進行賦值(參考C預處理器和C函數庫)
2. 不要將這種結構體用於將結構體作爲傳入值的函數,原因與1相同;請使用傳入結構體地址的函數進行處理
3. 有flexible array member的結構體不能組成數組,也不能作爲其他結構體的成員
Anonymous Structure(C11)
anonymous structure是未命名的結構體成員,常用於union嵌套(nested union)
例:
struct name { struct person {
char first[20]; int id;
char last[20]; struct {char first[20]; char last[20];};
}; == };
struct person {
int id;
struct name pn;
};
Union
union能夠在相同的存儲空間中存儲不同的數據類型(但不是同時),通常的用途是存儲許多不規律或預先不知道的數據類型,另一個用途是當結構體中的某個數據的類型是什麼取決於其他member時。通過使用union數組可以創建一個單位大小相同的數組,每一個單位都可以存儲不同類型的變量,並且每一個單位都能夠以其他單位的形式進行訪問和讀取
union的創建很像結構體,有union模板和union變量,如:
union hold {
int digit;
double bigfl;
char letter;
};
hold這個union可以存儲一個int或一個double或一個char型變量(只能儲存一個!),編譯器會分配能夠存儲佔空間最大的變量所需的空間給一個union變量。union的初始化有四種方法:a) 直接對union中的某個變量進行初始化 b) 使用一個union變量去初始化另一個同類型的union變量 c) 初始化union的第一個元素 d) 使用designated initializer(C99支持)
例:
union hold val_a;
val_a.letter = 'R';
union hold val_b = val_a;
union hold val_c = {88};
union hold val_d = {.bigfl = 118.2}; // designated initializer
以val_a爲例,它使用了char類型,但依然可以通過val_a.digit來訪問val_a存儲的內容(即將以char的形式存儲的內容用int型的方式進行訪問和讀取)
union的使用方式:
1. 使用點(.):union hold fit; fit.digit = 23;
2. 使用"->":union hold* pu; pu = & fit; x = pu->digit;
Anonymous Union(C11)
anonymous union和anonymous structure用法類似,就是說anonymous union是一個結構體或union的未命名member
枚舉類型(Enumerated Types)
可以使用枚舉類型聲明象徵性的名字來代表整型常量,通過使用關鍵詞enum可以創建新“類型”並且指明他們可能有的值(實際上,enum型常量就是int型)。使用枚舉值的目的是增強程序的可讀性,它的語法和結構體相似:
enum spectrum {red, orange, yellow, green, blue, violet};
enum spectrum color;
"red"等這些象徵性的常量叫做枚舉成員(enumerator),他們本質上就是int常數,只是"red"變成了代表整數0的命名了的常數,可以在任何可以使用整數常量的地方使用枚舉常量。要使用這些常量,如red,必須輸入數字1而不是字符串"red"。枚舉常量默認從0開始,也可以使用其他整數值來代替,如:
enum feline { cat, lynx = 10, puma, tiger}; // cat == 0, lynx == 10, puma == 11, tiger == 12
儘管枚舉成員(如"red")是int型,但枚舉變量並不嚴格地侷限於int型,只要可以容納枚舉的常量即可(比如也可以是unsigned char型)
有些C下的枚舉性質並不能移植到C++,比如C允許對枚舉變量使用"++"而C++則不允許。所以如果想讓程序兼容C++就儘量少使用枚舉類型
共享命名空間(shared namespace)
C用命名空間(namespace)來指明程序中名字可以被識別的部分。在一個scope中的structure tag,union tag,enumeration tag共享同樣的命名空間,這個命名空間和普通變量的不同。也就是說,一個變量名在一個scope中可以同時用於變量和tag而不會產生問題。但是強烈不建議這麼做,因爲會產生混淆,並且C++也不允許這樣,因爲它把所有的tag和變量名放在同一個namespace中
typedef
typedef是一個能讓你爲一個類型(type)創建新名字的高級數據特性,它類似#define,但是有三點不同(注意typedef最後要接分號):
1. 不像#define,typedef只能給類型(type)命名而不能給值命名
2. typedef的實現是由compiler完成的,不是預處理器
3. 在這些限制內時,typedef比#define更靈活
#define UCHAR unsigned char == typedef unsigned char UCHAR;
但是typedef char* STRING;,不能用#define STRING char*實現。如果使用前者,STRING name, sign; == char* name, *sign;,name和sign都是指針;而如果使用後者,STRING name, sign; == char* name, sign;,只有name是指針,而sign是char型
這個定義的作用域取決於typedef語句的位置
Fancy Declarations
"[]"代表數組,"()"代表函數,"*"代表指針
一些符號的優先級:
1. "[]"和"()"有同樣的優先級,比"*"的優先級高,所以int* risk[10]先是一個數組,然後它的類型是int*
2. "[]"和"()"的順序是從左到右,因此int good[12][50]先是一個長度爲12的數組,然後每個元素又是長度爲50的int型
3. 因爲"[]"和"()"的優先級相同且是從左到右,所以int (* rusk)[10]先是一個指針,然後這個指針指向了一個長度爲10的int型數組
根據上面的理論可得:
char* fump(int):是一個函數;函數名是fump,參數是int型,返回值是char*
char (* frump)(int):是一個指針,指針名是frump,指向一個函數;函數的參數是int型,返回值是char型
char (* flump[3])(int):是一個數組,數組名是flump,長度爲3;數組的元素是指針,指向一個函數;函數的參數是int型,返回值是char型
typedef可以用來簡化上面的定義,例:
typedef int arr5[5];
typedef arr5 * p_arr5;
typedef p_arr5 arrp10[10];
arr5 togs; // togs是一個長度爲5的int型數組
p_arr5 p2; // p2是一個指向長度爲5的int型數組的指針
arrp10 ap; // ap是一個長度爲10的數列,數列的元素是指針,指針指向長度爲5的int型數組
函數指針
指針可以指向函數,函數指針通常用作另一個函數的參數,告訴這個“另一個函數”應該調用什麼函數。函數指針也是存放一個地址,這個地址標誌了一段函數的開始代碼。當聲明函數指針時,必須要指明所指向函數的類型。函數的類型由函數簽名(signature)決定,即函數的參數類型和返回值,例如【void (*pf)(char *)】就聲明瞭一個函數指針,這個函數的參數是char*,返回值爲空。確定一個函數指針格式的快速方法是將函數的prototype中的函數名用指針(*pf)代替即可,注意要有括號,如果沒有括號【void *pf(char*)】是一個函數,函數名是pf,參數是char*,返回值是pointer-to-void(參考前面fancy declaration部分)
聲明瞭函數指針後,就要將同類型函數的地址賦給它。在這種情況下,函數名就可以代表函數的地址
例:
void ToUpper(char *);
void (*pf)(char *);
pf = ToUpper;
char mis[] == "Nina"
(*pf)(mis); // 合法,因爲pf指向ToUpper函數,所以*pf是ToUpper函數
pf(mis); // 合法,因爲函數名是一個指針,所以指針名和函數名可以互換
函數指針的一個常見用法是作爲另一個函數的參數,例:
void show(void (* fp)(char *), char* str);
...
show(ToUpper, mis);
show(pf, mis);
函數不能定義成數組,但是函數指針可以定義成數組