陣列,存盤同型別的復合型別;結構體,存盤不同型別的復合型別,用于自定義資料結構,
計算機中,針對存盤大量資料的集合,有著兩種方式,一種是以塊式集中存盤資料,這就是陣列的存盤方式,大量同型別的資料集中放在一塊;另外一種大量資料逐個分開,但其存盤的資料項就包括下一個資料的存盤地址,就像一個方向標,指向下一個資料,整體來看就像連接起來的表格,所以這種結構被稱為鏈表,
一、陣列和指標
陣列作為順序存盤的典型,存盤相同型別的值,以該型別存盤大小為單位劃分,其長度就是其容量,可容納多少個該型別的值在宣告之初就定好了,陣列內元素的訪問可通過下標進行訪問(下標就是整數索引,從0開始),
1.1 初探陣列
陣列的常見宣告如下:
type array[size];
陣列常見的宣告就是資料型別加,陣列名后面中括號括起來陣列大小,在程式中,陣列往往用來存盤重要資料,它們的使用往往需要先進行初始化,比較直接的就是用大括號括起來的數值串列對陣列進行初始化(數值個數不得大于陣列大小),如下:
//完整的初始化
int nums1[4] = {1, 2, 3, 4};
//部分初始化
int nums2[4] = {1, 2};
//C99后支持的新特性,只初始化最后一個元素
int nums3[4] = {[3] = 4};
//不明確指定陣列大小的宣告初始化
int nums4[] = {1, 2, 3, 4, 5};
//錯誤示范
int nums5[4] = {1, 2, 3, 4, 5};
int nums6[4];
//下面這步是非法的
nums6 = {1, 2, 3, 4};
如上,陣列的初始化是在宣告之初就該進行了的,而且初始化接受部分的初始化,這種初始化是從陣列第一個元素開始初始化直到用完常量值,剩下的由編譯器自動賦以型別零值,上面這種用大括號括起來的串列來對陣列進行初始化,在c++里面被稱為串列初始化,所以這里也這么稱呼吧,方便點,如上面的錯誤例子,當陣列在初始化時初始化串列大于陣列容量時,編譯器直接報錯,而且宣告以后,在后面的陳述句進行串列初始化也是非法的,同樣會報錯,但可以宣告固定大小的陣列而不初始化,還有如上面nums4這樣不指定size的,宣告初始化以后,其大小就是初始化的串列長度,也就是說nums4的長度是5,
陣列的使用
陣列是一個集合,陣列的使用往往就是陣列內元素的讀取和寫入,而陣列內元素的呼叫可以通過陣列下標來索引該元素,而陣列的下標索引從0開始,下面是幾個應用方式:
//1
int nums[4] = {1, 2, 3, 4};
scanf("%d", &nums[0]); //"5"
printf("%d\n", nums[0]); //"5"
//2
int nums1[4] = {[3] = 10};
printf("%d, %d\n", nums1[0], nums1[3]); //"0, 10"
scanf("%d", &nums[0]); //"8"
printf("%d, %d\n", nums1[0], nums1[3]); //"8, 10"
//3
int nums2[4];
printf("%d, %d, %d, %d\n", nums2[0], nums2[1], nums2[2], nums2[3]); //"8, 0, 20, 0"
scanf("%d%d%d%d", &nums2[0], &nums2[1], &nums2[2], &nums2[3]); //"0 1 2 3"
printf("%d, %d, %d, %d\n", nums2[0], nums2[1], nums2[2], nums2[3]); //"0, 1, 2, 3"
如上面的三個例子所示,三個整型陣列,三種狀態下(完全初始化、部分初始化,只宣告)的讀取和寫入,其實,陣列的使用,和for這種計數回圈天然適配,如下:
int nums[4], i;
//寫入資料
for(i = 0;i < 4;i++)
scanf("%d", &nums[i]); //逐行輸入"0"、"1"、"2"、"3"
//讀取
for(i=0 ;i < 4; i++)
printf("%d, ", nums[i]);
//"0, 1, 2, 3,"
呼叫陣列元素的另一種方式
上面使用陣列元素的方式是基于下標來進行,但也有另一種方式進行呼叫,陣列名的值,本身就是一個指標常量,它是陣列第一個元素的地址,所以,陣列元素的呼叫也可以用指標來進行,如下:
int nums[4] = {0, 1, 2, 3};
printf("%d, %d\n", *nums, *(nums + 2)); //"0, 2"
int *p = nums, i;
for(i = 0;i < 4; i++)
printf("%d, ", *(p + i)); //"0, 1, 2, 3, "
如上使用都是可以的,不過使用指標要注意的就是*取值符和&取址符,指標本身是指向某地址的變數,而*取值符的作用就是用來取指標指向地址的值,而取址符對指標本身往往沒有多大使用,因為使用場景往往更關注指標指向地址的值,這里是想提醒不能把指標當做尋常變數那樣使用取址符&,
要注意的,陣列的使用和指標有共通之初,但并非等同于指標,
1.2 多維陣列
在實際生活中,資料集有集合形式,也有矩陣形式,針對這種種資料的處理,C中往往都用陣列進行,只是陣列的形式有所不同,集合串列用一維陣列,矩陣用二維陣列,乃至有三維陣列、四維陣列應付更復雜的資料結構,這部分進行的就是對陣列的學習解讀,
二維陣列
陣列的所謂二維三維方面,在這里的體現,用下標展示會更加直觀,如下宣告定義一個二維陣列:
//完全初始化
int matrix[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
//部分初始化
int matrix1[2][3] = {
{0, 1},
{3, 4}
};
//不指定陣列大小的宣告初始化
int matrix2[][3] = {
{0, 1, 2},
{3, 4, 5}
};
回顧一下,陣列是相同型別元素的串列,一維陣列是一個簡單串列,里面存放著同型別的一個個常量值,那二維陣列呢?它則是存放著一個個一維陣列的另類串列,所以,不去深究陣列內的元素,其實二維陣列和一維陣列乃至多維陣列都是一樣的,它們都是一個有序串列,
就著上面的結論來看上面的例子就簡單多了,matrix是一個存放著兩個陣列的串列,內層陣列則存放著三個整型資料,因此,可以在陣列大小范圍已定的情況下,用不足其大小的串列去對其進行初始化,比如matrix1,還有不明確外層陣列數量的情況用符合內層大小的一定個數的陣列去對陣列初始化,比如matrix2(還是有點拗口),上面的例子也可以改成下面的樣子:
//完全初始化
int matrix[2][3] = { {0, 1, 2},{3, 4, 5} };
int matrix[2][3] = {0, 1, 2, 3, 4, 5};
//部分初始化
int matrix1[2][3] = { {0, 1},{3, 4} };
int matrix1[2][3] = {0, 1, 3, 4};
//不指定陣列大小的宣告初始化
int matrix2[][3] = { {0, 1, 2},{3, 4, 5} };
int matrix2[][3] = {0, 1, 2, 3, 4, 5};
這樣來看,就比較直觀,實際上二維陣列的元素也是順序存放的,針對二維陣列元素的訪問,還是使用下標的方式比較直觀,但使用指標的方式進行訪問也是可以的,不過相對來說,就形式來看,比較麻煩,一層疊一層的,
int matrix[2][3] = {0, 1, 2, 3, 4, 5};
int *p = matrix; //GNU中會有警告,沒有在vs嘗試
printf("%d\n", *(p+4)); //"4"
printf("%d\n", *((p+1)+1)); //"2",這里被計算器理解成p+2的取值
int (*p1)[3] = matrix; //定義一個int [3]型別的指標,初始化其值使其指向matrix
printf("%d, %d\n", *(*p1 + 1), *(*(p1+1)+1)); //讀取二維陣列第一行第二列和第二行第二列的值,輸出"1, 4"
總的來說,二維陣列的指標呼叫方式有兩種,一種是當做一維陣列的正常偏移呼叫,第二種就是宣告一個指向內層陣列型別的指標,并初始化為指向matrix指向地址,因為*運算子的優先度要低于[]運算子,所以為了表明指標身份,需要把變數名和*運算子括起來;另外,需要重點說明的是上面的p1是一個int [4]型別的指標,(指標,是指向某地址的變數,具體來說是某種型別值的地址),所以,實際上型別對于指標就是束縛,防止它訪問的存盤越界從而得到期望以外的值,而int [4]型別也是束縛,可以把這樣的4個整型連起來的存盤空間看做一個單位,現在的指標就指向這么一種單位的地址,當它初始化成matrix的地址后進行偏移,它實際上就是以matrix的內層陣列為單位進行偏移,順便說一句,有確定型別的指標也有空型別的指標,
和for回圈配合使用的二維陣列
因為二維陣列可以展開成一維陣列,所以用回圈呼叫就有嵌套和不嵌套的使用,如下:
int matrix[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
int i, j, *p = matrix;
//嵌套回圈
for(i = 0;i< 2;i++) {
for(j = 0;j < 3;j++)
printf("%d, ", matrix[i][j]);
printf("\n");
}
//"0, 1, 2, "
//"3, 4, 5, "
//不嵌套回圈,編譯器報警系列
for(i = 0;i < 6; i++)
printf("%d, ", *(p + i));
//"0, 1, 2, 3, 4, 5, "
//嵌套回圈
int (*p1)[3] = matrix;
for(i = 0;i<2;i++) {
for(j=0;j<3;j++)
printf("%d, ", *(*(p + i) + j));
printf("\n");
}
//"0, 1, 2, "
//"3, 4, 5, "
多維陣列和二維陣列共通,只不過是集合往更深一層嵌套,這里就不展開了,
1.3 變長陣列和動態陣列
在日常應用中,陣列的長度往往是固定的,固定的方式各不一,比較常見的,就是使用宏定義定下陣列長度,而這種陣列往往存盤的就是一個個常量資料,是日常生活中基本不動的資料,比如year陣列就該有12個數,每個數存放每個月的天數;week陣列就該存著周日到周一這么幾個數值,具體是字串還是整型數就看需要了,如下:
#define MONTH 12
#define WEEK 7
//const限定符
const int leap_year[MONTH] = {31, 28, 31, 30, 31, 30, 31, 30, 31, 31, 30, 31};
const char week[WEEK] = {"Sunday", "Monday", "Tuesday", "Wedsday", "Thursday", "Friday", "Saturday"};
除此以外,還可以用整型變數或者整型運算式來確定陣列長度,在C99之前,規定的標準是以整形常量運算式為陣列確定大小,而不是整型運算式,這是C99后添加的新特性,其實就個人來看,無非就是一句陳述句和兩句陳述句的區別,不過這里也說明相對一詞的重要性,變數,不確定的,但在運行程式中它是確定的,也就是相對程式運行來說,它是固定的,這個就是變長陣列--VLA,
int sum, a, b;
//輸入a、b值
//c99之前
sum = a * b;
int num[sum];
//c99以后
int num[a*b];
相較于一開始就用常量給定區域的陣列而言,變長陣列也算是動態陣列了,這種陣列長度在程式運行時確定的陣列就是動態陣列,反過來說,運行以前確定的,就是靜態陣列了(因為常量就是運行前就確定了的),除此以外,還有一種動態陣列,這種陣列隨程式需要而確定大小,記憶體空間也是自己申請,使用完畢自己釋放,這種申請的記憶體, 來自于堆,由于陣列是可以逐層嵌套的,對于這種陣列就需要自外向里,逐層創建,而釋放則是反過來,由里向外逐層釋放,而這種,才是常說的動態陣列,
堆區和堆疊區
對于一個運行程式來說,記憶體常被分為堆疊區、堆區、常量區、靜態區和代碼區,初始,程式以可執行代碼的形式存放在磁盤中,作業系統在運行程式的時候就會把代碼和靜態資料(如初始化變數)加載到記憶體中,加載完畢后,分配記憶體給運行時堆疊(存放區域變數、函式引數和回傳地址,main也是函式),除此以外,還有著堆記憶體由程式顯式請求分配,對于陣列、一些資料結構(鏈表、散串列、樹等)都需要堆區存盤,隨程式運行的時候逐漸變化,
在C語言中,記憶體的顯式請求和顯式釋放都有兩個專門的函式--malloc函式和free函式,而c++中則是new運算子和delete運算子申請和釋放,上面兩函式原型如下:
#include <stdlib.h>
void *malloc(int num);
void free(void *address);
//另外幾個相關函式
void *calloc(int num, int size);
//在記憶體中動態地分配 num 個長度為 size 的連續空間,并將每一個位元組都初始化為 0,所以它的結果是分配了 num*size 個位元組長度的記憶體空間,并且每個位元組的值都是0,
void *realloc(void *address, int newsize);
//該函式重新分配記憶體,把記憶體擴展到 newsize,
malloc函式的用法,申請num指定位元組的記憶體并回傳指向該記憶體的空指標,一般在賦值給特定指標前需要強制型別轉換;free則是把address指向的堆記憶體進行釋放,沒有回傳值,常見的配合陣列的使用如下:
#include <stdlib.h>
int *p;
p = (int *)malloc(4* sizeof(int));
for(i = 0;i < 4; i++)
p[i] = i;
for(i = 0;i < 4;i++)
printf("%d, ", p[i]);
free(p);
如上,申請一個4整型大小的記憶體空間,在強制型別轉換后賦值給整形指標p,這時候就可以針對p指標做陣列操作了(指標操作也行),重點,用完后記得free,
動態二維陣列
上面已經有二維陣列的學習,所以這里主要是一個嵌套malloc和free的呼叫例子
#include <stdio.h>
#include <stdlib.h>
int main() {
int **p, i, j;
//定義一個指向指標陣列的指標,并指向能存下3個整型指標的連續記憶體
p = (int **)malloc(3*sizeof(int *));
//逐層分配空間并賦值
for (i = 0;i < 3; i++){
p[i] = (int *)malloc(4*sizeof(int));
for(j = 0;j < 4;j++)
p[i][j] = i * 3 + j + 1;
}
//從陣列末開始讀取值,一行讀取完畢就釋放那一行的記憶體
for(i = 2;i >= 0;i--){
for(j=3;j>= 0;j--)
printf("%d, ", p[i][j]);
printf("\n");
free(p[i]);
}
//釋放最外層的指標存放記憶體
free(p);
return 0;
}
/*輸出
10, 9, 8, 7,
7, 6, 5, 4,
4, 3, 2, 1,
*/
如上,針對動態二維陣列的記憶體申請和釋放就更能體現二維陣列的性質,外層陣列存放內層陣列地址,通過地址訪問到該陣列內容,而內陣列也用下標把資訊拆分得更加細致,嗯,雖說上面的例子是從陣列最深處開始訪問并逐漸向外面開始釋放,但我正常從第一行內層陣列開始釋放也沒有出現問題,暫時沒有出現什么問題,但還是能倒著釋放就倒著來吧,
1.4 指標的更多形式
指標就一個功能,指向某個地址,然后根據其指向的地址,它就有了不同的稱呼:
- 簡單指標,指向簡單的同型別變數,如簡單陣列;
- 二重指標,指向簡單指標的指標;
- 指標陣列,存放指標的陣列,其陣列可以賦值給二重指標;
- 函式指標,指向函式的指標,就使用上來看有點像別名,其實是為了方便呼叫的;
- 結構體指標,指向某個資料結構的指標
1.5 陣列的邊界
陣列是持續記憶體,在訪問內部元素的時候一旦越界,就會出現意料之外的行為,比如有一個元素長度為4的陣列a,但在訪問a[4]這種行為的時候,就屬于明顯的越界,但編譯器并不會報錯,這種問題的爆發會出現在程式運行的時候,有時候會輸出期望值以外的值,有時候則會意外停止,所以這里是做一個警告,在進行下標訪問的時候,要注意不能越界,另外像字串這種字符陣列,要習慣性地在陣列末尾放置一個'\0'符,
1.6 陣列和指標的異同
- 從記憶體存盤上看,陣列是同型別元素的集合,是一大塊固定數量型別的存盤;指標是存盤同型別地址的物件,根據系統的不同,其存盤也不同,但其不大于8位元組記憶體
- 從宣告定義上看,陣列的整體定義要在宣告之時,否則就只能逐一賦值,而指標可以拆開進行,而且也可以指向不同的地址
- 從使用上看,陣列使用下標進行元素訪問,指標用*運算子取值,配合陣列可以進行地址偏移
陣列和指標相互區別相互聯系,不可分割也不可等同,兩者應用場景和使用角度不一樣,
二、結構體
作為復合型別的重要組成,結構體可以由其他各種資料型別組成,共同構成一個表達特殊意義的資料結構,也是面向物件編程的一個重要出發點,場景切入,當我們要描述一個物件的時候,比如一個學生,那我們可以用哪幾個資料?成績、身高、體重、年級、班級,等等皆可,針對我們需求的場景,抽取重要屬性成結構體,程式需要處理的,就是結構體的屬性資料,當我們需要做一個成績系統,就需要學生的各科成績,當我們需要做健康檢測,就需要學生的身高、體重、視力等資料,需求不同,使用的屬性不同,當我們確定好一個資料結構以后,就是確定了一個自定義型別,我們可以用這個型別來宣告定義需要的變數,結構體的通用形式如下:
struct tag {
member-list
member-list
member-list
...
} variable-list ;
上面上面中,member-list就是成員串列,用字符型、整型、浮點型來填充,其變數名就是結構體tag的屬性名,variable-list就是成員串列,一般來說如果不是宣告為函式內區域變數,都不會在宣告結構體時就宣告這種型別的變數,所以一般全域的tag這里都是空留一個分號收尾,
結構體的宣告,其實是用戶自定義了一個型別,它告訴了編譯器這種型別由哪些資料構成,
2.1 初始化結構體
結構體是不同型別的集合,陣列是同型別的集合,但型別其實從記憶體角度上來說,就是劃分位元組和讀取規則不一樣,兩者都是一大塊記憶體,所以陣列的初始化,很多可以參考陣列,
要初始化一個結構體,先要宣告一個結構體,現在就抽取學生這一物件作為一個結構體,抽象其姓名、年齡和成績作為基本屬性,如下:
struct student {
char name[10];
int age;
float total_score;
};
struct student a = {"Xiao Ming", 14, 89.7};
如上為student結構體的一個宣告(這個宣告一般放在所有函式外面作為全域變數使用),宣告student型別的a物件,用的也是括號初始化,需要注意,C和C++針對結構體物件的宣告有點不同,C中進行宣告需要添加struct突出結構體型別,c++中則是可選項,可加可不加,
當然也可以進行宣告,然后逐個元素進行初始化,不過student的name屬性是陣列,所以它只能在宣告之時初始化,或者使用strcpy等方法進行賦值,
//只宣告不初始化
struct student temp;
temp.name = "Siri"; //error: assignment to expression with array type
strcpy(temp.name, "Siri"); //使用前要引入頭檔案string.h
temp.age = 10; //合法
printf("%f\n", temp.total_score); //"0.000000",編譯器自動初始化為float型別零值
另外初始化的方法還有:
struct student b = {.name = "Xiao Fei",
.age = 15,
.total_score = 98.6};
如上面這種方式就是C99和C11后為結構體添加的指定初始化器(designated initializer),在大括號內用點運算子和屬性名標識特定元素,等號賦值初始化,當然也可以部分初始化,這樣的話,沒有初始化的部分就會被編譯器自動初始化為型別零值,
2.3 訪問結構體
目前,關于結構體屬性的訪問,可以使用點運算子來進行,如上面的指定初始化器中的用法,點運算子加屬性名就可以訪問對應屬性,這就有點類似陣列下標,實際上,一般的結構體物件只有這種訪問方式,而結構體指標有著另外的方式來訪問結構體屬性,
struct student *p = &a;
printf("%f\n", p->total_score); //"98.6"
如上,作為指向結構體的指標,適用的訪問結構體屬性的方法就是箭頭,突出指標的存在,
2.4 結構體型別衍生
結構體是自定義型別,c中任何型別都可以產生指標和陣列,所以就也有結構體陣列和結構體指標,它們的宣告定義和具體元素的訪問沒有特殊之處,只不過湊在一起會讓人一下子有點不適,
struct student stu[3] = {
{"Xiao Ming", 14, 89.7},
{"Siri", 11, 80},
{"Xiao Fei", 88}
};
//錯誤,這種宣告只能在結構體型別宣告的時候進行,如下
struct student {
char name[10];
int age;
float total_score;
} stu[3] = {
{"Xiao Ming", 14, 89.7},
{"Siri", 11, 80},
{"Xiao Fei", 8}
};
printf("%d\n", stu[2].age); //"8"
//只宣告,后續可以像陣列只宣告那樣進行類似的初始化
struct student stu[4];
上面就是結構體陣列的宣告定義,關于結構體指標的宣告比較簡單,就簡單的型別加*標識即可,然后用同型別普通變數的地址給予賦值就算初始化了,另外結構體陣列的元素呼叫用的是下標訪問,而結構體指標用的是箭頭訪問,但當指標指向陣列時,它用的,還是下標訪問方法,
復合嵌套
上面從一開始,結構體的屬性就納入了陣列這一復合型別,所以結構體也是可以內嵌指標的,而且把陣列改換成指標,也會方便很多,當然,在C語言中,方便與危險等同,越是簡單的地方,越容易出事,不過這里不討論,作為描述物件的結構體,物件內部也可以有物件屬性,所以結構體同樣可以嵌套結構體,比如給student拆解名字屬性,名字屬性就可以作為一個物件,內部填充姓和名,如下:
struct Name {
char *surname;
char *name;
};
struct student {
struct Name name;
int age;
float total_score;
};
本來是想直接把student放前面,Name搞個前置宣告的,發現不行,而且結構體的宣告也不能進行初始化,還有各種各樣的特性,,,,,,還是c++用多了,總把c的struct當做類class,
宣告還是比較簡單的,下面介紹一下其初始化和使用:
//main內
int i;
//普通student物件
struct student a = {{"Jack", "Chen"}, 25, 100.0};
//student陣列
struct student stu[2] = {
{{"Hong", "Pan"}, 26, 88},
{{"Jin", "Jiang"}, 27, 98}
};
//student指標
struct student *p = &a;
printf("name: %s %s\n", a.name.surname, a.name.name);
for(i=0;i<2;i++)
printf("name: %s %s, score: %.2f\n", stu[i].name.surname, stu[i].name.name, stu[i].total_score);
printf("age: %d\n", p->age);
//宣告student物件但不初始化
struct student temp;
temp.name.surname="Siri";
temp.name.name="MI";
scanf("%d", &temp.age); //80
printf("age: %d\n", temp.age); //"80"
算是比較簡單的了,具體的實驗進行,就看個人經驗了,當這些個簡單的概念組合在一起變得龐大的時候,就大而化之,關注最底層的那一部分的特性,往往能更好理解和使用,
2.5 從堆記憶體分配的結構體
上面有介紹過動態陣列,它是由malloc顯式向堆記憶體申請特定大小的空間并由free顯式釋放的;同樣的,結構體也可以,應該說,它往往是這么使用的,當它作為一個重要的資料結構的時候,
#include <stdlib.h>
struct student *p;
p = (struct student *)malloc(2*sizeof(struct student));
/*一堆應用操作*/
free(p);
其實這里并不復雜,但也只是一個概念的記錄,復雜的是引入資料結構以后的事,這里篇幅也不夠,只做記錄,
重要的位元組對齊
先舉一個例:
struct Name {
char *surname;
char *name;
};
struct A {
int age;
struct Name name;
float total_score;
};
struct B {
int age;
float total_score;
struct Name name;
};
printf("A: %d, B: %d\n", sizeof(struct A), sizeof(struct B));
//輸出:A: 32, B: 24
printf("ptr: %d, int: %d, float: %d, Name: %d\n", sizeof(char *), sizeof(int), sizeof(float), sizeof(struct Name));
//輔助資料:"ptr: 8, int: 4, float: 4, Name: 16"
上面這個例子,指的是有著同樣屬性的A、B兩個結構體,卻因為屬性的先后宣告順序不一樣,使得sizeof得到的占用記憶體不一樣,出現這種情況的原因,就是這里要介紹的內容--位元組對齊,
首先,說明一下位元組對齊是如何對齊,在C中,一個變數,一塊記憶體,一個復合變數,一大塊記憶體,比如陣列,但結構體就不一樣,因為它存的是不同型別的變數,不像陣列那樣內部元素一致整齊,所以,位元組對齊,就是針對結構體而言的一種調整了,所謂對齊,就是結構體中所有成員在分配記憶體時都要向成員中最大那個對齊,最大的那個作為一個標準,當第一個成員沒有超出這個標準,后面緊跟著的也沒超出這個標準,就會加在一起,看看是否到了這個標準,到了就可以新開一塊最高標準的記憶體,沒到就繼續疊,最后湊起來的記憶體塊就是最大那塊的倍數(這里考慮的都是基本型別,不包括復合型別),如下:
struct A {
char a;
int i;
};
struct B {
char a;
char b;
int i;
};
struct C {
char a;
char b;
char c;
char d;
char e;
int i;
};
struct D {
char a;
int i;
double db;
};
struct E {
char a;
int i;
int i2;
double db;
};
printf("char: %d, int: %d, double: %d\n", sizeof(char), sizeof(int), sizeof(double));
//"char: 1, int: 4, double: 8"
printf("A: %d, B: %d, C: %d, D: %d\n", sizeof(struct A), sizeof(struct B), sizeof(struct C), sizeof(struct D));
//"A: 8, B: 8, C: 12, D: 16, E: 24"
上面的例子的空間結構大概可以參考如下:
struct A
| a | 空 | 空 | 空 |
|---|---|---|---|
| i | i | i | i |
| 占了8位元組 | |||
struct B |
|||
| a | b | 空 | 空 |
| --- | --- | --- | --- |
| i | i | i | i |
| 占了8位元組 | |||
struct C |
|||
| a | b | c | e |
| --- | --- | --- | --- |
| e | 空 | 空 | 空 |
| i | |||
| 占了12位元組 | |||
struct D |
|||
| a | 空 | 空 | 空 |
| ---- | ---- | --- | --- |
| db | |||
| 占了16個位元組 |
struct E
|a|空|空|空|i|i|i|i|
|----|----|---|---|---|-|-|-|-|-|
|i2|i2|i2|i2|空|空|空|空|
|db||||||||||
占了24個位元組
上面都是針對結構體由簡單的基本型別進行的解讀,如果是復合型別呢?加進去陣列或者其他結構體呢?這種存在就比較特殊,屬于好好的記憶體塊里面凸出去的大頭,就像臉上的粉刺,讓臉看得沒有那么整潔雅觀,
其實加進去陣列也沒什么,結構體也是基本型別的集合,雖說不至于拆開陣列留空,但填補空缺還是夠的,最要命的還是內嵌結構體,如下:
struct Name {
char *surname;
char *name;
};
struct A {
int a;
struct Name name;
float f;
}sa;
struct B {
int a;
float f;
struct Name name;
}sb;
struct C {
struct Name name;
int a;
float f;
}sc;
printf("ptr: %d, int: %d, float: %d, Name: %d\n", sizeof(char *), sizeof(int), sizeof(float), sizeof(struct Name));
//輔助資訊:"ptr: 8, int: 4, float: 4, Name: 16"
printf("A: %d, B: %d, C: %d\n", sizeof(struct A), sizeof(struct B), sizeof(struct C));
//"A: 32, B: 24, C: 24"
printf("A: %p, %p\nB: %p, %p\nC: %p, %p\n", &sa.a, &sa.name, &sb.f, &sb.name, &sc.name, &sc.a);
/*輔助性地址資訊
A: 00000000004079C0, 00000000004079C8
B: 00000000004079A4, 00000000004079A8
C: 0000000000407980, 0000000000407990
*/
上面的例子就是自定義了一個存放兩個指標的算是規整的結構體,然后把它作為大頭放進三個同樣存放基本型別的結構體中,分不同順序來進行實驗,直觀點的記憶體結構如下:
struct A
| a | a | a | a | 空 | 空 | 空 | 空 |
|---|---|---|---|---|---|---|---|
| name | |||||||
| name | |||||||
| f | f | f | f | 空 | 空 | 空 | 空 |
| 占據32位位元組,name獨占16位元組,分成8位元組,a和f被name分開,所以各自占了8位元組,但型別限制,實占4位元組,其余位元組位空置 |
struct B
| a | a | a | a | f | f | f | f |
|---|---|---|---|---|---|---|---|
| name | |||||||
| name | |||||||
| 占24位元組,a和f合在一起湊足8位元組,name獨占兩個8位元組,同理,struct C也是一樣構造 |
struct C
| name | |||||||
|---|---|---|---|---|---|---|---|
| name | |||||||
| a | a | a | a | f | f | f | f |
| 和上面的大B同理,上面就很明白了,哪怕是復合型別如結構體,也不是簡單看其整體記憶體,在這部分其實要有個上限,那就是系統限定的位元組對齊最大單位,當所有成員中的最大成員超出這個單位,那就按系統的來,64位系統限定最大對齊為8位元組,32則是限定為4位元組, |
位元組對齊的意義
在沒有位元組對齊的時候,結構體的記憶體分配和陣列一樣,都貼在一起的,這樣一來就會出現那么幾個資料湊成奇數記憶體塊,而CPU讀取資料是根據資料總線來讀取的,16位資料總線可以一次讀取兩位元組的資料,32位資料總線一次能讀取4位元組資料,64位系統則是8位元組資料,當char型成員和int型成員湊在一起時為5位元組記憶體,32位系統就沒辦法一次讀取正確,需要讀取第二次,所以拓展開,char成員占4位元組記憶體的一位元組,其余置空,這樣對于CPU就能每次讀取正確,這算是空間記憶體換取時間的意思了,位元組對齊的存在是針對CPU的讀取效率問題,
手動位元組對齊
當兩臺機器進行通信,它們需要使用一種資料結構傳輸資訊,那它們采取的位元組對齊規則一致就顯得很有必要,這種時候就可以進行手動的位元組對齊規則調整,方法有兩種,一種是預編譯中設定規則,另一種就是陣列宣告時設定對齊規則,如下:
//設定其后的代碼中位元組對齊以n為準
#pragma pack(n)
struct A {
...//balabala
};
//設定其后的代碼中位元組對齊根據默認進行
#pragma pack()
struct B {
...
}__attribute__((packed));
//上面的__attribut__是一個給編譯器看的設定,重點關注里面的引數
//packed引數,表示按照實際來進行位元組對齊,也就是不對齊
末言
以上僅為個人參考書籍博客等資料而后實驗所得,一家之言,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/530505.html
標籤:C
