1. 程式人生 > >C語言進階指南(2)丨數組和指針、打樁

C語言進階指南(2)丨數組和指針、打樁

編譯器 atexit text ret The 共享 tex 語言 .org

三、指針和數組

盡管在某些上下文中數組和指針可相互替換,但在編譯器看來二者完全不同,並且在運行時所表達的含義也不同。

當我們說對象或表達式有類型的時候,我們通常想的是定位器值的類型,也叫做左值。當左值有完全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;是錯的,譯者註)。下面是一個小示例:

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見的類型區別,因此聲明外部變量時,指明正確的類型很重要。

int a[];

int *pa;

但在另外的編譯單元使用下述聲明是不正確的,將毀壞代碼:

extern int *a;

extern int pa[];

3.1 數組作為函數形數

某些類型數組變為指針的另一個場合在函數聲明中。下述三個函數聲明是等價的:

void sum(int data[10]) {}

void sum(int data[]) {}

void sum(int *data) {}

編譯器應報告函數 sum 重定義相關錯誤,因為在編譯器看來上述三個例子中的參數都是 int型的。.

多維數組是有點棘手的話題。首先,雖然用了“多維”這個詞,C並不完全支持多維數組。數組的數組可能是更準確的描述。

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 是相同的。從內存的角度講,兩個數組都如同括號內展示的內容那樣,排布在連續的內存區域。訪問到的和聲明的完全一致。

int *p = n[1];

int y = p[2];

通過使用下標符號 n[1],我們獲取到了每個元素大小為4字節的整型數組。因為我們要定位數組的第二個元素, 其位置在多維數組中是數組開始偏移四倍的整型大小。我們知道,在這個表達式中整型數組被轉為指向 int 的指針,然後存為 p。然後 p[2] 將訪問之前表達式產生的數組中的第三個元素。上面代碼中的 y 等價於下面代碼中的 z:

int z = *(*(n+1)+2);

也等價於我們初學C時寫的表達式:

int x = n[1][2];

當把上文中的二維數組作為參數傳輸時,第一“維”數組會轉為指針,指向再次陣列的數組的第一個元素。因此不需要指明第一維。剩余的維度需要明確指出其長度。否則下標將不能正確工作。當我們能夠隨心所欲地使用下述表格中的任一形式來定義函數接受數組時,我們總是被強制顯式地定義最裏面的(即維度最低的)數組的維度。

void sum(int data[2][4]) {}

void sum(int data[][4]) {}

void sum(int (*data)[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[max_i*i+j];

            printf("%i, ", x);

        }

        printf("\n");

    }

}

另一種方法是main函數用以傳輸參數列表的方式。main函數接收二級指針而非二維數組。這種方法的缺陷是,必須建立不同的數據,或者轉換為二級指針的形式。不過,好在它運行我們像以前一樣使用下標符號,因為我們現在有了每個子數組的首地址。

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");

    }

}

用字符串類型的話,初始化部分變得相當簡單,因為它允許直接初始化指向字符串的指針。

const char *strings[] = {

    "one",

    "two",

    "three"

};

但這有個陷阱,字符串實例被轉換成指針,用 sizeof 操作符時會返回指針大小,而不是整個字符串文本所占空間。另一個重要區別是,若直接用指針修改字符串內容,則此行為是未定義的。

假設你能使用變長數組,那就有了第三種傳多維數組給函數的方法。使用前面定義的變量來指定最裏面數組的維度,變量 arr 變為一個指針,指向未完成的int數組。

void list(int max_i, int max_j, int arr[][max_j]){

    /* ... */

    int x = arr[1][3];

}

此方法對更高維度的數組仍然有效,因為第一維總是被轉換為指向數組的指針。類似的規則同樣作用於函數指示器。若函數指示器不是 sizeof 或一元操作符 & 的參數,它的值是一個指向函數的指針。這就是我們傳回調函數時不需要 & 操作符的原因。

static void catch_int(int no) {

    /* ... */

};

 

int main(){

    signal(SIGINT, catch_int);

 

    /* ... */

}

四、打樁(Interpositioning)

打樁是一種用定制的函數替換鏈接庫函數且不需重新編譯的技術。甚至可用此技術替換系統調用(更確切地說,庫函數包裝系統調用)。可能的應用是沙盒、調試或性能優化庫。為演示過程,此處給出一個簡單庫,以記錄GNU/Linux中 malloc 調用次數。

/* _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次調用!):

$ 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 限定符相同。此符號不會被放入動態符號表,其他共享目標或可執行文件看不到此符號。

#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”))) 才將被導出。

持續更新中。

另外筆者是一個有著7年工作經驗的架構師,對於c++,自己有做資料的整合,一個完整學習C語言c++的路線,學習資料和工具。可以進我的Q群7418,18652領取,免費送給大家。希望你也能憑自己的努力,成為下一個優秀的程序員!另外博主的微信公眾號是:C語言編程學習基地,歡迎關註!

C語言進階指南(2)丨數組和指針、打樁