Android JNI 程式設計 - C語言基礎知識 (一)
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
然後再編譯一下
再看下 我們引用的stdio 標頭檔案,這裡能看出來 引用的是 linux上的標頭檔案了,
有興趣的可以看下 這個標頭檔案linux的實現和 mac上的 標頭檔案實現有何不同
這裡有一點要注意,clion預設的cmake版本比較高,你要連centos7 ,預設的cmake版本低,會編譯失敗
這裡 我們只要設定一下 版本號為centos的cmake 版本號就行了
另外就是如果一個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中 寫個變長引數的函式 真的累。。。 還要寫這種模版程式碼
最坑的是,你在呼叫這個引數的時候,第一個引數 還必須得是你引數的個數。。。
巨集
標頭檔案
這個include 其實就是一個巨集,和你直接用define 其實就是一樣的
本質上就是在你編譯的時候 把其他 函式/變數 直接匯入過來,好讓編譯器知道 你這裡應該怎麼呼叫一個函式
就這麼簡單
自己寫標頭檔案
這個也挺麻煩的,其實就是當你想對外提供一個功能,做一個模組的時候 就會用到這個東西,相比java 的無腦,c裡面實在是太麻煩了
可以新建include和src 2個路徑,一個放實現,一個放標頭檔案
然後我們新建一個頭檔案:
``` // // 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")
這個技巧要掌握一下,否則很多開原始碼 你會搞暈的, 換句話說 拿到開源的程式碼 還是先看一下 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; } ``` 看下執行結果:
是不是和預想中的不太一樣?
巨集函式和普通函式還是有區別的,他就是直接在編譯的時候替換掉的
稍微改一下:
```
define MAX(a, b) (a) >(b) ? (a) :(b)
```
應該就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; } ```
條件編譯
看下之前寫的程式碼:
這些黃色的程式碼代表啥意思?作用是什麼?
其實你想一下,這個標頭檔案裡面 聲明瞭一個函式
如果 你的程式很大的情況下, 如果有多處地方都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)
跑一下:
很有意思,你明明沒有定義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)
```
還有更進一步的:
我們希望列印的時候 可以自動把我們的行號,所屬的檔案 ,檔名都打印出來,這樣列印除錯資訊的就很清晰了 大型工程的時候特別有用
```
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; } ``` 來看下執行結果:
第一行的結果肯定是不對的,其實問題就是 沒有多開闢一個元素位置 用來放\0 這一點和其他語言又不一樣
指標
指標的只讀
這裡 可以看到 編譯報錯了, cp的地址是不能改的,但是值可以修改
反過來看一下:
這裡就是 地址可以改,但是值不能修改
還有一種寫法 可以看出來,這裡甚至連值都不能改了,
主要還是看 *的位置 總結起來就是:
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; } ```
執行結果:
要搞清楚一個概念,等號 右邊 永遠是取出來的值,而等號左邊 永遠是一塊地址空間
第一次是把2 這個值 賦給 位置為0的元素
第二次是 pa這個指標的位置 的值 也就是位置為0的元素 改為3,然後 這個pa的指標 挪後了一位
第三次 就是pa這個指標 再挪兩位
指標引數作為返回值
這個務必要搞清楚了,很多開源專案都是大量運用這個技巧
比如說 我們前面求和的這個函式
如果你在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));
這裡有點繞,但是沒關係 我們可以在覺得看不懂程式碼的時候 複製一下型別 到下面的網站 讓他給你答案即可