Android JNI 程式設計 - C語言基礎知識 (一)

語言: CN / TW / HK

Android工程師 為啥要學習c/c++呢?

主要還是自身遇到瓶頸了吧, 學習下c的知識,擴充下自己編寫so的能力,不然很多框架確實也是看不懂,特別是涉及到跨端的元件,不懂點底層是真的難搞

乾脆重新學一遍c的知識,順便把開源庫中喜歡用到的pthread,mmap,檔案流 等linux 操作也過一遍

基礎環境搭建

我是mac, 大部分情況下 下個clion 就可以直接寫c語言了, 但是考慮到Android 底層是linux,並不是mac的osx, 所以理想情況下 還是希望 我們編寫的c程式能直接跑在 linux上,

所以基礎環境的搭建 就是基於docker 來搭建一個linux ,然後讓我們的clion 直接遠端到這個docker上即可

```# CLion remote docker environment (How to build docker container, run and stop it)

Build and run:

docker build -t clion/centos7-cpp-env:0.1 -f Dockerfile.centos7-cpp-env .

docker run -d --cap-add sys_ptrace -p127.0.0.1:2222:22 clion/centos7-cpp-env:0.1

ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[localhost]:2222"

stop:

docker stop clion_remote_env

ssh credentials (test user):

user@password

FROM centos:7

RUN sed -e 's|^mirrorlist=|#mirrorlist=|g' \ -e 's|^#baseurl=http://mirror.centos.org|baseurl=https://mirrors.tuna.tsinghua.edu.cn|g' \ -i.bak \ /etc/yum.repos.d/CentOS-*.repo

RUN yum -y update \ && yum -y install openssh-server \ make \ autoconf \ automake \ locales-all \ dos2unix \ ninja-build \ build-essential \ gcc \ gcc-c++ \ gdb \ clang \ cmake \ rsync \ tar \ python \ && yum clean all

RUN ssh-keygen -A

RUN ( \ echo 'LogLevel DEBUG2'; \ echo 'PermitRootLogin yes'; \ echo 'PasswordAuthentication yes'; \ echo 'Subsystem sftp /usr/libexec/openssh/sftp-server'; \ ) > /etc/ssh/sshd_config_test_clion

RUN useradd -m user \ && yes password | passwd user

CMD ["/usr/sbin/sshd", "-D", "-e", "-f", "/etc/ssh/sshd_config_test_clion"] ```

去clion 那邊設定一下 toolchians

image.png

然後再編譯一下

image.png

再看下 我們引用的stdio 標頭檔案,這裡能看出來 引用的是 linux上的標頭檔案了, 有興趣的可以看下 這個標頭檔案linux的實現和 mac上的 標頭檔案實現有何不同 image.png

這裡有一點要注意,clion預設的cmake版本比較高,你要連centos7 ,預設的cmake版本低,會編譯失敗

這裡 我們只要設定一下 版本號為centos的cmake 版本號就行了

image.png

另外就是如果一個project 下 你有多個main函式入口 需要額外設定一下cmake

``` cmake_minimum_required(VERSION 2.8.12.2) project(cstudy C)

set(CMAKE_C_STANDARD 11)

一個project 下面如果有多個main函式入口 需要設定多個add_executable()

add_executable(cstudy date_types.c) add_executable(cstudy2 func_test.c) ```

說實話 寫多了java kotlin js 啥的,你會發現 cmake 這東西是真的蠢。。。。

函式的變長引數

```

include

include

void HandleVarargs(int arg_count,...) { // 用於獲取變長引數 va_list args; // 開始遍歷 va_start(args, arg_count); int j; for ( j = 0; j < arg_count; ++j) { // 取出對應引數 int arg = va_arg(args, int); printf("%d: %d \n", j, arg); } // 結束遍歷 va_end(args); }

int main() { HandleVarargs(4,1,2,5,6); return 0; } ``` 在c中 寫個變長引數的函式 真的累。。。 還要寫這種模版程式碼

最坑的是,你在呼叫這個引數的時候,第一個引數 還必須得是你引數的個數。。。

巨集

標頭檔案

image.png

這個include 其實就是一個巨集,和你直接用define 其實就是一樣的

image.png

本質上就是在你編譯的時候 把其他 函式/變數 直接匯入過來,好讓編譯器知道 你這裡應該怎麼呼叫一個函式

就這麼簡單

自己寫標頭檔案

這個也挺麻煩的,其實就是當你想對外提供一個功能,做一個模組的時候 就會用到這個東西,相比java 的無腦,c裡面實在是太麻煩了

可以新建include和src 2個路徑,一個放實現,一個放標頭檔案

image.png

然後我們新建一個頭檔案:

``` // // Created by 吳越 on 2023/2/18. //

ifndef CSTUDY_INCLUDE_FAC_H_

define CSTUDY_INCLUDE_FAC_H_

int sum(int x,int y);

endif //CSTUDY_INCLUDE_FAC_H_

```

再新建一個對應的實現 .c 檔案

``` // // Created by 吳越 on 2023/2/18. //

include "../include/fac.h"

int sum(int x,int y){ return x+y; } ```

此時不要忘記cmake檔案要改一下

add_executable(cstudy3 hong_test.c src/fac.c)

最後呼叫一下

```

include

include "include/fac.h"

int main() { printf("sum: %d \n", sum(3, 5)); return 0; } ``` 就這麼簡單!

<> 和 ““ 的區別

前面的程式碼可以看到 我們都是用的"" 來引用的標頭檔案, 那麼有麼有辦法 像引用系統標頭檔案一樣就用括號呢? 其實也是的可以的, 但是要去cmake 裡面增加一行程式碼

include_directories("include")

image.png

這個技巧要掌握一下,否則很多開原始碼 你會搞暈的, 換句話說 拿到開源的程式碼 還是先看一下 cmake檔案吧。

巨集函式

```

include

define MAX(a, b) a>b?a:b

int main() { printf("max : %d \n", MAX(1, 3)); printf("max : %d \n", MAX(1, MAX(4,3)));

return 0; } ``` 看下執行結果:

image.png

是不是和預想中的不太一樣?

巨集函式和普通函式還是有區別的,他就是直接在編譯的時候替換掉的

image.png

稍微改一下:

```

define MAX(a, b) (a) >(b) ? (a) :(b)

```

image.png

應該就ok了

巨集本質上就是 程式碼替換,和函式是不一樣的

個人認為,巨集函式比函式好的地方就在於 他沒有型別限制

巨集函式如何換行

```

include

define MAX(a, b) (a) >(b) ? (a) :(b)

#define is_hex_char(c) \ ((c)>='0'&& (c)<='9') || \ ((c)>='a' && (c)<='f') int main() { printf("max : %d \n", MAX(1, 3)); printf("max : %d \n", MAX(1, MAX(4, 3))); printf("is hex : %d \n", is_hex_char('a')); return 0; } ```

條件編譯

看下之前寫的程式碼:

image.png

這些黃色的程式碼代表啥意思?作用是什麼?

其實你想一下,這個標頭檔案裡面 聲明瞭一個函式

如果 你的程式很大的情況下, 如果有多處地方都include了這個標頭檔案,等於你是不是有多個 同名的函式?

那編譯不是會報錯嘛,這裡的黃色程式碼 就代表是這個意思

條件編譯 在某些時候 挺有用的,比如你想本地編譯的時候 列印除錯資訊 ,正式版本不列印

```

include

void dump(){

ifdef DEBUG

printf("debug info");

endif

}

int main(){ dump(); return 0; } ``` 然後在cmake中寫一下:

add_executable(cstudy5 macro_test2.c) target_compile_definitions(cstudy5 PUBLIC DEBUG)

跑一下:

image.png

很有意思,你明明沒有定義debug 這個巨集,但是執行的結果 卻列印了除錯資訊, 這個其實也是cmake 在發揮作用

通常我們可以利用條件編譯來判斷 我們到底是在c還是cpp,在mac,還是在windows 還是在linux的環境中

=printf 自動換行

提供了2個版本的實現,明顯巨集的實現更簡潔一些

```

include

include

define PRINTFLINE(format, ...) printf(format"\n",##VA_ARGS)

void Printf(const char *format, ...) { va_list args; va_start(args, format); vprintf(format, args); printf("\n"); va_end(args); }

int main() { Printf("hello"); PRINTFLINE("world"); return 0; } ```

有的人覺得 c語言 列印個變數也太麻煩了,還要寫百分號,有沒有更簡單的

```

define PRINT_INT(value) PRINTFLINE(#value": %d",value)

```

image.png

還有更進一步的:

我們希望列印的時候 可以自動把我們的行號,所屬的檔案 ,檔名都打印出來,這樣列印除錯資訊的就很清晰了 大型工程的時候特別有用

```

define PRINTFLINE(format, ...) printf("("FILE":%d) %s : "format"\n",LINE, FUNCTION, ##VA_ARGS)

```

字串

字串基本知識

c 語言中的字串 必須以 null 也就是\0 為結尾

```

include

include

int main(){ char string[5]="wuyue"; char string2[6]="wuyue"; PRINTFLINE("string: %s",string); PRINTFLINE("string2: %s",string2);

return 0; } ``` 來看下執行結果:

image.png

第一行的結果肯定是不對的,其實問題就是 沒有多開闢一個元素位置 用來放\0 這一點和其他語言又不一樣

指標

指標的只讀

image.png

這裡 可以看到 編譯報錯了, cp的地址是不能改的,但是值可以修改

反過來看一下:

image.png

這裡就是 地址可以改,但是值不能修改

image.png

還有一種寫法 可以看出來,這裡甚至連值都不能改了,

主要還是看 *的位置 總結起來就是:

const 如果修飾的是指標,則地址不能修改 const 如果修飾的是值,則值不能修改

其實就是看const的左邊有沒有*

左右值問題

先看下 下面這段程式碼:

```

include

include

int main() {

int array[4] = {0}; int pa= array; pa=2; (pa++)=3; (pa+2)=4; PRINT_INT_ARRAY(array,4); return 0; } ```

執行結果:

image.png

要搞清楚一個概念,等號 右邊 永遠是取出來的值,而等號左邊 永遠是一塊地址空間

第一次是把2 這個值 賦給 位置為0的元素

第二次是 pa這個指標的位置 的值 也就是位置為0的元素 改為3,然後 這個pa的指標 挪後了一位

第三次 就是pa這個指標 再挪兩位

指標引數作為返回值

這個務必要搞清楚了,很多開源專案都是大量運用這個技巧

比如說 我們前面求和的這個函式

image.png

如果你在main函式中呼叫他 會發生什麼呢?

首先在sum這個函式把結果計算出來之後, 是先把這個結果 從記憶體中拷貝到cpu的暫存器中

第二步: 再從cpu的暫存器中將這個值拷貝到記憶體中

這裡是不是就涉及到兩次拷貝了? 如果你這個函式很複雜,是個結構體,這個開銷還是有的,

所以很多時候 我們會這麼寫:

void sum2(int x, int y, int *result) { *result = x + y; }

函式的最後1個或者n個引數 作為接收函式的返回值,可以實現函式多返回值,並且省略記憶體拷貝的開銷

動態分配記憶體

別的語言 動態宣告1個數組是可以的, 但是在c語言 就比較麻煩了, 需要像下面的程式一樣 才可以辦到

```

include

include

include

int main() { int size = 10; int *array = malloc(sizeof(int) * size); int i; for (i = 0; i < size; ++i) { array[i]=i; } PRINT_INT_ARRAY(array,size) free(array); return 0;

} ```

這裡要注意2點,第一 malloc和free 要成對出現

第二 malloc分配的記憶體塊 最好要第一時間初始化, 因為malloc是在堆區上分配的一塊記憶體,你不知道這塊記憶體上是什麼值,所以你申請完以後 第一時間要做初始化

注意了,這裡有一個大坑, 比如我們想把這個 初始化的過程作為一個函式 來方便呼叫, 很多人會這麼寫:

``` void InitIntArray(int *a, int size, int defaultValue) { a = malloc(sizeof(int) * size); int i; for (i = 0; i < size; ++i) { a[i] = defaultValue; } }

int main() { int size = 10; int *a; InitIntArray(a,size,0); PRINT_INT_ARRAY(a, size) free(a); return 0; } ```

看上去好像這段程式碼沒什麼問題 但是你執行起來就會報錯了, 來看下正確的程式碼 應該怎麼寫: ``` void InitIntArray2(int aparams, int size, int defaultValue) { aparams = malloc(sizeof(int) * size); int i; for (i = 0; i < size; ++i) { (aparams)[i] = defaultValue; } }

int main() { int size = 10; int *a; InitIntArray2(&a,size,0); PRINT_INT_ARRAY(a, size) free(a); return 0; } ```

可以體會一下 這2個寫法的區別, 我們首先看一下 第一個寫法 為什麼不對,

在c語言中,函式都是值傳遞,什麼是值傳遞?

也就是對於第一種寫法來說,

雖然你在方法內部 成功malloc了一塊記憶體,但是指向這塊記憶體的是 你函式的引數,並不是main函式中的 指a

一定要切記,函式都是值傳遞的。

那第二種寫法為什麼正確?

首先你傳入的是一個指標的地址

也就是說 aParams = &a

那麼 *aParams 就等於 a

另外也可以關注下 calloc這個函式, 這是自動初始化值的,還有一個realloc 重新分配一段記憶體, 通常可以用於動態擴大陣列的大小

// 自動初始化 int *b = calloc(size, sizeof(int)); // 在之前的基礎上,重新開闢一段空間,等於是擴大了 b = realloc(b, size * 2); if (b != NULL) { PRINTFLINE("分配成功"); PRINT_INT_ARRAY(b, size); } else{ PRINTFLINE("分配失敗"); } return 0;

函式指標

在c語言中 是可以定義一個函式指標的,很意外吧 , 比如說 上面那個小節的例子,還可以這麼寫

void (*func)(int **aparams, int size, int defaultValue) = &InitIntArray2; size = 30; func(&a, size, 30); PRINT_INT_ARRAY(a, size);

這裡真的很容易迷惑 可以看下 下面的幾種寫法

``` // f1是一個函式,這個函式的返回值 是一個int * 指標 int *f1(int,double );

// f2是一個函式指標,指向一個 引數為int,double 返回值是int的 函式 int (*f2) (int,double );

// f3和f2 一樣,只不過返回值 是一個 int * 的函式 int (f3) (int,double );

// 函式的指標可以定義陣列,但是函式不能定義陣列 int (*f5[]) (int,double); ```

當然還可以指定別名

// 定義這個函式指標的別名 typedef int (*Func)(int, int); int Add(int x, int y) { return x + y; } 然後去呼叫他

Func func_1 = &Add; PRINTFLINE("result : %d" ,func_1(3, 4));

這裡有點繞,但是沒關係 我們可以在覺得看不懂程式碼的時候 複製一下型別 到下面的網站 讓他給你答案即可

到這裡看一下 到底是啥型別