1. 程式人生 > >Android NDK開發(一)C語言基礎語法

Android NDK開發(一)C語言基礎語法

最近一段時間在攻克Android NDK開發。雖然大學的時候主要的學習是放在C/C++上的,但是自從大學畢業之後,就把所有學到的知識都還給老師了,所以,趁著這個機會,將C語言和NDK開發好好的總結一下,學習一下。

自己在網上也看了很多部落格,感覺大神們寫的都是比較難以理解,特別是像現在這種工作了一天的狀態,想要再看這些東西的時候,都感覺花眼了。所以,自己希望能夠將基礎知識理順。

首先先來看一張圖,這張圖相信很多做Android開發的人肯定非常熟悉,但是熟悉並不代表理解。再次看到這張圖的時候,我發現之前在一些外包公司做的時候,大部分都是活躍在應用層次,深入理解卻是少之又少,就算偶爾有框架的內容,也是別人封裝好的。
這裡寫圖片描述


在這種圖裡我們會發現,現在市面上一些非常厲害的App都是要跟C/C++進行互動的,比如抖音,微博,微信等。因為這些應用軟體都會跟一些音訊,視訊,圖片處理等內容掛鉤。所以,如果想要成為高階或者終極程式設計師,C/C++這個坎是邁不過去的。

為什麼是C語言?

看你這麼好看,那就告訴你。這是我工作了三年之後的自我體會。相信很多小夥伴們都有看原始碼的經歷,那麼原始碼裡很多東西,都會牽扯到底層的內容,所以,對於我來說,再看原始碼的時候,很多是看不懂的。再加上很多地方C語言是作為支撐語言的,也就是我們常說的技術支援,如果C語言不好,可能會導致我們很多東西都沒有辦法從核心上去優化。所以,千言萬語匯成一句話,C語言非學不可。

C語言基礎

變數

對於任何一門語言來說,我們都是會先從基礎開始學習的,那麼這個基礎學習又大部分是從變數開始。在C語言中,變數是用來表示所佔的儲存空間大小的。如下所示

#include<stdio.h>
int main(){
    int i = 90;
    printf("i所佔的儲存空間是:%d\n",sizeof(i));
    printf("i的值是:%d\n",i);
    return 0;
}

在程式碼裡我們使用了#include "studio.h"這樣的程式碼。這就是我們所說的標頭檔案,在C語言中,我們需要引入各種各樣的標頭檔案,標頭檔案都是以.h

結尾的,包含一些函式宣告這樣的內容。我們也可以說是標頭檔案,而以.c結尾的,我們就說是原始檔,函式的實現會在原始檔中

在命令列中執行下面命令

gcc hellowordl.c
./a.out

執行結果是:
這裡寫圖片描述
我們會發現into所佔的就是4個位元組,那麼我們可以將剩下的補全
這裡寫圖片描述

使用printf輸出內容的時候,需要將資料的型別也要跟上,例如int型別就是/dchar型別就是/c.

/*     
        C 語言的基本資料型別 , 輸出佔位符
        int - %d 
        short - %d 
        long - %ld 
        float - %f 
        double - %lf 
        char - %c
        字串 - %s
        八進位制 - %o
        十六進位制 - %x
*/

指標

指標就是為了記憶體操作而產生的。學過java語言,我們知道,java中有垃圾回收機制,是固定時間內幫我們清除記憶體,優化記憶體,但是在C語言中,計算機並不會幫我們去執行,所以所有的關於記憶體操作的部分都要我們自己去執行。
例如:

#include<stdio.h>
int main(){
    int num = 100;
    int *numPoint = &num;
    return 0;
}

指標儲存的是變數的記憶體地址,而且只能儲存記憶體地址,就算我們給他賦值了一個值,比如一個整數,他還是會變成一個地址

這裡寫圖片描述
執行結果:
這裡寫圖片描述

指標也是一個變數,建議以後再寫指標的時候使用int* p = &num的方式。p本身就是一個變數,用來儲存num的記憶體地址,而當我們使用的時候,p就代表的是記憶體地址,而如果是* p表示的是p物件所代表的記憶體地址的值,是地址指向的值。

就像上面所說,指標也是變數,同樣可以進行變數的計算

#include<stdio.h>
int main(){
    int arr[] = {89,80,13,45,68};
    printf("輸出陣列arr的地址是:%#x\n",&arr);
    printf("另一種方法獲取arr的地址:%#x\n",arr);
    printf("輸出第一個元素的地址:%#x\n",&arr[0]);

    int* p = &arr;
    for(int i=0;i<5;i++){
        printf("陣列的內容是:%d\n",arr[i]);
    }
    printf("\n");

    printf("以指標運算的方式輸出陣列資料");
    for(int i=0;i<5;i++){
        printf("新的方式下陣列內容是:%d\n",*p);
        p++;
    }
}

執行結果是:
這裡寫圖片描述
取地址的結果都是一樣的,輸出的方式也相同的。
其實我們可以這樣理解,陣列第一個物件的地址值就是陣列的地址值。

通過上面p++實現迴圈獲取資料,這裡我們先認為陣列是一塊連續的記憶體空間

函式

關於函式就不具體的介紹了,這裡我們說一個知識點,就是如果形參是一個數據,那麼再傳入之前和在函式中,我們得到的地址值是不一樣的,因為在函式中,我們會為形參再次建立一個物件,如下

#include<stdio.h>
void changeNum(int i){
    printf("函式中i的地址值是:%#x\n",&i);
    i = 300;
}
int main(){
    int i = 100;
    printf("傳入函式之前i的地址值是:%#x\n",&i);
    changeNum(i);
    printf("修改之後的值是:%d\n",i);
    return 0;
}

執行結果是
這裡寫圖片描述
傳入函式之前的值與在函式中的值是不一樣的,而且雖然在函式中我們對資料進行了修改,但是並沒有改變在main方法中的資料。下面我們傳遞的是一個地址的例子

#include<stdio.h>
void changeNum(int i){
    printf("函式中i的地址值是:%#x\n",&i);
    i = 300;
}
void changeNum2(int* p){
    printf("函式中變數的地址只是:%#x\n",p);
    *p = 200;
}
int main(){
    int i = 100;
    printf("傳入函式之前i的地址值是:%#x\n",&i);
    changeNum2(&i);
    printf("修改之後的值是:%d\n",i);
    return 0;
}

這裡寫圖片描述
我們會發現,地址值是一樣的,數值也發生了改變

二級指標

所謂的二級指標,我們可以理解為是指標的指標,也就是說一個儲存空間中儲存的是不是數值,而是地址,而這塊儲存空間的地址,就是我們所說的二級地址。

#include<stdio.h>
int main(){
    int i = 10;
    int* p = &i;
    int** p1 = &p;
    int * p2 = 100;

    printf("指標作為普通變數:%d\n",p2);
    printf("i的地址:%#x\n",&i);
    printf("p的地址:%#x\n",&p);
    printf("通過p1獲取p的地址:%#x\n",p1);
    printf("通過p1獲取i的地址:%#x\n",*p1);
    printf("通過p1獲取i的值:%#x\n",**p1);

    //修改i的值
    ** p1 = 100;
    printf("修改之後的i的值:%d\n",i);
    printf("通過p獲取修改之後i的值:%d\n",*p);
    printf("通過p1獲取修改之後的i的值:%d\n",**p1);
    return 0;
}

這裡寫圖片描述

其實一句話概括就是:多級指標指向的就是上級指標的地址

函式指標

當我們建立一個函式之後,就會像變數一樣,為函式分配一個記憶體地址

#include <stdio.h>
void message(){
    printf("呼叫了message函式\n");
}
int main(){
    void(*func_p)() = &message;
    func_p();
    printf("函式指標的地址是:%#x\n",func_p);
    printf("如果直接呼叫函式名稱獲取地址:%#x\n",message);
    return 0;
}

這裡寫圖片描述
那麼函式指標能有什麼樣的作用呢?

#include<stdio.h>
int add(int num1,int num2){
    return num1+num2;
}
int min(int num1,int num2){
    return num1-num2;
}
void showMsg(int(*fun)(int num1,int num2),int a,int b){
    int r = fun(a,b);
    printf("計算之後的結果是:%d\n",r);
}
int main(){
    showMsg(add,11,12);
    showMsg(min,1,14);
    return 0;
}

這裡寫圖片描述
這個例子的主要作用就是,我們可以將函式作為我們的形參傳遞過來,類似於java中的多型。
同樣,我們這裡使用的是函式的名稱,直接傳遞過來的,我們也可以傳遞函式的地址,可以起到同樣的效果

#include<stdio.h>
void requestNet(char* url,void(*callback)(char*)){
    printf("請求的地址是:%s,正在請求網路...\n",url);
    char* ss = "獲取到網路請求資料,為人性僻耽佳句,語不驚人死不休";
    callback(ss);
}
void netCallback(char* ss){
    printf("網路請求回撥\n");
    printf("請求得到的資料是:%s\n",ss);
}
int main(){
    char* url = "http://www.baidu.com";
    requestNet(url,netCallback);
}

這裡寫圖片描述

動態記憶體分配

在java中我們通過JVM實現對記憶體的分配,這樣做的好處是很少會造成記憶體洩漏,但是也會存在記憶體越來越大的問題。所以在一些Android手機應用就是這樣子,剛開始很流暢,結果越到後面越卡,特別是在處理比較大的檔案或gif圖片的時候。那麼這時候,我們通過JNI,讓C語言在需要的特定時間,釋放記憶體,可以極大限度的讓手機執行更加流暢。
C語言的記憶體分為下面的幾個部分:
四區分配:

記憶體 描述 特性
棧區 是一個確定的常數,不同的作業系統會有不同的大小,超出之後會stackoverflow 自動建立,自動釋放
堆區 用於動態記憶體分配 手動申請和釋放,可以佔用80%的記憶體
全域性區或靜態區 在程式中明確被初始化的全域性變數,靜態變數(包括全域性靜態變數和區域性靜態變數)和常量資料(包括字串常量) 只初始化一次
程式程式碼區 程式碼取指令根據程式設計流程依次執行,對於順序指令,只會執行一次,如果需要反覆,需要跳出指令,如果需要遞迴,需要藉助棧來實現 程式碼區的指令包括操作碼和要操作的物件(或物件地址引用)

動態分配記憶體

C語言中動態分配記憶體實在堆區中的,java通過new一個物件出來的時候,也是在堆區中申請一塊記憶體。如果我們想要在堆區中申明一塊記憶體,則需要使用關鍵字malloc,函式定義如下

void* __cdecl malloc(
    _In_ _CRT_GUARDOVERFLOW size_t _Size
);

使用方式如下:

// 動態記憶體分配,使用malloc函式在對記憶體中開闢連續的記憶體空間,單位是:位元組
// 申請一塊40M的堆記憶體
int * p = (int* )malloc(1024*1024*10*sizeof(int));

這裡我們可以試著寫一個小程式(小病毒,之前寫過一個類似於清楚磁碟所有內容的小病毒)

#include<stdio.h>
void func(){
    //在函式中要求申請記憶體空間,那麼如果我們一直申請記憶體空間,就會造成記憶體空間不足
    int* p = (int*)malloc(1021 * 1024 * 3 * sizeof(int));
}
int main(){
    while(1){
        func();
    }
    return 0;
}

這個地方我就不運行了。

靜態分配記憶體

在使用靜態分配記憶體的時候,記憶體大小是固定的,很容易超出棧記憶體的最大值。使用malloc申請記憶體,最重要的內容就是可以規定申請記憶體的大小,也可以使用realloc重新申請記憶體大小
關於realloc函式的定義:

void* __cdecl realloc(
    _Pre_maybenull_ _Post_invalid_ void*  _Block,
    _In_ _CRT_GUARDOVERFLOW        size_t _Size
    );

使用方式:

// 重新申請記憶體大小 , 傳入申請的記憶體指標 , 申請記憶體總大小
int* p = realloc(p,(len + add) * sizeof(int));

一個例子,一開始申請一個空間內容,然後再增加到一定的內容:

#include<stdio.h>
int main(){

  int len;
  printf("請輸入首次分配記憶體的大小:");
  scanf("%d",&len);
  //動態分配記憶體,這裡注意記憶體空間是連續的
  int* p = (int*)malloc(len*sizeof(int));
  //給申請的內從空間賦值
  int i = 0;
  for(;i<len;i++){
    p[i] = rand() % 100;
    printf("array[%d] = %d,%#x\n",i,p[i],&p[i]);
  }
  printf("請輸入增加記憶體的大小");
  int add ;
  scanf("%d",&add);

  //更改記憶體分配大小之後,之前賦值的內容是不變的
  int* p2 = (int*)realloc(p,(len + add) * sizeof(int));

  //給申請的記憶體空間賦值
  int j = len;
  for(;j < len + add;j++){
    p2[j] = rand()%200;

  }
  for(int k=0;k<len+add;k++){
    printf("array[%d] = %d,%#x\n",k,p2[k],&p2[k]);
  }
  //釋放記憶體
  if(p2 != NULL){
   free(p2);
   p2 = NULL;
  }
 return 0;
}

這裡寫圖片描述
在這裡我們會發現,就算我們改變了記憶體大小,但是之前儲存的內容依然沒有改變,保留了下來。

動態分配記憶體空間注意點:
1. 不能多次釋放
2. 釋放完成之後,給指標設定為NULL,表示釋放完成
3. 記憶體洩漏(p重新賦值之後,呼叫free,並沒有真正的完全釋放,要在賦值之前釋放前一個記憶體空間,也就是先釋放,在賦值)

參考資料

C語言基礎
NDK開發