數組
開發中數組是一種常見的資料結構,當然我們知道數組相當於一種容器,但是它不僅僅只能存數值和字元,同時它還可以存放函數的入口地址,以及結構體的資料。
typedef struct _value{ int val_1; int val_2;}VALUE;typedef struct _table{ char index; char data[100];int (*UniSetFunc)(VALUE*);}TABLE; int add(VALUE *val ){ int temp = 0; temp = val->val_1 + val->val_2; return temp;} TABLE page[10]{ {“ First section ”, “abcdefghijklmn”, add}, {“Second section”, “opqrstuvwxyz”, NULL}};int main(){ VALUE AddValue; AddValue.val_1 = 2; AddValue.val_2 = 4; int result = 0; result = page[0]-> UniSetFunc(&AddValue); printf(“The Result of add is %d\n”, result); return 0;}
此時數組就轉換為類似於Python語言中的字典的結構,便於後續的開發利用以及追加升級和維護。
程式碼分析:首先我們知道函數的名字可以做為函數的入口地址(類似於數組的名代表數組的地址一樣),所以在TABLE結構體中我們定義了一個成員函數int (*UniSetFunc)(VALUE*); 此處UniSetFunc作為函數的入口參數,VALUE*代表函數的形參類型;TABLE類型的數組 page[10]即包含了結構體的資料以及函數的入口地址,可以通過調用page[0]-> UniSetFunc(&AddValue)來間接地調用add函數並實現AddValue中AddValue.val_1和AddValue.val_1兩個數求和的運算。
記憶體命中率問題:
為了可以提高代碼運行效率,要才充分的利用記憶體空間,應連續的訪問記憶體地區。
我們知道數組在記憶體中存放的位置是一整塊的地區,並且是連續存放的,對於定義的數組array[2][2]來說,假設array[0][0]的地址為0x04030,則array[0][1],array[1][0],array[1][1] 的地址分別為0x04031, 0x04032, 0x04033;提高記憶體命中率的即是應該儘可能的連續的訪問記憶體空間地區,而非跳躍式的訪問;接下來讓我們來看一個矩陣相乘的問題。
for (int i = 0; i < 2; ++i) { for (int j = 0; j < 2; ++j) { for (int k = 0; k < 3; ++k) { matrix[i][j] += matrix1[i][k] * matrix2[k][j]; } } }
以上代碼是常用的將矩陣matrix1與matrix2相乘然後賦值給matrix的方法,即用matrix1矩陣得到行向量乘以矩陣matrix2的列向量,然後賦值給matrix,這樣由於矩陣在記憶體儲存的結構,我們可以清楚的知道訪問matrix2的時候並非採用連續的訪問方式,故記憶體的命中率較低。接下來我們看一種高記憶體命中率的方法。
for (int i = 0; i < 2; ++i) { for (int k = 0; k < 3; ++k) { for (int j = 0; j < 2; ++j) { matrix[i][j] += matrix1[i][k] * matrix2[k][j]; } } }
可以看出代碼僅僅將第二個for迴圈與第三個for迴圈交換了位置,而其他的部分沒有任何變化,然而記憶體的命中率卻大大的提高了,我們採用將matrix1與matrix2矩陣內部各原素依次相乘然後再累加的方式,來進行矩陣相乘的目的,這樣在訪問matrix1與matrix2矩陣時沒有發生任何記憶體未命中的問題,從而提高了記憶體命中的機率。
volatile,const以及static之間的關係:
const關鍵字為常量關鍵字,它作用的量為常量,不允許程式去改變該常量的值,如const int value = 12;此常量value值不允許程式將其改變,在開發的過程const關鍵字會經常用到,為了防止程式意外的改變某一固定的常量,我們應及時的給其加上const關鍵字;另外const關鍵字作用於常量時必須直接給常量初始化,因為在整個程式運行大的過程中不允許對其改變,故必須立即初始化,例如:const int value = 12 是正確的,而const int value; value = 12;這樣的文法是錯誤的! 接下來我們來研究一個稍微難一點的問題,即常量指標與指標常量。先看一段代碼:
#define SWITCH 1int main(){ int val_1 = 5; int val_2 = 10; const int *p1 = &val_1; int const *p2 = &val_1; int *const p3 = &val_1;#ifdef SWITCH // This is a switch *p1 = 20; *p2 = 21; *p3 = 22;#endif#ifndef SWITCH p1 = &val_2; p2 = &val_2; p3 = &val_2;#endif printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); return 0;}
在cygwin編譯器下執行,我們可以看到這樣的錯誤:
我們可以清楚的看到,指標p1與p2僅能讀取val_1中的值為指標常量,即不能改變它所指的變數的內容,所以*p1 = 20; *p2 = 21;兩條命令是錯誤的!(#ifdef SWITCH … #endif 為條件編譯即為宏開關)。然後我們將#define SWITCH 1 語句給注釋掉,此時將運行第二塊代碼,得到結果如下:
從錯誤中可以看出p3為常量指標,它只能指向一個固定的地址,而不能改變它所指的方向,故p3 = &val_2;的操作是錯誤的,因此正確的代碼如下:
int main(){ int val_1 = 5; int val_2 = 10; const int *p1 = &val_1; int const *p2 = &val_1; int *const p3 = &val_1; printf("Frist\n"); printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); p1 = &val_2; p2 = &val_2; *p3 = 22; printf("Second\n"); printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); return 0;}
啟動並執行結果為:
最後終結:常量指標(const int *p或int const *p)表示指標p不能改變它所指向地址裡面所存的值,而可以改變它所指向的地址;指標常量(int *const p)表示指標p不能改變它所指向的地址,即指標不能改變它所指向的位置,但是可以改變它所指的位置中的內容。若想要指標既不能改變所指向的位置,又不能改變該處的內容,那麼可以這樣定義:
const int * const p = &a;或int const *const p = &a; 在定義函數的時候,若該入口參數在程式執行的過程中不希望被改變,則一定要將該形參用const來修飾,一來這樣可以防止該段程式將其改變,二來對於形參而言,一個無論是否是const修飾的實參都可以將其傳入const形的形參,而一個const形的實參是無法傳入非const形的形參中,所以為了使編譯不出錯在定義函數的時候,一定要將不希望被改變的量用const關鍵字來修飾。
Static關鍵字為靜態關鍵字,它的作用是將作用的變數存入記憶體中,而非存入寄存器中(即將變數存入堆中而非棧中),並且該作用的變數僅儲存最近一次擷取的值。接下來我們來看一段代碼。
void countfun (){ static int count = 0;++count;printf(“This is %d number, enter into this function !\n”, count );}int main(){ for (int i = 0; i < 5; ++i) { countfun();}return 0;}
這段代碼的運行結果如下:
而若將除去static關鍵字,則啟動並執行結果如下:
由此我們可以清楚的看出,static作用的變數count只會存入當前的結果,因此迴圈調用countfun( )函數的時候並沒有從新將count變數置為0,而是儲存了前一次的值。
Static關鍵字在項目中的應用是很廣泛的,它不僅僅有上述所介紹的特點,同時若想要定義的全域變數在整個工程中僅在當前.C檔案中有效時,也應該將這個全域變數用static來修飾,這樣在其他的檔案中是無法訪問這個變數,從而降低了模組間的耦合度,提高了模組的內聚性,防止其他檔案將其改變,從而更加的安全。
volatile關鍵字在嵌入式領域中是十分重要的一個關鍵字,尤其是在與硬體相關或多線程的編程中更為重要。volatile關鍵字修飾的變數說明它是可以隨時發生改變的,我們不希望編譯器去最佳化某些代碼的時候,需要將這個變數用volatile關鍵字來修飾,從而程式每次訪問該變數的時候是直接從記憶體中提取出來,而不是從臨時的寄存器中將該變數的副本給提取出來利用!例如當我們想要實現某個中斷處理時,其用來做判斷條件的標記位則應該用volatile來修飾,這樣當這個中斷在別的地方被觸發的時候就可以被即時的檢測到,不至於由於最佳化而忽略中斷。接下來我們看一段代碼:
int main(){ volatile int i = 10; int a = i; printf(“i = %d\n”, a);__asm{ mov dword ptr[ebp-4], 0x10}int b = i;printf(“i = %d\n”, b);return 0;}
此程式輸出結果為i = 10;i = 16; 若將volatile關鍵字去掉,則結果為i = 10;i = 10;
即不加關鍵字會將彙編代碼忽略掉,所以為了防止代碼最佳化以及可以及時檢測到外部程式對該變數的改變,我們必須將該變數加上volatile關鍵字。我們知道volatile關鍵字表徵該量是易變的,const關鍵字代表該量是常量不能改變,那麼volatile與const是否可以一起修飾同一個量呢,是肯定的,例如在硬體編程中ROM所儲存的資料是不允許使用者改變的,即指向該資料的指標必須為常量指標(const int *p = &ram_data),然而開發商卻可以將其意外的改變,為了防止ROM的內容被意外的改變時,而使用者程式沒有及時的發現,必須將該量用volatile修飾,所以應這樣定義該指標(volatile const int *p = &rom_data)。
位元運算
在數字解碼與編碼的過程中,位元運算的操作是司空見慣的事,同時位元運算在提高程式的效能方面也獨佔鰲頭,因此位元運算操作是必需要深入瞭解的問題。
在乘法以及除法的操作中我可以使用未運行來提高代碼的品質,例如:a = a * 16;這種操作完全可以替換為:a = a << 4;我們知道左移一位相當於將原數乘以2,左移N位則相當於乘以2^N,前提是在沒有發生溢出的情況下;故上例即相當於將數a左移4位,對於某些乘以非2的整數冪情況,如 a = a * 9;則可以改寫為a = (a << 3) + a; 同理右移相當於除以2的整數冪,當然以上所有情況都是在沒有發生資料溢出的情況下,因此位元運算操作要格外的小心,否則極有可能發生出錯的情況。
在資料類型轉換的過程中也需要做位元運算操作,例如我們想將一個unsigned short類型的資料存入unsigned char類型的數組中,就需要進行位元運算,首先分析知道unsigned short佔用16個位元組,unsigned char佔用8個位元組,想要將大位元組的資料存入小位元組,必須要對大位元組進行分割,即將高8位與低8為分離開來分別存放,來看實現代碼:
unsigned char * DrawCompo_Pj_BT_Change(unsigned short *subarray){ unsigned char temp[500]; (void)_sys_memset(&temp, 0x00, sizeof(temp) ); unsigned short i = 0; while (subarray[i] != 0x0000) { if( (subarray[i] & 0xff00) == 0x0000) { temp[i++] = (unsigned char)(subarray[i] & 0x00ff); } else { temp[i] = (unsigned char)( (subarray[i] & 0xff00) >> 8); temp[i++] = (unsigned char)(subarray[i] & 0x00ff); } } temp[i] = '\0'; return temp;}
temp[i] = (unsigned char)( (subarray[i] & 0xff00) >> 8);即取subarray[i]資料的高8位,temp[i++] = (unsigned char)(subarray[i] & 0x00ff);取低8位。這樣就可以實現將高位元組的資料完整的存入到低位元組中。
位元運算還可以用來判斷變數的符號,我們知道對於一個有符號的變數,其最高位為其符號位,故檢查改變的最高位即可知道該變數為正還是為負。看一段代碼:
int main(){ short test_data = -12; if (test_data & 0xF000) { printf("This number is negative "); } else { printf("This number is positive "); } return 0;}
對於想要交換兩個數的值的時候,通常我們的做法如下:
void swap(int &data1, int &data2){ int temp = 0; temp = data1; data1 = data2; data2 = temp;}
這樣的代碼比較簡單易懂,然而美中不足的是它會產生一個臨時變數temp,接下來我們用位元運算來重寫這個程式;
void swap(int &data1, int &data2){ data1 = data1 ^ data2; data2 = data1 ^ data2; data1 = data1 ^ data2;}
從上面的代碼我們可以看出少了一個臨時變數,同時也加快了代碼的運行效率。
尾遞迴:
遞迴調用給我們帶來了很多方便,也簡化了代碼,使程式看起來更加的簡潔和明了,但遞迴調用也通常伴隨著一個潛在的危險:出棧,接下來我們來看一個常見的遞迴。
int factorial(int n){ if (n < 1) { return 1; } else { return factorial(n-1)*n; } }
通常在求一個數的階乘的時候我們習慣於採用上述方法,然而分析來看當輸入的n值較大時,factorial(n-1)*n所計算的值會不斷的壓入堆棧,產生很多的臨時變數,等待下一個的值的確定才得以計算,然而在記憶體中堆棧的大小是固定的,當輸入的n值很大時,極有可能產生堆疊溢位!因此,有一個好的方法解決這種問題即尾遞迴調用。接下來我們來看這種強大的演算法。
int factorial(int n, int m){ if (n < 2) { return m; } else { factorial(n-1, n*m); }}
從代碼中可以看出,通過引入一個新的參數m來存放每次遞迴所產生的值,這樣就避免了每次遞迴都要進行壓棧操作,也就不會產生堆疊溢位的現象,而且普通的遞迴每次遞迴只能等待下一次遞迴得到的結果後才能繼續運算,而尾遞迴每次執行都會進行運算,一次迴圈執行完畢即可得到結果,其時間複雜度為O(n);