上一篇:《C進階指南(1):整型溢位和型別提升、記憶體申請和管理》
下一篇:《C進階指南(3):顯式內聯、向量擴充套件、C的逸聞軼事》
三、指標和陣列
儘管在某些上下文中陣列和指標可相互替換,但在編譯器看來二者完全不同,並且在執行時所表達的含義也不同。
當我們說物件或表示式有型別的時候,我們通常想的是定位器值的型別,也叫做左值。當左值有完全non-const型別時,此型別不是陣列型別(因為陣列本質是記憶體的一部分,是個只讀常量,譯者注),我們稱此左值為可修改左值,並且此變數是個值,當表示式放到賦值運算子左邊的時候,它被賦值。若表示式在賦值運算子的右邊,此變數不必被修改,變數成為了修改左值的的內容。若表示式有陣列型別,則此表示式的值是個指向陣列第一個元素的指標。
上文描述了大多數場景下陣列如何轉為指標。在兩種情形下,陣列的值型別不被轉換:當用在一元運算子 &(取地址)或 sizeof 時。參見C99/C11標準 6.3.2.1小節:
(Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type “array of type” is converted to an expression with type “pointer to type” that points to the initial element of the array object and is not an lvalue.)
除非它是sizeof或一元運算子&的運算元,再或者它是用於初始化陣列的字元文字,否則有著“型別陣列”型別的表示式被轉換為“指向型別”型別的指標,此指標指向陣列物件的首個元素且指標不是左值。
由於陣列沒有可修改的左值,並且在絕大多數情況下,陣列型別的表示式的值被轉為指標,因此不可能用賦值運算子給陣列變數賦值(即int a[10]; a = 1;是錯的,譯者注)。下面是一個小示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
short a[] = {1,2,3}; short *pa; short (*px)[]; void init(){ pa = a; px = &a; printf("a:%p; pa:%p; px:%p\n", a, pa, px); printf("a[1]:%i; pa[1]:%i (*px)[1]:%i\n", a[1], pa[1], (*px)[1]); } |
(譯者注:%i能識別輸入的八進位制和十六進位制)
a 是 int 型陣列,pa 是指向 int 的指標,px 是個未完成的、指向陣列的指標。a 賦值給 pa 前,它的值被轉為一個指向陣列開頭的指標。右值表示式 &a 並非意味著指向 int,而是一個指標,指向 int 型陣列因為當使用一元符號&時右值不被轉換為指標。
表示式 a[1] 中下標的使用等價於 *(a+1),且服從如同 pa[1] 的指標算術規則。但二者有一個重要區別。對於 a 是陣列的情況,a 變數的實際記憶體地址用於獲取指向第一個元素的指標。當對於 pa 是指標的情況,pa 的實際值並不用於定位。編譯器必須注意到 a 和 pa見的型別區別,因此宣告外部變數時,指明正確的型別很重要。
1 2 |
int a[]; int *pa; |
但在另外的編譯單元使用下述宣告是不正確的,將毀壞程式碼:
1 2 |
extern int *a; extern int pa[]; |
3.1 陣列作為函式形數
某些型別陣列變為指標的另一個場合在函式宣告中。下述三個函式宣告是等價的:
1 2 3 4 5 |
void sum(int data[10]) {} void sum(int data[]) {} void sum(int *data) {} |
編譯器應報告函式 sum 重定義相關錯誤,因為在編譯器看來上述三個例子中的引數都是 int 型的。.
多維陣列是有點棘手的話題。首先,雖然用了“多維”這個詞,C並不完全支援多維陣列。陣列的陣列可能是更準確的描述。
1 2 3 |
typedef int[4] vector; vector m[2] = {{1,2,3,4}, {4,5,6,7}}; int n[2][4] = {{1,2,3,4}, {4,5,6,7}}; |
變數 m 是長度為2的 vector 型別,vector 是長為4的 int 型陣列。除了儲存的記憶體位置不同外,陣列 n 與 m 是相同的。從記憶體的角度講,兩個陣列都如同括號內展示的內容那樣,排布在連續的記憶體區域。訪問到的和宣告的完全一致。
1 2 |
int *p = n[1]; int y = p[2]; |
通過使用下標符號 n[1],我們獲取到了每個元素大小為4位元組的整型陣列。因為我們要定位陣列的第二個元素, 其位置在多維陣列中是陣列開始偏移四倍的整型大小。我們知道,在這個表示式中整型陣列被轉為指向 int 的指標,然後存為 p。然後 p[2] 將訪問之前表示式產生的陣列中的第三個元素。上面程式碼中的 y 等價於下面程式碼中的 z:
1 |
int z = *(*(n+1)+2); |
也等價於我們初學C時寫的表示式:
1 |
int x = n[1][2]; |
當把上文中的二維陣列作為引數傳輸時,第一“維”陣列會轉為指標,指向再次陣列的陣列的第一個元素。因此不需要指明第一維。剩餘的維度需要明確指出其長度。否則下標將不能正確工作。當我們能夠隨心所欲地使用下述表格中的任一形式來定義函式接受陣列時,我們總是被強制顯式地定義最裡面的(即維度最低的)陣列的維度。
1 2 3 4 5 |
void sum(int data[2][4]) {} void sum(int data[][4]) {} void sum(int (*data)[4]) {} |
為繞過這一限制,可以轉換陣列為指標,然後計算所需元素的偏移。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void list(int *arr, int max_i, int max_j){ int i,j; for(i=0; i<max_i; i++){ for(j=0; j<max_j; j++){ int x = arr[max_i*i+j]; printf("%i, ", x); } printf("\n"); } } |
另一種方法是main函式用以傳輸引數列表的方式。main函式接收二級指標而非二維陣列。這種方法的缺陷是,必須建立不同的資料,或者轉換為二級指標的形式。不過,好在它執行我們像以前一樣使用下標符號,因為我們現在有了每個子陣列的首地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int main(int argc, char **argv){ int arr1[4] = {1,2,3,4}; int arr2[4] = {5,6,7,8}; int *arr[] = {arr1, arr2}; list(arr, 2, 4); } void list(int **arr, int max_i, int max_j){ int i,j; for(i=0; i<max_i; i++){ for(j=0; j<max_j; j++){ int x = arr[i][j]; printf("%i, ", x); } printf("\n"); } } |
用字串型別的話,初始化部分變得相當簡單,因為它允許直接初始化指向字串的指標。
1 2 3 4 5 |
const char *strings[] = { "one", "two", "three" }; |
但這有個陷阱,字串例項被轉換成指標,用 sizeof 操作符時會返回指標大小,而不是整個字串文字所佔空間。另一個重要區別是,若直接用指標修改字串內容,則此行為是未定義的。
假設你能使用變長陣列,那就有了第三種傳多維陣列給函式的方法。使用前面定義的變數來指定最裡面陣列的維度,變數 arr 變為一個指標,指向未完成的int陣列。
1 2 3 4 |
void list(int max_i, int max_j, int arr[][max_j]){ /* ... */ int x = arr[1][3]; } |
此方法對更高維度的陣列仍然有效,因為第一維總是被轉換為指向陣列的指標。類似的規則同樣作用於函式指示器。若函式指示器不是 sizeof 或一元操作符 & 的引數,它的值是一個指向函式的指標。這就是我們傳回撥函式時不需要 & 操作符的原因。
1 2 3 4 5 6 7 8 9 |
static void catch_int(int no) { /* ... */ }; int main(){ signal(SIGINT, catch_int); /* ... */ } |
四、打樁(Interpositioning)
打樁是一種用定製的函式替換連結庫函式且不需重新編譯的技術。甚至可用此技術替換系統呼叫(更確切地說,庫函式包裝系統呼叫)。可能的應用是沙盒、除錯或效能優化庫。為演示過程,此處給出一個簡單庫,以記錄GNU/Linux中 malloc 呼叫次數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/* _GNU_SOURCE is needed for RTLD_NEXT, GCC will not define it by default */ #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include <stdint.h> #include <inttypes.h> static uint32_t malloc_count = 0; static uint64_t total = 0; void summary(){ fprintf(stderr, "malloc called: %u times\n", count); fprintf(stderr, "total allocated memory: %" PRIu64 " bytes\n", total); } void *malloc(size_t size){ static void* (*real_malloc)(size_t) = NULL; void *ptr = 0; if(real_malloc == NULL){ real_malloc = dlsym(RTLD_NEXT, "malloc"); atexit(summary); } count++; total += size; return real_malloc(size); } |
打樁要在連結libc.so之前載入此庫,這樣我們的 malloc 實現就會在二進位制檔案執行時被連結。可通過設定 LD_PRELOAD 環境變數為我們想讓連結器優先連結的全路徑。這也能確保其他動態連結庫的呼叫最終使用我們的 malloc 實現。因為我們的目標只是記錄呼叫次數,不是真正地實現記憶體分配,所以我們仍需要呼叫“真正”的 malloc 。通過傳遞 RTLD_NEXT 偽處理程式到 dlsym,我們獲得了指向下一個已載入的連結庫中 malloc 事件的指標。第一次 malloc 呼叫 libc 的 malloc,當程式終止時,會呼叫由 atexit 註冊的獲取和 summary 函式。看GNU/Linxu中打樁行為(真的184次呼叫!):
1 2 3 4 5 6 7 8 |
$ gcc -shared -ldl -fPIC malloc_counter.c -o /tmp/libmcnt.so $ export LD_PRELOAD="/tmp/libstr.so" $ ps PID TTY TIME CMD 2758 pts/2 00:00:00 bash 4371 pts/2 00:00:00 ps malloc called: 184 times total allocated memory: 302599 bytes |
4.1 符號可見性
預設情況下,所有的非靜態函式可被匯出,所有可能僅定義有著與其他動態連結庫函式甚至模板檔案相同特徵標的函式,就可能在無意中插入其它名稱空間。為防止意外打樁、汙染匯出的函式名稱空間,有效的做法是把每個函式宣告為靜態的,此函式在目標檔案之外不能被使用。
在共享庫中,另一種控制匯出的共享目標的方式是用編譯器擴充套件。GCC 4.x和Clang都支援 visibility 屬性和 -fvisibility 編譯命令來對每個目標檔案設定全域性規則。其中 default 意味著不修改可見性,hidden 對可見性的影響與 static 限定符相同。此符號不會被放入動態符號表,其他共享目標或可執行檔案看不到此符號。
1 2 3 4 5 6 7 |
#if __GNUC__ >= 4 || __clang__ #define EXPORT_SYMBOL __attribute__ ((visibility ("default"))) #define LOCAL_SYMBOL __attribute__ ((visibility ("hidden"))) #else #define EXPORT_SYMBOL #define LOCAL_SYMBOL #endif |
全域性可見性由編譯器引數指定,可通過設定 visibility 屬性被本地覆蓋。實際上,全域性策略設定為 hidden,則所有符號會被預設為本地的,只有修飾 __attribute__ ((visibility (“default”))) 才將被匯出。
下一篇: