【C\C++語言入門篇】-- 結構體
前面兩篇基本把指針給介紹完了,相信大家對指針已經不是那么陌生了。也不會因為指針和數組之間的關系而導致混淆了。大家可能也迫不及待想了解下后來的知識。今天我們就介紹下結構體。
對于結構體,既然叫結構體,形象上我們可以理解其就是一堆數據集合在一起形成一個結構。就比如一個學生的信息包括:學號、姓名、班級、年齡等等。這些信息都是屬于這個學生的,因此我們就可以將這些信息統一綁定在一起。形成一個學生實體,這里有點C++的味道。我們學C也還是有必要這樣思考。在我們周圍幾乎每一樣東西都有它自己的信息或者組成。比如藥品,它有什么功效,有什么成分等等都能統一綁定在一起形成一個實體,我們在程序中就能方便的訪問這些實體的每一個信息或組成。因此,當我們在設計一個程序的時候,我們就能把一些具有共同特性或者組成元素集合到一起構成一個結構體。比如我們的學生就可以寫成:
{
char name[ 13 ]; //姓名
char className[ 16 ]; //班級名
char age; //年齡
....
};
這樣一來,學生這個活生生的實體就把所有關于他的信息集中在一起了。這樣就能集中管理了,里面的每一個信息就能通過結構體變量來訪問。先看看怎么訪問:
C語言:
student.age = 22;
C++:
student.age = 22;
從上面可以看出要訪問一個結構體成員是很方便的,同時也體現了實體的概念。我們將學生實體的年齡信息取出來賦值為22歲。就好像在使用某個東西的某個功能一樣。這也是眾多面向對象語言的一種思想。就是將程序數據封裝話、結構化,我們要操作一個數據就跟現實生活中的使用某個工具的某個功能一樣。我們看到上面C和C++版本訪問唯一不同的就是C++版本在聲明結構體變量的時候不需要在前面加上struct關鍵字,個人覺得后來C++覺得struct沒有必要再寫了吧,麻煩!省略了不是更好!在語法和意義上兩個版本是相同的。
結構體還可以不需要名字,比如:
{
char age;
char name[ 16 ];
}
student, stu[ 10 ];
這里這個結構體就省略了名字,后面的student并不是名字,而是結構體變量。這種就是匿名結構體。跟普通的沒有什么區別,后面的stu就是一個結構體數組,普通結構體定義也可以在聲明結構體的時候緊跟著就聲明變量的。只是這樣你要定義其它變量就麻煩了,呵呵!這種一般用得比較固定或者就用這么一次就可以不要名字。
再來看看結構體別名。所謂別名就是可以使用另外一個名字。
{
char age;
char sex;
char class;
}STU,*PSTU;
這里的STU就是SStudent結構體的別名,就相當于是另外一個名字,使用的時候就可以不用加可惡的struct標識符了。
PSTU pStuednt; //別名為指針類型
好了,結構體就這么簡單,就是把不同類型或者同類型的一些數據集中到一起管理,構成一個實體。這個實體也可以理解為結構體。通常這樣設計是為了程序的模塊化結構化,這樣理解起來更容易更接近于現實,計算機本來就是服務于現實的。再比如我們的鏈表(將一組數據串聯成一個鏈,我們可以通過指針訪問到這個鏈中的每一個結點,形象的叫著是一個鏈,本質其實就是一組數據通過指針鏈接在一起,通常存放在內存中是不連續的),舉個簡單的例子:
{
Node* pNext;
char name[ 16 ];
};
這里也是一個結構體,里面包含一個指針和一個名字。假如我們這個名字就是某個學生的名字,這個結構體我們就形象看成是一個結點,什么是結點?結點你可以想象我有一條很漂亮的珍珠項鏈,項鏈上有很多顆珍珠串聯在一起,那么每一顆珍珠就可以想象成是一個結點。項鏈就是由很多個結點串聯在一起形成的。可能有的讀者覺得這樣比喻倒是很容易理解,但是聯想到程序里面還是感覺有點抽象。其實也不能說是抽象,咱們就想成它就是這么回事。就好比我們要安裝一個工具,注意到這句話里面出現了兩個現實生活中的詞:“安裝”“工具”。在計算機里我們使用的所謂工具其實都是虛擬化的,這些名字只是為了形象一點,再說安裝,也是如此,在現實生活中我們會在組裝或者安裝某個零件的時候才會使用這個詞,在計算機里使用這個詞也是為了大家能夠更容易理解形象化罷了。所以我們不必太拘泥于叫法。
好,我們這里定義了一個結構體作為結點,我們的目的是想把全班所有學生的名字全部串聯在一起,假如全班有50個人,那么就有50個結點。因此我們必須的有50個結構體結點來保存這50個學生的名字,而且我們這50個學生的名字還能夠通過循環遍歷能夠找到其中任意一個。那么我們就得這樣做:
struct Node secSt; //第2個學生結點
上面我們定義了2個學生結點,現在把這兩個結點鏈接在一起。
strcpy( secSt.name,"Tim");
root.pNext = &secSt;
secSt.pNext = NULL;
上面我們已經把這兩個結點鏈接起來了。root結點的next指針指向的就是secSt,secSt的next指針這里賦值為NULL,如果還想指向下一個學生結點同理。再看看層級關系:
|----pNext---|----name ("Tim")
|----pNext---|----name ......
|----pNext ......
上面的層級關系很清晰的描述了這些結點的關系,這樣就能夠成一個鏈,我們可以通過遍歷找到其中任何一個結點。我們也稱這種存儲在內存中為鏈式存儲。其本質就是通過指針將一個一個數據塊鏈接在一起。這里我只列舉了兩個結點。
問題一:我們怎么將50個結點鏈接在一起?(提示:每個結點可以malloc申請內存空間)
通過上面的描述,我們對結構體的用法和概念上有了初步的認識了。再來看看結構體指針(怎么總是離不開指針,呵呵,沒辦法指針在CC++里本來就是個永恒的主題)。
struct Node* pNode = &stuNode;
strcpy( pNode->name,"masefee");
上面,我們定義了一個Node結構體指針,該指針指向了stuNode,最后我們將stuNode結構體的name拷貝成了“masefee”。同樣我們可以使用庫函數給申請空間,大小為Node結構的大小:
這里我們使用malloc函數給申請了Node結構大小的一塊內存,然后讓pNode指針指向這塊空間。因此我們就可以向這塊內存中寫入值了。
pNode->pNext = NULL;
這里的pNext也可以指向下一塊申請的內存空間(可以用來回答問題一),這里就不寫了,大家要自己摸索才行。
說到這里,不得不說說結構體的對齊問題,什么是結構體對齊,為什么要對齊。我們都知道計算機的內存單位換算都是以2的多少次方來計算的,這樣計算是有目的性的。當然是為了計算機的執行效率,大家可以想象一下,假如我們一個變量的類型占用3字節,一個5字節,一個1字節。計算機在尋址的時候對于這種參差不齊的內存會降低它的效率。所以通常默認情況下,結構體采用4字節對齊,意思就是說一些不足4字節的變量會可能被擴充到4字節大小或者與其它結構體成員變量進行合并成4字節。這樣浪費小小的一點內存效率上會提高很多。這里說到4字節,當然就有8字節,16字節,1字節,2字節對齊了。我們這里就默認談談4字節對齊,其它都是同理的。先舉個例子:
{
char age;
int num;
};
sizeof( struct Align ) = ?
這里求sizeof的結果我們得到的確是8,而不是我們想要的5。這里是8的原因是默認為4字節對齊,這里char占用1字節,int占用4字節,首先編譯器編譯的時候遇到char會去尋找周圍有沒有更多的可以合并的字節,一共合并成4字節,或者合并一部分然后擴充一部分構成4字節,但是這里沒有找到,那么age將被擴充到4字節,加上int的4字節,一共被擴充到了8字節。
align.age = 0xff;
align.num = 0xeeeeeeee;
我們以為在內存中分布為:
age num ff ee ee ee ee
然而:
age num ff cc cc cc ee ee ee ee
age多出來了3個字節,這里未初始化時填充的是0xcc。假如我們定義成:
{
char age;
char age1;
char age2;
int num;
};
那么age2將被擴充為2字節,age age1 age2合并成3字節再擴充一個字節就組成4字節了。這里sizeof還是為8字節。再比如:
{
char age;
int num;
char age1;
};
這樣sizeof結果出來將是12字節,原因也很簡單,首先在編譯age的時候,查找挨著沒有能合并成4字節的成員,那么就會擴充成4字節,age1同理,假如age為0xff,num為0xeeeeeeee,age1為0xaa,內存分布就為:
ff cc cc cc ee ee ee ee aa cc cc cc
問題二:這里為什么不將age和age1分別擴充為2字節然后再合并成4字節,結構體一共8字節?
再舉個例子。
{
char age;
double num;
char age1;
};
這里的sizeof將是24字節,原因就是結構體對齊還是有標準的,假如默認是4字節對齊,常理這里完全可以將age和age1分別擴充成4字節,整個結構體16字節。但是編譯器并沒有這么做,而是都擴充成了8字節,這是因為結構體在處理對齊問題的時候,都是以最大的基本類型數據成員為標準進行對齊(注意這里是基本數據類型)。假如:
{
int a[ 2 ];
}
struct Align
{
char age1;
struct SStudent stu;
};
這個Align結構體同樣還是12字節,而不是16字節。
再比如:
{
char a[ 13 ];
};
struct Align
{
char age1;
struct SStudent stu;
};
問題三:上面程序中Align結構體的大小是多少?為什么?
同樣再來看看結構體指針和任意指針強制類型轉換。
struct SStudent
{
byte age;
byte sex;
byte class;
};
byte array[ 99 ];
struct SStudent* pStu;
array[ 0 ] = 0xaa;
array[ 1 ] = 0xbb;
array[ 2 ] = 0xcc;
pStu = ( struct SStudent*)array;
上面這段程序,我們將array的前3個元素賦值為0xaa,0xbb,0xcc。這樣做的目的是想看看我們強制類型轉換過后,pStu結構體指針的pStu[ 0 ]三個成員是否就是array數組的前3個成員。答案是肯定的,大家可以自己調試監視看。這個array數組強制類型轉換過去后,pStu[ 0 ], pStu[ 1 ],..., pStu[ 31 ], pStu[ 32 ]。一共就有33個結構體數據塊。同樣pStu++類似的加減及累加都會跳躍SStudent結構體大小個字節。跟前面一篇提到的原理一樣。
在C++中結構體發生了翻天覆地的變化,跟C的結構體很大差別,這里暫時不說了。等我們說了函數的時候再談C++的結構體。不過本文提到的結構體相關在C++中同樣有效。在結構體中很多時候會用到位域,這里暫時不說,先留個思路在這里。等我們專門談位運算的時候再來詳細說明