導言
假設我們現在要處理一堆學生數據信息,如果使用前面學過的數據類型來表示學生信息,由於學生信息中各項內容的數據類型有所不同,因此,需要為每一項內容分別定義一個變量或數組。當要訪問某個學生信息時,只能分別訪問這些分離的變量或數組。這會給操作帶來很多不便之處。更重要的是,這幾項內容同屬於某個學生,它們之間是有內在聯系的,為每一項內容分別定義變量或數組的方法割裂了它們之間的關聯關系。請思考,我們能不能用某種方法,能夠將把有內在聯系的不同類型的數據匯聚成一個整體,使它們相互關聯?
結構類型是一種允許我們把一些數據分量聚合成一個結構中,它包含的每個數據分量都有名字,這些數據分量稱為結構成員或者結構分量,結構成員可以是C語言中的任意變量類型,程序員可以使用結構類型來創建適合於問題的數據聚合。像數組和指針一樣,結構也是一種構造數據類型,它與數組的區別在於:數組中所有元素的數據類型必須是相同的,而結構中各成員的數據類型可以不同。因此,在學習結構時要注意與數組進行類比,這樣更助於理解與掌握。
結構體
定義
在C語言中,整型、實型等基本數據類型是被系統預先定義好了的,我們用其直接定義變量。而結構類型是由用戶根據需要,按規定的格式自行定義的數據類型。結構類型定義的一般形式為:
struct tag {
member-list
member-list
member-list
...
};
tag 是結構體標簽,即結構名,member-list 是標准的變量定義,比如 int i; 或者 float f,或者其他有效的變量定義。
- 關鍵字struct和它后面的結構名一起組成一個新的數據類型名。結構的定義以分號結束,這是因為C語言中把結構的定義看做一條語句。
嵌套定義
如果把數組比喻成整齊的隊列,那么結構體就可以被比喻為一個團隊,這個團隊是一個包容的團隊,它把不同類型,不同功能的數據緊密聯系為一個整體,讓這些本來毫無內在聯系的數據有了關聯,共同來為我們的數據處理提供方便。在實際生活中,一個較大的實體可能由多個成員構成,而這些成員中有些又有可能由一些更小的成員構成。一個結構的成員是由合法的C語言數據類型和變量名組成的,進一步地說,在定義結構成員時所用的數據類型也可以是結構類型,這樣就形成了結構類型的嵌套。
- 在定義嵌套的結構類型時,必須先定義成員的結構類型,再定義主結構類型。
結構變量定義
一般情況下,結構名,結構成員定義,結構變量在一次結構體定義中,至少要出現兩部分:
struct
{
int a;
char b;
double c;
} s1;
無類型名定義,此聲明聲明了擁有3個成員的結構體,分別為整型的a,字符型的b和雙精度的c,同時又聲明了結構體變量s1,但是這個結構體並沒有標明其標簽。
struct SIMPLE
{
int a;
char b;
double c;
};
struct SIMPLE t1, t2[20], *t3;
單獨定義,此聲明聲明了擁有3個成員的結構體,分別為整型的a,字符型的b和雙精度的c,結構體的標簽被命名為SIMPLE,沒有聲明變量。在定義結構體之后,用SIMPLE標簽的結構體,另外聲明了變量t1、t2、t3。
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
混合定義,在定義結構體類型的同時定義了結構變量。
- 要注意的是,由於無類型名定義時,沒有給出結構名,在此定義語句后面無法再定義這個類型的其他結構變量,除非把定義過程再寫一遍。一般情況下,除非變量不會再增加,還是建議采用前兩種結構變量的定義形式。
初始化
和其它類型變量一樣,對結構體變量可以在定義時指定初始值。例如:
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {"C 語言", "RUNOOB", "編程語言", 123456};
結構體變量的使用
在上文,我把結構體比喻為一個團隊,那么當我們要找出這個團隊的某位成員時,就需要先把這位成員叫出來,這個時候我們就可以把對結構體變量的使用比喻為點名,這是我們就需要通過某種方式來實現給結構體點名的效果。
為了訪問結構的成員,我們使用成員訪問運算符“.”。成員訪問運算符是結構變量名稱和我們要訪問的結構成員之間的一個句號,格式為:
結構變量名.結構成員名
在C語言中,對結構變量成員的使用方法和同類型的變量完全相同,而嵌套結構成員的引用方法和一般成員的引用方法類似,也是采用結構成員的操作符來進行的,例如:
strcpy( Book1.title, "C Programming");
Book1.book_id = 6495407;
printf( "Book 1 title : %s\n", Book1.title);
printf( "Book 1 book_id : %d\n", Book1.book_id);
- 由於結構成員運算符的優先級屬於最高級別,所以一般情況下都是優先執行,即和一般運算符混合運算時,結構成員運算符優先。
結構體變量的整體賦值
如果兩個結構變量具有相同的結構變量,則允許將一個結構體變量的值直接賦給另一個結構變量。賦值時,將賦值符號右邊結構變量中相應的成員都賦值給另一個結構變量,這是結構中唯一的整體操作方式。例如:
struct Books Book1;
struct Books Book2;
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
Book2 = Book1; //整體賦值
這個問題也不難理解,因為結構體在本質上也是變量,只是它是多個變量的集合體,因此它的操作和一般的變量是一樣的,賦值的時候,它的本質就是把結構體變量中每一個成員變量都賦值進另一個結構體中。
- 只有相同類型的變量之間才能直接賦值。
結構變量作為函數參數
在一個由多個函數組成的C語言程序中,如果程序中含有結構類型的數據,就有可能需要用結構變量作為函數的參數或返回值,以便在函數間傳遞數據。例如:
void printBook( struct Books book )
{
printf( "Book title : %s\n", book.title);
printf( "Book author : %s\n", book.author);
printf( "Book subject : %s\n", book.subject);
printf( "Book book_id : %d\n", book.book_id);
}
printBook( Book1 );
結構變量作為函數參數的特點是,可以傳遞多個數據且參數形式較簡單。但是,我們知道結構體是一個變量的集合體,因此對於成員較多的大型結構,本質上也是把結構中的每一個成員都傳進函數,因此參數傳遞時所進行的結構數據復制使得效率較低。
結構體數組
我們還是沿用上面的比喻,這就像是一支軍隊,我們結構體數組名就像是一個長官,這個長官下屬有n個小隊,而這n個小隊也自然有各自的隊長,結構體數組的每個單元就是一個隊長,這個隊長下屬有很多隊員,這些隊員就是結構體里的各個成員了。
結構數組是結構與數組的結合體,與普通數組的不同之處在於每個數組元素都是一個結構類型的數據,包括多個成員項。結構數組的定義方法與結構變量類似,每個數組元素都是對應結構類型的變量,這樣就可以存儲50個學生的信息。也可以同時對其進行初始化,其格式與二維數組的初始化。由於每個結構數組元素的類型都是結構類型,因此利用下標和“.”運算符以引用結構數組元素的成員。對結構數組元素成員的引用是通過使用數組下標與結構成員操作符“.”相結合的方式來實現的,其一般格式為:
結構數組名[下標].結構成員名
此外,由於結構數組中的所有元素都屬於相同的結構類型,因此,數組元素之間可以直接賦值。
結構體數組排序
請看例題:實驗9-8-結構 通訊錄排序
偽代碼:
定義結構體變量friends,包含成員name[10]存儲姓名信息,birthday存儲生日信息,telephone[17]存儲電話號碼數據;
定義變量number接收有幾個通訊錄信息;
定義變量i1, i2作循環控制變量;
定義結構體數組friends fri[11];
輸入變量number;
for i1 = 0; i1 < number; i1++ do //輸入多個通訊錄信息,存儲入結構體數組
scanf("%s %ld %s", &fri[i1].name, &fri[i1].birthday, &fri[i1].telephone);
end for
for i1 = 0; i1 < number; i1++ do
{
for (i2 = 0; i2 < number - i1 - 1; i2++ do //對結構體數組按照生日進行冒泡排序
{
if fri[i2].birthday > fri[i2+1].birthday do
{
fri[10] = fri[i2];
fri[i2] = fri[i2+1];
fri[i2+1] = fri[10];
}
end if
}
end for
end for
}
for ( i1 = 0; i1 < number; i1++) //遍歷輸入結構體數組
printf("%s %ld %s\n", fri[i1].name, fri[i1].birthday, fri[i1].telephone);
end for
代碼實現:
結構指針
指針可以指向任何一種類型的變量,而結構體也是C語言中的一種合法變量,因此,指針也可以指向結構變量,這就是結構指針。即:結構指針就是措向結構類型變量的指針。我們可以定義結構指針變量,在指針變量中存儲結構變量的地址,為了查找結構變量的地址,請把 & 運算符放在結構名稱的前面。例如:
struct Books *struct_pointer;
struct_pointer = &Book1;
結構類型的數據往往由多個成員組成,結構指針的值實際上是結構變量的首地址 即第一個成員的地址。有了結構指針的定義,我們能通過結構指針針變量間接訪問它所指向的結構變量中的各個成員。為了使用指向該結構的指針訪問結構的成員,我們一般使用 -> 指向運算符,例如:
struct_pointer->title;
結構指針作為函數參數
結構變量也可以作為函數參數,在參數傳遞時,把實參結構中的每一個成員值傳遞給形參結構的成員。但是,當結構成員數量眾多時,在參數傳遞過程中就需要消耗很多空間。而使用結構指針作為函數參數只要傳遞一個地址,因此,這么做能極大地提高參數傳遞的效率。例如:
void printBook( struct Books *book )
{
printf( "Book title : %s\n", book->title);
printf( "Book author : %s\n", book->author);
printf( "Book subject : %s\n", book->subject);
printf( "Book book_id : %d\n", book->book_id);
}
struct Books Book1;
printBook( &Book1 );
共用體
我們再來做個比喻,假設你有一把瑞士軍刀,這把刀集成了圓珠筆、牙簽、剪刀、平口刀、開罐器、螺絲刀、鑷子等,因此雖然這只是一把刀,但是你可以拿來做很多事情,要使用這些工具時,只要將它從刀身的折疊處拉出來,就可以使用。但是很明顯,你不能拿瑞士軍刀去砍樹,因為這么做超出了瑞士軍刀的能力范圍。
共用體是一種特殊的數據類型,允許您在相同的內存位置存儲不同的數據類型。您可以定義一個帶有多成員的共用體,但是任何時候只能有一個成員帶有值。共用體提供了一種使用相同的內存位置的有效方式。共用體與“結構”有一些相似之處。但兩者有本質上的不同。在結構中各成員有各自的內存空間,一個結構體變量的總長度大於等於各成員長度之和。而在共用體中,各成員共享一段內存空間,一個聯合變量的長度等於各成員中最長的長度。這里所謂的共享不是指把多個成員同時裝入一個共用體變量內,而是指該共用體變量可被賦予任一成員值,但每次只能賦一種值,如定義為一個可裝入“班級”或“教研室”的共用體后,就允許賦予整型值(班級)或字符型(教研室)。要么賦予整型值,要么賦予字符型,不能把兩者同時賦予它。
定義共用體
為了定義共用體,您必須使用 union 語句,方式與定義結構類似。union 語句定義了一個新的數據類型,帶有多個成員。union 語句的格式如下:
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
union tag 是可選的,每個 member definition 是標准的變量定義,比如 int i; 或者 float f; 或者其他有效的變量定義。在共用體定義的末尾,最后一個分號之前,您可以指定一個或多個共用體變量,這是可選的,共用體占用的內存應足夠存儲共用體中最大的成員。
訪問共用體
為了訪問共用體的成員,我們使用成員訪問運算符“.”。成員訪問運算符是共用體變量名稱和我們要訪問的共用體成員之間的一個句號。您可以使用 union 關鍵字來定義共用體類型的變量。
例如我們先定義一個共用體:
union Data
{
int i;
float f;
char str[20];
};
執行以下操作:
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
你就會看到這樣的結果:
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
在這里,我們可以看到共用體的 i 和 f 成員的數據不正常,這是因為最后賦給變量的值占用了內存位置,這也是 str 成員能夠完好輸出的原因。現在讓我們給代碼稍加修改:
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
運行代碼,你會看到:
data.i : 10
data.f : 220.500000
data.str : C Programming
在這里,所有的成員都能完好輸出,因為同一時間只用到一個成員,因此我們要非常注意共用體訪問成員的准確性。
枚舉
枚舉類型用於聲明一組命名的常數,當一個變量有幾種可能的取值時,可以將它定義為枚舉類型。這種變量能設置為已經定義的一組之中的一個,有效地防止用戶提供無效值。該變量可使代碼更加清晰,因為它可以描述特定的值。在C語言中,枚舉類型是被當做 int 或者 unsigned int 類型來處理的。
枚舉語法定義格式為:
enum 枚舉名 {枚舉元素1,枚舉元素2,……};
接下來我們舉個例子,比如:一星期有 7 天,如果不用枚舉,我們需要使用 #define 來為每個整數定義一個別名:
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
這個看起來代碼就比較繁瑣了,因此我們使用枚舉的方式定義:
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
- 第一個枚舉成員的默認值為整型的 0,后續枚舉成員的值在前一個成員上加 1。我們在這個實例中把第一個枚舉成員的值定義為 1,第二個就為 2,以此類推,我們可以在定義枚舉類型時改變枚舉元素的值。
與結構體類似,我們可以通過以下三種方式來定義枚舉變量:
1、先定義枚舉類型,再定義枚舉變量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
2、定義枚舉類型的同時定義枚舉變量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
3、省略枚舉名稱,直接定義枚舉變量
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
文件
許多程序在實現過程中,依賴於把數據保存到變量中,而變量是通過內存單元存儲數據的,數據的處理完全由程序控制。當一個程序運行完成或終止運行,所有變量的值不再保存。另外,一般的程序都會有數據輸入與輸出,如果輸人輸出數據量不大,通過鍵盤和顯示器即可解決。當輸入輸出數據量較大時,就會受到限制,帶來不便。文件是解決上述問題的有效辦法,它通過把數據存儲在磁盤文件中,得以長久保存。當有大量數據輸入時,可通過編輯工具事先建立輸入數據的文件,程序運行時將 不再從鍵盤輸入,而從指定的文件上讀人,從而實現數據一次輸人多次使用。同樣,當有大量數據輸出時,可以將其輸出到指定文件,不受屏幕大小限制,並且任何時候都可以查看結果文件。一個程序的運算結果還可以作為其他程序的輸入,進行進一步加工。
文件指針
在C語言中用一個指針變量指向一個文件,這個指針稱為文件指針。通過文件指針就可對它所指的文件進行各種操作。定義說明文件指針的一般形式為:
FILE *指針變量標識符;
其中FILE應為大寫,它實際上是由系統定義的一個結構,該結構中含有文件名、文件狀態和文件當前位置等信息。在使用文件時,需要在內存中為其分配空間,用來存放文件的基本信息,給結構體類型是由系統定義的,C語言規定該類型為FILE型。
打開文件
您可以使用 fopen( ) 函數來創建一個新的文件或者打開一個已有的文件,這個調用會初始化類型 FILE 的一個對象,類型 FILE 包含了所有用來控制流的必要的信息。下面是這個函數調用的原型:
FILE *fopen( const char * filename, const char * mode );
在這里,filename 是字符串,用來命名文件,訪問模式 mode 的值可以是下列值中的一個:
模式 | 描述 |
---|---|
r | 打開一個已有的文本文件,允許讀取文件。 |
r+ | 打開一個文本文件,允許讀寫文件。 |
w | 打開一個文本文件,允許寫入文件。如果文件不存在,則會創建一個新文件。在這里,您的程序會從文件的開頭寫入內容。如果文件存在,則該會被截斷為零長度,重新寫入。 |
w+ | 打開一個文本文件,允許讀寫文件。如果文件已存在,則文件會被截斷為零長度,如果文件不存在,則會創建一個新文件。 |
a | 打開一個文本文件,以追加模式寫入文件。如果文件不存在,則會創建一個新文件。在這里,您的程序會在已有的文件內容中追加內容。 |
a+ | 打開一個文本文件,允許讀寫文件。如果文件不存在,則會創建一個新文件。讀取會從文件的開頭開始,寫入則只能是追加模式。 |
如果處理的是二進制文件,則需使用下面的訪問模式來取代上面的訪問模式:"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"。
關閉文件
關閉文件
為了關閉文件,請使用 fclose( ) 函數。函數的原型如下:
int fclose( FILE *fp );
如果成功關閉文件,fclose( ) 函數返回零,如果關閉文件時發生錯誤,函數返回 EOF。這個函數實際上,會清空緩沖區中的數據,關閉文件,並釋放用於該文件的所有內存。EOF 是一個定義在頭文件 stdio.h 中的常量。C標准庫提供了各種函數來按字符或者以固定長度字符串的形式讀寫文件。
寫入文件
fputc()函數:把參數 c 的字符值寫入到 fp 所指向的輸出流中。如果寫入成功,它會返回寫入的字符,如果發生錯誤,則會返回 EOF。函數原型:
int fputc( int c, FILE *fp );
fputs()函數:我們可以使用這個函數來把一個以 null 結尾的字符串寫入到流中,利用這個函數把字符串 s 寫入到 fp 所指向的輸出流中。如果寫入成功,它會返回一個非負值,如果發生錯誤,則會返回 EOF。函數原型:
int fputs( const char *s, FILE *fp );
fprintf()函數:根據指定的格式(format),向輸出流(stream)寫入數據(argument),函數原型:
int fprintf (FILE* stream, const char*format, [argument]);
讀取文件
fgetc()函數:從 fp 所指向的輸入文件中讀取一個字符。返回值是讀取的字符,如果發生錯誤則返回 EOF。函數原型:
int fgetc( FILE * fp );
fgets()函數:從 fp 所指向的輸入流中讀取 n - 1 個字符。它會把讀取的字符串復制到緩沖區 buf,並在最后追加一個 null 字符來終止字符串,如果這個函數在讀取最后一個字符之前就遇到一個換行符 '\n' 或文件的末尾 EOF,則只會返回讀取到的字符,包括換行符。函數原型:
char *fgets( char *buf, int n, FILE *fp );
fscanf()函數:函數來從文件中讀取字符串,但是在遇到第一個空格、換行符或制表符時,它會停止讀取。函數原型:
int fscanf(FILE *fp, const char *format, ...);
feof()函數
feof是C語言標准庫函數,其原型在stdio.h中,其功能是檢測流上的文件結束符,如果文件結束,則返回非0值,否則返回0(即,文件結束:返回非0值,文件未結束,返回0值)。
程序實戰
左轉我另一篇博客C語言程序設計——成語學習系統程序
參考資料
《C語言程序設計(第3版)》——何欽銘、顏輝
菜鳥教程