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
型別就是/d
;char
型別就是/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 = #
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,並沒有真正的完全釋放,要在賦值之前釋放前一個記憶體空間,也就是先釋放,在賦值
)