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

这里有点绕,但是没关系 我们可以在觉得看不懂代码的时候 复制一下类型 到下面的网站 让他给你答案即可

到这里看一下 到底是啥类型