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

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

到這裏看一下 到底是啥類型