Android工程师 为啥要学习c/c++呢?

首要仍是本身遇到瓶颈了吧, 学习下c的常识,扩展下自己编写so的能力,不然许多框架的确也是看不明白,特别是涉及到跨端的组件,不明白点底层是真的难搞

爽性从头学一遍c的常识,顺便把开源库中喜欢用到的pthread,mmap,文件流 等linux 操作也过一遍

基础环境建立

我是mac, 大部分情况下 下个clion 就能够直接写c言语了, 可是考虑到Android 底层是linux,并不是mac的osx, 所以理想情况下 仍是期望 咱们编写的c程序能直接跑在 linux上,

所以基础环境的建立 便是根据docker 来建立一个linux ,然后让咱们的clion 直接远程到这个docker上即可

#
# 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

Android  JNI  编程 - C语言基础知识 (一)

然后再编译一下

Android  JNI  编程 - C语言基础知识 (一)

再看下 咱们引证的stdio 头文件,这儿能看出来 引证的是 linux上的头文件了, 有爱好的能够看下 这个头文件linux的完成和 mac上的 头文件完成有何不同

Android  JNI  编程 - C语言基础知识 (一)

这儿有一点要注意,clion默认的cmake版别比较高,你要连centos7 ,默认的cmake版别低,会编译失败

这儿 咱们只需设置一下 版别号为centos的cmake 版别号就行了

Android  JNI  编程 - C语言基础知识 (一)

别的便是假如一个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 <stdio.h>
#include <stdarg.h>
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中 写个变长参数的函数 真的累。。。 还要写这种模版代码

最坑的是,你在调用这个参数的时分,榜首个参数 还有必要得是你参数的个数。。。

头文件

Android  JNI  编程 - C语言基础知识 (一)

这个include 其实便是一个宏,和你直接用define 其实便是相同的

Android  JNI  编程 - C语言基础知识 (一)

本质上便是在你编译的时分 把其他 函数/变量 直接导入过来,好让编译器知道 你这儿应该怎么调用一个函数

就这么简略

自己写头文件

这个也挺麻烦的,其实便是当你想对外供给一个功用,做一个模块的时分 就会用到这个东西,相比java 的无脑,c里边实在是太麻烦了

能够新建include和src 2个途径,一个放完成,一个放头文件

Android  JNI  编程 - C语言基础知识 (一)

然后咱们新建一个头文件:

//
// 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 <stdio.h>
#include "include/fac.h"
int main() {
  printf("sum: %d \n", sum(3, 5));
  return 0;
}

就这么简略!

<> 和 ““ 的差异

前面的代码能够看到 咱们都是用的”” 来引证的头文件, 那么有么有办法 像引证系统头文件相同就用括号呢? 其实也是的能够的, 可是要去cmake 里边添加一行代码

include_directories("include")

Android  JNI  编程 - C语言基础知识 (一)

这个技巧要把握一下,不然许多开源代码 你会搞晕的, 换句话说 拿到开源的代码 仍是先看一下 cmake文件吧。

宏函数

#include <stdio.h>
#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;
}

看下履行成果:

Android  JNI  编程 - C语言基础知识 (一)

是不是和预想中的不太相同?

宏函数和普通函数仍是有差异的,他便是直接在编译的时分替换掉的

Android  JNI  编程 - C语言基础知识 (一)

稍微改一下:

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

Android  JNI  编程 - C语言基础知识 (一)

应该就ok了

宏本质上便是 代码替换,和函数是不相同的

个人认为,宏函数比函数好的地方就在于 他没有类型约束

宏函数如何换行

#include <stdio.h>
#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;
}

条件编译

看下之前写的代码:

Android  JNI  编程 - C语言基础知识 (一)

这些黄色的代码代表啥意思?作用是什么?

其实你想一下,这个头文件里边 声明晰一个函数

假如 你的程序很大的情况下, 假如有多处地方都include了这个头文件,等于你是不是有多个 同名的函数?

那编译不是会报错嘛,这儿的黄色代码 就代表是这个意思

条件编译 在某些时分 挺有用的,比方你想本地编译的时分 打印调试信息 ,正式版别不打印

#include <stdio.h>
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)

跑一下:

Android  JNI  编程 - C语言基础知识 (一)

很有意思,你明明没有界说debug 这个宏,可是运行的成果 却打印了调试信息, 这个其实也是cmake 在发挥作用

一般咱们能够使用条件编译来判别 咱们到底是在c仍是cpp,在mac,仍是在windows 仍是在linux的环境中

=printf 主动换行

供给了2个版别的完成,显着宏的完成更简练一些

#include <stdio.h>
#include <stdarg.h>
#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)

Android  JNI  编程 - C语言基础知识 (一)

还有更进一步的:

咱们期望打印的时分 能够主动把咱们的行号,所属的文件 ,文件名都打印出来,这样打印调试信息的就很清晰了 大型工程的时分特别有用

#define PRINTFLINE(format, ...) printf("("__FILE__":%d) %s : "format"\n",__LINE__, __FUNCTION__, ##__VA_ARGS__)

字符串

字符串基本常识

c 言语中的字符串 有必要以 null 也便是\0 为结尾

#include <stdio.h>
#include <ioprint.h>
int main(){
  char string[5]="wuyue";
  char string2[6]="wuyue";
  PRINTFLINE("string: %s",string);
  PRINTFLINE("string2: %s",string2);
  return 0;
}

来看下履行成果:

Android  JNI  编程 - C语言基础知识 (一)

榜首行的成果肯定是不对的,其实问题便是 没有多拓荒一个元素方位 用来放\0 这一点和其他言语又不相同

指针

指针的只读

Android  JNI  编程 - C语言基础知识 (一)

这儿 能够看到 编译报错了, cp的地址是不能改的,可是值能够修正

反过来看一下:

Android  JNI  编程 - C语言基础知识 (一)

这儿便是 地址能够改,可是值不能修正

Android  JNI  编程 - C语言基础知识 (一)

还有一种写法 能够看出来,这儿甚至连值都不能改了,

首要仍是看 *的方位 总结起来便是:

const 假如润饰的是指针,则地址不能修正 const 假如润饰的是值,则值不能修正

其实便是看const的左边有没有*

左右值问题

先看下 下面这段代码:

#include <stdio.h>
#include <ioprint.h>
int main() {
  int array[4] = {0};
  int *pa= array;
  *pa=2;
  *(pa++)=3;
  *(pa+2)=4;
  PRINT_INT_ARRAY(array,4);
  return 0;
}

履行成果:

Android  JNI  编程 - C语言基础知识 (一)

要搞清楚一个概念,等号 右边 永远是取出来的值,而等号左边 永远是一块地址空间

榜首次是把2 这个值 赋给 方位为0的元素

第二次是 pa这个指针的方位 的值 也便是方位为0的元素 改为3,然后 这个pa的指针 挪后了一位

第三次 便是pa这个指针 再挪两位

指针参数作为返回值

这个务必要搞清楚了,许多开源项目都是很多运用这个技巧

比方说 咱们前面求和的这个函数

Android  JNI  编程 - C语言基础知识 (一)

假如你在main函数中调用他 会产生什么呢?

首先在sum这个函数把成果计算出来之后, 是先把这个成果 从内存中拷贝到cpu的寄存器中

第二步: 再从cpu的寄存器中将这个值拷贝到内存中

这儿是不是就涉及到两次拷贝了? 假如你这个函数很复杂,是个结构体,这个开销仍是有的,

所以许多时分 咱们会这么写:

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

函数的最终1个或者n个参数 作为接纳函数的返回值,能够完成函数多返回值,而且省略内存拷贝的开销

动态分配内存

别的言语 动态声明1个数组是能够的, 可是在c言语 就比较麻烦了, 需要像下面的程序相同 才能够办到

#include <stdio.h>
#include <ioprint.h>
#include <malloc.h>
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));

这儿有点绕,可是不要紧 咱们能够在觉得看不明白代码的时分 仿制一下类型 到下面的网站 让他给你答案即可

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