NDK 之 C 語言 入門與指針

在 java 語言中,萬物皆對象

在 Linux 中,萬物皆文件

而在 C 和 C++ 中,萬物皆置針

C 語言

C 語言為面向過程的語言,在函數加載的時候,main 函數作為程序入口,應在文件的最后進行聲明

C 語言不允許函數重載,java 和 C++可以

.h 文件為聲明文件;.c 文件為實現文件(在c++ 中,為.cpp)

#include 引入其他文件,其中,尖括號(<>)引入的,是 C 語言庫中的模塊, 雙引號("")引入的,是用戶實現的聲明文件

基礎類型:int、long、float、double、char 等,值得注意的是,沒有string,只有char[]

對應的占位符:%d、%ld、%f、%lf,%c 等,可以直接打印 char[],其占位符為%s;地址占位符為 %p

// C 語言中,沒有所謂的 string 類型,字符串的定義用的是 char[]
char[] str = "abc"

指針

定義變量時,會給變量開辟一個內存空間地址。

指針是一類數據類型,具體對應的是數據的內存地址

// 定義了一個 int 類型的變量 a 
// 將 a 賦值為 1
int a = 1;
// 定義了一個 int * 類型的變量 ap 
// 將 ap 賦值為 變量 a 的存儲地址
int *ap = &a;

// 定義了一個 int 類型變量 b,開辟了新的空間地址
// 將變量 a 值,賦值給 b
int b = *ap;
// 定義了一個 int 類型變量 c,開辟了新的空間地址
// 將地址 ap 中儲存的值,賦值給 c
int c = a;

// 將 ap 地址對應的位置,修改為 2,即 a 修改為 2
// 而 b 和 c 的地址是新開辟的,所以修改 ap 地址對應位置,并不會修改 b 和 c 的值
*ap = 2;

其在內存映射中的圖示大致為:


pointers.001.jpeg

在函數變量傳參時,會重新定義一次入參,所以如果需要通過函數修改變量的值,需要給函數傳入對應的變量地址,而不是直接傳入一個變量

void change(int c, int d) {
    int temp = c;
    c = d;
    d = temp;
}
int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    change(a, b);
    return 0;
}

相當于:

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
  
    int c = a;
    int d = b;
    // 修改的是新開辟出存儲空間的 c 和 d 的值,并不會影響到 a 或者 b
    int temp = c;
    c = d;
    d = temp;
    // 運行結果為:a: 1, b: 2, c: 2, d: 1 
    // printf("a: %d, b: %d, c: %d, d: %d \n", a, b, c, d);
    return 0;
}

采用指針作為函數參數:

void change2(int *c, int *d) {
    int temp = *c;
    *c = *d;
    *d = temp;
}

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;
    change2(&a, &b);   
    return 0;
}

相當于:

int main(int argc, const char * argv[]) {
    int a = 1;
    int b = 2;

    // 將 a 的地址賦值給 c
    int *c = &a;
    int *d = &b;
    // 將 c 地址存儲的數據取出,并賦值給 temp
    int temp = *c;
    // 修改 c 地址存儲的值,即修改了 a 的值
    *c = *d;
    *d = temp;
    // 運行結果: a: 2, b: 1, c: 2, d: 1
    // printf("a: %d, b: %d, *c: %d, *d: %d \n", a, b, *c, *d);
    return 0;
}

多級指針

當一個指針變量指向另一個指針變量時,例如在上述的例子中,int 類型指針變量(int *) ap 指向了 a;新建另一個新的int * 類型指針變量(int **) app,指向指針變量 ap,此時 app 則被稱為二級指針。實際開發中,一般不會有超過三級指針的多級指針。

// 新建 int 類型變量 a,賦值為 0
int a = 0;
// 新建 int * 類型變量 ap,賦值為 a 的地址
int *ap = &a;
// 新建 int ** 類型變量 app,賦值為 ap 的地址
int **app = &ap;
// 新建 int *** 類型變量 appp,賦值為 app 的地址
int ***appp = &app;

// 不能用以下代碼對多級指針賦值
// 因為指針是指向具體內存地址的變量,在 &(&(&a)) 中,只有 a 有具體內存地址值,&a 操作可成立
// 但 &a 操作的值沒有另外開辟內存空間進行存儲,所以無法計算 &(&a)
// int ***appp = &(&(&a));

printf("a: %d, ap: %p, app: %p, appp:%p\n", a, ap, app, appp);
printf("&a: %p, &ap: %p, &app: %p, &appp: %p\n", &a, &ap, &app, &appp);
printf("a: %d, *ap: %d, *app: %p, *appp: %p", a, *ap, *app, *app);

在指針變量的定義時,類型變量后面添加了多少個 *,即代表指針的層級,指針的層級決定這個指針需要進行幾次取值操作后,方能獲取最開始存儲的數據。其內存示意圖,大致如下:


pointers.002.jpeg

& 操作 -> 獲取變量的地址

* 操作 -> 對指針變量的值進行尋址,并獲取該地址上的內容

注意: * 操作可以疊加,而 & 操作不可以

數組指針

定義數組時,C 與 java 不同,[] 寫在變量名后,而不是類型后;此外,無法在定義時,將一個數組的內容賦值給另一個數組。數組的拷貝需要用到 memcpy 函數

數組在內存空間內的存儲是連續的,因而 a[0] 的位置就是數組的起始位置。
當輸出 a 的值時,輸出的,其實是 a 的地址。可以理解為在 java 中,直接輸出一個對象時,輸出的其實也是 hash 值(內存映射值)。
因而:a、&a、&a[0],都是數組的起始地址位置,輸出的值相同

在 C 中,沒有直接獲取數組長度的方法,需要通過 數組的內存大小 除以 數組元素類型的大小 來進行計算,并獲得結果

數組 a 作為指針時,可以進行位移運算,但無法進行自增自減操作。(可以將 a 視為 finnal 變量)

可以定義一個專門的指針 (aar)對新定義的指針進行增減操作;

指針加減時,指針每 + 1,輸出的指向地址 + 類型占位字節數(char -> 1, int -> 4, long ->8)

在 C 語言中,不一定存在數組越界而崩潰的情況。
根據不同的操作系統,不同的編譯器,可能會有不同的表現。
有些可以讀取出一個野值(即未知意義的一個內存地址值,這也是為什么有些時候 C 代碼很難排查 bug,取出了一個不正確的數值,但沒有任何報錯,只是最終運行的結果與期望值不同);mac 系統上,運行時會直接拋出異常。

// 定義數組 a
int a[] = {1,2,3,4,1000};
// 輸出的三個值相同
printf("a: %p, &a: %p, &a[0]: %p\n", a, &a, &a[0]);

// 計算數組的長度
int aSize = sizeof(a) / sizeof(int);

// 拷貝數組時,以下代碼不合法:
// int b[] = a;
//  需使用以下方法:
// 其中,數組的大小,在定義時就必須明確,且其值為元素個數
int b[aSize];
// 拷貝時,將 a 數組的 sizeof(a) 字節拷貝到 b 數組對應的地址中
memcpy(b, a, sizeof(*a));

printf("sizeof(a): %lu, sizeof(int): %lu, sizeof(a)/sizeof(int): %lu\n", sizeof(a), sizeof(int), aSize);

//定義一個指針,指向 a 的起始位置
int *aar = a; // a == &a == &a[0]
for (int i = 0; i < aSize; i ++) {
    // a 可以進行運算,得出遍歷的下一個元素值
    // 輸出 a + i 時,可發現,a 每 + 1,地址 + 4。因為 int 占位 4字節
    printf("*(a + i): %d, a + i: %p\n", *(a + i), a + i);

    // 需要對地址進行位移運算后,再進行指針取值運算,需要將 a + i 用括號括起
    // 如果沒有括起的話,則是取出了第一個元素的值,進行了值的加運算后再輸出
    // 在 1,2,3,4,5 這樣的數組中,輸出一致,但意義不對,是 bug
    // printf("test: %d\n", *a + i);

    // 可以通過 a[i] 的方式,取出對應位置元素值
    printf("a[i]: %d\n", a[i]);

    // 也可以通過取出自增指針對應位置的元素值,來遍歷數組
    printf("aar: %p\n", aar ++);
}
// 以下代碼,在 xcode 中運行時報錯
// printf("a[1000]: %d", a[1000]);
// a[1000] = 20;

函數指針

C 語言中,函數在傳參時,會給基礎類型開辟新的存儲空間,而復雜的結構體,則會傳遞結構體指針。
數組屬于結構體,傳遞的是指針。
即便參數用的是 int a[] 類型,實際傳遞的也是 int *a。
int size = sizeof(a) / sizeof(*a); 在定義 a 數組的結構體中,得到的值是數組的長度, sizeof(a) 能成功獲取到 a 數組的字節數;而在參數為 int a[] 的函數體內,得到的值為 1,此時 sizeof(a) 相當于 sizeof(*a) ,因為在函數參數中, int a[] 的實質為 int *a(int 指針)類型的變量。

函數也是一種復雜結構,因而直接使用函數名 和 直接使用數組名 一樣,獲取到的,都是對應的指針位置。

函數指針作為 函數參數時的定義結構:{函數參數返回值類型} (*{函數體中使用的函數參數別名})({函數參數的入參類型,用逗號隔開})

// 第一種打印
// 等同于 void printArray1(int *a, int aSize, char *name)
void printArray1(int a[], int aSize, char *name) {
    int *aar = a;
    // 在函數體內,無法獲取數組類型參數的長度,因為此參數的實質是指針
    // int size = sizeof(a) / sizeof(*a);
    int i;
    for (i = 0; i < aSize; i ++) {
        printf("%sar: %d\n", name, *(aar ++));
        // 在此修改數組內元素的值。此處修改,會改變原來數組內的值。即其他方法打印與此方法一致。
        // 從第二個元素開始,將值改為第幾個元素
        * aar = i + 2;
    }
}

// 第二種打印
void printArray2(int *a, int aSize, char *name) {
    int i;
    for (i = 0; i < aSize; i ++) {
        printf("%s[i]: %d\n", name, a[i]);
    }
}

// 必須在 main 之前先聲明函數,但實現可以在 main 之后進行
// 第三種打印
void printArray3(int *a, int aSize, char *name);

// 打印數組的函數,可以將具體函數作為參數傳遞
// void (*method)(int[], int, char *) 為第一個參數,類型為:函數地址,其中:
// 函數參數返回值類型:void;函數體中使用的函數參數別名:method;
// 函數參數的入參類型,用逗號隔開:int[], int, char *
void print(void (*method)(int[], int, char *), int a[], int aSize, char * name) {
    // 取出 method 地址對應的函數,并調用
    (*method)(a, aSize, name);
    // 由于在參數中已聲明 method 是一個函數指針,因而 * 可省略,等價于:
    // method(a, aSize, name);
}

int main(int argc, const char * argv[]) {
    int a[] = {7,2,3,4,1000};
    int size = sizeof(a) / sizeof(*a);
    // 采用不同的打印方法打印數組
    print(printArray1, a, size, "a");
    print(printArray2, a, size, "a");
    print(printArray3, a, size, "a");
    return 0;
}

// 在 main 函數之后實現之前聲明的函數
void printArray3(int *a, int aSize, char *name) {
    int i;
    for (i = 0; i < aSize; i ++) {
        printf("*(%s + i): %d\n", name, *(a + i));
    }
}

C語言內存地址劃分

指針是內存地址的映射,因而,在進一步了解指針前,先對 C 語言的內存劃分有個大致概念:

常量(1、“ABC”)、static 修飾的靜態變量,會在 常量區 進行存儲,由操作系統在程序的運行結束的時候進行銷毀

當函數運行時,通過int a = 1;char str[] = "abc"靜態開辟 的臨時變量會在 棧區 開辟存儲空間;當函數運行結束后,對應的棧區變量會被銷毀。

通過 malloc 動態開辟 的變量屬于 堆區 變量、需要手動調用 free進行銷毀

當創建變量時,內存分配如下圖所示:

pointers.003.jpeg

Note:字符串類型常數,在存儲時,會自動以 '\0' 結尾,表示字符數組結束

C 語言內存四區

  • 全局區

    該區域由操作系統管理,在程序結束后,由操作系統進行釋放。

    全局區存放了:在函數外部定義的全局變量(全局變量區)、static 修飾定義的全局靜態變量和局部靜態變量(靜態變量區) 以及 由const修飾的全局變量和字符常量(常量區)

    常量區內存放的數據不可更改,const修飾的局部變量可通過指針更改,但它是局部變量,不在全局常量區內。

// 以下代碼中,常量 "abc" 和 常量 "abcdef" 在所有函數里面打印出來的地址都一致
char *getString1() {
    char *test1 = "abcdef";
    printf("string1 abc: %p, test1: %p\n", &"abc", test1);
    return test1;
}

char *getString2() {
    char *test2 = "abcdef";
    printf("string2 abc: %p, test2: %p\n", &"abc", test2);
    return test2;
}


int main(int argc, const char * argv[]) {
    char *test1 = getString1();
    char *test2 = getString2();
    printf("test1 in main: %p, test2 in main: %p\n", test1, test2);
    char *test3 = "abcdef";
    printf("string3 abc: %p, test3: %p\n", &"abc", test3);
    return 0;
}
  • 棧區

    定義在函數內部的 靜態開辟 的變量都存放在棧區

    靜態開辟操作:int a、float b、char c、char string[] 等 直接定義一個類型變量

    棧區內空間大小有限,不同的操作系統平臺限制不同,不提倡在棧內開辟超過1M的變量空間

    棧區的變量,在函數入棧(開始執行)時創建;在函數出棧(執行完畢)后自動釋放

char *getString1() {
    // char test1[] 會在棧區開辟一塊內存地址,并賦值為 "abc"
    // 而 char *test1 = "abc" 則會直接取 "abc" 常量的地址
    char test1[] = "abc";
    // 此處打印正常,test1 的值為 "abc",地址由系統分配
    printf("test1 in getString1: %s, address: %p\n", test1, test1);
    return test1;
}

int main(int argc, const char * argv[]) {
    // getString1 的函數出棧后,函數內的局部變量會被釋放
    // 因而此處 test1 的地址與 getString1 內打印地址相同
    // 但取出來的字符為亂碼(空間被釋放)
    char *test1 = getString1();
    printf("test1 in main: %s, address: %p\n", test1, test1);
    return 0;
}
  • 堆區

    堆區由程序員手動申請(malloc)和釋放(free),稱為動態開辟

    只申請不釋放會造成內存泄漏。

    釋放后,需要將對應指針置空,否則會出現懸空指針。

    空間釋放只進行一次,否則可能會崩潰(不同編譯平臺和操作系統平臺可能會有不同處理)

    在 mac 中,棧區的空間被釋放后再進行讀取,會出現亂碼;而堆區的空間被釋放后進行讀取,不會亂碼;但如果被釋放的空間被重新分配過,會讀取到錯誤數據。

    可以使用 realloc 對原本申請了的區域進行擴充,如果緊接著的內存區域有足夠的空閑,會直接在原有的地址延伸空間大小;如果沒有足夠的空閑,會重新開辟一個足夠大的地址,把原先的數據拷貝到新的地址,并釋放原先地址,返回新的地址。

char *getString1(const char* string, int index) {
    // 實際開發中不要這么分配空間大小,可能不足,也可能盈余
    char *test1 = (char *) malloc(20);
    if (test1) {
        // 拼接字符串,填充申請的空間
        sprintf(test1, "%s%d", string , index);
    }
    return test1;
}

int main(int argc, const char * argv[]) {
    int i = 1;
    const char *string = "string";
    char *p = getString1(string, i);
    if (p) {
        printf("%d, get string from heap: %s, address %p\n", i, p, p);
        // 錯誤的釋放內存方式:
        free(p);
        // 在 free 之后,緊接著,應該把相關的指針置為 NULL,否則出現懸空指針
        // 懸空指針可能導致未知錯誤
        // p = NULL;
        // 重新申請一次堆內存,因為 p 已經被釋放,所以這里重新申請的內存,很可能與 p 的內存一致
        char *p2 = getString1(string, i + 1);
        // 如果 p2 與 p 的內存地址一致,則此處 p 取出了一個異常值 string2(應為string1)
        printf("%d, p1 from heap after free: %s, address: %p\n", i, p, p);
        printf("%d, p2 from heap after free: %s, address: %p\n", i, p2, p2);
        // 釋放前,先對指針進行判空處理,防止二次釋放
        if (p2) {
            // free(pointer) 之后,緊接著將 pointer = NULL,防止后續再次使用懸空指針 和 二次釋放
            free(p2);
            p2 = NULL;
        }
    }
    return 0;
}
  • 代碼區(程序區)

    存放代碼,將函數題轉化為二進制數存放進該區域

C 語言函數的壓棧和出棧

C 是面向過程的語言,在程序執行的的時候,從 main 函數開始,將 main 函數壓入執行棧,然后調用哪個函數,就將該函數壓入棧中,執行完畢后將該函數從棧中移除,并且銷毀該函數創建的所有臨時變量。

void getStringSize(char *str, int *sp) {
    char *cur = str;
    *sp = 0;
    while (*cur) {
        cur ++;
        (*sp) ++;
    }
}

int main(int argc, const char * argv[]) {
    char a[] = "abc";
    int size = 0;
    getStringSize(a, &size);
    printf("size of string a is %d\n", size);
    return 0;
}
pointers.004.jpeg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容